线程间的通信
一,概述。
1.什么叫做线程间通信: 在1个进程中,线程往往不是孤立存在的,线程之间需要一些协调通信,来共同完成一件任务。也就是通过一定的方法来实现线程间的“交流”。
因为wait和notify方法定义在Object类中,因此会被所有的类所继承。这些方法都是final的,即它们都是不能被重写的,不能通过子类覆写去改变它们的行为。
1.wait()方法。
语法:锁对象.wait()
特点:wait()方法的调用使得当前线程必须要等待,直到另外一个线程调用notify()或者notifyAll()方法(注意必须调用notify/notifyAll方法才能唤醒)。
wait()方法的调用必须在同步的前提下。(因为该方法是要用锁对象调用,而只有在同步的情况下才有锁)
wait()方法的调用会导致锁的释放。
线程调用wait()方法,释放它对锁的拥有权,然后等待另外的线程来通知它(通知的方式是notify()或者notifyAll()方法),这样它才能重新获得锁的拥有权和恢复执行。要确保调用wait()方法的时候拥有锁,即,wait()方法的调用必须放在synchronized方法或synchronized块中。
一个小比较:
当线程调用了wait()方法时,它会释放掉对象的锁。
另一个会导致线程暂停的方法:Thread.sleep(),它会导致线程睡眠指定的毫秒数,但线程在睡眠的过程中是不会释放掉对象的锁的
1 class Bank1 {//公共资源 2 int total=100; 3 } 4 class PresonA implements Runnable{ 5 Bank1 b; 6 //因为要确保两个用户使用的是同一家银行,所以在主函数中new一个银行,然后通过构造函数将这一银行传递给这两个用户 7 public PresonA(Bank1 b){ 8 this.b=b;//将银行的引用传给两个用户 9 } 10 @Override 11 public void run() { 12 while (true){ 13 synchronized (b){//同步代码块,因为要确保两个用户使用的是同一个锁,所以选择银行对象作为锁 14 if(b.total>=0){//打钱,当卡里钱大于0,就唤醒另一个线程来取钱 15 b.notify();//唤醒另一个线程来取钱,注意该方法是要用锁对象调用的 16 try { 17 b.wait();//接着将自己睡眠,然后把锁释放给两一个线程 18 } catch (InterruptedException e) { 19 e.printStackTrace(); 20 } 21 } 22 b.total=b.total+100;//A先存了100 23 System.out.println("A存了100,目前存款为:"+b.total); 24 } 25 } 26 } 27 } 28 class PresonB implements Runnable{ 29 Bank1 b; 30 public PresonB(Bank1 b){ 31 this.b=b; 32 } 33 @Override 34 public void run() { 35 while (true){ 36 synchronized (b){ 37 if(b.total<=0){//取钱,当卡里钱小于0,就唤醒另一个线程来存钱 38 b.notify();//唤醒另一个线程 39 try { 40 b.wait();//将自己睡眠 41 } catch (InterruptedException e) { 42 e.printStackTrace(); 43 } 44 } 45 b.total=b.total-100; 46 System.out.println("****B取了100,目前存款为:"+b.total); 47 } 48 } 49 } 50 } 51 public class CommunicationDemo { 52 public static void main(String[] args) { 53 Bank1 b=new Bank1();//作为公共资源传递给两个用户 54 new Thread(new PresonA(b)).start(); 55 new Thread(new PresonB(b)).start(); 56 } 57 }
运行结果:
【代码演示】:生产者与消费者
1 class Resource{ 2 int count=0;//商品库存数 3 } 4 class Producer implements Runnable{ 5 Resource r; 6 public Producer(Resource r){ 7 this.r=r; 8 } 9 public void run(){ 10 while (true){ 11 synchronized (r){ 12 if(r.count>0){ 13 try { 14 r.wait(); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 } 19 System.out.println("生产了一件商品,此时商品数为:"+(++r.count)); 20 r.notify(); 21 } 22 } 23 } 24 } 25 class Consumer implements Runnable{ 26 Resource r; 27 public Consumer(Resource r){ 28 this.r=r; 29 } 30 public void run(){ 31 while (true){ 32 synchronized (r){ 33 if(r.count<=0){ 34 try { 35 r.wait(); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 } 40 System.out.println("消费了一件商品,此时商品数为:"+(--r.count)); 41 r.notify(); 42 } 43 } 44 } 45 } 46 public class ProducerConsumerDemo { 47 public static void main(String[] args) { 48 Resource r=new Resource(); 49 new Thread(new Producer(r)).start(); 50 new Thread(new Consumer(r)).start(); 51 } 52 }
运行结果:
这是在只有两个线程在运行时的结果。如果有四个线程在同时运行呢?(两个生产者两个个消费者)
会发现运行结果是混乱的,为什么这样??我们先来分析一下这个运行的过程,假设是0号线程(生产者)先获得了CPU的执行权,于是它生产了一个商品,然后接着调用了notify()方法来唤醒其他线程,假设这次随机唤醒的是1号线程(生产者),虽然1号线程这时被唤醒了,但是由于没有CPU 的执行权,1号线程还在运行不了,在进行第二次while循环时,由于判断资源数是大于0的,所以调用了wait()方法,此时0号线程进行等待,并释放了CPU 的执行权,由于被唤醒的只有1号线程,所以1号线程获得了CPU的执行权开始运行,当进入while循环后,判断资源数发现是大于0的,所以1号线程也被挂起进行等代,此时只剩下2号线程和3号线程(消费者),假设2线程先获得CPU执行权,进行判断满足条件,于是就消费了一个商品,然后调用了notify()方法,因为0号线程先进入线程池,所以此时先唤醒的是0号线程,在2号线程在进行第二次while循环时,由于不满足条件,所以调用了wait()方法,此时2号线程进行等待,由于0号线程已被唤醒,所以0号线程接着被锁之前的代码接着运行,生产了一个商品,然后唤醒了1号线程,当0号线释放掉CPU后,1号线程也接着被锁之前的代码进行运算,没有进行判断就直接生产了一个商品。
由于线程在被唤醒后是接着被冻结之前的代码接着运行的,所以唤醒之后不会进行判断,就直接生产或消费了。所以就会出现问题。
解决的方法就是:就是让各个线程在被唤醒之后仍然进行条件判断,所以将if改为while,同时将notify()改为notifyAll(),否则会发生死锁情况。
定义while判断标记:让被唤醒的线程再一次判断标记。
定义notifyAll():因为需要唤醒对方线程。如果只要notify,容易出现只唤醒本方线程的情况,导致程序中的所有都等待。
改过之后运行的结果:
1、 总结notify()和notifyAll()的区别与联系
1) notify只是唤醒一个正在wait当前对象锁的线程,而notifyAll唤醒所有。值得注意的是:notify是本地方法,具体唤醒哪一个线程由虚拟机控制;如果有多个线程等待,则线程规划器任意挑选出其中一个wait()状态的线程来发出通知
2) 当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只有一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
3) 调用notify和notifyAll方法后,当前线程并不会立即放弃锁的持有权,而必须要等待当前同步代码块执行完才会让出锁
4) 优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。
锁池:获取同一把锁的线程,在为抢夺锁时会进入到锁池
等待池:调用wait操作的线程,会释放掉锁,进入到该对象的等待池中
三,线程另一种通信的方法---Condition,Lock1的具体使用。
使用notifyAll()方法是将所有被冻结的线程都唤醒,那如果只唤醒对方线程该怎么办?
在jdk1.5之后添加了java.util.concurrent.locks包,在此包中有Condition,Lock,ReadWriteLock接口供使用。与synchronized相比,lock更加灵活,使用更加广泛。
其中Lock代替了同步代码块synchronized;Condition的await(),signal(),signalAll()代替了Object中的wait(),notify(),notifyAll()方法。
1 public interface Condition{ 2 void await() throws InterruptedException;//使线程进入休眠,类似于wait() 3 void awaitUninterruptibly(); 4 long awaitNanos(long nanosTimeout) throws InterruptedException; 5 boolean await(long time,TimeUnit unit) throws InterruptedException; 6 boolean awaitUnit(Date deadline) throws InterruptedException; 7 void signal();//唤醒因await进入休眠的一个线程,类似于notify(); 8 void signalAll();//唤醒因await进入休眠的所有线程,类似于notifyAll(); 9 }
注意:1.Condition需要和lock结合使用。且lock和Condition必须是同一个ReentrantLock下的,否则会报iLLegalMonitorStateException异常。
2.作用于同一个Condition实例下的线程之间才能进行通信。
Lock lock=new ReentrantLock(); Condition condition=lock.newCondition();
【代码演示】:多个线程间的通信。 (使用signalAll方法,唤醒所有线程)
1 import java.util.concurrent.locks.Condition; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 class Resource{ 5 int count=1;//商品库存数 6 Lock lock=new ReentrantLock(); 7 Condition condition=lock.newCondition(); 8 } 9 class Producer implements Runnable{ 10 Resource r; 11 Lock lock; 12 Condition condition; 13 public Producer(Resource r){ 14 this.r=r; 15 this.lock=r.lock; 16 this.condition=r.condition; 17 } 18 public void run(){ 19 lock.lock();//加锁,如果获取失败,将进入休眠状态 20 try { 21 while(true){ 22 while (r.count>0){ 23 condition.await();//将自己阻塞 24 } 25 System.out.println(Thread.currentThread().getName()+"生产了一件商品,此时商品数为:"+(++r.count)); 26 condition.signalAll();//唤醒其他所有线程 27 } 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 }finally { 31 lock.unlock();//释放锁 32 } 33 } 34 } 35 class Consumer implements Runnable{ 36 Resource r; 37 Lock lock; 38 Condition condition; 39 public Consumer(Resource r){ 40 this.r=r; 41 this.lock=r.lock; 42 this.condition=r.condition; 43 } 44 public void run(){ 45 lock.lock();//加锁 46 try { 47 while(true){ 48 while (r.count<=0){ 49 condition.await(); 50 } 51 System.out.println(Thread.currentThread().getName()+"消费了一件商品,此时商品数为:"+(--r.count)); 52 condition.signalAll(); 53 } 54 } catch (InterruptedException e) { 55 e.printStackTrace(); 56 }finally { 57 lock.unlock();//释放锁() 58 } 59 } 60 } 61 public class ProducerConsumerDemo { 62 public static void main(String[] args) { 63 Resource r=new Resource(); 64 new Thread(new Producer(r)).start(); 65 new Thread(new Producer(r)).start(); 66 new Thread(new Consumer(r)).start(); 67 new Thread(new Consumer(r)).start(); 68 } 69 }
在Condition中,用await()替换wait(),用signal()替换notify(),用signalAll()替换notifyAll(),传统线程的通信方式,使用Condition都可以实现,这里注意,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
这样看来,Condition和传统的线程通信可能没什么区别,但Condition的强大之处在于它可以为多个线程间建立不同的Condition,用下面这个代码演示一下。
【代码演示】:多个线程间的通信。(使用signalAll()方法,只唤醒对方的线程)
1 import java.util.concurrent.locks.Condition; 2 import java.util.concurrent.locks.Lock; 3 import java.util.concurrent.locks.ReentrantLock; 4 class Resource{ 5 int count=0;//商品库存数 6 Lock lock=new ReentrantLock(); 7 Condition condition_con=lock.newCondition(); 8 Condition condition_pro=lock.newCondition(); 9 } 10 class Producer implements Runnable { 11 Resource r; 12 Lock lock; 13 Condition condition_con; 14 Condition condition_pro; 15 public Producer(Resource r) { 16 this.r = r; 17 this.lock = r.lock; 18 this.condition_con = r.condition_con; 19 this.condition_pro = r.condition_pro; 20 } 21 public void run() { 22 lock.lock();//加锁 23 try { 24 while(true){ 25 26 while (r.count > 0) { 27 condition_pro.await(); 28 } 29 System.out.println(Thread.currentThread().getName() + "生产了一件商品,此时商品数为:" + (++r.count)); 30 condition_con.signal(); 31 } 32 } catch (InterruptedException e) { 33 e.printStackTrace(); 34 } finally { 35 lock.unlock(); 36 //释放锁,因为await方法发生异常时,程序停止运行,则就会一直持有锁,而造成死锁。所以将unlock放在finally中 37 } 38 } 39 } 40 41 class Consumer implements Runnable{ 42 Resource r; 43 Lock lock; 44 Condition condition_con; 45 Condition condition_pro; 46 public Consumer(Resource r){ 47 this.r=r; 48 this.lock=r.lock; 49 this.condition_con=r.condition_con; 50 this.condition_pro=r.condition_pro; 51 } 52 public void run(){ 53 lock.lock();//加锁 54 try { 55 while (true){ 56 while (r.count<=0) { 57 condition_con.await(); 58 } 59 System.out.println(Thread.currentThread().getName()+"消费了一件商品,此时商品数为:"+(--r.count)); 60 condition_pro.signal(); 61 } 62 } catch (InterruptedException e) { 63 e.printStackTrace(); 64 }finally { 65 lock.unlock();//释放锁 66 } 67 } 68 } 69 public class ProducerConsumerDemo { 70 public static void main(String[] args) { 71 Resource r=new Resource(); 72 new Thread(new Producer(r)).start(); 73 new Thread(new Producer(r)).start(); 74 new Thread(new Consumer(r)).start(); 75 new Thread(new Consumer(r)).start(); 76 } 77 }
四,线程通信的方法--CountDownLatch方式
- CountDownLatch是在java1.5被引入的,存在java.util.concurrent包下。
- CountDownLatch能够使一个线程等待其他线程完成各自的工作之后,在执行。
- CountDownLatch是通过一个计数器来实现的,计数器的初始值设置为要等待的线程的数量。每当一个线程完成自己的任务后,计数器的值就会减一,当计数器的值到达0时,它表示所有线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
【代码演示】:教练需要等待所有运动员到齐且准备好之后,教练才能开始训练。
1 import java.util.concurrent.CountDownLatch; 2 public class CoachRacerDemo { 3 private CountDownLatch countDownLatch=new CountDownLatch(3);//设置要等待的运动员是3个 4 /** 5 * 运动员方法 6 */ 7 public void racer(){ 8 //获取运动员的名称 9 String name=Thread.currentThread().getName(); 10 //运动员开始准备:打印准备信息 11 System.out.println(name+"正在准备。。。"); 12 //准备中 13 try { 14 Thread.sleep(1000); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 //准备完毕:打印准备完毕的信息 19 System.out.println(name+"准备完毕"); 20 countDownLatch.countDown(); 21 } 22 23 /** 24 *教练方法 25 */ 26 public void coach(){ 27 //获取教练线程的名称 28 String name=Thread.currentThread().getName(); 29 //教练等待运动员准备完毕 30 System.out.println(name+"等待运动员准备"); 31 //等待中。。。。 32 try { 33 countDownLatch.await(); 34 } catch (InterruptedException e) { 35 e.printStackTrace(); 36 } 37 //运动员都准备好了,教练开始训练 38 System.out.println("所有运动员都准备完毕"+name+"开始训练"); 39 } 40 public static void main(String[] args) { 41 //创建CoachRacerDemo实例 42 CoachRacerDemo coachRacerDemo=new CoachRacerDemo(); 43 //创建三个运动员线程 44 Thread thread1=new Thread(new Runnable() { 45 @Override 46 public void run() { 47 coachRacerDemo.racer(); 48 } 49 },"运动员1"); 50 Thread thread2=new Thread(new Runnable() { 51 @Override 52 public void run() { 53 coachRacerDemo.racer(); 54 } 55 },"运动员2"); 56 Thread thread3=new Thread(new Runnable() { 57 @Override 58 public void run() { 59 coachRacerDemo.racer(); 60 } 61 },"运动员3"); 62 //创建一个教练线程 63 Thread thread4=new Thread(new Runnable() { 64 @Override 65 public void run() { 66 coachRacerDemo.coach(); 67 } 68 },"教练"); 69 thread4.start(); 70 thread1.start(); 71 thread2.start(); 72 thread3.start(); 73 74 } 75 }
运行结果:
五,线程通信的方法--CyclicBarrier方法
- CyclicBarrier是在Java1.5被引入的,存在于java.util.concurrent包下。
- CyclicBarrier实现让一组线程等待至某种状态之后在全部同时执行。
- CyclicBarrier底层是基于ReentrantLock和Condition实现的。
【代码演示】:类似于运动员比赛,当所有运动员都准备好之后,在枪声一响,所有运动员立马同时起跑。
1 import java.util.Date; 2 import java.util.concurrent.BrokenBarrierException; 3 import java.util.concurrent.CyclicBarrier; 4 5 public class ThreadRunnerDemo { 6 private CyclicBarrier cyclicBarrier=new CyclicBarrier(4);//设置线程数量 7 public void StartRun(){ 8 //获取线程的信息 9 String name=Thread.currentThread().getName(); 10 //调用CyclicBarrier的await方法开始准备 11 try { 12 cyclicBarrier.await(); 13 } catch (InterruptedException e) { 14 e.printStackTrace(); 15 } catch (BrokenBarrierException e) { 16 e.printStackTrace(); 17 } 18 System.out.println(name+"已经准备好"+new Date().getTime()); 19 } 20 public static void main(String[] args) { 21 final ThreadRunnerDemo threadRunnerDemo=new ThreadRunnerDemo(); 22 Thread thread1=new Thread(new Runnable() { 23 @Override 24 public void run() { 25 threadRunnerDemo.StartRun(); 26 } 27 },"运动员1号"); 28 Thread thread2=new Thread(new Runnable() { 29 @Override 30 public void run() { 31 threadRunnerDemo.StartRun(); 32 } 33 },"运动员2号"); 34 Thread thread3=new Thread(new Runnable() { 35 @Override 36 public void run() { 37 threadRunnerDemo.StartRun(); 38 } 39 },"运动员3号"); 40 Thread thread4=new Thread(new Runnable() { 41 @Override 42 public void run() { 43 threadRunnerDemo.StartRun(); 44 } 45 },"运动员4号"); 46 thread1.start(); 47 thread2.start(); 48 thread3.start(); 49 thread4.start(); 50 } 51 }
运行结果:
根据后面的时间,我们可以确定四个线程是同时启动的。
六,线程间通信的方法--Semaphore方法
- Semaphore是在Java1.5被引入的,存在于java.util.concurrent包下。
- Semaphore用于控制对某组资源的访问权限。
【代码演示】:8个工人使用3台机器工作,机器为互斥资源(即每次只能一个人使用)。
1 import java.util.concurrent.Semaphore; 2 public class WorkerDemo { 3 static class Work implements Runnable{ 4 private int workId=0;//工人的工号 5 private Semaphore machineNumber;//机器数量 6 public Work(int workId,Semaphore machineNumber){ 7 this.workId=workId; 8 this.machineNumber=machineNumber; 9 } 10 @Override 11 public void run() { 12 //工人要去获取机器 13 try { 14 machineNumber.acquire(); 15 } catch (InterruptedException e) { 16 e.printStackTrace(); 17 } 18 //打印当前工作的工人的信息 19 String name=Thread.currentThread().getName(); 20 System.out.println(name+"获取机器,开始工作了。。。"); 21 //工作过程 22 try { 23 Thread.sleep(1000); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 //工作完释放机器 28 machineNumber.release(); 29 System.out.println(name+"工作完了,释放机器"); 30 } 31 } 32 public static void main(String[] args) { 33 int workers=8;//工人数 34 Semaphore semaphore=new Semaphore(3);//机器数 35 for (int i = 0; i < workers; i++) { 36 new Thread(new Work(i,semaphore)).start(); 37 } 38 } 39 }
运行结果:
面试题:wait和sleep的区别?
面试题:wait和notify的异同?
相同点:
- 都Object中的方法。
- 调用必须在同步的前提下。(因为该方法是要用锁对象调用,而只有在同步的情况下才有锁,也就是notify方法调用必须放在synchronized方法或synchronized块中)。
不同点:
wait():
- wait()方法的调用使得当前线程必须要等待,直到另外一个线程调用notify()或者notifyAll()方法(注意必须调用notify/notifyAll方法才能唤醒)。
- wait()方法的调用会导致锁的释放。
notify():
- notify()方法的调用可以唤醒当前锁对象下等待的另一个单个线程。优先级较高的优先唤醒,如果优先级一样,则随机唤醒。notifyAll()方法的调用可以唤醒当前锁对象下等待的所有线程。