synchronized 内部原理、常见锁策略、CAS、 以及死锁的产生和解决

1、synchronized 内部原理

对象头锁状态:
(1)无锁
(2)偏向锁
(3)轻量级锁
(4)重量级锁

在这里插入图片描述
重量级锁:通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex 锁实现

synchronized 内部monitor机制:
1.进入synchronized 代码行,编译为monitorenter字节码,其他线程不能进入
2.退出synchronized 代码行,编译为monitorexit字节码命令。
3.synchronized 使用计数器判断获取对象锁的次数,可重入性的表现。

缺点: 1.当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
2.操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

注:synchronized 锁只能升级 不能降级。
无锁---->偏向锁 ---->轻量级锁---->重量级锁

1.1、Synchronized锁优化

锁粗化:锁粗化就是将多次连接在一起的加锁、解锁操作合并为一次,将多个连续的锁扩展成为一个范围更大的锁。举例如下:

public class Test{    
	private static StringBuffer sb = new StringBuffer();    
	public static void main(String[]args){        
		sb.append("a");        
		sb.append("b");        
		sb.append("c");  
	}
} 

这里每次调用stringBuffer.append方法都需要加锁和解锁,如果虚拟机检测到有一系列连串的对同一个对象加锁和解锁操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。

锁消除:锁消除即删除不必要的加锁操作。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那 么可以认为这段代码是线程安全的,不必要加锁。看下面这段程序:

public class Test{    
	public static void main(String[] args) {        
		StringBuffer sb = new StringBuffer();        
		sb.append("a").append("b").append("c");  
	} 
} 

虽然StringBuffer的append是一个同步方法,但是这段程序中的StringBuffer属于一个局部变量,并且不会从该方法中 逃逸出去,所以其实这过程是线程安全的,可以将锁消除。

2、常见的锁策略
2.1、乐观锁 vs 悲观锁

乐观锁:乐观锁假设认为数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
悲观锁的问题:总是需要竞争锁,进而导致发生线程切换,挂起其他线程;所以性能不高。
乐观锁的问题:并不总是能处理所有问题,所以会引入一定的系统复杂度。 执行时间很快,线程都是处于运行态尝试修改值的操作。或是很多线程一直执行修改值的操作,效率比较低。

2.2、读写锁 (readers-writer lock)

多线程之间,数据的读取方之间不会产生线程安全问题,但数据的写入方互相之间以及和读者之间都需要进行互斥。如果两种场景下都用同一个锁,就会产生极大的性能损耗。所以读写锁因此而产生。
读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。

2.3、自旋锁(Spin Lock)

按之前的方式处理下,线程在抢锁失败后进入阻塞状态,放弃 CPU,需要过很久才能再次被调度。但经过测算,实际的生活中,大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。基于这个事实,自旋锁诞生了。 你可以简单的认为自旋锁就是下面的代码

while (抢锁(lock) == 失败) {} 

只要没抢到锁,就死等。
自旋锁的缺点: 缺点其实非常明显,就是如果之前的假设(锁很快会被释放)没有满足,则线程其实是光在消耗 CPU 资源,长期在做无用功的。

2.4、可重入锁

可重入锁的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。
比如一个递归函数里有加锁操作,递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。 Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized 关键字锁都是可重入的。

3、CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“,

  • 前提条件:原始值、预期值、修改值、版本号
  • CAS实现:自旋尝试设置值操作
  • 自旋的实现:
    (1)循环死等
    (2)可中断的方式:interrupt
    (3)判断循环次数,达到阈值退出
    (4)判断循环总耗时,达到阈值退出

一个 CAS 涉及到以下操作: 我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

  1. 比较 A 与 V 是否相等。(比较)
  2. 如果比较相等,将 B 写入 V。(交换)
  3. 返回操作是否成功。

当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到 操作失败的信号。可见 CAS 其实是一个乐观锁。

缺点:(1)如果之前的前提(锁很快会被释放)没有满足,线程一直处于运行态循环执行CAS,光在消耗 CPU 资源,长期在做无用功。
(2) 线程数量较多,前提可能满足不了或者CPU在多线程间切换---->性能消耗大

1.为什么引入CAS?
执行时间很快的代码,需要同步的时候,使用synchronized 方式效率会比较低。synchronized执行时间比较长的同步,效率还算比较好
2.ABA 问题如何处理
ABA 的问题,就是一个值从A变成了B又变成了A,预期是满足要求的, 但这个期间我们不清楚这个过程,可能是其他线程修改过的。
解决方法:加入版本信息,每次修改后,版本号加1,保证不会出现老的值

4、死锁

前提:
同步的本质在于:一个线程等待另外一个线程执行完毕后才可以继续执行。但是如果现在相关的几个线程彼此之间都在等待着,那么就会造成死锁。
至少有两个线程,互相持有对方申请的对象锁,造成互相等待,导致没法继续执行

4.1、死锁产生的四个必要条件:

1、互斥:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。
2、不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
4、环路等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了 一个等待环路。
当上述四个条件都成立的时候,便形成死锁。

4.2、解决死锁的基本方法
4.2.1、预防

1.资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
2.只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
3.可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
4.资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

4.2.2、避免

预防死锁的几种策略,会严重地损害系统性能。因此在避免死锁时,要施加较弱的限制,从而获得 较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源。因而,系统在进行资源分配之前预先计算资源分配的安全性。若此次分配不会导致系统进入不安全的状态,则将资源分配给进程;否则,进程等待。其中最具有代表性的避免死锁算法是银行家算法。

银行家算法:首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况。因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)。安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态。如果是,同意这个请求;如果不是,阻塞该进程知道同意该请求后系统状态仍然是安全的。

4.2.3、检测

检测方式:
1.Jstack命令
2.JConsole工具

4.2.4、解除

当发现有进程死锁后,便应立即把它从死锁状态中解脱出来,常采用的方法有:
1.剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
2.撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态.消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用中提到了破坏死锁产生必要条件的方法,即使用lock进行非阻塞的获取,并在获取不到时自动释放已经占有的。这种方式不能使用synchronized,因为synchronized的机制是占有资源一直等待,只有当要执行的所有操作执行完成后才会释放。而引用中展示了一种解决死锁的方法,即将if语句中的分别进行占有,先住一个再住另一个,避免了的争抢。具体实现如下: 如果choice为0,先住jinZi,再休眠一段时间,然后再住kouHong。 如果choice不为0,先住kouHong,再休眠一段时间,然后再住jinZi。这样就避免了两个线程同时争抢同一把而导致死锁的情况。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [使用synchronized产生死锁问题及其解决](https://blog.csdn.net/zcyt085/article/details/108435529)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [线程用synchronized产生死锁的问题及解决](https://blog.csdn.net/wycyiyang/article/details/130179808)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [synchronized 内部原理常见策略CAS、 以及死锁产生解决](https://blog.csdn.net/qq_41437542/article/details/109708968)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值