第三篇: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、轻量级锁

所有线程枪锁的时候,CAS的预期取值为加锁前对象头markWord的拷贝。
轻量级锁获取:
假设当前轻量级锁还未被获取,线程将会在栈帧中创建一个锁记录空间lock record,然后将锁对象的mark word拷贝到lock record中的mark word中,
执行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、Synchronized中的队列

Synchronized关键字是通过在对象上加锁实现的,每个对象对应的有两个队列
1、同步队列
当线程请求锁失败的时候,会进入同步队列。当有线程释放锁后,会唤醒队列中第一个线程来请求锁。如果请求锁成功,会将线程从队列中移除。

2、等待队列
当调用wait()的时候,当前线程会释放锁,并会将当前线程放入等待队列中。
当其他线程调用notify()的时候会唤醒等待队列中的第一个线程,将其移出等待队列,将其加入到同步队列中。
当其他线程调用notifyAll()的时候,会将等待队列中的所有线程移除,将它们加入到同步队列中。

5、总结

名字适用场景原因是否可以关闭
偏向锁单线程申请多次锁偏向锁的做法简单,就只在锁对象中的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互斥锁,当出现锁冲突时,将会执行一定次数的自旋锁来请求锁,如果请求不成功则会阻塞,并将线程挂到阻塞队列中。

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

Synchronized是非公平锁(首先进来的时候就会尝试抢锁),可重入的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值