Demo04.java 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. package course;
  2. /**
  3. * @author WanJl
  4. * @version 1.0
  5. * @title Demo04
  6. * @description 线程安全问题
  7. * @create 2026/8/10
  8. */
  9. public class Demo04 {
  10. /*
  11. 我们知道进程和进程之间的数据和内存空间是不共享的,是相互独立的。
  12. 一个进程中的多个线程之间的数据和内存是共享的。
  13. 那么多线程所带来的最大的隐患就是:数据竞争。
  14. 当多个线程同时读写同一个共享变量的时候,程序的执行结果将会变得不可预测。有时正确,有时错误。而且错误无法复现。
  15. 我们理解线程安全的第一步,就是亲手制作一个bug:
  16. 卖票问题:
  17. 模拟3个售票窗口(3个线程)同时卖100张票
  18. 运行后看效果:
  19. 1、相同的票出现了多次
  20. 2、出现了负数的票
  21. 原因:线程执行的随机性(抢占式调度)导致的,可能在卖票的过程中丢失了CPU的执行权,导致出现问题
  22. 相同的票出现了多次:
  23. 多个线程同时执行到if (tickets>0)判断通过,这个时候tickets还是同一个值,所以都是打印出 窗口3正在出售第10张票
  24. 出现了负数的票:
  25. tickets已经减到1的时候,多个线程同时通过if判断if (tickets>0),然后一个线程执行tickets--变成0,另一个线程继续执行tickets--变成-1
  26. 程执行的随机性(抢占式调度)导致某个线程在“判断-输出-减票数”这三步的执行过程中,可能丢失CPU执行权,
  27. 由另一个线程趁机插入到对共享数据的操作,破坏了数据的完整性。
  28. 计数器问题:
  29. 看:course.Counter类
  30. 理论上应该是1000,但是实际上少于1000。
  31. 为什么结果不对?
  32. 因为count++ 并不是原子操作(原子操作就是无论是多少步,都是一次性全部成功,要么全部失败回滚)
  33. count++,表面上是一行代码,但在JVM中被拆成了3步:
  34. 1、从内存读取count到寄存器(读)
  35. 2、在寄存器中加1(改)
  36. 3、把结果写会到内存(写)
  37. 当多个线程同时执行 读-改-写 三步 的时候,就可能发生 读到了旧值,用旧值进行计算,覆盖别人写入的结果。
  38. 导致丢失最新的结果
  39. 多线程安全问题引发原因(缺一不可):
  40. 1、多线程环境,至少两条路径在并发执行。
  41. 2、存在共享数据,多个线程访问同一个变量/对象。
  42. 3、有多条语句操作共享数据,并且这些语句之间存在 读-改-写 的符合操作,比如count++,比如ticket--等
  43. 解决多线程安全性问题的基本方案思想:
  44. 让程序不在具备产生安全问题的环境:
  45. 1、把多条操作共享数据的语句锁起来。
  46. 2、让任意时刻只有一个线程能执行这段代码,其他线程只能排队等待。
  47. 线程安全的三大特性,必须背下来 多线程的核心
  48. 原子性:操作不可分割,要么全部执行,要么全部不执行。防止 读-改-写 被并发打断。
  49. |-解决方案:synchronized(线程同步)、Lock(锁)、Atomic类(原子类)
  50. 可见性:一个线程的修改对另一个线程立即可见。防止线程把变量换成在CPU缓存中,其他线程看不到修改。
  51. |-解决方案:volatile、synchronized(线程同步)、Lock(锁)
  52. 有序性:代码按照书写顺序执行,不会被指令重排。防止编译器和CPU在执行的可能会对指令进行重排。
  53. |-解决方案:volatile、synchronized(线程同步)
  54. 原可序(原子性、可见性、有序性),但凡是线程安全方案,最终都是围绕这三个特性的。
  55. 保证原子性:
  56. 最简单的最可靠的同步手段保证原子性:synchronized(线程同步)
  57. synchronized(线程同步)是Java内置的关键字,也是使用最广泛的同步手段。
  58. 每个Java对象在底层都对应一把监视器锁(Monitor Lock),当线程进入到synchronized修饰的代码块之前,必
  59. 须要先获取这个锁。如果这个锁已经被其他线程拿到了,那么当前现在就会进入到BLOCKED(阻塞)状态排队等待。
  60. 持有锁的线程会执行完代码然后自动释放锁,在等待队列中的线程会重新开始进行新一轮的竞争。
  61. synchronized优点:
  62. 解决了多线程的数据安全问题,保证了同一时刻只有一个线程能执行被保护的代码。
  63. synchronized弊端:
  64. 当线程很多的时候,每个线程都有去竞争同一把锁,比较耗资源,无形当中就会降低程序的运行效率。
  65. 所以锁的粒度应该尽量小,只锁真正操作共享数据的代码,而不是整个方法。
  66. synchronized修饰方法,该方法会变为同步方法。
  67. synchronized修饰代码块,会变成同步代码块。
  68. synchronized锁的是什么?
  69. public synchronized void method01(){
  70. //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
  71. }
  72. synchronized修饰的方法也就是同步方法。它锁的其实是当前的对象(this),锁住同一个对象的方法调用
  73. public static synchronized void method01(){
  74. //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
  75. }
  76. synchronized修饰的静态方法也就是同步静态方法,它锁的其实是当前对象所属的类的Class对象。锁住的所有这个类的静态调用,全局唯一。
  77. Object obj=new Object();
  78. synchronized(obj){
  79. //这个synchronized代码块只锁 那几行修改共享数据的代码,这样粒度就小很多。
  80. }
  81. synchronized修饰的代码块,锁的是指定的任意对象obj,更加灵活,只锁需要保护的部分。
  82. */
  83. public static void main(String[] args) {
  84. SellTicket st=new SellTicket();
  85. //创建三个线程对象,模拟3个售票窗口
  86. Thread t1=new Thread(st,"窗口1");
  87. Thread t2=new Thread(st,"窗口2");
  88. Thread t3=new Thread(st,"窗口3");
  89. t1.start();
  90. t2.start();
  91. t3.start();
  92. }
  93. public synchronized void method01(){
  94. //假如300行代码,只有3行是修改共享数据的。那么先锁的是整个方法的300代码,粒度就太大了。
  95. }
  96. public void method02(){
  97. Object obj=new Object();
  98. synchronized(obj){
  99. //这个synchronized代码块只锁 那几行修改共享数据的代码,这样粒度就小很多。
  100. }
  101. }
  102. }