volatile

  1. 两个线程之间互相通信的问题
  2. 1A2B3C4D的问题(公开课)—->之后会讲

image.png

  1. 第一个线程完成到某个位置的时候,要通知某个线程
  2. 一边加add一边检测

第一种写法

  1. /**
  2. * 曾经的面试题:(淘宝?)
  3. * 实现一个容器,提供两个方法,add,size
  4. * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
  5. *
  6. * 分析下面这个程序,能完成这个功能吗?
  7. * @author mashibing
  8. */
  9. package com.mashibing.juc.c_020_01_Interview;
  10. import java.util.ArrayList;
  11. import java.util.List;
  12. import java.util.concurrent.TimeUnit;
  13. public class T01_WithoutVolatile {
  14. List lists = new ArrayList();
  15. public void add(Object o) {
  16. lists.add(o);
  17. }
  18. public int size() {
  19. return lists.size();
  20. }
  21. public static void main(String[] args) {
  22. T01_WithoutVolatile c = new T01_WithoutVolatile();
  23. new Thread(() -> {
  24. for(int i=0; i<10; i++) {
  25. c.add(new Object());
  26. System.out.println("add " + i);
  27. try {
  28. TimeUnit.SECONDS.sleep(1);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }, "t1").start();
  34. new Thread(() -> {
  35. while(true) {
  36. if(c.size() == 5) {
  37. break;
  38. }
  39. }
  40. System.out.println("t2 结束");
  41. }, "t2").start();
  42. }
  43. }

第二种写法

  1. 第一种写法不能满足需求—->不行、有问题(同步的角度)
    1. ArrayList不是线程安全的—->要用同步容器(加元素和size的更新必须得是同步的,参照c问题—->d也能够实现?)
    2. 要保证线程之间的可见性
    3. 第二个线程读的时候,可能读到的size是之后的,因为是先加进去再更新size,有可能在这之间被打断
    4. 没加同步—->要在add和size方法上加synchronized?不对???
    5. 第二个线程没有检测到size==5,因为线程之间不可见的原因—->第一个线程变了的话,第二个线程不能马上看见—->加volatile?……
  2. 既然是线程不安全的容器,那么为什么不会出现ConcurrentModificationException的异常???
  3. 什么时候会出现ConcurrentModificationException,foreach中只能遍历访问元素,不能修改元素???
  4. 同时修改、读取会出现ConcurrentModificationException?modcount在读取的时候会++?
  5. 用同步容器Collections.synchronizedList(new LinkedList<>())
  6. 只加volatile也能解决问题,但是具有偶然性,因为睡了一段时间正好错开了
  7. 更重要的是:volatile这里修饰的是一个列表的引用,照理说只能在引用改变时保持同步,而不能在引用所指向的对象发生发生改变时保持刷新
  8. ❓volatile修饰的引用指向的对象中的值发生了改变是观察不到的
  1. /**
  2. * 曾经的面试题:(淘宝?)
  3. * 实现一个容器,提供两个方法,add,size
  4. * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
  5. *
  6. * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,
  7. * 而且,如果在if 和 break之间被别的线程打断,得到的结果也不精确,
  8. * 该怎么做呢?
  9. * @author mashibing
  10. */
  11. package com.mashibing.juc.c_020_01_Interview;
  12. import java.util.Collections;
  13. import java.util.LinkedList;
  14. import java.util.List;
  15. import java.util.concurrent.TimeUnit;
  16. public class T02_WithVolatile {
  17. //添加volatile,使t2能够得到通知
  18. // 加锁也是不行的,因为自己释放锁之后自己又会获得锁!
  19. // 线程缓存的可见性与sleep的关系!!!
  20. //volatile List lists = new LinkedList();
  21. volatile List lists = Collections.synchronizedList(new LinkedList<>());
  22. public void add(Object o) {
  23. lists.add(o);
  24. }
  25. public int size() {
  26. return lists.size();
  27. }
  28. public static void main(String[] args) {
  29. T02_WithVolatile c = new T02_WithVolatile();
  30. new Thread(() -> {
  31. for(int i=0; i<10; i++) {
  32. c.add(new Object());
  33. System.out.println("add " + i);
  34. /*try {
  35. TimeUnit.SECONDS.sleep(1);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }*/
  39. }
  40. }, "t1").start();
  41. new Thread(() -> {
  42. while(true) {
  43. if(c.size() == 5) {
  44. break;
  45. }
  46. }
  47. System.out.println("t2 结束");
  48. }, "t2").start();
  49. }
  50. }

wait、notify解决上述面试题

  1. notify是不释放锁的,假如用的是一把锁,是不行的
  2. 释放锁的方式:
    1. wait释放锁!
    2. synchronized代码块执行完毕释放锁!

下面这种方式仍然无法解决

  1. /**
  2. * 曾经的面试题:(淘宝?)
  3. * 实现一个容器,提供两个方法,add,size
  4. * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
  5. *
  6. * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
  7. *
  8. * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
  9. * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
  10. *
  11. * 阅读下面的程序,并分析输出结果
  12. * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
  13. * 想想这是为什么?
  14. * @author mashibing
  15. */
  16. package com.mashibing.juc.c_020_01_Interview;
  17. import java.util.ArrayList;
  18. import java.util.List;
  19. import java.util.concurrent.TimeUnit;
  20. public class T03_NotifyHoldingLock { //wait notify
  21. //添加volatile,使t2能够得到通知
  22. volatile List lists = new ArrayList();
  23. public void add(Object o) {
  24. lists.add(o);
  25. }
  26. public int size() {
  27. return lists.size();
  28. }
  29. public static void main(String[] args) {
  30. T03_NotifyHoldingLock c = new T03_NotifyHoldingLock();
  31. final Object lock = new Object();
  32. new Thread(() -> {
  33. synchronized(lock) {
  34. System.out.println("t2启动");
  35. if(c.size() != 5) {
  36. try {
  37. lock.wait();
  38. } catch (InterruptedException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. System.out.println("t2 结束");
  43. }
  44. }, "t2").start();
  45. try {
  46. TimeUnit.SECONDS.sleep(1);
  47. } catch (InterruptedException e1) {
  48. e1.printStackTrace();
  49. }
  50. new Thread(() -> {
  51. System.out.println("t1启动");
  52. synchronized(lock) {
  53. for(int i=0; i<10; i++) {
  54. c.add(new Object());
  55. System.out.println("add " + i);
  56. if(c.size() == 5) {
  57. lock.notify();
  58. }
  59. try {
  60. TimeUnit.SECONDS.sleep(1);
  61. } catch (InterruptedException e) {
  62. e.printStackTrace();
  63. }
  64. }
  65. }
  66. }, "t1").start();
  67. }
  68. }

正确解决方式

  1. notify的线程t1进行wait让出这把锁,才能让另一个线程继续
  2. t2中也要加个notify
  1. /**
  2. * 曾经的面试题:(淘宝?)
  3. * 实现一个容器,提供两个方法,add,size
  4. * 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,当个数到5个时,线程2给出提示并结束
  5. *
  6. * 给lists添加volatile之后,t2能够接到通知,但是,t2线程的死循环很浪费cpu,如果不用死循环,该怎么做呢?
  7. *
  8. * 这里使用wait和notify做到,wait会释放锁,而notify不会释放锁
  9. * 需要注意的是,运用这种方法,必须要保证t2先执行,也就是首先让t2监听才可以
  10. *
  11. * 阅读下面的程序,并分析输出结果
  12. * 可以读到输出结果并不是size=5时t2退出,而是t1结束时t2才接收到通知而退出
  13. * 想想这是为什么?
  14. *
  15. * notify之后,t1必须释放锁,t2退出后,也必须notify,通知t1继续执行
  16. * 整个通信过程比较繁琐
  17. * @author mashibing
  18. */
  19. package com.mashibing.juc.c_020_01_Interview;
  20. import java.util.ArrayList;
  21. import java.util.List;
  22. import java.util.concurrent.TimeUnit;
  23. public class T04_NotifyFreeLock {
  24. //添加volatile,使t2能够得到通知
  25. volatile List lists = new ArrayList();
  26. public void add(Object o) {
  27. lists.add(o);
  28. }
  29. public int size() {
  30. return lists.size();
  31. }
  32. public static void main(String[] args) {
  33. T04_NotifyFreeLock c = new T04_NotifyFreeLock();
  34. final Object lock = new Object();
  35. new Thread(() -> {
  36. synchronized(lock) {
  37. System.out.println("t2启动");
  38. if(c.size() != 5) {
  39. try {
  40. lock.wait();
  41. } catch (InterruptedException e) {
  42. e.printStackTrace();
  43. }
  44. }
  45. System.out.println("t2 结束");
  46. //通知t1继续执行
  47. lock.notify();
  48. }
  49. }, "t2").start();
  50. try {
  51. TimeUnit.SECONDS.sleep(1);
  52. } catch (InterruptedException e1) {
  53. e1.printStackTrace();
  54. }
  55. new Thread(() -> {
  56. System.out.println("t1启动");
  57. synchronized(lock) {
  58. for(int i=0; i<10; i++) {
  59. c.add(new Object());
  60. System.out.println("add " + i);
  61. if(c.size() == 5) {
  62. lock.notify();
  63. //释放锁,让t2得以执行
  64. try {
  65. lock.wait();
  66. } catch (InterruptedException e) {
  67. e.printStackTrace();
  68. }
  69. }
  70. try {
  71. TimeUnit.SECONDS.sleep(1);
  72. } catch (InterruptedException e) {
  73. e.printStackTrace();
  74. }
  75. }
  76. }
  77. }, "t1").start();
  78. }
  79. }

总结

  1. volatile在没有把握时就不要用,除了面试的时候
  2. volatile尽量不要使用
  3. volatile最好修饰简单的,越简单越好,不要去修饰引用值
  4. wait和notify不一定所有情况下都需要成对出现—->根据自己的需求自己设置合适的同步策略即可!
  5. 好多线程阻塞用notifyAll,只有一个线程阻塞用notify
  6. notify是不释放锁的。notify唤醒其他线程之后,这里的其他线程还是需要拿到锁之后才能继续向下执行的。
  7. wait释放锁之后,还需要拿到锁才能继续往下执行
  8. 加锁后还出现问题的原因:释放锁后,因为自己执行的太快了,**自己又再次获得了锁!这让t2得不到恰当的判断时机**
  9. 这题的正确解法是用wait和notify来做,是一个线程之间的互斥同步问题,而不是去想直接用synchronized或者用volatile
  10. 考察的是线程的同步,而不是可见性问题(线程缓存sleep?)
  11. 考察的是线程之间的同步,即一个线程必须在另一个线程执行到什么步骤时输出,然后另一个线程才能继续执行
  12. sleep的作用是留时间给t2唤醒并输出(因为线程执行的太快了—->会造成即使能够正确可见,依旧会因为t1继续快速向下执行而造成t2输出的位置不对!!!)
  13. 尽量不要用volatile!!!
  14. 尽量不要用volatile!!!
  15. 尽量不要用volatile!!!