synchronized(背过这种写法、现场写法也可以)

  • 有现成的阻塞队列吗?put和take===> BlockingQueue
  • 这是一种同步容器:可以支持多线程往里装,也可以支持多线程往外拿
  • get和put加了synchronized,所以可以直接调用,不必在调用的时候再加锁了
  • 为什么要加synchronized?
    • 因为count是共享变量,需要互斥访问
    • 不加的话,会造成得到的不是最新数据,会出现错误(会出现容器中有空位却加不进来,容器中有东西却拿不出来)
  • 为什么用while而不是用if?
    • 因为生产者和消费者都不止一个
    • 一个生产者因为容器满了阻塞等待,消费者消费后唤醒生产者,这时可能被其他得生产者获得了执行得机会,这样原先得生产者还是不能执行,因此得循环判断!消费者的while循环同理。
    • 不循环的话,会造成非互斥访问共享资源,还是会出问题;所以醒了之后还是得回来判断一遍容器是不是还是满着的!!!
  • 对于复杂问题一定要解耦!!!一定要细分成小情况仔细讨论!!!这样才能看出奥秘,而不至于被其中得多种情况冲昏头脑。比如上面得生产者因为没有空位阻塞和消费者因为没有产品而阻塞是两种相反的、矛盾的情况,假如放在一起很容易把自己搞晕,会造成两个结论相互否定,从而怀疑自己,觉得自己想的不对,会陷入事实上两个是两种不同情况下得到的都是正确的结论但是又相互矛盾的情况。逻辑是正确的,但是因为是不同的情况而造成了觉得自己错了的情况!!!
  1. /**
  2. * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
  3. * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
  4. *
  5. * 使用wait和notify/notifyAll来实现
  6. *
  7. * @author mashibing
  8. */
  9. package com.mashibing.juc.c_021_01_interview;
  10. import java.util.LinkedList;
  11. import java.util.concurrent.TimeUnit;
  12. public class MyContainer1<T> {
  13. final private LinkedList<T> lists = new LinkedList<>();
  14. final private int MAX = 10; //最多10个元素
  15. private int count = 0;
  16. public synchronized void put(T t) {
  17. while(lists.size() == MAX) { //想想为什么用while而不是用if?
  18. try {
  19. // 扔满了当前线程停住
  20. this.wait(); //effective java
  21. } catch (InterruptedException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. lists.add(t);
  26. ++count;
  27. this.notifyAll(); //通知消费者线程进行消费
  28. }
  29. public synchronized T get() {
  30. T t = null;
  31. while(lists.size() == 0) {
  32. try {
  33. // 拿完了当前线程停住
  34. this.wait();
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. t = lists.removeFirst();
  40. count --;
  41. this.notifyAll(); //通知生产者进行生产
  42. return t;
  43. }
  44. public static void main(String[] args) {
  45. MyContainer1<String> c = new MyContainer1<>();
  46. //启动消费者线程
  47. for(int i=0; i<10; i++) {
  48. new Thread(()->{
  49. // 一个消费者线程get5次
  50. for(int j=0; j<5; j++) System.out.println(c.get());
  51. }, "c" + i).start();
  52. }
  53. try {
  54. TimeUnit.SECONDS.sleep(2);
  55. } catch (InterruptedException e) {
  56. e.printStackTrace();
  57. }
  58. //启动生产者线程
  59. for(int i=0; i<2; i++) {
  60. new Thread(()->{
  61. // 一个生产者线程put25次
  62. for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
  63. }, "p" + i).start();
  64. }
  65. }
  66. }

CAS

  1. put中上来先读出count,再将值放入队列,最后count++时用CAS来加
  2. CAS的期望值是5,更新值是6,只要现有值等于期望的值5,就说明这之间(存入队列的过程中)没有任何一个线程打断过他
  3. ABA问题
  4. 并且队列也是一个共享资源也是需要互斥访问的,不光要互斥访问count,也要互斥访问队列!!!


利用Condition更加细化地处理等待队列(阻塞与唤醒)

  1. 上述代码仍然有一些瑕疵:在代码中唤醒地时候用的是notifyAll,会唤醒所有等待的线程(synchronized一加任意时刻就只有一个线程在那运行,其他生产者消费者线程都在等待),所有等待线程争抢这把锁,抢到的线程仍然只有一个;wait是阻塞自己,并且释放锁,让别的线程去执行;假如生产者生产满了,会wait释放锁叫醒其他非wait线程,叫醒消费者线程是最好的(本意也是如此),但是不幸的是,他同样可能会叫醒另一个生产者线程,即另一个线程拿到了这把锁,拿到锁之后,这个生产者线程也会wait一遍;假如一直唤醒的是生产者,会一直wait,造成效率低下
  2. wait会释放锁,会让其他线程执行,而不是唤醒阻塞线程;notify是唤醒那些wait的线程
  3. 一个线程刚好填满容器,然后notifyAll,会唤醒其他线程,有可能唤醒的还是生产者线程,他会wait,其他线程会补上,假如还是生产者就不行,而是消费者时就行
  4. 是生产者wait的,是没有必要去叫醒其他生产者的,只要叫醒消费者即可
  5. 是消费者wait的,是没有必要去叫醒其他消费者的,只要叫醒生产者即可
  6. 生产者线程只负责叫醒消费者,消费者线程只负责叫醒生产者
  7. 像完成这种需求,可以用Condition+ReentrantLock(对于不同的情况有不同的阻塞队列)
  8. 这也是ReentrantLock和synchronized的最大区别:ReentrantLock可以有条件
  9. Condition能够准确地指定哪些线程被叫醒!!!(是哪些而不是哪个)
  10. lock Condition的本质是什么(consumer和producer)—->本质是这把锁
    1. 之前一把锁对应一个等待队列(阻塞队列)

image.png

  1. 用了Condition之后就变成了多个等待队列(阻塞队列)
  2. Condition的本质就是等待队列的个数!!!
    1. Condition的本质就是不同的等待队列

image.png

  1. /**
  2. * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
  3. * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
  4. *
  5. * 使用wait和notify/notifyAll来实现
  6. *
  7. * 使用Lock和Condition来实现
  8. * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒
  9. *
  10. * @author mashibing
  11. */
  12. package com.mashibing.juc.c_021_01_interview;
  13. import java.util.LinkedList;
  14. import java.util.concurrent.TimeUnit;
  15. import java.util.concurrent.locks.Condition;
  16. import java.util.concurrent.locks.Lock;
  17. import java.util.concurrent.locks.ReentrantLock;
  18. public class MyContainer2<T> {
  19. final private LinkedList<T> lists = new LinkedList<>();
  20. final private int MAX = 10; //最多10个元素
  21. private int count = 0;
  22. private Lock lock = new ReentrantLock();
  23. private Condition producer = lock.newCondition();
  24. private Condition consumer = lock.newCondition();
  25. public void put(T t) {
  26. try {
  27. lock.lock();
  28. while(lists.size() == MAX) { //想想为什么用while而不是用if?
  29. producer.await();
  30. }
  31. lists.add(t);
  32. ++count;
  33. consumer.signalAll(); //通知消费者线程进行消费
  34. } catch (InterruptedException e) {
  35. e.printStackTrace();
  36. } finally {
  37. lock.unlock();
  38. }
  39. }
  40. public T get() {
  41. T t = null;
  42. try {
  43. lock.lock();
  44. while(lists.size() == 0) {
  45. consumer.await();
  46. }
  47. t = lists.removeFirst();
  48. count --;
  49. producer.signalAll(); //通知生产者进行生产
  50. } catch (InterruptedException e) {
  51. e.printStackTrace();
  52. } finally {
  53. lock.unlock();
  54. }
  55. return t;
  56. }
  57. public static void main(String[] args) {
  58. MyContainer2<String> c = new MyContainer2<>();
  59. //启动消费者线程
  60. for(int i=0; i<10; i++) {
  61. new Thread(()->{
  62. for(int j=0; j<5; j++) System.out.println(c.get());
  63. }, "c" + i).start();
  64. }
  65. try {
  66. TimeUnit.SECONDS.sleep(2);
  67. } catch (InterruptedException e) {
  68. e.printStackTrace();
  69. }
  70. //启动生产者线程
  71. for(int i=0; i<2; i++) {
  72. new Thread(()->{
  73. for(int j=0; j<25; j++) c.put(Thread.currentThread().getName() + " " + j);
  74. }, "p" + i).start();
  75. }
  76. }
  77. }