常见的锁策略,死锁问题


常见的锁策略

乐观锁和悲观锁

乐观锁:
乐观锁认为产生并发冲突的概率不大,只在数据提交更新时,才会检测数据是否产生并发冲突,如果发生并发冲突,就返回错误信息,让用户决定怎么做。

悲观锁:
悲观锁认为产生并发冲突的概率大,认为每次拿数据的时候都会被别人修改,所以会先上锁,再去拿数据进行操作,这样别人想拿这个数据就会阻塞等待。


轻量级锁和重量级锁

轻量级锁:

加锁解锁,过程更快更高效。
通常是纯用户态的,用户态不能完成才切换内核态。

  • 少量的内核态于用户态之间的切换。
  • 不容易引发线程调度。

重量级锁:

加锁解锁,过程更慢更低效。
重度依赖内核态。

  • 大量的内核态于用户态之间的切换。
  • 很容易引发线程调度。

简单理解用户态和内核态:

用户态可以理解为代码层面上完成的,时间是可控的。
内核态可以理解为操作系统内部完成的,时间不太可控。
因为操作系统要处理很多事,比较忙,所以用户态和内核态的切换效率很低。


自旋锁和挂起等待锁

自旋锁(Spin lock):

  • 自旋锁是轻量级锁的一种典型实现
  • 加锁失败时,自旋锁会循环进行加锁,一旦锁被释放可以第一时间拿到锁,速度较快。但是消耗 cpu 资源,忙等。

挂起等待锁:

  • 挂起等待锁是重量级锁的一种典型实现
  • 加锁失败时,通过内核的机制来挂起等待,锁被释放不能第一时间拿到锁。

互斥锁和读写锁

synchronized 只是单纯的互斥锁, 没有具体的细分。

多线程读取同一个数据时是没有线程安全问题的。但是多线程中,对同一个数据,读取的线程和写入的线程之间需要进行互斥,如果这种场景下使用 synchronized 效率较低, 因为如果写入数据的线程没在工作,只有读取的线程也会有锁竞争。

读写锁分为读锁和写锁,Java 标准库提供了 ReentrantReadWriteLock 类,实现了读写锁。

  • ReentrantReadWriteLock.ReadLock 类表示一个读锁。这个对象提供了 lock / unlock 方法进行加锁解锁。
  • ReentrantReadWriteLock.WriteLock 类表示一个写锁。这个对象也提供了 lock / unlock 方法进行加锁解锁。

读写锁的规则是:

  1. 读锁和读锁之间,不会互斥
  2. 读锁和写锁之间,互斥
  3. 写锁和写锁之间,互斥

互斥意味着会有阻塞等待,线程一旦阻塞挂起,就不知道何时能被唤醒。减少互斥,就是提高效率的办法,这就是读写锁的意义。
读写锁适用于 “频繁读取,不频繁写入” 的场景中。


公平锁和非公平锁

  • 公平锁:遵守 “先来后到” ,B比C先来,A释放锁,那B就能先拿到锁。
  • 非公平锁:不遵守 “先来后到” ,B和A都有可能拿到锁。

操作系统内部的线程调度是随机的。
如果不做额外的限制,锁就是非公平锁。
想实现公平锁,需要依靠额外的数据结构来记录先后顺序。


可重入锁和不可重入锁

可重入锁:

  • 允许同一个线程多次获取同一把锁。

  • 例如一个递归函数里有加锁操作,递归过程中这个锁不会阻塞自己,那么这个锁就是可重入锁(也叫递归锁)。

不可重入锁:

  • 不允许同一个线程获取同一把锁。

  • 一个线程获取到锁,还没释放锁,然后又尝试再次加锁,这时就会死锁,因为第二次加锁需要等锁释放,但是释放锁也是由这个线程来完成。

Java 中的锁都是可重入锁:Reentrant开头命名的锁、JDK提供的所有现成的Lock实现类,synchronized 关键字锁。
而 Linux 系统提供的 mutex 是不可重入锁。


死锁

死锁就是线程无限期的阻塞,等待某个资源被释放。

死锁的情况:

  • 一个线程对同一个锁多次加锁,如果是不可重入锁就死锁了。
  • 两个线程两把锁,两个线程都获取到一把锁,然后都要获取对方的锁,这时候这两个线程就僵住了,这就是死锁。

死锁是很严重的BUG,会导致程序卡死。


如何避免死锁

死锁产生的必要条件:

  1. 互斥使用:当一个线程拿到了一把锁,其他的线程不能使用。
  2. 不可抢占:一个线程拿到锁后,只能由这个线程主动释放,其他线程不能抢占。
  3. 请求和保持:一个线程拿到锁后,没有释放,想要拿另一个锁。
  4. 循环等待:闭环等待,即A等待B释放锁,B等待C释放锁,C又等待A释放锁。

这四个条件都成立时,死锁就产生了,当然,只要等打破任何一个条件,就能避免死锁。

最容易打破的条件是:循环等待。

如何破打破循环等待:

针对锁进行编号,如果需要获取多个锁,规定一个加锁顺序,先获取编号小的锁,再获取编号大的锁。

不规定加锁顺序:

Object locker1 = new Object();
Object locker2 = new Object();

Thread A = new Thread(() -> {
	synchronized(locker1) {
		synchronized(locker2) {

		}
	}
});
Thread B = new Thread(() -> {
	synchronized(locker2) {
		synchronized(locker1) {
		
		}
	}
});
A.start();
B.start();

如果线程A拿到locker1,线程B拿到locker2,两个线程就会产生循环等待。

规定加锁顺序:

Object locker1 = new Object();
Object locker2 = new Object();

Thread A = new Thread(() -> {
	synchronized(locker1) {
		synchronized(locker2) {

		}
	}
});
Thread B = new Thread(() -> {
	synchronized(locker1) {
		synchronized(locker2) {
		
		}
	}
});
A.start();
B.start();

规定加锁顺序后,如果 A 线程获取到 locker1,B 线程就不能获取 locker1,更不会获取到 locker2,那么 A 线程就正常执行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值