一文搞懂所有的锁

1.Monitor(重量级锁)

1.1 对象头结构

Klass Word指向这个对象所对应的类的数据地址。

在这里插入图片描述

MarkWord结构中一共占位32位,下图中四行每一行都占32位,是MarkWord在不同状态时的情况
在这里插入图片描述

1.2Monitor结构

在这里插入图片描述
当线程中出现synchronized锁之后,加锁对象obj中的MarkWord会指向一个Monitor的地址,然后Monitor中的Owner变为占用这个Monitor的线程,其他线程若同样运行到obj加锁的临界区,先去查看Owner中是否有其他线程,没有就占用,有就链接到EntryList并进入阻塞状态等待锁释放,锁释放后,即Owner中没东西了,则唤醒EntryList中所有的阻塞线程去竞争这个锁。
不同加锁的对象,关联不同的Monitor。

2. 轻量级锁

轻量级锁没有阻塞概念

2.1 轻量级加锁过程

轻量级锁的使用还是使用synchronized
每个线程都会包含一个锁记录(Lock Record),在线程中需要进入锁住的临界区进行操作时,如下图所示,将所记录中的lock record地址与加锁对象的MarkWord交换(一般交换后MarkWord的后两位是01,交换前是00,一般查看是否有线程已经进入临界区获得该锁,查看后两位就行了),并用Object reference指向被加锁对象。
在这里插入图片描述

2.2 锁重入

当同一个线程中还需要对该对象加锁的临界区进行操作时,且上一个锁还未释放,进入锁重入操作(锁重入代表着同一个线程再次尝试获取锁的操作,这个时候锁不会阻拦这个线程进入临界区,但是一些不可重入锁会阻拦线程进入临界区,因为在他们眼里就算线程ID一样,不同时间来获取锁,就是不同的线程,所以会把线程踢进阻塞队列)如图所示。
栈帧中再创造一个LockRecord压栈,先去尝试采用CAS替换锁对象的MarkWord(会有消耗),但是会失败,该锁记录的地址被置为空,当临界区代码执行完毕需要释放锁时,看到栈帧里面有null地址的Record(栈结构,最先看到的永远是栈顶),则将这个锁记录删除。
在这里插入图片描述

2.3锁膨胀

在这里插入图片描述
上图表示当有线程已经获得轻量级锁时,另一个线程来竞争,当存在竞争状态时,轻量级锁会转化为重量级锁,如下图所示:
先根据Markword中被交换来的锁记录地址得到对象信息,将Owner属性置为该线程, 并将Markword转化为Monitor的地址,并将来竞争的所有线程连接到这个Monitor的Entrylist中并阻塞(一般会有自旋优化,即先不立即陷入阻塞,先进行短时间的循环反复尝试获得锁,适合多核,因为单核,获得锁的线程占用时间片,其他线程根本自旋不了),当原本的线程想要释放锁时,这个线程的锁记录发现指向的锁对象中的MarkWord存储的不是自己想要的值了,发现是Monitor的地址,那就开始走Monitor释放的流程,将Owner置为null并唤醒EntryList所有BLOCKED的线程。
在这里插入图片描述

3.偏向锁

为了防止锁重入时每次都尝试去CAS替换锁对象的MarkWord值,使用偏向锁让来想要获得锁的线程查看MarkWord前几位线程ID是否就是本线程,是的话,就不尝试采用轻量级锁交换了。
偏向锁默认开启(可以手动禁用,禁用默认使用轻量锁),一个对象创建之后,他就是偏向锁状态,即MarkWord后三位位101,且偏向锁是延迟的,刚创建出来是不会显示后三位是101,可以手动关闭
用a.hashCode()方法可以禁用偏向锁,如下图所示,MarkWord在不使用轻量或者重量锁的时候只有Normal和Biased两种状态,既然hashCode需要存储hash码,那么对象头的MarkWord只有采用第一种方式,这样偏向锁就被禁用了。
Java语言里面一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因 为用户可以重载hashCode()方法按自己的意愿返回哈希码),否则很多依赖对象哈希码的API都可能存 在出错风险。而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的是对象的一致性哈希 码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一 次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。因此,当一个对象已经计算过一 致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要 计算其一致性哈希码请求[1]时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。
使用wait/notify方法也会禁用偏向锁,因为该方法是重量级锁才拥有的方法,所以需要先转换至重量级锁再执行wait方法。
在这里插入图片描述
当有其他线程需要获得该锁时,注意:这里两个线程不存在竞争关系,想要获得锁的线程是在上一个线程已经释放锁退出临界区的时候来获得锁,查看该锁的MarkWord是偏向锁,且前几位表示的线程ID不是自己,那么就在临界区内将这个锁转化为轻量级锁,并在释放锁后将该锁的偏向状态取消(这两步称为撤销偏向锁操作),即置后三位为001,但是这两个操作都是暂时的,因为系统认为第二个线程需要获得锁是偶然现象,这个锁的MarkWord之后马上就会回到偏向第一个线程的状态,即从001->101,如果想让他不回到之前的状态,需要这两个操作执行频率达到一个阈值。

3.1 批量重偏向和批量撤销

之前说当撤销操作需要达到阈值,这个阈值一般是20。当第二个线程反复获取该锁,并反复执行撤销操作20次及以上,该锁的偏向就会偏向于第二个线程。
但是当执行撤销操作达到40次及以上,比如又有第三个线程,那么系统会觉得这个这个锁就不该偏向,它就会取消锁偏向状态转化为轻量级锁,毕竟撤销是有系统消耗的。

4.锁消除

当一个锁对象被初始化在某个方法中等一些不能被其他线程共享的地方,那么在这个方法中存在反复获得锁的操作,JIT即时编译器就会去优化这个操作,撤销这个锁对象,不去尝试获得锁的一些操作,那么效率就会提高。

5.Wait和notify

已经获得了锁,但是因为某些原因放弃了锁,就会进入Waiting中,当满足条件被唤醒后,这些WAITING的线程就会重新进入BLOCKED线程列里面。

synchronized (o)
{
    o.wait();
    o.notify();
    o.notifyAll();
}

在这里插入图片描述
Wait是Object的方法,sleep和unpark是Thread的方法。
Wait状况下是不占用锁的,sleep状态下是占用锁的。
Notify()方法是从waiting中随机唤醒一个线程,NotifyAll()是唤醒所有的waiting线程。

6.LockSupport.park()

6.1 Park和unpark运行原理

每个线程都有自己的parker对象。
当使用park()方法时,检查对象的一个属性count,该count为0,park()就会阻塞住线程,如果count为1,则继续运行并消耗掉这个count,即count-1=0。
当调用unpark方法时,如果线程阻塞,则唤醒该线程,给这个count+1,count最多只能加到1。如果线程还在运行,就只给这个count+1;

7.线程状态切换

Runnable->waiting: join(), LockSupport.park(),
Runnable->TIMED_WAITING: wait(time), join(time), sleep(time), Locksupport.park(time)

8.线程活跃性

8.1死锁

当有两把锁A,B一个线程需要先获得A,再获得B,另一个线程先获得B,再获得A。
当在同一时刻第一个线程获得A了,第二个线程获得B了,双方都需要对方的锁,那么就造成了死锁。
定位死锁的方式(也可以用jconsle):
1、 用jps打印出所有JAVA相关线程
2、 用jstack 线程id 打印线程死锁的信息

8.2哲学家就餐

当五个哲学家坐在桌子上,一共有5根筷子,当哲学家两个筷子都拿起来时才能吃饭,吃完饭才能把筷子放下,当哲学家都只拿起了一根筷子,没有人能拿起第二根筷子了就发生了哲学家就餐的问题
解决方法:我们规定哲学家先拿起左手的筷子,再拿右手的筷子,那么解决方法就是规定其中一个哲学家先拿起右手的筷子,再拿左手的筷子
方法原理:根本达不到所有哲学家都拿起一根筷子的条件,因为那个独特的哲学家会去抢另一个人想要拿起的筷子,如果抢到了,另一个人就一个筷子都没拿起来,如果没抢到,这个独特的人就一根筷子都没拿起来,破坏了哲学家问题发生的条件。
之后用ReentanrentLock.tryLock锁可以设置超时时间,当哲学家问题发生时,所有人持锁超过指定事件后,就会释放锁,或者不设置时间,没获得到就释放所有的锁。

8.3活锁

当两个线程运行时,两个线程互相改变对方结束任务的条件,使两个都不能到达,则产生了活锁

8.4 饥饿

当线程进入阻塞队列后被唤醒,所有线程需要再次竞争时间片的运行权力,再不公平竞争的情况下,有些锁一直抢不到这个权力,就一直不能运行,出现饥饿状况。

9.ReentanrentLock

Synchronized不能被中断,之前interrupted中断的是sleep方法,而ReentanrentLock方法可以被中断。
是公平锁,可以设置锁定时间,支持多个条件变量,可重入。

static ReentrantLock reentrantLock=new ReentrantLock();
public static void main(String[] args ) throws ExecutionException, InterruptedException {

    reentrantLock.lock();
    try{
        //临界区
    }finally {
        reentrantLock.unlock();// 释放锁
    }
}

9.1被打断特性

Thread t= new Thread(()->{
     try {
         reentrantLock.lockInterruptibly();
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
	Return;
     }
     try {
         //临界区
     }finally {
         reentrantLock.unlock();
     }
 });
 reentrantLock.lock();
 t.start();
 t.interrupt();

在上述代码中,因为主线程先获得锁,所以t线程被阻塞在reentrantLock.lockInterruptibly();处,但是当主线程打断该线程时,线程会直接抛出异常,所以没有获得锁,我们需要在catch方法中即使结束这个任务,以防无锁线程进入临界区,造成线程不安全的问题。

9.2 锁超时

Reetanrent.tryLock()方法会尝试去获得锁,没有获得到锁就直接不获得锁了,继续向下运行,该方法同样支持可打断。
Reetanrent.tryLock(long time, TimeUnit.second)方法可以让线程在指定时间内去获得锁,在指定时间内无法获得锁则放弃获得锁。

9.3 公平锁

Synchronized是不公平锁,ReetanrentLock是公平锁

9.4 条件变量

Condition condition1= reentrantLock.newCondition();
 Condition condition2= reentrantLock.newCondition();
new Thread(()->{
    reentrantLock.lock();
    try{
        condition1.await();
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        reentrantLock.unlock();
    }
}).start();
 new Thread(()->{
     reentrantLock.lock();
     try{
         condition2.await();
     } catch (InterruptedException e) {
         throw new RuntimeException(e);
     } finally {
         reentrantLock.unlock();
     }
 }).start();
 condition1.notify();
 condition2.notify();
 condition1.notifyAll();
 condition2.notifyAll();

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值