线程安全
线程不安全
//定义静态共享变量count , t1和t2两个线程会同时操作这同一个变量 private static int count=0; public static void main(String[] args) throws InterruptedException { //创建线程1 Thread t1=new Thread(()->{ //循环5000次,每次对count执行自增操作 for(int i=0;i<50000;i++) { count++; } }); //创建线程2 Thread t2=new Thread(()->{ //循环50000次,每次对count执行自增操作 for(int i=0;i<50000;i++) { count++; } }); t1.start(); //启动线程t1,进入就绪状态,等待cpu调度 t2.start(); //启动线程t2,进入就绪状态,等待cpu调度 //join() 方法:让当前线程阻塞,等待调用join的线程执行完毕之后,主线程才继续往下执行 t1.join(); t2.join(); // t2.join();主线程等待t2执行结束 System.out.println("count: "+count); }
- 核心:count++ 不是一条原子指令
count++在 CPU 层面会拆成3 步独立操作:
(1)load:把 count 的值,从主内存读到 CPU 寄存器
(2)add:寄存器里的值 + 1
(3)save:把寄存器新值写回主内存的 count
原子:不可分割,一步执行完,中间不会被别的线程打断- join() 的作用:
main 线程阻塞等待 t1、t2 执行完毕,等两个线程全部自增完,再打印 count。
如果没有 join,main 线程会直接打印,大概率输出 0。- 怎么解决这个线程安全问题
(1)加 synchronized 同步锁或给自增方法加synchronized (锁对象) { count++; }synchronized,保证同一时刻只有一个线程执行 count++
(2)使用原子类AtomicInteger,它里面的自增是 CAS 原子操作,不需要锁private static AtomicInteger count = new AtomicInteger(0); count.getAndIncrement(); // 等价 count++- 总结
线程不安全根本原因:多个线程并发修改同一个共享变量,修改操作不是原子的,加上线程抢占调度,造成指令交错、数据覆盖。线程安全定义
多线程环境运行结果,和单线程预期结果一样,这个代码就是线程安全的
线程不安全的几个根源
线程是随机抢占式调度(前提条件)
操作系统什么时候切线程是不确定的,执行顺序有无数种可能。
代码必须保证任意调度顺序下结果都正确,不然就线程不安全。- 多个线程修改同一个共享数据(必要条件)
只有读共享变量没问题;并发写 / 读写同时发生才会出问题。- 原子性
(1)原子性:一段操作不可分割,执行中途不会被其他线程打断
(2)例子:把代码块当成房间,原子性 = 一次只能进去一个人,进去之后关上门,别人不能中途闯进来打断。锁(synchronized)就是这个房门锁,实现互斥同步。- 可见性
(1)可见性:一个线程修改共享变量,其他线程可以立刻看到最新值
(2)JMM 模型规则:
a)共享变量存在主内存
b)每个线程有自己独立的工作内存(寄存器 / 缓存)
c)线程读变量:先拷贝一份副本到自己工作内存,之后读的是副本
d)线程改变量:先改自己工作内存里的副本,不一定立刻刷回主内存
(3)现象:线程 1 修改了变量 a,但是没有及时同步回主内存;
线程 2 一直读自己本地旧副本,永远看不到最新的值,这是可见性问题。
(4)经典场景:一个线程修改 flag 标记,另一个 while 循环里读 flag,循环停不下来。
解决方案:volatile关键字,保证可见性,每次读写都和主内存同步。- 有序性:
(1)CPU/JVM 会做指令重排序,为了提高执行效率,调整代码指令执行顺序,
单线程没问题,多线程下会出现逻辑错乱
(2)volatile 可以禁止指令重排
(3)指令重排:JVM、CPU、编译器为了提升执行效率,在不改变单线程执行结果的前提下,打乱代码执行顺序
(4)多线程下重排会出问题
a)编译器只保证单线程语义不变,无法预判其他线程的行为。
b)多线程环境,指令顺序被打乱,就会出现意料之外的错误。- 总结三大特性
(1)原子性:操作不可分割,中途不能被打断 → 靠synchronized/ 锁保证
(2)可见性:一个线程修改,其他线程及时看到最新值
→volatile、synchronized 都能保证
(3)有序性:代码按照我们写的顺序执行,不被 CPU / 编译器乱排
→volatile禁止重排
(4)线程不安全 = 多线程并发修改共享变量 + 破坏原子性 / 可见性 / 有序性
- 区分两个关键字
- (1)synchronized:保证原子性 + 可见性,会加锁,阻塞其他线程
(2)volatile:保证可见性、禁止重排序,不能保证原子性解决线程不安全问题
private static int count=0; //共享变量,两个线程都会修改它 public static void main(String[] args)throws InterruptedException { Object locker=new Object(); //锁对象,t1,t2必须共用这同一个锁,才能实现互斥 // 线程t1: 循环5万次,每次对count++自增 Thread t1=new Thread(()->{ for(int i=0;i<50000;i++) { // 同步代码:以locker作为锁 synchronized(locker) { // count++ 不是原子操作,(读取,+1,写回三步) //加锁之后,这三步会整体执行,不会被其他线程打断 count++; } } }); //线程t2:同样循环5万次,自增 Thread t2=new Thread(()->{ for(int i=0;i<50000;i++) { //和t1使用同一个locker锁,同一时刻只有一个线程能进入代码块 synchronized (locker){ count++; } } }); t1.start(); t2.start(); //启动t1线程 // join():主线程等待t1执行完毕,才继续往下走 t1.join(); // join():主线程等待t1执行完毕,才继续往下走 t2.join(); //俩线程执行完后,打印100000 System.out.println("count: "+count); }- 核心原理:synchronized 同步锁
synchronized(锁对象) 是互斥锁:
(1)同一个锁对象 locker,同一时刻,只能有一个线程拿到锁,
进入大括号里面的代码执行;
(2)其他想要拿这个锁的线程,会阻塞等待,
直到持有锁的线程执行完同步代码块、释放锁;
(3)把非原子操作 count++ 包裹在同步块里面,
保证 count++ 这三步(load、add、save)一次性完整执行,
中途不会被别的线程打断 → 保证原子性;
(4)同时 synchronized 自带内存屏障:
释放锁的时候会把工作内存的数据刷回主内存,
加锁的时候从主内存读取最新值,自动保证可见性,也会禁止指令重排。- 重点:两个线程必须用同一个锁对象
如果 t1 锁 A 对象、t2 锁 B 对象,两把不同的锁,互不影响,
锁就失效了,依然线程不安全。
synchronized 监视器锁(monitor lock)
synchronized
synchronized 是 Java 内置关键字,也叫监视器锁、内置锁,锁信息保存在 Java 对象头中,底层重量级锁依托操作系统 mutex lock 实现,两大核心特性:互斥、可重入
互斥(独占)
同一个对象的锁,同一时刻只能有一个线程持有。
(1)进入synchronized修饰的代码块 / 方法:加锁
(2)退出synchronized修饰的代码块 / 方法:解锁
(3)例子:synchronized void increase(){ count++; }修饰实例方法,
锁是当前 this 对象形象理解:
每个对象自带锁状态(类似厕所:有人 / 无人)
(1)无人 → 线程占用,标记为有人
(2)有人 → 其他线程进入阻塞等待队列排队阻塞等待细节(重点面试点)
操作系统为这把锁维护等待队列:
(1)锁被占用时,其他线程获取锁失败,阻塞;
(2)线程 A 释放锁后,操作系统唤醒等待队列里的线程;
(3)synchronized 是非公平锁:不遵守先来后到
比如 A 先拿到锁,B、C 依次排队;
A 释放锁后,B 和 C 重新竞争,先排队的 B 不一定优先拿到锁可重入
同一条线程,已经持有这把锁,再次去获取同一把锁,可以成功获取,不会自己把自己卡死(不会死锁)
- 对比:不可重入锁(会自己死锁)
lock(); // 第一次加锁成功 lock(); // 第二次再加锁,锁被自己占用,阻塞等待 // 线程卡住,永远无法执行解锁,死锁- synchronized 可重入的底层原理
Monitor 内部维护两个信息:
(1)持有者 owner:记录当前哪个线程持有锁
(2)计数器 count:记录重入次数
1)同一个线程再次获取锁:计数器 + 1
2)退出同步块:计数器 - 1
3)只有计数器减到0,锁才算真正释放,其他线程才能抢锁- 面试
(1)锁在哪:对象头里的 monitor 监视器,不是代码;
不同对象是不同的锁,互不干扰
(2)互斥:保证原子性,同一对象同时只能一个线程执行同步代码
(3)可重入:依靠 owner + 计数器实现,支持嵌套同步
(4)公平性:非公平锁,释放锁后线程竞争,不保证排队顺序
(5)类型:JVM 层面隐式锁,自动加锁、自动释放,不用手动 unlock- 例子:嵌套 synchronized,合法不会死锁
for (int i = 0; i < 50000; i++) { synchronized (locker) { synchronized (locker) { count++; } } }
synchronized 的 4 种写法
核心:synchronized 锁的是【对象】,不是代码 / 方法;
只有争抢同一个锁对象,才会互斥阻塞
修饰代码块
锁任意对象(自定义锁对象)
public class SynchronizedDemo { private Object locker = new Object(); public void method() { synchronized (locker) { // 同步代码 } } }锁对象:locker 这个 Object 实例
- 锁当前实例对象 this
public class SynchronizedDemo { public void method() { synchronized (this) { // 同步代码 } } }锁对象:当前实例 this,等价于同步普通成员方法
修饰普通成员方法(实例方法)
public class SynchronizedDemo { public synchronized void method() { // 同步代码 } }(1)等价写法:
synchronized(this)
(2)锁对象:调用这个方法的实例对象(this)
(3)不同实例,锁互不干扰;同一个实例,多个synchronized普通方法互斥修饰 static 静态方法
public class SynchronizedDemo { public synchronized static void method() { // 同步代码 } }(1)等价写法:synchronized(SynchronizedDemo.class)
(2)锁对象:类对象(Class 对象),这个类全局唯一
(3)静态锁 和 实例锁(this)是两把完全不同的锁,不会互斥面试高频坑点
两个线程竞争同一把锁对象,才会阻塞等待;拿两把不同的锁,互不影响,并行执行
- 容易错的例子:
(1)线程 A 调用同一个对象的synchronized普通方法,
线程 B 调用同一个对象另一个synchronized普通方法 → 抢 this 锁,互斥
(2)线程 A 调用对象 A 的同步方法,线程 B 调用对象 B 的同步方法
→ 两个 this,两把锁,不互斥
(3)线程 A 调用静态同步方法(锁 class),线程 B 调用同一个实例的
普通同步方法(锁 this)→ class 锁 vs this 锁,两把锁,不互斥
Java 标准库 线程安全 、线程不安全类
线程不安全类
这些类在多线程并发修改共享对象的时候,会出现数据错乱、并发异常:
(1)ArrayList
(2)LinkedList
(3)HashMap
(4)TreeMap
(5)HashSet
(6)TreeSet
(7)StringBuilder- 例:StringBuilder 方法没有加锁,多线程同时 append,很容易数据丢失、结果错乱
线程安全类
依靠 synchronized 加锁实现安全(有锁保护,并发性能一般)
(1)Vector:数组集合,所有方法synchronized,不推荐使用
(2)HashTable:哈希表,方法synchronized,不推荐使用
(3)StringBuffer:字符串拼接,核心 append 等方法都带synchronized@Override public synchronized StringBuffer append(Object obj) { toStringCache = null; super.append(String.valueOf(obj)); return this; }对比:StringBuilder 无锁,不安全;StringBuffer 加 synchronized,安全
- 不可变类:不加锁,天然线程安全(只读,不能修改)
(1)String
a)String 一旦创建,内容不能修改;
所有看似修改的操作(substring、+ 拼接)都是生成新对象,
原对象不变,没有写竞争,天然线程安全。- 总结区分
(1)StringBuilder(不安全,无锁) vsStringBuffer(安全,synchronized 锁)
(2)HashMap(不安全) vsHashTable(安全,过时) vsConcurrentHashMap(安全,推荐)
(3)ArrayList(不安全) vsVector(安全,过时)
(4)String:不可变,天生线程安全,不靠锁- CopyOnWriteArrayList、CopyOnWriteArraySet:写时复制,适合读多写少场景