首页 > 代码库 > 黑马程序员_Java多线程

黑马程序员_Java多线程

- - - - - android培训、java培训、期待与您交流! - - - - - -

进程:正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。进程负责的是应用程序的空间的标示。

线程:其实就是进程中一个程序执行控制单元,一条执行路径。线程负责的是应用程序的执行顺序。

  • 一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序。
  • 每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。

jvm在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数,主线程执行的代码都在main方法中。当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样主线程中的代码执行会停止,而去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。 

随机性的原理:哪个线程获取到了cpu的执行权,哪个线程就执行,实质是cpu的快速切换造成。

返回当前线程的名称:Thread.currentThread().getName();线程的名称是由:Thread-编号定义的。编号从0开始。线程要运行的代码都统一存放在了run方法中。

线程要运行必须要通过类中指定的方法【start方法】开启。(启动后,就多了一条执行路径)

start方法

  1. 启动了线程
  2. 让jvm调用了run方法。

创建线程的第一种方式:继承Thread ,由子类复写run方法:

步骤:

  1. 定义类继承Thread类;
  2. 目的是复写run方法,将要让线程运行的代码都存储到run方法中;
  3. 通过创建Thread类的子类对象,创建线程对象;
  4. 调用线程的start方法,开启线程,并执行run方法。
    class Show extends Thread       {          public void run()          {              for (int i =0;i<5 ;i++ )              {                  System.out.println(name +"_" + i);              }          }                public Show(){}          public Show(String name)          {              this.name = name;          }          private String name;                public static void main(String[] args)           {              new Show("csdn").start();              new Show("黑马").start();          }      }  

【可能的运行结果】:

为什么我们不能直接调用run()方法呢?原因是线程的运行需要本地操作系统的支持。

查看start的源代码发现:

    public synchronized void start() {                      if (threadStatus != 0)                 throw new IllegalThreadStateException();                   group.add(this);                   boolean started = false;             try {                 start0();                 started = true;             } finally {                 try {                     if (!started) {                         group.threadStartFailed(this);                     }                 } catch (Throwable ignore) {                     /* do nothing. If start0 threw a Throwable then                      it will be passed up the call stack */                 }             }         }            private native void start0();  

这个方法用了native关键字,native表示调用本地操作系统的函数,多线程的实现需要本地操作系统的支持。

线程状态

  • 被创建:start()
  • 运行:具备执行资格,同时具备执行权;
  • 冻结:sleep(time),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
  • 临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
  • 消亡:stop()

创建线程的第二种方式:实现一个接口Runnable:

步骤:

  1. 定义类实现Runnable接口。
  2. 覆盖接口中的run方法(用于封装线程要运行的代码)。
  3. 通过Thread类创建线程对象;
  4. 将实现了Runnable接口的子类对象作为实际参数传递给Thread类中的构造函数。【为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象】
  5. 调用Thread对象的start方法,开启线程,并运行Runnable接口子类中的run方法。

Ticket t = new Ticket();

直接创建Ticket对象,并不是创建线程对象。【因为创建线程对象只能通过new Thread类,或者new Thread类的子类才可以

Thread t1 = new Thread(t); //创建线程。

只要将t作为Thread类的构造函数的实际参数传入即可完成线程对象和t之间的关联。【为什么要将t传给Thread类的构造函数呢?其实就是为了明确线程要运行的代码run方法】

t1.start();//开启线程

为什么要有Runnable接口的出现?

1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。

可是该类中的还有部分代码需要被多个线程同时执行,这时怎么办呢?

只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。

所以,通常创建线程都用第二种方式。【因为实现Runnable接口可以避免单继承的局限性】

2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。

所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。

实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。

new Thread(new Runnable(){  //匿名public void run(){    System.out.println("runnable run");}}){public void run(){    System.out.println("subthread run");}}.start();  //结果:subthread run
Try {Thread.sleep(10);}catch(InterruptedException e){}// 当刻意让线程稍微停一下,模拟cpu 切换情况。

Thread和Runnable的区别:

如果一个类继承Thread,则不能资源共享(有可能是操作的实体不是唯一的);但是如果实现了Runable接口的话,则可以实现资源共享。

    class Show implements Runnable      {          private int count = 10;//假设有10张票          @Override          public void run()          {              for (int i = 0; i < 5 ; i++ )              {                  if (this.count > 0)                  {                      System.out.println(Thread.currentThread().getName()+"正在卖票" + this.count--);                  }              }          }                public static void main(String[] args)           {              Show s = new Show(); //注意必须保证只对1个实体s操作              new Thread(s,"窗口1").start();              new Thread(s,"窗口2").start();              new Thread(s,"窗口3").start();          }      }  

实现Runnable接口比继承Thread类所具有的优势:

  • 适合多个相同的程序代码的线程去处理同一个资源
  • 可以避免java中的单继承的限制
  • 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。

设置线程优先级

Thread t = new Thread(myRunnable);  t.setPriority(Thread.MAX_PRIORITY);//一共10个等级,Thread.MAX_PRIORITY表示最高级10  t.start();

//MAX_PRIORITY : 其值是 10 
//MIN_PRIORITY : 其值是 1
//NORM_PRIORITY: 其值是 5

提示:主线程的优先级是5,不要误以为优先级越高就先执行。谁先执行还是取决于谁先去的CPU的资源

控制线程的方法:

  • join方法:假如你在A线程中调用了B线程的join方法B.join();,这时B线程继续运行,A线程停止(进入阻塞状态)。等B运行完毕A再继续运行。
  • sleep方法:线程中调用sleep方法后,本线程停止(进入阻塞状态),运行权交给其他线程。
  • yield方法:线程中调用yield方法后本线程并不停止,运行权由本线程和优先级不低于本线程的线程来抢。(不一定优先级高的能先抢到,只是优先级高的抢到的时间长)
package heimablog;// 定义Runnable 接口的实现类public class JoinTest implements Runnable {	// 重写run() 方法	public void run() {		for (int i = 0; i <= 10; ++i)			System.out.println(Thread.currentThread().getName() + "..." + i);	}	public static void main(String args[]) throws InterruptedException {		// 创建Runnable 接口实现类的实例		JoinTest t = new JoinTest();		// 通过 Thread(Runnable target) 创建新线程		Thread thread = new Thread(t);		Thread thread1 = new Thread(t);		// 启动线程		thread.start();		// 只有等thread 线程执行结束,main 线程才会向下执行;		thread.join();		System.out.println("thread1 线程将要启动");		thread1.start();	}} 
  • wait方法:当前线程转入阻塞状态,让出cpu的控制权,解除锁定。
  • notify方法:唤醒因为wait()进入阻塞状态的其中一个线程。
  • notifyAll方法: 唤醒因为wait()进入阻塞状态的所有线程。

这三个方法都必须用synchronized块来包装,而且必须是同一把锁,不然会抛出java.lang.IllegalMonitorStateException异常。

多线程安全问题的原因:

发现一个线程在执行多条语句时,并运算同一个数据时,在执行过程中,其他线程参与进来,并操作了这个数据,导致了错误数据的产生。

涉及到两个因素:

  1. 多个线程在操作共享数据。
  2. 有多条语句对共享数据进行运算。

如下面程序:

package heimablog;/*  * 多个线程同时访问一个数据时,出现的安全问题。 * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票 */class Ticks implements Runnable {    private int ticks = 100 ;     public void run()    {        while (ticks > 0)        {        	//加入sleep 方法是为了更明显的看到该程序中出现的安全问题。            try{Thread.sleep(10);}catch (Exception e) {}             System.out.println(Thread.currentThread().getName()                    +"...卖出了第"+ticks+"张票");            ticks -- ;        }    }}public class ShowTest {    public static void main(String args[])    {    	//创建 Runnable 实现类 Ticks 的对象。        Ticks t = new Ticks() ;          //开启4个线程处理同一个 t 对象。        new Thread(t , "一号窗口").start() ;         new Thread(t , "二号窗口").start() ;         new Thread(t , "三号窗口").start() ;         new Thread(t , "四号窗口").start() ;     }}

运行的结果会出现"一号窗口...卖出了第0张票,四号窗口...卖出了第-1张票,二号窗口...卖出了第-2张票"这样的安全问题。

解决安全问题的原理

只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他线程不能进来执行就可以解决这个问题。

解决这类问题的方法:

  1、同步代码块。

  2、同步函数。

同步代码块

synchronized(obj)  // 任意对象都可以,这个对象就是锁。{    //此处的代码就是同步代码块,也就是需要被同步的代码;}

synchronized 后括号里面的 obj 就是同步监视器。代码含义:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。即只有获得对同步监视器的锁定的线程可以在同步中执行,没有锁定的线程即使获得执行权,也不能在同步代码块中执行。

注意:虽然JAVA 程序允许使用任何对象来作为同步监视器。但是还是推荐使用可能被并发访问的共享资源来充当同步监视器。

修改代码如下:

package heimablog;/*  * 多个线程同时访问一个数据时,出现的安全问题。 * 模拟一个卖火车票系统:一共有100张票,多个窗口同时卖票 */class Ticks implements Runnable {	private int ticks = 100;	public void run() {		while (ticks > 0) {			synchronized (Ticks.class) {				if (ticks > 0) {					try {						Thread.sleep(10);					} catch (Exception e) {}					System.out.println(Thread.currentThread().getName()							+ "...卖出了第" + ticks + "张票");					ticks--;				}			}		}	}}public class ShowTest {	public static void main(String args[]) {		// 创建 Runnable 实现类 Ticks 的对象。		Ticks t = new Ticks();		// 开启4个线程处理同一个 t 对象。		new Thread(t, "一号窗口").start();		new Thread(t, "二号窗口").start();		new Thread(t, "三号窗口").start();		new Thread(t, "四号窗口").start();	}}

加入同步监视器之后的程序就不会出现数据上的错误了。

  虽然同步监视器的好处是解决了多线程的安全问题。但也也因为多个线程需要判断锁,较为消耗资源。

同步前提:

  1. 必须要有两个或者两个以上的线程,才需要同步。
  2. 多个线程必须保证使用的是同一个锁。

如果加入了synchronized 同步监视器,还出现了安全问题,则可以按照如下步骤找寻错误:

  1. 明确那些代码是多线程代码。
  2. 明确共享数据。
  3. 明确多线程运行代码中那些代码是操作共享数据的。

同步函数

  把 synchronized 作为修饰符修饰函数。则该函数称为同步函数。

  注意:同步函数无需显示的指定同步监视器,函数都有自己所属的对象this,同步函数的同步监视器是this,也就是该对象本身。

  注意:synchronized 关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等。

上面通过模拟火车卖票系统的小程序,通过加入 synchronized 同步监视器,来解决多线程中的安全问题。下面模拟银行取钱问题,通过同步函数来解决多线程的安全问题。

package heimablog;class Account {	// 账户余额	private double balance;	public Account(double balance) {		this.balance = balance;	}	// get和set方法	public double getBalance() {		return balance;	}	public void setBalance(double balance) {		this.balance = balance;	}	// 同步函数	// 提供一个线程安全的draw的方法来完成取钱操作。	public synchronized void Draw(double drawAmount) {		if (balance >= drawAmount) {			System.out.println(Thread.currentThread().getName() + "取钱成功!吐出金额"					+ drawAmount);			try {				Thread.sleep(10);			} catch (Exception e) {			}			balance -= drawAmount;			System.out.println("卡上余额:" + balance);		} else {			System.out.println(Thread.currentThread().getName() + "取钱失败!卡上余额:"					+ balance);		}	}}class DrawThread implements Runnable {	// 模拟账户	private Account account;	// 希望所取钱的金额	private double drawAmount;	private boolean flag = true;	public DrawThread(Account account, double drawAmount) {		this.account = account;		this.drawAmount = drawAmount;	}	// 当前取钱	public void run() {		try {			while (flag) {				// 调用取钱函数				account.Draw(drawAmount);			}		} catch (Exception e) {			flag = false;		}	}}public class ShowTest {	public static void main(String args[]) {		Account account = new Account(10000);		DrawThread draw = new DrawThread(account, 50);		Thread t = new Thread(draw, "A.....");		Thread t1 = new Thread(draw, "B.");		t.start();		t1.start();	}}

同步方法的监视器是 this ,因此对于同一个 Account 而言,任意时刻只能有一条线程获得 Account 对象的锁定。

提示:可变类的线程安全是以降低运行程序的运行效率作为代价,为了减少线程安全所带来的负面影响,程序可以采用如下策略:

  • 只对会改变竞争资源的方法进行同步。
  • 在两个或两个以上的线程操作同一个锁的环境中使用同步。

当如下情况发生时会释放同步监视器的锁定:

  • 当前线程的同步方法、同步代码块执行结束。
  • 当前线程的同步方法、同步代码块中遇到break 、 return终止了该代码块、该方法的继续执行。
  • 当前线程的同步方法、同步代码块出现了未处理的Error或Exception,导致该代码块、该方法异常结束时会释放同步锁。
  • 当线程执行同步方法、同步代码块,程序执行了同步监视器对象的wait() 方法时。

当同步函数被static修饰时,这时的同步用的是哪个锁呢?

静态函数在加载时所属于类,这时有可能还没有该类产生的对象,但是该类的字节码文件加载进内存就已经被封装成了对象,这个对象就是该类的字节码文件对象。

所以静态加载时,只有一个对象存在,那么静态同步函数就使用的这个对象。这个对象就是 类名.class

同步代码块和同步函数的区别?

  • 同步代码块使用的锁可以是任意对象。
  • 同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。

在一个类中只有一个同步,可以使用同步函数。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些

死锁

  当两线程相互等待对方释放锁时,就会发生死锁。由于JVM没有监测,也没有采用措施来处理死锁,所以多线程编成时应该采取措施来避免死锁。

单例模式之懒汉式

懒汉式:延迟加载方式。

当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作,所以容易出现线程安全问题。

  • 为了解决安全问题,加入同步机制。但是却带来了效率降低。
  • 为了解决效率问题,通过双重判断的形式解决。
class Single {	private static Single s = null;     private Single() {	
     } public static Single getInstance() { if (s == null) { synchronized (Single.class) {//用字节码文件对象作为锁; if (s == null) s = new Single(); } } return s; }}

同步死锁:通常只要将同步进行嵌套,就可以看到现象。

线程通信

思路:多个线程在操作同一个资源,但是操作的动作却不一样。

  1. 将资源封装成对象。
  2. 将线程执行的任务(任务其实就是run方法。)也封装成对象。

模拟生产消费者:系统在有两条线程,分别代表生成者和消费者。

  程序的基本流程:

  1. 生成者生产出一件商品。
  2. 消费者消费生成出的商品。

  通过上诉流程要了解:生成者和消费者不能连续生成或消费商品,同时消费者只能消费已经生产出的商品。

package heimablog;class Phone {	// 定义商品的编号	private int No;	// 定义商品的名字	private String name;	private boolean flag = true;	// 初始化商品的名字和编号,同时编号是自增的	public Phone(String name, int No) {		this.name = name;		this.No = No;	}	// 定义商品中的消费方法和生产方法。用synchronized 修饰符修饰	public synchronized void Production() {		// 导致当前线程等待,知道其他线程调用notify()或notifyAll()方法来唤醒		// if (!flag)		while (!flag)			try {				this.wait();			} catch (Exception e) {			}		System.out.println(Thread.currentThread().getName() + ":生产" + name				+ ";编号为:" + ++No);		// 唤醒在此同步监视器上等待的单个线程。		// this.notify() ;		// 唤醒在此同步监视器上等待的所有线程。		this.notifyAll();		flag = false;	}	public synchronized void Consumption() {		// if(flag)		while (flag)			try {this.wait();} catch (Exception e) {}		System.out.println(Thread.currentThread().getName() + ";消费商品:" + name				+ "商品的编号为" + No);		// this.notify() ;		this.notifyAll();		flag = true;	}}class ProducerThread implements Runnable {	Phone phone;	private boolean flag = true;	// 同步监视器的对象	public ProducerThread(Phone phone) {		this.phone = phone;	}	public void run() {		try {			while (flag)				phone.Production();		} catch (Exception e) {			flag = false;		}	}}class ConsumptionThread implements Runnable {	Phone phone;	private boolean flag = true;	// 同步监视器的对象	public ConsumptionThread(Phone phone) {		this.phone = phone;	}	public void run() {		try {			while (flag)				phone.Consumption();		} catch (Exception e) {			flag = false;		}	}}public class ShowTest {	public static void main(String args[]) {		Phone phone = new Phone("iPhone 5", 0);		new Thread(new ProducerThread(phone), "生成者000").start();		new Thread(new ProducerThread(phone), "生成者111").start();		new Thread(new ConsumptionThread(phone), "消费者000").start();		new Thread(new ConsumptionThread(phone), "消费者111").start();	}}

上面的程序中:flag 标志位 是判断 是由生产者生成还是由消费者进行消费。其实,在现实生活中,不可能只有一个生成者和消费者,而是多个生成者和消费者。所以用 while 循环来进行 flag 的判断 , 而不是用 if 。如果用if 容易出现线程安全问题;而且在用while 循环进行flag的判断时,则必须用 notifyAll() 方法来唤醒同步监视器中所有等待中的线程,而不是 用notify() 方法。用notify()  则会导致所有线程进入等待状态。

上面的小程序借助Object 类提供的 wait()、notify()、notifyAll 三个方法【等待唤醒机制涉及的方法】

  • wait() :导致当前线程等待,知道其他线程调用该同步监视器的notify()或notifyAll() 方法来唤醒线程。
  • notify() : 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择一个其中一个唤醒。
  • notifyAll() :唤醒此同步监视器上等待的所有单个线程。

注意

  • 这三个方法必须用同步监视器对象来调用:
    1. 同步函数:因为该类的默认实例(this)就是同步监视器,所以可以在同步方法中直接调用。
    2. 同步代码块:必须使用 synchronized 括号中的对象来调用。
  • 这些方法都需要定义在同步中:因为这些方法必须要标示所属的锁。你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
  • 这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。

wait和sleep区别

分析这两个方法从执行权和锁上来分析:

wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。

sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。

wait:线程会释放执行权,而且线程会释放锁。

Sleep:线程会释放执行权,但不是不释放锁。

线程的停止【stop方法已过时】

原理:让线程运行的代码结束,也就是结束run方法。

怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。

  • 第一种方式:定义循环的结束标记。
  • 第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。

Thread‘s functions

  • interrupt():中断线程。
  • setPriority(int newPriority):更改线程的优先级。
  • getPriority():返回线程的优先级。
  • toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
  • Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
  • setDaemon(true):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
  • join:临时加入一个线程的时候可以使用join方法。当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。

同步锁LOCK

  JDK 1.5之后,JAVA提供了另外一种线程同步机制:显示定义同步锁来实现同步,解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。同步锁应该使用Lock对象充当。在面向对象中谁拥有数据谁就对外提供操作这些数据的方法 ,发现获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。

class X {	// 定义锁对象	private final ReentrantLock lock = new ReentrantLock();	// 定义需要保证线程安全的方法	public void m() {		// 加锁		lock.lock();		try {			// 需要保证线程安全的代码		} finally {			lock.unlock();		}	}}

所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。

现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装,并提供了功能一致的方法 await()、signal()、signalAll().

Condition接口:await()、signal()、signalAll();

Condition 实例实质上被绑定在一个Lock 对象上。如:

//定义锁对象private final ReentrantLock lock = new ReentrantLock() ; //指定Lock 对象对应的条件变量private final Condition condition = lock.newCondition() ; 
  • await() : 类似 wait() 方法。
  • signal() : 类似 notify() 方法。
  • signalAll() : 类似 notifyAll() 方法。