并发编程 - 偏向锁、轻量级锁、重量级锁的升级

一、线程安全 & 线程不安全

线程不安全: 多个线程同时去访问一块共享资源,会出现数据不一致的问题。

demo:

public class AtomicDemo {
    private static int count = 0;
    public static void incr(){
        try {
            Thread.sleep(1);
        }catch (Exception e){
            e.printStackTrace();
        }
        count ++;
    }
    public static void main(String[] args) throws InterruptedException {
        //开了1000个线程去执行count的++操作
        for(int i = 0;i<1000;++i){
            new Thread(() -> AtomicDemo.incr()).start();
        }
        Thread.sleep(5000);
        System.out.println(count);
    }
}

运行结果:
在这里插入图片描述
不稳定且不符合预期,这就是线程不安全啦。

线程安全性:管理对于数据状态的访问。
对数据的要求:
1、数据肯定是共享的;
2、数据在生命周期里是有变化的 可变性;
判定数据是否安全的条件:
1、数据是否会被多个线程访问;
2、数据是如何被使用的;

二、保证线程安全 - 锁

锁:处理并发访问的同步手段。
要求:即达到安全性又能保证性能。
Java锁:synchronized 同步锁/互斥锁
基本使用:
1、修饰实例方法 -》锁的是调用方法的实例
2、修饰静态方法 -》锁的是类
3、修饰代码块
{
第一种:synchronized(某一个对象) -》则锁的是对象
第二种:synchronized(对象.class) -》则锁的是类
}
使用方式决定了锁的范围,范围越大,性能越差。

 public synchronized void demo1() {
}
public void demo2() {
        // 保证存在线程安全的变量
        synchronized (this) {
        }
}
public synchronized static void demo3(){
}
public void demo4() {
        synchronized (SyncDemo.class){         
        }
}

demo1和demo2 -》对象锁(跨线程不被保护)
demo3和demo4 -》类锁(跨线程被保护)

synchronized 修复线程不安全问题的方式:
在这里插入图片描述
在这里插入图片描述
锁的范围其实也就是跟对象的生命周期有关。

三、那么,如何知道对象是不是被锁了呢?

1、对象在内存中的布局:

在这里插入图片描述
如上,锁的状态其实会存在对象头里了。

2、对象头种Mark word的详细信息:

32位下:
Mark word利用32Bit的空间存储锁状态。
无锁态 = 对象的hashcode(25bit) + 分代年龄(4bit) + 是否偏向锁(1 bit) + 锁标志位(01)
偏向锁 = 线程ID(23bit) + Epoch周期(2bit)+ 分代年龄(4bit) + 是否偏向锁(1 bit) + 锁标志位(01)
其余状态下存储信息如下:
在这里插入图片描述
epoch:偏向锁的撤销会有一种情况,即全局安全点的时候,会进行批量撤销,对epoch做一个升级,所以epoch可以表示为一个朝代。

四、synchronized 锁

4.1 synchronized 锁的状态

JDK1.6之前,synchronized锁是基于重量级锁来实现
重量级锁:获得锁的时候,如果存在线程竞争,会把线程直接挂起,等锁被释放资源之后,唤醒挂起线程。因为线程的阻塞和唤醒需要从内核态到用户态的切换,切换需要操作系统来支持,所以很耗性能。

JDK1.6之后,对其做了优化,synchronized 锁的状态优化成了:无锁-> 偏向锁->轻量级锁->重量级锁(真正意义上的加锁)
注:偏向锁和轻量级锁 -> 无锁状态,而 重量级锁(真正意义上的加锁);

偏向锁和轻量级锁是什么意思呢?
如下,如果synchronized锁住了lock对象

synchronized(lock){
}

ThreadA/ThreadB 两个线程 访问同步代码块会有如下三种情况:
1、只有ThreadA去访问; --> 大部分情况下是属于这种,引入了偏向锁
(1)、首先在锁的对象头里存储 ThreadA的线程ID和偏向锁标记1等信息;
(2)、然后线程A再去访问这个对象的时候,就只需要判断一下ThreadId是否和当前线程的ID是否一致;
2、ThreadA/ThreadB 交替访问; --> 轻量级锁(自旋)
3、ThreadA/ThreadB 并行访问; --> 阻塞

4.2 锁的升级(偏向锁->轻量级锁->重量级锁)

4.2.1 偏向锁

只适用于大部分只有一个线程去访问。
在这里插入图片描述
注:
1、检查对象头中是否存储了线程ID,通过CAS比较(CAS表示原子性),CAS是乐观锁的一种实现。
Compare And Swap(value,expect,update)
参数:value:要比较的值 except:预期值 update:更新的值
比较 value 是否 等于 except,如果相等,认为读取到的是最新的,那么更新成update。
2、T1获得偏向锁后,T1 | epoch | 1 | 01 表示:
在这里插入图片描述
3、如果该对象已经有偏向锁了,其他线程在来访问的时候,就会被撤销偏向锁(暂停原来的线程,解锁,将对象置空)。
4、暂停线程的时候,有两种情况,一种是原有线程已经访问完被锁住的代码块,那么就直接置空;
另一种是原有线程未访问完,那么就会升级成轻量级锁(变成了多个线程交替访问的情况,所以锁就要升级了)。
5、可以根据实际的情况通过JVM参数 useBiasedLock = 0/1 去开启或者关闭偏向锁。

4.2 轻量级锁

轻量级锁采用自旋的方式来实现。
自旋: 通过一个无意义的循环去判断cas的状态。

for(;;){
if(cas()){
//获得锁了
}
}

在这里插入图片描述
(1)、cas修改失败后,会自旋重试;
(2)、绝大部分的线程在获得锁以后,会在非常短的时间内去释放锁,让线程挂起/阻塞的开销 大于 自旋。
(3)、如果自旋超过了我们的限定,还没有获得轻量级锁,会过分的浪费CPU资源,锁会膨胀为重量级锁,线程被阻塞。
限定方式:设置自旋次数(preBlockSpin参数可修改),自适应自旋(根据上一次在这个锁上自旋的时间和锁拥有者的状态来来判断)。
(4)、轻量级锁的存储:
在这里插入图片描述

4.3 重量级锁

升级到重量级锁之后,没有获得锁的线程会被阻塞(即状态为BLOCKED),重量级锁是基于监视器(ObjectMonitor)来实现。
Monitor -> MutexLock(互斥锁),是系统级别的线程切换。
ObjectMonitor的实现:(monitorenter和monitorexit 是指令)
在这里插入图片描述

4.4 总结

在这里插入图片描述
锁只有升级,木有降级的哦~

原创文章 88 获赞 21 访问量 3万+

猜你喜欢

转载自blog.csdn.net/cfy1024/article/details/97302193