无锁与死锁(发生死锁了怎么办?)
仅作为笔记
文章目录
前言
仅作为笔记
一、无锁
对于锁、无锁等根本是为了控制好在高并发情况下临界区资源的安全性。锁,和无锁实际上是对待临界区资源保护的悲观和乐观态度而已,锁,是总觉得线程总是在临界区产生冲突,而无锁则是乐观的认为线程在临界区产生冲突的概率较小,采取的乐观的态度去处理临界区线程可能出现的冲突。
1.1、比较交换(CAS)
- CAS算法过程:包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值,仅当V值等于E值时才会将V的值设置为N,如果V值和E值不相同,则说明已经有了其他线程做了更新,则当前线程什么都不做,重新拿到更新后的值继续上述步骤就可以了。
- CAS并发原语体现在Java语言中就是sun.misc.Unsafe类的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令,这是一种完全依赖于硬件的功能,通过它实现了原子操作,再次强调,由于CAS是一种系统原语,原语属于操作系统应用范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致的问题,也就是说CAS是线程安全的。
1.2、无锁的线程安全整数:AtomicInteger
- 属于JDK并发包atomic里面的类,可以说是CAS等CPU指令的一个应用封装,可以把这个看成是一个整数,但是和Integer不同,这是可改变的整数,对其所做的一切操作都是CAS指令进行的,其主要方法如下:
其内部实现而言,核心字段是:
private volatile int value;
注意一点:CAS操作本身是一种CPU的指令,但是他并不总是成功的,这里指的成功是并不总是成功修改值的(如果发现V参参数不等于N了就会失败),但是他会不断地尝试,直到修改成功。
1.3、Java中的指针:Unsafe类
- Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的。
- Unsafe类里面的方法都是native的。
- 主要功能有:操纵对象属性、操纵数组元素、线程挂起与恢复、CAS
- 使用getUnsafe()工厂方法调用Unsafe()实例。
1.4、无锁的对象引用:AtomicReference
- 与AtomicInteger类似,只不过AtomicReference是对对象引用的保证,同样也是采取的CAS操作。
- CAS:的不足,如果在检查C和N是否相等之前,C发生了两次改变,也就是被修改了一次但是又被修改了回来,那么这个事情不会被CAS检查出来,意味着丢失了过程细节信息。会在一些应用场景带来不足。如下:
1.5、带有时间戳的对象引用:AtomicStampedReference
- 为了解决上面提到的丢失过程信息的问题而提出的解决方法。总之上面的问题就是对象的状态信息和对象本身等价了,这时候我们应该剥离其状态信息和对象本身,只需要使用随意一个数字或者其他的标记来作为对象的状态信息即可,用于监视对象是否被修改过,算是双重保证吧。
1.6、数组也能无锁:AtomicIntegerArray
- 由JDK提供的原子数组,使用Unsafe类通过CAS的方式控制。
- 包含三种:封装int[]类型的AtomicIntegerArray,针对Long型的AtomicLongArray,针对对象的AtomicReferenceArray。
- 都是线程安全的。
1.7、让普通变量也享受原子操作:AtomicIntegerFieldUpdater
- 开闭原则:系统对功能的扩展应该是开放的,对功能的修改应该是保守的。
- 用于开发到一半或者说是后面才发现前面的某些普通变量也需要原子操作而不想大范围更改,减少代码的修改量。
- 包含三种:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,分别对象int、long、对象三种。
- 使用场景注意事项:1.Updater只修改它可见范围内的变量,因为这是靠反射得到的这个变量。2.为了保证正确的读取(可见性),所使用的变量必须是volatile修饰的。3.由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此它不支持static。
1.8、挑战无锁算法:无锁的Vector实现
- Vector继承于AbstractList,实现了List、RandomAccess、Cloneable、 Serializable等接口。
(1)Vector 继承了AbstractList,实现了List接口。
(2)Vector实现了RandmoAccess接口,即提供了随机访问功能。
(3)Vector 实现了Cloneable接口,即实现克隆功能。
(4)Vector 实现Serializable接口,表示支持序列化。 - 线程安全:Vector本来是使用synchronized实现的,但是也可以替换使用CAS实现。
- 构造方法:Vector实际上是通过一个数组去保存数据的。当我们构造Vecotr时;使用默认构造函数,默认容量大小是10。
- 增加元素:当Vector容量不足以容纳全部元素时,Vector的容量会增加。若容量增加系数 大于0,则将容量的值增加“容量增加系数”;否则,将容量大小增加一倍
- 克隆:Vector的克隆函数,即是将全部元素克隆到一个数组中。
1.9、让线程之间互帮互助:细看SynchronousQueue的实现
- SynchronousQueue是BlockingQueue的一种,所以SynchronousQueue是线程安全的。SynchronousQueue和其他的BlockingQueue不同的是SynchronousQueue的capacity是0。即SynchronousQueue不存储任何元素。SynchronousQueue的每一次insert操作,必须等待其他线性的remove操作。而每一个remove操作也必须等待其他线程的insert操作。
- 内部实现使用了大量的无锁。
- 应用在需要在线程中传递对象的情况。比如:生产者消费者模式。
二、有关死锁的问题
死锁前面已经提到过了,简而言之就是相互占用对方的资源,而且谁都不释放资源。解决方法除了前面提到过的使用重入锁的中断或者等待限时,就还有本章提到的使用无锁去替换锁,利用无锁天然的避免死锁优势。
- 产生死锁的条件:
1.互斥,共享资源 X 和 Y 只能被一个线程占用;
2.占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
3.不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
4.循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 - 破坏产生死锁的条件:
1、第一条无法解决,因为这本来就是我们的目的。
2、解决占用且等待的方式是直接把要申请的资源一次性申请,而不是分阶段申请,这就是需要一个专门的全局类来做一次性申请所有资源的操作,而全局类其实也是一个全局的资源锁,会涉及到性能低下和超时放弃等问题。
3、不可抢占其实就是要求线程遇到阻塞了,主动放弃现有的资源,不过 java 在语言层面并没有解决这个问题。
4、循环等待,之所以会造成死锁的一个原因就是因为资源获取是无序的,线程A先获取 X 资源再获取 Y 资源,而线程B先获取 Y 资源再获取 X 资源,导致循环等待了,如果线程 A 和 B 都是先获取 X 资源便没有这个问题了,所以可以给资源排序,例如 X 资源排在比 Y 资源之前,那么线程 A、B 都是先获取 X 资源,这样子就不会出现死锁了。
注意:
1.当什么时候容易出现死锁呢?当使用锁且大量的线程BLOCK(阻塞状态,注意区分和wait的区别)时。
2.blocked是被动卡住的。是因为该线程没拿到这段代码的执行权也就是锁资源,只要别的线程退出这段代码,jvm就会自动让你进去。也就是说别的线程无需唤醒你,由jvm自动来干。而waiiting调用wait()等函数,主动卡住自己,jvm在满足某种条件后比如另条线程调用了notify()后唤醒。这个唤醒的操作由别的线程调用唤醒函数完成。但是实际上这只是在jvm上体现的,在Linux内核上他们都是等待状态,没区别,这样只是在jvm层面上利于管理而已。
三、解决死锁的等待通知策略
如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。那 Java 语言是否支持这种等待 - 通知机制呢?
答案是:一定支持。就是Java 语言的支持等待 - 通知机制。
- wait和sleep的异同:
1.wait会释放所有锁而sleep不会释放锁资源.
2.wait只能在同步方法和同步块中使用,而sleep任何地方都可以.
3.wait无需捕捉异常,而sleep需要.(都抛出InterruptedException ,wait也需要捕获异常)
4.wait()无参数需要唤醒,线程状态WAITING;wait(1000L);到时间自己醒过来或者到时间之前被其他线程唤醒,状态和sleep都是TIME_WAITING
两者相同点:都会让渡CPU执行时间,等待再次调度! - notify()和notifyall()的区别:
打个比方:有两个顾客要买水果,但同时只能有一个人进店里买(也就是只有有抢到锁的人才能进去买水果),顾客A想要买橘子,顾客B想要买苹果,但是目前店里什么都没有,那么A和B都在while循环里面调wait方法进行阻塞等待(这时候锁已经释放),然后店员C去进货进了苹果,然后开始通知大家可以来买水果了(也就是调用锁的notify方法),这里notify方法随机唤醒一个顾客,假设唤醒了顾客B,顾客B拿到锁之后发现要的橘子还是没有(对应while循环的条件还是没满足)又调了wait进行阻塞等待,结果这样就导致明明有苹果,但是A还是等在死那。但如果是notifyAll方法的话,那么就同时通知A和B(唤醒A和B),这时两个顾客竞争锁,假设拿到锁的还是B,他发现没有橘子于是接着wait释放锁,这时候A就能拿到B释放的锁,然后就可以买到想要的苹果了,这样就不会出现上面发生的死等现象。