六、各种锁
1、公平锁、非公平锁(默认)
- 公平锁:非常公平,不能插队,必须先来后到
- 非公平锁:非常不公平,可以插队,(默认都是非公平锁)
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、可重入锁
Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
![可重入锁](https://i-blog.csdnimg.cn/blog_migrate/f8dfb1332a17964638309cd726ad0394.png#pic_center)
获取外部锁的同时,内部锁也会被获取!
class Phone2{
Lock lock = new ReentrantLock();
public void sms(){
lock.lock(); //细节问题!两把锁,进到call(),也会拿到一把锁!
//lock.lock(); 死锁
//lock锁必须配对
try{
System.out.println(Thread.currentThread().getName() + " sms");
call();
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void call(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName() + " call");
}catch(Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
3、自旋锁
//自旋锁
public class SpinLockDemo {
AtomicReference<Thread> atomicReference = new AtomicReference<>();
//加锁
public void myLock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==> myLock");
//自旋锁
while(!atomicReference.compareAndSet(null,thread)){ //获取不到锁就自旋等待!!!
}
}
//解锁
public void myUnlock(){
Thread thread = Thread.currentThread();
System.out.println(Thread.currentThread().getName()+"==> myUnlock");
atomicReference.compareAndSet(thread,null);
}
}
4、死锁
死锁是什么? 两个线程持有自己的锁,并试图获取对方的锁!
public static void main(String[] args) { Object object1 = new Object(); Object object2 = new Object(); new Thread(()->{ synchronized (object1){ System.out.println(Thread.currentThread().getName()+"\t 持有a锁,想获得b锁"); try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}//使得线程b也启动 synchronized (object2){ System.out.println(Thread.currentThread().getName()+"\t 成功获得b锁"); } } },"A").start(); new Thread(()->{ synchronized (object2){ System.out.println(Thread.currentThread().getName()+"\t 持有b锁,想获得a锁"); synchronized (object1){ System.out.println(Thread.currentThread().getName()+"\t 成功获得a锁"); } } },"B").start(); }
- 四要素:互斥、占有等待、循环等待、不可抢占
如何排除死锁,解决问题
1 使用 **jps -l **定位进程号
2 使用jstack 进程号查看进程信息
5、乐观锁,悲观锁
特点:
-
悲观锁认为自己在使用数据的时候一定有别的线程来修改数据,因此在获取数据的时候会先加锁,确保数据不会被别的线程修改。(适合写多的场景)
-
乐观锁认为自己在使用数据时不会有别的线程修改数据,所以不会添加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。(适合读多的场景)
实现方式:
-
悲观锁的实现方式
synchronized
关键字Lock
的实现类都是悲观锁
-
乐观锁的实现方式
- 最常采用的是CAS算法,Java原子类中的递增操作就通过CAS自旋实现的。
- 版本号机制Version。(只要有人提交了就会修改版本号,可以解决ABA问题)
- ABA问题:再CAS中想读取一个值A,想把值A变为C,不能保证读取时的A就是赋值时的A,中间可能有个线程将A变为B再变为A。
- 解决方法:Juc包提供了一个AtomicStampedReference,原子更新带有版本号的引用类型,通过控制版本值的变化来解决ABA问题。
6、syn锁 和 lock 锁
底层实现方式:
-
synchronized同步代码块,实现使用的是
moniterenter
和moniterexit
指令(moniterexit
可能有两个)。- Monitor(监视器),也就是我们平时说的锁。监视器锁
- 任何一个对象都可以成为一个锁,每个对象天生都带着一个对象监视器
-
Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的MarkWord中。
-
lock 锁,加锁
lock
,解锁unlock
要成对!
7、syn锁升级
(1) 偏向锁
JDK6中引入的锁优化,通常锁总是由同一线程多次获得,偏向锁会偏向于第一个获得它的线程:
- 如果在运行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。
- 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁升级为标准的轻量级锁。
(2)轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
- 如果在线程1复制对象头的同时(在线程1CAS之前),线程2也准备获取锁,复制了对象头到线程2的锁记录空间中,但是在线程2CAS的时候,发现线程1已经把对象头换了,线程2的CAS失败,那么线程2就尝试使用自旋锁来等待线程1释放锁。
- 自旋的时间太长也不行会消耗CPU资源,轻量级锁在以下情况膨胀为重量级锁:
- 自旋次数到了线程1还没有释放锁
- 线程1还在执行,线程2还在自旋等待,这时又有一个线程3过来竞争这个锁对象
(3)重量级锁
重量级锁Synchronized,它可以把任意一个非NULL的对象当作锁。
- 作用于方法时,锁住的是对象的实例(this);
- 当作用于静态方法时,锁住的是Class实例,又因为Class的相关数据存储在永久带PermGen(jdk1.8则是metaspace),永久带是全局共享的,因此静态方法锁相当于类的一个全局锁,会锁所有调用该方法的线程;
- synchronized作用于一个对象实例时,锁住的是所有以该对象为锁的代码块
Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的MarkWord中。
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
Synchronized锁对象是存在哪里的呢?答案是存在锁对象的对象头的MarkWord中。
![img](https://i-blog.csdnimg.cn/blog_migrate/e43f2077207969b3d2e28f926c5ebf92.jpeg)
8、锁消除与锁粗化
- 锁消除:给每个线程new一个锁,相当于没起作用。
- 锁粗化:大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度;在以下场景下需要粗化锁的粒度:
假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的;
9、读写锁
ReentrantReadWriteLock 是读写锁,和ReentrantLock会有所不同,
-
对于读多写少的场景使用ReentrantReadWriteLock 性能会比ReentrantLock高出不少。在多线程读时互不影响,不像ReentrantLock即使是多线程读也需要每个线程获取锁。
-
不过任何一个线程在写的时候就和ReentrantLock类似,其他线程无论读还是写都必须获取锁。
-
需要注意的是同一个线程可以拥有 writeLock 与 readLock (但必须先获取 writeLock 再获取 readLock, 反过来进行获取会导致死锁)
问题:读写互斥,读读共享,但是读没完成的时候其他线程写锁无法获得!
锁降级
锁降级 : 是指保持住当前的写锁(已拥有),再获取读锁,随后释放写锁的过程。(写锁能降级为读锁,读锁不能升级为写锁!)
好处:使用锁降级可以在释放写锁前获取读锁,这样其他的线程就只能获取读锁,对这个数据进行读取,但是不能获取写锁进行修改,只有当前线程释放了读锁之后才可以进行修改。
总结:同一个线程自己持有写锁时再拿读锁,相当于重入,完事了再释放写锁,防止其他线程乱修改刚写入的数据。(缓存一致性)
10、StampedLock邮戳锁
StampedLock是对ReentrantReadWriteLock读写锁的优化,在其基础上增加了乐观读—tryOptimisticRead() 方法
总结:读的过程中也允许获取写锁!
![image-20210913222746190](https://i-blog.csdnimg.cn/blog_migrate/edd93cfa3e99f595b077cad21b281928.png)
锁饥饿问题:
虽然ReentrantReadWriteLock实现了读写分离提升了速度,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了:
- 假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了
- 因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写。
StampedLock的特点
- 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
- 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
- StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
StampedLock有三种访问模式
- Reading(读模式
readLock()
):功能和ReentrantReadWriteLock的读锁类似 - Writing(写模式
writeLock()
):功能和ReentrantReadWriteLock的写锁类似 - Optimistic reading(乐观读模式
tryOptimisticRead()
),支持读写并发,很乐观认为读取时没人修改。若读的过程被修改了(通过validate(stamp)
方法获得通知),再进行重新读等操作。