多线程安全问题原理和 4 种解决办法
本文分享自华为云社区《多线程安全问题原理和解决办法Synchronized和ReentrantLock使用与区别》,作者:共饮一杯无。
线程安全问题概述
卖票问题分析
单窗口卖票
一个窗口(单线程)卖 100 张票没有问题
单线程程序是不会出现线程安全问题的
多个窗口卖不同的票
3 个窗口一起卖票,卖的票不同,也不会出现问题
多线程程序,没有访问共享数据,不会产生问题
多个窗口卖相同的票
3 个窗口卖的票是一样的,就会出现安全问题
多线程访问了共享的数据,会产生线程安全问题
线程安全问题代码实现
模拟卖票案例
创建 3 个线程,同时开启,对共享的票进行出售
线程安全问题原理分析
线程安全问题产生原理图
线程安全问题产生原理图
分析:线程安全问题正常是不允许产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了 cpu 的执行权;让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票。
解决线程安全问题办法 1-synchronized 同步代码块
同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
使用 synchronized 同步代码块格式:
synchronized(锁对象){可能会出现线程安全问题的代码(访问了共享数据的代码)}
代码实现如下:
注意:
代码块中的锁对象,可以使用任意的对象。
但是必须保证多个线程使用的锁对象是同一个。
锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。
同步技术原理分析
同步技术原理:
使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器
3 个线程一起抢夺 cpu 的执行权,谁抢到了谁执行 run 方法进行卖票。
t0 抢到了 cpu 的执行权,执行 run 方法,遇到 synchronized 代码块这时 t0 会检查 synchronized 代码块是否有锁对象
发现有,就会获取到锁对象,进入到同步中执行
t1 抢到了 cpu 的执行权,执行 run 方法,遇到 synchronized 代码块这时 t1 会检查 synchronized 代码块是否有锁对象
发现没有,t1 就会进入到阻塞状态,会一直等待 t0 线程归还锁对象,t0 线程执行完同步中的代码,会把锁对象归 还给同步代码块 t1 才能获取到锁对象进入到同步中执行
总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。
解决线程安全问题办法 2-synchronized 普通同步方法
同步方法:使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void payTicket(){可能会出现线程安全问题的代码(访问了共享数据的代码)}
代码实现:
分析:定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。
同步方法的锁对象是谁?
就是实现类对象 new RunnableImpl(),也是就是 this,所以同步方法是锁定的 this 对象。
解决线程安全问题办法 3-synchronized 静态同步方法
同步方法:使用 synchronized 修饰的方法,就叫做同步方法,保证 A 线程执行该方法的时候,其他线程只能在方法外等着。对于 static 方法,我们使用当前方法所在类的字节码对象(类名.class)。
格式:
public static synchronized void payTicket(){可能会出现线程安全问题的代码(访问了共享数据的代码)}
代码实现:
分析:静态的同步方法锁对象是谁?
不能是 this,this 是创建对象之后产生的,静态方法优先于对象
静态方法的锁对象是本类的 class 属性–>class 文件对象(反射)。
解决线程安全问题办法 4-Lock 锁
Lock 接口中的方法:
public void lock() :加同步锁。
public void unlock() :释放同步锁
使用步骤:
在成员位置创建一个 ReentrantLock 对象
在可能会出现安全问题的代码前调用 Lock 接口中的方法 lock 获取锁
在可能会出现安全问题的代码后调用 Lock 接口中的方法 unlock 释放锁
代码实现:
分析:java.util.concurrent.locks.Lock 接口
Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。相比 Synchronized,ReentrantLock 类提供了一些高级功能,主要有以下 3 项:
等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized 来说可以避免出现死锁的情况。通过 lock.lockInterruptibly()来实现这个机制。
公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized 锁非公平锁,ReentrantLock 默认的构造函数是创建的非公平锁,可以通过参数 true 设为公平锁,但公平锁表现的性能不是很好。
公平锁、非公平锁的创建方式:
锁绑定多个条件,一个 ReentrantLock 对象可以同时绑定多个对象。ReenTrantLock 提供了一个 Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像 synchronized 要么随机唤醒一个线程要么唤醒全部线程。
ReentrantLock 和 Synchronized 的区别
相同点:
它们都是加锁方式同步;
都是重入锁;
阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善); ** **
| 不同点 | SynChronized | ReentrantLock(实现了 Lock 接口) |
| — | — | — |
| 原始构成 | 它是 java 语言的关键字,是原生语法层面的互斥,需要 jvm 实现 | 它是 JDK 1.5 之后提供的 API 层面的互斥锁类 |
| 实现 | 通过 JVM 加锁解锁 | api 层面的加锁解锁,需要手动释放锁。 |
| 代码编写 | 采用 synchronized 不需要用户去手动释放锁,当 synchronized 方法或者 synchronized 代码块执行完之后,系统会自动让线程释放对锁的占用,更安全, | 而 ReentrantLock 则必须要用户去手动释放锁,如果没有主动释放锁,就有可能导致出现死锁现象。需要 lock()和 unlock()方法配合 try/finally 语句块来完成, |
| 灵活性 | 锁的范围是整个方法或 synchronized 块部分 | Lock 因为是方法调用,可以跨方法,灵活性更大 |
| 等待可中断 | 不可中断,除非抛出异常(释放锁方式:
1.代码执行完,正常释放锁;
2.抛出异常,由 JVM 退出等待) | 持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,(方法:
1.设置超时方法 tryLock(long timeout, TimeUnit unit),时间过了就放弃等待;
2.lockInterruptibly()放代码块中,调用 interrupt()方法可中断,而 synchronized 不行)
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/c9af9266f9f20bff41c98024b】。文章转载请联系作者。
评论