在(一)中,简历介绍了何为线程安全,以及线程安全所产生的过程和原因。
本篇主要介绍线程安全问题的解决方案。
一.加锁
什么是加锁
加锁是针对修改操作并非原子的线程安全问题的解决方案,主要的办法是把“非原子”的操作打包成“原子”。
以下通过图例来形象解释
假设一个“滑稽老哥”要上厕所,在一个人最脆弱的时候,往往是不希望被人打扰的,这时,他把厕所上了锁,在滑稽老哥结束前,其他滑稽老铁不能强行闯进来。
当滑稽老哥完事后,就会打开厕所的锁,让后面的滑稽老铁进来上厕所。
这一系类操作就是操作加锁,程序中,加锁使一个线程在执行的过程中,其他的进程不能插队进来。
如何实现加锁
JAVA中,提供了synchronized()关键字,来完成 加锁操作。
synchronized()是关键字,并不是函数,后面的()并非参数,需指定一个锁对象(可以指定任何对象),来进行后面的判断。
public static Object lock=new Object();
synchronized (lock) {
count++;
}
此为synchronized的使用方法,后面{}里填写需加打包成原子的代码。
{}还可以放任意的其他代码,包括调用别的方法,只要有合法的Java代码都可以。
private static int count;
public static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (lock){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
注:只有锁{}里的代码是串行的,之外的是并发执行
有了加锁后,线程安全问题得到了解决。
synchronized如何实现加锁
假设t1线程加锁成功,则继续执行{}里的代码。(相当于上厕所操作)
t2线程后加锁,发现t1线程已经加锁(厕所里有人了),于是进入阻塞(等t1),当t1加锁{}代码执行完毕,t2线程开始执行加锁{}里的代码。
本质上是把随机的并发执行强制变成了串行,从而解决线程安全。
加锁的注意事项
上述代码有效的前提是,两个线程都加锁了,而且是针对同一个对象加锁。
1)假如t1加锁,t2不加锁,count结果会怎样?
public class Demo1 {
private static int count;
public static Object lock=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
}
从运行结果上,可以看到count结果又随机起来了。
这可以形象的理解为,一个滑稽老铁上厕所加锁了,另一个滑稽老哥不走门,走窗户进去了。
2)假如t1与t2加不同对象的锁,结果又如何
public class Demo1 {
private static int count;
public static Object lock=new Object();
public static Object lock2=new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (lock) {
count++;
}
}
});
Thread t2=new Thread(()->{
for (int i = 0; i < 50000; i++) {
synchronized (lock2) {
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count="+count);
}
}
可以发现,结果又出bug了
这可以形象理解为,两个滑稽老铁在不同的厕所上厕所,两个厕所没有联系。
但线程里的count++也变成并行执行了,开始抢占式执行。
锁对象的作用,就是区分多个线程是否针对“同一个对象”加锁
是针对同一个对象加锁,此时就会出现阻塞(锁竞争)
不是针对同一个对象加锁,此时不会出现阻塞,两个线程仍是随机并发执行
锁对象填哪个对象并不重要,重要的是多个线程是否是同一把锁。
注 :JAVA为了减少程序员写出死锁的概率,引入了特殊机制"可重复锁",可以针对一个线程重复加锁。
使用锁的缺点
锁应该在需要的时候使用,不需要的时候不要使用。
使用锁,就有可能发生阻塞,一旦某个线程阻塞,啥时能恢复阻塞,继续执行是不可期的(需要付出性能的代价)。
胡乱的加锁也可能产生“死锁”。
二.死锁
死锁是指在多线程或多进程环境中,两个或多个进程因争夺资源而导致相互等待,从而无法继续执行的状态。
死锁场景
1.一个线程,重复锁
void fun(){
synchronized (this){ //第一次加锁
synchronized (this){ //第二次加锁
}
}
}
上述代码进行重复加锁,当加到第一个锁后,对第二个锁加锁需释放第一个锁,但第二个锁在第一个锁内,于是就发生了阻塞。由于无法释放锁,也就无法对第二个锁进行加锁,代码就走不下去了,此时发生了“死锁”。
但是,JAVA为了减少程序员写出死锁的概率,引入了特殊机制"可重复锁",对一个线程重复加锁进行了优化,根据当前锁是否被占用来判断是对哪个锁加锁了(它暖,我哭)。
2.两个线程,两把锁
设有线程1,线程2。
锁A,锁B。
1)首先,线程1针对A加锁,线程2对B加锁。
2)线程1在不释放锁A的情况下,再对B加锁。同时,线程2不释放锁B对A加锁。
具体代码如下
public class Demo2 {
private static Object lockerA=new Object();
private static Object lockerB=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (lockerA){
System.out.println("t1加锁A完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockerB){
System.out.println("线程t1对B加锁");
}
}
});
Thread t2=new Thread(()->{
synchronized (lockerB){
System.out.println("t2加锁B完成");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockerA){
System.out.println("线程t2对A加锁");
}
}
});
t1.start();
t2.start();
}
}
我们发现,程序进入了死锁状态
打开jconsole,可以发现两个线程都进入了BLOCKED状态
3.多个线程,多把锁(哲学家用餐)
围绕一张桌子有5个哲学家,卓上放着食物和5根筷子。
每个哲学家吃饭,需要拿起两边筷子
假如1哲学家拿起左右两边筷子吃饭,此时2也开始吃饭,发现右手边筷子在哲学家1手中,此时就发生了阻塞等待
当哲学家1放下筷子,哲学2才能拿起两边筷子吃饭。
每位哲学依次这样执行,此模型便能通过。
但是,当每位哲学家同时拿起左手边筷子时
此时再想拿起右手边筷子,发现右手筷子没了。
由于哲学家比较固执,当他吃不到饭也不会放下左手边筷子,导致所有的哲学家都无法吃饭,于是发生了死锁。
死锁产生的必要条件(重点)
1.锁是互斥的【基本特性】
2.锁是不可被抢占的【基本特性】
假如线程1拿到了锁A,如果不线程1不主动释放锁,线程2就不能把锁抢过来
3.请求与保持。【代码逻辑】
线程1拿到锁A之后,不释放A的前提下去拿锁B。
4.循环等待,也叫环路等待【代码逻辑】。
多个线程获取锁的过程中,出现循环等待。
上诉死锁条件,缺一不可。
死锁场景的解决方案
1.针对一个线程重复锁
在JAVA中以对其进行了优化,引入了可重复锁机制,通过引用技计数,维护当前已经加了几次锁,以及表述出何时真正释放锁
2.针对两个线程,两把锁。
从死锁必要条件“请求与保持”入手,在释放锁后再加锁
优化代码如下
public class Demo1 {
private static Object lock1=new Object();
private static Object lock2=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (lock1){
System.out.println("线程t1对lock1加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("线程t1对lock2加锁成功");
}
});
Thread t2=new Thread(()->{
synchronized (lock2){
System.out.println("线程t2对lock2加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock1){
System.out.println("线程t2对lock1加锁成功");
}
});
t1.start();
t2.start();
}
}
代码并未死锁,成功运行。
3.针对哲学家用餐问题
给锁进行编号,约定所有线程在加锁时,都按照一定的顺序进行加锁(比如先针对编号小的加锁,再针对编号大的加锁)
所有哲学家拿起低号的筷子,从哲学家2开始,拿起1号筷子,哲学家3拿起2号筷子.....当轮到哲学家1时,应该拿起1号筷子,但1号筷子已经被哲学家2拿起,那哲学家就陷入阻塞,不拿筷子。
此时,5号哲学家面前就有了两根筷子,就能够用餐,当5号哲学用餐结束后,就释放筷子,4号哲学家就能再拿起4号筷子用餐.....如此下去,每个哲学家都能够用餐
只要遵循以上约定进行加锁,无论接下来这个模型运行顺序如何,都不会死锁了。
代码实现如下
public class Demo2 {
private static Object lock1=new Object();
private static Object lock2=new Object();
public static void main(String[] args) {
Thread t1=new Thread(()->{
synchronized (lock1){
System.out.println("线程t1对lock1加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("线程t1对lock2加锁成功");
}
});
Thread t2=new Thread(()->{
synchronized (lock1){
System.out.println("线程t2对lock2加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("线程t2对lock1加锁成功");
}
});
Thread t3=new Thread(()->{
synchronized (lock1){
System.out.println("线程t3对lock1加锁成功");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (lock2){
System.out.println("线程t3对lock2加锁成功");
}
});
t1.start();
t2.start();
t3.start();
}
}
按照一定顺序进行加锁,所有线程都执行完毕,未发生死锁。
4.银行家算法
银行家算法是一个用于避免死锁的资源分配算法,特别适用于多进程系统中的资源管理。该算法由 Edsger Dijkstra 提出,主要用于确保系统在分配资源时处于安全状态。(过于复杂,不过多讲)
以上就是全部内容了,如有不对,欢迎指正。