同步锁的等待唤醒机制及线程交替


同步锁的等待唤醒机制及线程交替


日常我们会经常使用同步锁,原先使用synchronized我们了解等待唤醒机制,那么同步锁中该怎么实现。

等待唤醒机制

先看synchronized原先怎么处理。

测试代码

/*
 * 生产者和消费者案例
 */

public class TestProductorAndConsumerFake {

 public static void main(String[] args) {
  ClerkFake clerk = new ClerkFake();
  
  ProductorFake pro = new ProductorFake(clerk);
  ConsumerFake cus = new ConsumerFake(clerk);
  
  new Thread(pro, "生产者 A").start();
  new Thread(cus, "消费者 B").start();
//
//  new Thread(pro, "生产者 C").start();
//  new Thread(cus, "消费者 D").start();
 }
 
}

//店员
class ClerkFake{
 private int product = 0;
 
 //进货
 public synchronized void get(){//循环次数:0
  if(product >= 10){//触发虚假唤醒问题
   System.out.println("产品已满!");
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
  }else {
   System.out.println(Thread.currentThread().getName() + " : " + ++product);
   this.notifyAll();
  }
 }
 
 //卖货
 public synchronized void sale(){//product = 0; 循环次数:0
  if(product <= 0){
   System.out.println("缺货!");
   
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
  }else {

   System.out.println(Thread.currentThread().getName() + " : " + --product);
   this.notifyAll();
  }
 }
}

//生产者
class ProductorFake implements Runnable{
 private ClerkFake clerk;

 public ProductorFake(ClerkFake clerk) {
  this.clerk = clerk;
 }

 @Override
 public void run() {
  for (int i = 0; i < 20; i++) {
   try {
    Thread.sleep(200);
   } catch (InterruptedException e) {
   }
   
   clerk.get();
  }
 }
}

//消费者
class ConsumerFake implements Runnable{
 private ClerkFake clerk;

 public ConsumerFake(ClerkFake clerk) {
  this.clerk = clerk;
 }

 @Override
 public void run() {
  for (int i = 0; i < 20; i++) {
   clerk.sale();
  }
 }
}

定义店员,用于执行消费和生产,而且生产和消费是同一个店员。

定义消费者,循环消费产品;定义生产者,循环生产产品。

**等待唤醒机制的目的:**当生产跟不上消费时,避免浪费资源的线程处理消费操作。

this.wait(); //执行等待

this.notifyAll();//唤醒等待线程

问题一:此代码存在无法唤醒的情况

当生产者每次生产间隔200毫秒,存在同时触发消费线程等待和生产线程等待的情况,导致貌似唤醒完成其实进程处于长期等待状态,无法被唤醒

解决:

将唤醒执行与等待线程剥离开来,即与等待条件剥离,保证等待线程被唤醒时可继续执行唤醒操作,保证卖货和买货两个唤醒操作执行可执行一个

//进货
 public synchronized void get(){//循环次数:0
  if(product >= 1){
   System.out.println("产品已满!");
   
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
   
  }
  
  System.out.println(Thread.currentThread().getName() + " : " + ++product);
  this.notifyAll();
 }
 
 //卖货
 public synchronized void sale(){//product = 0; 循环次数:0
  if(product <= 0){
   System.out.println("缺货!");
   
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
  }
  
  System.out.println(Thread.currentThread().getName() + " : " + --product);
  this.notifyAll();
 }

问题二:存在虚假唤醒的情况

目前是一个生产者和一个消费者,不存并发的情况,如果是两个生产者和两个消费者,就会存在出现负数的情况

 public static void main(String[] args) {
  ClerkIn clerk = new ClerkIn();
  
  ProductorIn pro = new ProductorIn(clerk);
  ConsumerIn cus = new ConsumerIn(clerk);
  
  new Thread(pro, "生产者 A").start();
  new Thread(cus, "消费者 B").start();
//
  new Thread(pro, "生产者 C").start();
  new Thread(cus, "消费者 D").start();
 }

输出:

生产者 A : -15
生产者 C : -14
生产者 A : -13
生产者 C : -12
生产者 A : -11
生产者 C : -10
生产者 A : -9
生产者 C : -8
生产者 A : -7
生产者 C : -6
生产者 A : -5
生产者 C : -4
生产者 A : -3
生产者 C : -2
生产者 A : -1
生产者 C : 0

原因很简单,就是因为消费线程可能同时被唤醒,从而导致同时被–的情况,出现负数这样错误的结果。

查看jdk源码:

     //Object.Java
     * As in the one argument version, interrupts and spurious wakeups are
     * possible, and this method should always be used in a loop://一些版本中,可能出现虚假唤醒的可能,这个方法应该总被使用在循环中while
     * <pre>
     *     synchronized (obj) {
     *         while (&lt;condition does not hold&gt;)
     *             obj.wait();
     *         ... // Perform action appropriate to condition
     *     }
     * </pre>
    public final void wait() throws InterruptedException {
        wait(0);
    }

解决:

源码说的很清楚,需要将if改成while

//进货
 public synchronized void get(){//循环次数:0
  while(product >= 1){//为了避免虚假唤醒问题,应该总是使用在循环中
   System.out.println("产品已满!");
   
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
   
  }
  
  System.out.println(Thread.currentThread().getName() + " : " + ++product);
  this.notifyAll();
 }
 
 //卖货
 public synchronized void sale(){//product = 0; 循环次数:0
  while(product <= 0){
   System.out.println("缺货!");
   
   try {
    this.wait();
   } catch (InterruptedException e) {
   }
  }
  
  System.out.println(Thread.currentThread().getName() + " : " + --product);
  this.notifyAll();
 }

言归正传,同步锁的唤醒机制其实理解了原先synchronized的方式后,也很容易理解,因为同步锁中是使用condition对象来实现唤醒机制的,而对应Object的await、notify和notifyAll方法,condition中为await、signal和signalAll,在此就不多说了,详见代码


/*
 * 生产者消费者案例:
 */

public class TestProductorAndConsumerForLock {

 public static void main(String[] args) {
  Clerk clerk = new Clerk();

  Productor pro = new Productor(clerk);
  Consumer con = new Consumer(clerk);

  new Thread(pro, "生产者 A").start();
  new Thread(con, "消费者 B").start();

//   new Thread(pro, "生产者 C").start();
//   new Thread(con, "消费者 D").start();
 }

}

class Clerk {
 private int product = 0;

 private Lock lock = new ReentrantLock();
 private Condition condition = lock.newCondition();

 // 进货
 public void get() {
  lock.lock();

  try {
   if (product >= 1) { // 为了避免虚假唤醒,应该总是使用在循环中。
    System.out.println("产品已满!");

    try {
     condition.await();
    } catch (InterruptedException e) {
    }

   }
   System.out.println(Thread.currentThread().getName() + " : "
     + ++product);

   condition.signalAll();
  } finally {
   lock.unlock();
  }

 }

 // 卖货
 public void sale() {
  lock.lock();

  try {
   if (product <= 0) {
    System.out.println("缺货!");

    try {
     condition.await();
    } catch (InterruptedException e) {
    }
   }

   System.out.println(Thread.currentThread().getName() + " : "
     + --product);

   condition.signalAll();

  } finally {
   lock.unlock();
  }
 }
}

// 生产者
class Productor implements Runnable {

 private Clerk clerk;

 public Productor(Clerk clerk) {
  this.clerk = clerk;
 }

 @Override
 public void run() {
  for (int i = 0; i < 20; i++) {
   try {
    Thread.sleep(200);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }

   clerk.get();
  }
 }
}

// 消费者
class Consumer implements Runnable {

 private Clerk clerk;

 public Consumer(Clerk clerk) {
  this.clerk = clerk;
 }

 @Override
 public void run() {
  for (int i = 0; i < 20; i++) {
   clerk.sale();
  }
 }

}

线程交替

线程交替这里我们是以一个面试题为例去做的

编写一个程序,开启 3 个线程,这三个线程的 ID 分别为 A、B、C,每个线程将自己的 ID 在屏幕上打印 10 遍,要求输出的结果必须按顺序显示。

  • 如:ABCABCABC……

我们实现原理也是利用同步锁的等待和唤醒机制去实现,详细查看如下代码

public class TestABCAlternate {
 
 public static void main(String[] args) {
  AlternateDemo ad = new AlternateDemo();
  
  new Thread(new Runnable() {
   @Override
   public void run() {
    
    for (int i = 1; i <= 20; i++) {
     ad.loopA(i);
    }
    
   }
  }, "A").start();
  
  new Thread(new Runnable() {
   @Override
   public void run() {
    
    for (int i = 1; i <= 20; i++) {
     ad.loopB(i);
    }
    
   }
  }, "B").start();
  
  new Thread(new Runnable() {
   @Override
   public void run() {
    
    for (int i = 1; i <= 20; i++) {
     ad.loopC(i);
     
     System.out.println("-----------------------------------");
    }
    
   }
  }, "C").start();
 }

}

class AlternateDemo{
 
 private int number = 1//当前正在执行线程的标记
 
 private Lock lock = new ReentrantLock();
 private Condition condition1 = lock.newCondition();
 private Condition condition2 = lock.newCondition();
 private Condition condition3 = lock.newCondition();
 
 /**
  * @param totalLoop : 循环第几轮
  */

 public void loopA(int totalLoop){
  lock.lock();
  
  try {
   //1. 判断
   if(number != 1){
    condition1.await();
   }
   
   //2. 打印
   for (int i = 1; i <= 1; i++) {
    System.out.println(Thread.currentThread().getName() + "t" + i + "t" + totalLoop);
   }
   
   //3. 唤醒
   number = 2;
   condition2.signal();
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
 }
 
 public void loopB(int totalLoop){
  lock.lock();
  
  try {
   //1. 判断
   if(number != 2){
    condition2.await();
   }
   
   //2. 打印
   for (int i = 1; i <= 1; i++) {
    System.out.println(Thread.currentThread().getName() + "t" + i + "t" + totalLoop);
   }
   
   //3. 唤醒
   number = 3;
   condition3.signal();
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
 }
 
 public void loopC(int totalLoop){
  lock.lock();
  
  try {
   //1. 判断
   if(number != 3){
    condition3.await();
   }
   
   //2. 打印
   for (int i = 1; i <= 1; i++) {
    System.out.println(Thread.currentThread().getName() + "t" + i + "t" + totalLoop);
   }
   
   //3. 唤醒
   number = 1;
   condition1.signal();
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   lock.unlock();
  }
 }
 
}

输出:

A 1 1
B 1 1
C 1 1
-----------------------------------
A 1 2
B 1 2
C 1 2
-----------------------------------
A 1 3
B 1 3
C 1 3
-----------------------------------
A 1 4
B 1 4
C 1 4
-----------------------------------
A 1 5
B 1 5
C 1 5
-----------------------------------
A 1 6
B 1 6
C 1 6
-----------------------------------
A 1 7
B 1 7
C 1 7
-----------------------------------
A 1 8
B 1 8
C 1 8
-----------------------------------
A 1 9
B 1 9
C 1 9
-----------------------------------
A 1 10
B 1 10
C 1 10
-----------------------------------
A 1 11
B 1 11
C 1 11
-----------------------------------
A 1 12
B 1 12
C 1 12
-----------------------------------
A 1 13
B 1 13
C 1 13
-----------------------------------
A 1 14
B 1 14
C 1 14
-----------------------------------
A 1 15
B 1 15
C 1 15
-----------------------------------
A 1 16
B 1 16
C 1 16
-----------------------------------
A 1 17
B 1 17
C 1 17
-----------------------------------
A 1 18
B 1 18
C 1 18
-----------------------------------
A 1 19
B 1 19
C 1 19
-----------------------------------
A 1 20
B 1 20
C 1 20
-----------------------------------


原文始发于微信公众号(云户):同步锁的等待唤醒机制及线程交替

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/25938.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!