自学内容网 自学内容网

多线程死锁与单例模式

目录

1.两个线程两把锁的死锁场景 

2.构成死锁的四个必要条件:

3.内存可见性问题

4.volatile的作用

5.wait与sleep的区别

6.单例模式


1.两个线程两把锁的死锁场景 

1)线程1先针对线程A加锁,线程2针对线程B加锁,线程1在不释放A锁的境况下对B加锁,

同时线程2在不释放B锁的情况下对A加锁。就会造成阻塞(请求和保持)

针对这种情况,可重入锁synchronized就无能为力了。

2)在疫情期间,公司需要一卡通才能进入,但是一卡通突然出bug了,显示不出来。程序员想要进入公司维护,但是保安说要出示一卡通才能进入,程序员要先进入公司去维护,才能出示。

这就构成了死锁的“循环依赖”

3)针对一个线程连续枷锁两次

在java中可重入锁synchronized对这个情况做出了特殊处理,没有出现真正的死锁,会判定当前是哪个线程对这个锁加锁了。但是在C++/python中就没有这样的功能,需要第三方库实现


2.构成死锁的四个必要条件:

1.锁是互斥的【锁的基本特性】

想要约束多个线程的并发执行,这里的锁的对象必须是互斥的,是唯一的。

2.所是不可抢占的。【基本特性】

线程1拿到锁A,线程1不主动释放锁A,那么线程2就不能将锁A抢过来使用。

3.请求和保持【取决于代码结构】

线程1先针对线程A加锁,线程2针对线程B加锁,线程1在不释放A锁的境况下对B加锁,

同时线程2在不释放B锁的情况下对A加锁。就会造成阻塞(请求和保持)

如果是先释放A,再去拿B就会避免这个问题。但这并不适用所情况。有的代码里就需要写成请求和保持。

4.循环等待/环路等待/循环依赖,多个线程获取锁的过程存在循环等待。【取决于代码结构】

为了防止多个线程获取锁的时候不发生循环等待,就可以把多个锁依次编号,所有线程按一定顺序加锁


3.内存可见性问题

引发线程安全问题的第四个原因:内存可见性

如果去一个线程修改,一个线程读取会发生什么问题呢?

可以看到修改n的值后,线程t1并没有结束,线程t1一直处于运行状态,很明显出现了线程安全问题。这就是内存可见性

原因:

t1循环的代码会执行很多次,每一次执行都要判断n是否为零,每一次判断开销都非常大,会进行两个操作:

1.从内存读取n值到寄存器

2.通过类似于cmp的指令,比较寄存器与0的值

而每次读取内存的值这个速度非常慢,而第二个指令就相对快一些,每次执行操作1会发现值都是一样的啊,于是jvm就把操作1优化掉了,每次只读取寄存器或者缓存里的数据。没有考虑到未来用户可能会修改n的值。

jvm做出这个决定后,循环的开销大幅度降低,但是用户修改n的时候,在内存中n的值已经改变,而且n的值不会再从内存里读取,而是直接比较寄存器或者缓存的,所以n的改变对于t1线程来说是不见的,这就是内存可见性问题。

那么如何解决这个内存可见性问题呢?

只要在定义n的时候在前面加上volatile,告诉编译器这是易变的变量,不要去优化。编译器在优化的时候是针对频繁读取给过固定的 去优化。加上volatile之后吗,每次循环都会从内存中重新读取数据。

引入volatile后,编译器就会给这个变量的读取操作附近加上”内存屏障“,后续jvm执行到这里的时候就知道,不能进行那个上述优化了。

还有一个方式也可以避免内存可见性问题,就是加上sleep,加上这个之后,sleep相对于内存的读取开销是非常大的,远远超过内存的读取,就算把内存读取优化掉也是没有什么意义的。


4.volatile的作用

主要作用有两个:

1.保证内存可见性:基于屏障指令的实现,即当一个线程修改一个共享变量的时候,另一个变量可以读到这个修改的值。

2.保证有序性:禁止指令重排。编译时jvm编译器遵循内存屏障约束,运行时靠屏障指令组织指令顺序。

3.注意:不能保证原子性,只能解决内存可见性问题


5.wait与sleep的区别

1.wait释放锁

2.进入阻塞等待被唤醒,准备接收通知

3.收到通知唤醒后,重新获取锁。

wait是Object的一个方法,sleep是Thread类中的方法。

wait必须要在synchronized修饰的代码块或者方法中使用,sleep可以在任意位置使用;

wait被调用后当前线程进入BLOCK状态并释放锁,可通过notify/notifyAll方法唤醒,sleep被调用后当前线程进入TIME WAIT状态,不涉及锁的操作。


6.单例模式

单例模式是一种设计模式,设计模式类似于棋盘,有自己的固定套路。设计模式是对代码的软性束缚可以遵守也可以不遵守。 框架是对代码的硬性约束。

在开发中如果不希望一个进程有存在多个实例就可以使用单例模式——限制某个类,只能有唯一实例。

单例模式的写法有两种,一种是饿汉模式,另一种是懒汉模式。

饿汉模式:一开始就创建好对象;饿汉就是迫切,在类被加载的时候就已经创建出单例的实例


懒汉模式:顾名思义就是懒,只有第一次词用的时候才会创建,不第一次使用不创建。

创建singletonLazy类成员,每次创建的实例用它作为引用,只有这一个实例。每次使用只需返回即可。

1.在最内层 if 语句外加上一层锁就是为了防止在多个线程调用的时候,出现覆盖的情况

2.照着这个代码执行,以后每次都要执行锁的代码部分,因为锁会使代码阻塞程序效率大幅减低,可以在外层再加上一个if条件判断,目的是避免重复执行锁操作。

3.经过两次修改后的代码还可能出现新的问题,就是指令重排序。

1.分配内存空间

2.执行构造方法

3.内存空间的地址,赋值给引用变量

指令重排序也是编译优化的一种方式,如果是但线程代码编译器可以准确地判断

如果是多线程代码,编译器是会可能误判的。

有可能在编译器内部指令执行的顺序是 1 ,2 ,3; 也可能是1,3,2这种方式可能会引起bug

解决方法就是在singletonlazy前加上volatile,告诉编译器这个部分是不可优化的。加上后也可以禁止在给singletonlazy赋值时插入别的操作。

因此vola有两个作用:

1.保证内存可见性

2.禁止指令重排(针对赋值)



原文地址:https://blog.csdn.net/2301_80026123/article/details/140602110

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!