线程间协作

一、线程间的协作

1、等待与通知(wait和notify/notifyAll)

  • 是本地Object方法,并且为final方法,无法被重写。

 

  • wait:一个线程因其执行目标动作所需的保护条件未满足而被暂停的过程

调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)。会释放当前的锁,然后让出CPU,进入等待状态。

obj是Java中任意一个类的实例,因执行obj.wait( )而被暂停的线程就称为对象obj上的等待线程,一个对象可以存在多个等待线程。

 

  • notify/notifyAll:一个线程更新了系统的状态,使得其他线程所需的保护条件得以满足的时候唤醒那些被暂停的线程的过程。

notify是随意唤醒一个具有不确定性,所以一般用notifyAll来唤醒所有的线程。

 

  • 步骤:

1. A线程取得锁,执行wait(),释放锁; 

2. B线程取得锁,完成业务后执行notify(),再释放锁; 

3. B线程释放锁之后,A线程取得锁,继续执行wait()之后的代码;

 

 

a.wait方法

  • wait方法必须放在临界区(sychronized)中使用:由于一个线程只有在持有一个对象的内部锁的情况下才能够调用该对象的wait方法。
  • obj.wait()暂停当前线程时释放的锁只是与该wait方法所属对象的内部锁。当前线程所持有的其它内部锁,显示锁不会被释放。
  • wait() 需要被try catch包围,中断也可以使wait等待的线程唤。
  • obj.wait()的调用总是应该放在相应对象所引导的临界区中的一个while循环语句中而不是if,原因是因为wait()的线程永远不能确定其他线程会在什么状态下notify(),所以必须在被唤醒、抢占到锁并且从wait()方法退出的时候再次进行指定条件的判断,以决定是满足条件往下执行呢还是不满足条件再次wait()呢。

sychronized(obj){ while(不成立保护条件){ obj.wait } doing.. }

 

b.notify方法

  • 调用obj.wait()方法之后,该方法并没有返回,只不过是将线程暂停了而已;当我们在后边调用obj.notify()方法之后,被唤醒的任意一个线程在其再次持有obj对应的内部锁的情况下继续执行obj.wait()方法中剩余的指令,直到wait方法返回。
  • notify本身并不会将这个内部锁释放,在临界区代码结束才释放。唤醒线程后需要能够尽快获得相应内部锁,所以尽量将notify放在临界区结束的地方。

 

c.这是一种非公平的调度方式

等待线程和通知线程是同步在同一对象之上的两种线程。

 

d.wait/notify的开销与问题

  • 过早唤醒问题

notifyAll会唤醒所有,把不需要唤醒都唤醒了,造成资源浪费。

(在程序正确的情况下使用notify解决)

  • 信号丢失问题

如果在wait执行前没有判断保护条件是否成立,那么等待线程错过了一个本来发送给它的信号,导致等待线程一直处于等待状态

(在wait方法执行的外面加上while循环解决)

  • 欺骗性唤醒问题

在没有线程执行notify和notifyAll被唤醒

(在wait方法执行的外面加上while循环解决)

  • 上下文切换 

运用wait和notify会导致上下文切换

(1在程序正确的情况下使用notify解决,2notify后尽快执行解锁)

 

e.生产者消费者

//生产者消费者实例 public class Test {     private int queueSize = 10;     private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);            public static void main(String[] args)  {         Test test = new Test();         Producer producer = test.new Producer();         Consumer consumer = test.new Consumer();                    producer.start();         consumer.start();     }            class Consumer extends Thread{                    @Override         public void run() {             consume();         }                    private void consume() {             while(true){                 synchronized (queue) {                     while(queue.size() == 0){                         try {                             System.out.println("队列空,等待数据");                             queue.wait();                         } catch (InterruptedException e) {                             e.printStackTrace();                             queue.notify();                         }                     }                     queue.poll();          //每次移走队首元素                     queue.notify();                     System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");                 }             }         }     }            class Producer extends Thread{                    @Override         public void run() {             produce();         }                    private void produce() {             while(true){                 synchronized (queue) {                     while(queue.size() == queueSize){                         try {                             System.out.println("队列满,等待有空余空间");                             queue.wait();                         } catch (InterruptedException e) {                             e.printStackTrace();                             queue.notify();                         }                     }                     queue.offer(1);        //每次插入一个元素                     queue.notify();                     System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));                 }             }         }     } }

 

f.Thread.join( )

使用wait和notify配合实现

当前线程调用,则其它线程等待当前线程执行完毕

 

 

 

2、Java条件变量:Condition(作为wait和notfiy的代替品)

 

Condition的强大之处在于它可以为多个线程间建立不同的Condition

 

a.Condition接口可作为wait/notify的替代品来实现等待和通知,它为解决过早唤醒问题提供了支持,对应方法包括await、singal和singalAll方法

 

  • Lock.newCondition( )的返回值就是一个Condition实例
  • Condition是个接口,基本的方法就是await()和signal()方法;
  • Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition() 
  • 调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用

 

b.解决过早唤醒

 

Lock的本质是AQS,AQS自己维护的队列是当前等待资源的队列,AQS会在资源被释放后,依次唤醒队列中从前到后的所有节点,使他们对应的线程恢复执行,直到队列为空。而Condition自己也维护了一个队列,该队列的作用是维护一个等待signal信号的队列。但是,两个队列的作用不同的,事实上,每个线程也仅仅会同时存在以上两个队列中的一个,流程是这样的:

1. 线程1调用reentrantLock.lock时,尝试获取锁。如果成功,则返回,从AQS的队列中移除线程;否则阻塞,保持在AQS的等待队列中。

2. 线程1调用await方法被调用时,对应操作是被加入到Condition的等待队列中,等待signal信号;同时释放锁。

3. 锁被释放后,会唤醒AQS队列中的头结点,所以线程2会获取到锁。

4. 线程2调用signal方法,这个时候Condition的等待队列中只有线程1一个节点,于是它被取出来,并被加入到AQS的等待队列中。注意,这个时候,线程1 并没有被唤醒,只是被加入AQS等待队列。

5. signal方法执行完毕,线程2调用unLock()方法,释放锁。这个时候因为AQS中只有线程1,于是,线程1被唤醒,线程1恢复执行。

所以:

发送signal信号只是将Condition队列中的线程加到AQS的等待队列中。只有到发送signal信号的线程调用reentrantLock.unlock()释放锁后,这些线程才会被唤醒。

整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作

 

c.生产者消费者

public class Test {     private int queueSize = 10;     private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);      private Lock lock = new ReentrantLock();     private Condition notFull = lock.newCondition();     private Condition notEmpty = lock.newCondition();           public static void main(String[] args)  {         Test test = new Test();         Producer producer = test.new Producer();         Consumer consumer = test.new Consumer();                    producer.start();         consumer.start();     }            class Consumer extends Thread{                 @Override         public void run() {             consume();         }                  private void consume() {             while(true){                 lock.lock();                 try {                     while(queue.size() == 0){                         try {                             System.out.println("队列空,等待数据");                             notEmpty.await();                         } catch (InterruptedException e) {                             e.printStackTrace();                         }                     }                     queue.poll();                //每次移走队首元素                     notFull.signal();                     System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");                 } finally{                     lock.unlock();                 }             }         }     }     class Producer extends Thread{                   @Override         public void run() {             produce();         }                   private void produce() {             while(true){                 lock.lock();                 try {                     while(queue.size() == queueSize){                         try {                             System.out.println("队列满,等待有空余空间");                             notFull.await();                         } catch (InterruptedException e) {                             e.printStackTrace();                         }                     }                     queue.offer(1);        //每次插入一个元素                     notEmpty.signal();                     System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));                 } finally{                     lock.unlock();                 }             }         }     } }

 

3、倒计时协调器:CountDownLatch

CountDownLatch可以迎来实现一个或者多个线程等待其他线程完成的一组特定的操作才继续运行,类似于join()。

 

    • 一个线程可能只需要等待其他线程执行的特定操作结束即可,而不必等待这些线程终止,可以使用CountDownLatch实现
    • CountDownLatch可以用来实现一个(或者多个)线程等待其他线程完成一组特定的操作之后才继续运行
    • 该协调器内部维护一个计数器,CountDownLatch.countDown()每被执行一次都会使计数器值减少1.
    • CountDownLatch.await( ),当计数器不为0时,该方法的调用将会导致执行线程被暂停,这些线程就叫做该CountDownLatch上的等待线程。
    • CountDownLatch.countDown()相当于一个通知方法,当计数器值达到0时,唤醒所有等待线程。
    • 当然对应还有CountDownLatch.await( long , TimeUnit)方法
    • CountDownLatch的使用是一次性的

 

public class Test {      public static void main(String[] args) {             final CountDownLatch latch = new CountDownLatch(2);  //将count设置2                new Thread(){              public void run() {                  try {                      System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");                     Thread.sleep(3000);                     System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");                     latch.countDown();//将count值减1,为0的时候可以继续                 } catch (InterruptedException e) {                     e.printStackTrace();                 }              };          }.start();                     new Thread(){              public void run() {                  try {                      System.out.println("子线程"+Thread.currentThread().getName()+"正在执行");                      Thread.sleep(3000);                      System.out.println("子线程"+Thread.currentThread().getName()+"执行完毕");                      latch.countDown();                 } catch (InterruptedException e) {                     e.printStackTrace();                 }              };          }.start();                     try {             System.out.println("等待2个子线程执行完毕...");             latch.await();//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行             System.out.println("2个子线程已经执行完毕");             System.out.println("继续执行主线程");         } catch (InterruptedException e) {             e.printStackTrace();         }      } }

 

1)CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:

    CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;

    而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;

    另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。

  2)Semaphore(下面5.b讲)其实和锁有点类似,它一般用于控制对某组资源的访问权限。

 

4、栅栏(CyclicBarrier)

通过它可以实现让一组线程等待至某个状态之后再全部同时执行。叫做回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。我们暂且把这个状态就叫做barrier,当调用await()方法之后,线程就处于barrier了。

  • 有时候多个线程可能需要相互等待对方执行到代码中的某个地方(集合点),这时这些线程才能够继续执行。
  • 使用CyclicBarrier实现等待的线程被称为参与方(Party)。参与方只需要执行CyclicBarrier.await()就可以实现等待
  • 该栅栏维护了一个显示锁,可以识别出最后一个参与方,当最后一个参与方调用await()方法时,前面等待的参与方都会被唤醒,并且该最后一个参与方也不会被暂停。
  • 内部实现:

维护了一个计数器变量count = 参与方的个数

调用await方法可以使得count-1;

当判断到是最后一个参与方时,调用singalAll唤醒所有线程

 

  • 应用场景:

使迭代算法并发化

在测试代码中模拟高并发

CyclicBarrier用来实现这些工作者线程中的任意一个线程在执行其操作前必须等待其他线程也准备就绪;

即实现这些工作者线程尽可能在同一时刻开始其操作

 

public class Test {     public static void main(String[] args) {         int N = 4; //第二个参数可可以为CyclicBarrier提供Runnable参数,当参数barrierAction为当这些线程都达到barrier状态时会执行的内容。         CyclicBarrier barrier  = new CyclicBarrier(N);//参数parties指让多少个线程或者任务等待至barrier状态;         for(int i=0;i<N;i++)             new Writer(barrier).start();     }      static class Writer extends Thread{         private CyclicBarrier cyclicBarrier;         public Writer(CyclicBarrier cyclicBarrier) {             this.cyclicBarrier = cyclicBarrier;         }           @Override         public void run() {             System.out.println("线程"+Thread.currentThread().getName()+"正在写入数据...");             try {                 Thread.sleep(5000);      //以睡眠来模拟写入数据操作                 System.out.println("线程"+Thread.currentThread().getName()+"写入数据完毕,等待其他线程写入完毕");                 cyclicBarrier.await(); //用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;             } catch (InterruptedException e) {                 e.printStackTrace();             }catch(BrokenBarrierException e){                 e.printStackTrace();             }             System.out.println("所有线程写入完毕,继续处理其他任务...");         }     } }

 

 

5、生产者消费者模式

a.阻塞队列

  • ArrayBlockingQueue (有界队列)

内部使用一个数组作为其存储空间,数组的存储空间是预先分配的。

优点:put和take操作不会增加GC的负担

缺点:put和take操作使用同一个锁,可能导致锁争用,导致较多的上下文切换

适合在生产者线程和消费者线程之间的并发程序较低的情况下使用

  • LinkedBlockingQueue (有界队列or无界队列)

内部存储空间是一个链表,而链表节点所需的存储空间是动态分配的

优点:put和take操作使用两个显示锁(putLock和takeLock)

缺点:增加了GC的负担

适合在生产者线程和消费者线程之间的并发程序较高的情况下使用

  • SynchronousQueue (特殊的有界队列)

生产者线程生产一个产品之后,会等待消费者线程来取走这个产品,才会接着生产下一个产品

适合在生产者线程和消费者线程之间的处理能力相差不大的情况下使用

b.流量控制与信号量(Semaphore)

因为无界队列put操作不会造成阻塞,但是无界队列元素的越来越多会导致占用资源过量。所以我们使用流量控制来避免元素过多(Semaphore)。

Semaphore可以控同时访问的线程个数,通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。

      • Semaphore用来控制同一时间内对虚拟资源的访问次数
      • Semaphore.acquire/Semaphore.release分别用于申请和释放许可
      • Semaphore.acquire()在成功获得一个许可后会立即返回
      • 如果可用配额不足,那么会使其执行线程暂停
      • Semaphore内部会维护一个等待队列用于存储这些被暂停的线程
      • Semaphore.release必须放在finally语句中,确保执行

public Semaphore(int permits) {          //参数permits表示许可数目,即同时可以允许多少线程进行访问     sync = new NonfairSync(permits); } public Semaphore(int permits, boolean fair) {    //这个多了一个参数fair表示是否是公平的,即等待时间越久的越先获取许可     sync = (fair)? new FairSync(permits) : new NonfairSync(permits); } public void acquire() throws InterruptedException {  }     //获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。 public void acquire(int permits) throws InterruptedException { }    //获取permits个许可 注意,在释放许可之前,必须先获获得许可。 public void release() { }          //释放一个许可 public void release(int permits) { }    //释放permits个许可 这4个方法都会被阻塞,如果想立即得到执行结果,可以使用下面几个方法: public boolean tryAcquire() { };    //尝试获取一个许可,若获取成功,则立即返回true,若获取失败,则立即返回false public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { };  //尝试获取一个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false public boolean tryAcquire(int permits) { }; //尝试获取permits个许可,若获取成功,则立即返回true,若获取失败,则立即返回false public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //尝试获取permits个许可,若在指定的时间内获取成功,则立即返回true,否则则立即返回false

 

//假若一个工厂有5台机器,但是有8个工人,一台机器同时只能被一个工人使用,只有使用完了,其他工人才能继续使用。那么我们就可以通过Semaphore来实现: public class Test {     public static void main(String[] args) {         int N = 8;            //工人数         Semaphore semaphore = new Semaphore(5); //机器数目         for(int i=0;i<N;i++)             new Worker(i,semaphore).start();     }           static class Worker extends Thread{         private int num;         private Semaphore semaphore;         public Worker(int num,Semaphore semaphore){             this.num = num;             this.semaphore = semaphore;         }                   @Override         public void run() {             try {                 semaphore.acquire();//获取一个许可,若无许可能够获得,则会一直等待,直到获得许可。                 System.out.println("工人"+this.num+"占用一个机器在生产...");                 Thread.sleep(2000);                 System.out.println("工人"+this.num+"释放出机器");                 semaphore.release();           //释放一个许可             } catch (InterruptedException e) {                 e.printStackTrace();             }         }     } } 工人0占用一个机器在生产... 工人1占用一个机器在生产... 工人2占用一个机器在生产... 工人4占用一个机器在生产... 工人5占用一个机器在生产... 工人0释放出机器 工人2释放出机器 工人3占用一个机器在生产... 工人7占用一个机器在生产... 工人4释放出机器 工人5释放出机器 工人1释放出机器 工人6占用一个机器在生产... 工人3释放出机器 工人7释放出机器 工人6释放出机器

 

c.双缓冲和Exchanger

设立两个缓冲区

消费者线程消费一个已填充的缓冲区时,另外一个缓冲区可以由生产者线程进行填充,实现数据的生产和消费的并发

 

 

6、线程中断机制

interrupt() : isInerrupted();//获取该线程的中断标志位

 

一个占有CPU资源的线程可以让休眠的线程调用interrupt()方法“吵醒”自己,即导致休眠的线程发生InterruptedException异常,从而结束休眠,重新排队等待CPU资源。

public class InterruptDemo { public static void main(String[] args) { //sleepThread睡眠1000ms final Thread sleepThread = new Thread() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } super.run(); } }; //busyThread一直执行死循环 Thread busyThread = new Thread() { @Override public void run() { while (true) ; } }; sleepThread.start(); busyThread.start(); sleepThread.interrupt(); busyThread.interrupt(); while (sleepThread.isInterrupted()) ; System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted()); System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); } } sleepThread isInterrupted: false busyThread isInterrupted: true

 

开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出 InterruptedException后清除标志位,而busyThread就不会清除标志位。

另外,同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,

即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。

一般在结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。

 

7、线程停止

 

当遇到某事情的时候我们需要把线程停止(比如服务或者系统关闭,错误的处理 用户取消任务 等等)

实现思路:为待停止的线程(目标线程)设置一个线程停止标记(布尔型),目标线程检测到该标志值为true时,设法让其run方法返回,实现线程的终止

 

通用的线程优雅停止办法:

发起线程更新目标线程的线程停止标记并给其发送中断,

目标线程仅在当前无待处理任务且不会产生新的待处理任务情况下才能使run方法返回

 

Web应用自身启动的工作者线程需要应用自身在Web应用停止的时候主动停止。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值