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