Java中Synchronized原理详解以及锁的升级

Java为了解决并发的原子性,提供了以下两个解决方案:
1、Synchronized关键字
2、Lock

这篇文章我们先说一下Synchronized关键字,Lock等着下篇文章再说。

Synchronized是隐式锁,当编译的时候,会自动在同步代码的前后分别加入monitorenter和monitorexit语句。

1、Synchronized的三种用法

package juc;

public class TestSyn {

    static int x = 0;
    int y = 0;
    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public static  void add(){
        x++;
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

上述的代码,我们实现了两个线程对变量分别加1000次的操作。

我们执行发现
在这里插入图片描述
这就是我们之前说的,x++不是一个原子操作,当出现多线程并发的时候,会出现线程不安全。

Synchronized是一个关键字修饰词,可以修饰静态方法、非静态方法、代码块。

Synchronized是一个锁,锁是加载对象上的。当加上静态方法上的时候,对应的对象是class对象。每个类只有一个class对象,是一个互斥锁。

1.1、静态方法

public static synchronized void add(){
        x++;
}

在add方法里加上synchronized关键字,程序就线程安全了
在这里插入图片描述
当在方法头上加了synchronized关键字后,同时只能有一个线程进入方法内执行。当一个线程获取锁后,如果其他线程进入该方法后,会被阻塞。当其他线程执行完毕后,会释放锁,然后唤醒对应的线程。

1.2、非静态方法。

package juc;

import java.util.concurrent.locks.Lock;

public class TestSyn {

    static int x = 0;
    int y = 0;
    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public  synchronized void add(){
        x++;
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

当Synchronized加在非静态方法的时候,是加载this对象上的。

1.3、代码块

package juc;

import java.util.concurrent.locks.Lock;

public class TestSyn {

    static int x = 0;
    int y = 0;
    final Object object = new Object();

    public static void main(String[] args) throws InterruptedException {

        TestSyn testSyn = new TestSyn();
        ThreadImpl thread1 = new ThreadImpl(testSyn);
        ThreadImpl thread2 = new ThreadImpl(testSyn);
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
        System.out.println(x);
    }

    public void add(){
        synchronized (object){
            x++;
        }
    }
}

class ThreadImpl extends Thread{
    TestSyn testSyn = null;

    public ThreadImpl(TestSyn testSyn){
        this.testSyn = testSyn;
    }

    @Override
    public void run() {
        for (int i = 0;i < 1000;i++){
            testSyn.add();
        }
    }
}

如上述代码所示,Synchronized修饰代码块的时候,我们用final Object object = new Object()来锁的对象。
锁的对象最好用final修饰,因为锁的对象最好不要变,否则如果锁的对象发生变化的话,两个线程会同时进入代码块内,造成了线程不安全。

2、Synchronized原理以及锁升级

在JDK1.6之前,Synchronized锁的原理是通过操作系统的mutex互斥锁实现的,需要线程从用户态切换到内核态,这个十分消耗资源的。在JDK1.6之后,Synchronized引入了三种状态的锁来提高了锁的性能!

从资源消耗级别从低到高分别为:偏向锁、轻量级锁、重量级锁。

Synchronized是在对象上加锁,我们首先说下Synchronized锁对象的内存布局。

我们都知道对象存在堆上,对象分为对象头和实例数据。对象头中有一个Mark Word,Mark Word中存储了锁相关的信息,如下图所示。

在这里插入图片描述
由上图所示,Mark Word中的最后两位代表锁的类别。
10代表重量级锁、00代表轻量级锁、01代表无锁或者偏向锁,这个时候需要看倒数第三位,如果倒数第三位位1代表为偏向锁,如果为0代表无锁。

1、偏向锁
Synchronized的默认锁是偏向锁。Mark Word中与偏向锁有关的属性有三个:锁标志(01)、偏向锁标志(1)、线程ID、Epoch(偏向锁占用的次数)
偏向锁对象初始化的时候,线程ID为null、Epoch为0。

偏向锁的获取过程:
当线程获取偏向锁的时候,首先查看偏向锁标志是否为0。
1、如果偏向锁标志位0,则进行CAS操作,CAS(偏向锁标志,锁标志,线程ID,Epoch)。
如果CAS成功的话,就是将线程ID设置为当前线程的ID,将锁标志设置为01,偏向锁设置为1,Epoch设置为Epoch+1,并在当前线程的栈帧空间开辟锁记录lock record写入当前线程ID。
如果CAS失败的话,说明有其他线程同时竞争,会将偏向锁升级为轻量锁。

如果偏向锁标志位1,则查看线程ID是否与自己的线程ID相同,如果相同,则直接执行同步区代码
如果线程ID不与自己相同,则判断拥有偏向锁的线程是否还存活,
如果死亡的话,就将锁标志设置为0,偏向锁标志设置为0,线程ID设置为null,当前线程开始CAS请求。
如果存活的话,从上到下遍历线程中的栈帧是否存在lock record,如果存在的话,就说明当前线程还拥有着偏向锁对象,就等到安全点的时候,将拥有偏向锁的线程暂停,将偏向锁升级为轻量锁。
如果不存在的话,就将锁标志设置为0,偏向锁标志设置为0,线程ID设置为null,这就是叫做偏向锁撤销

当线程使用完偏向锁的时候,是不会将对象头中的线程ID撤销的,只有其他线程来获取偏向锁的时候,才会撤销。

偏向锁适用于单线程 多次获取偏向锁的情况,减少了线程切换的开销。
如果存在高并发的情况下,不要设置偏向锁,将其关闭。因为要立马要升级锁,白白浪费了偏向锁创建和销毁的资源。

当epoch大于40的时候,也会自动升级为轻量锁。

2、轻量级锁
轻量级锁获取:
假设当前轻量级锁还未被获取,线程将会在栈帧中创建一个锁记录空间lock record,然后将锁对象的mark word拷贝到lock record中,接着
执行cas将lock record的地址写入到锁对象的mark word,如果锁对象中的mark word和lock record中之前拷贝的mark record相等,cas才会成功。
然后将lock record中的owner指针指向锁对象中的mark record。

如果cas失败的话,说明有其他线程捷足先登了,已经将其他线程的lock record地址写入锁对象的mark word了,就会将锁升级为重量级锁,对锁对象中的mark word改写为重量级锁对应的格局,线程会被阻塞。

如果当前轻量级锁已经被获取的话,直接就将锁升级为重量级锁。

轻量级锁的释放
当线程释放轻量级锁的时候,会进行cas将lock record中的mark word 写入到锁对象中的mark word中,如果锁对象中的mark word和lock record的地址一样,cas才会成功。

如果cas失败的话,说明锁对象中的mark word已经在升级为重量级锁的时候被改变了。这个时候会唤醒之前等待的线程。

3、重量级锁
重量级锁的时候,对象markword对应着一个C++对象,称之为Monitor(监视器),对应的类文件如下

ObjectMonitor() {
    _count        = 0; //用来记录该对象被线程获取锁的次数
    _waiters      = 0;
    _recursions   = 0; //锁的重入次数
    _owner        = NULL; //指向持有ObjectMonitor对象的线程 
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
  }

重量级锁,就是利用操作系统中的mutex互斥锁,当申请锁的时候,首先owner和当前线程一样或者owner为null,如果一致就将recursions+1,然后执行同步代码段。

如果owner不和当前线程一样,将会执行一定次数的自旋锁来请求锁,如果请求不成功则会阻塞,将其加入到EntryList中。

当线程调用了moniterenter时,将count+1,如果调用了monitorexit,将count-1,当count=0时,线程就要释放锁了,当有线程释放锁的时候,会唤醒waitset和entrylist中的线程。

如果获取锁对象的线程调用了wait方法,则将线程放入waitSet中。

3、Synchronized可重入锁的原理

Synchronized是一个可重入锁,看下面的例子我们理解下什么是可重入锁

package juc;

public class TestLoad {

    final static Object object = new Object();
    public static void main(String[] args) {

        test1();
    }

    public static void test1(){
        synchronized (object){
            System.out.println("test1");
            test2();
        }
    }

    public static void test2(){
        synchronized (object){
            System.out.println("test2");
        }
    }
}

如上述代码所示,test1方法中调用了object锁对象,在没有释放锁对象之前我们又调用了test2方法,test2方法中也需要申请object锁对象。

可重入锁的意思就是线程在还没释放锁对象的时候,又重新申请调用同一个锁,如果是可重入锁的话就可以申请成功,如上图所示。,如果是不可重入的话就会被阻塞,然后陷入死锁,因为当前对象在重新申请锁对象的前并没有释放锁对象,在重新申请的时候会被阻塞,等待锁对象释放,所以会陷入死循环。

在这里插入图片描述

Synchronized是怎么实现可重入的呢?

1、偏向锁
存储的有线程ID,如果线程ID一样就直接重入
2、轻量级锁
如果锁对象头记录的地址是在当前线程栈帧内,就直接重入。
3、重量级锁
如果mutex互斥锁中的owner和当前线程一样,则直接重入。

4、总结

名字 适用场景 原因 是否可以关闭
偏向锁 单线程申请多次锁 偏向锁的做法简单,就只在锁对象中的mark word中标明线程ID,只通过一次CAS来获取锁,通过比较线程ID来判断锁的冲突 可以关闭
轻量级锁 多个线程不同时申请锁 通过多次的CAS来申请锁,因为CAS消耗的cpu资源肯定是小于线程阻塞,然后被唤醒的切换消耗的cpu资源的。因为线程的阻塞和唤醒需要从用户态转化到内核态 可以关闭
重量级锁 多个线程锁冲突剧烈 如果线程之间的锁剧烈冲突,CAS消耗的cpu资源会远远大于线程阻塞然后唤醒的消耗的cpu资源。在阻塞前会进行一定次数的自旋操作 不可关闭

偏向锁做法:
1、当未加锁时,在锁对象mark word中cas写入线程ID。
2、当mark word中的线程ID与当前线程相等时,直接执行同步代码
3、当线程ID不等于当前线程ID时,查看mark word中对应的线程是否存活,并且是否引用当前锁对象,如果存活并引用,就在安全点时,将拥有锁的线程暂停,并将其升级为轻量级锁。
4、如果不存活,就将锁重置,变为无锁状态,转到1

2、轻量级锁
1、当未加锁时,当前线程将锁对象中的mark word复制到当前线程栈帧中的lock record中,然后通过cas将lock record的地址写入到锁对象的mark word中,旧值是lock record中的拷贝的mark word。如果更新成功,将lock record中的owner指针指向锁对象中的mark word。
2、如果cas不成功的话,说明有其他线程同时申请了锁对象,并且已经捷足先登的将其lock record的地址写入到了锁对象中的mark word了,就将锁升级到重量级锁,重写锁对象对应的mark word。
3、如果锁对象加锁了,就直接升级为重量级锁。

4、当线程释放轻量级锁时,将lock record中之前复制的mark word 通过cas写入到锁对象中的mark word,旧值为lock record的地址。
如果cas失败,就说明发生了重量级锁的升级。这个时候就会唤醒其他线程。

3、重量级锁
重量级锁利用的操作系统的mutex互斥锁,当出现锁冲突时,将会执行一定次数的自旋锁来请求锁,如果请求不成功则会阻塞,并将线程挂到阻塞队列中。

如果线程释放锁的时候,就唤醒阻塞队列中的其他线程。

posted @ 2021-08-22 10:12  张孟浩Jay  阅读(749)  评论(1编辑  收藏  举报