首页 > 技术文章 > JAVA多线程基础(二)--synchronized使用及升级逻辑

liweiweicode 2020-02-29 23:14 原文

一、synchronized基本使用方法

1、synchronized加锁的几种方式

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。不同的修饰类型,代表锁的控制粒度
public class SyncMethodType extends  Thread {

    Object lock ;
    public SyncMethodType(Object lock) {
        this.lock = lock;
    }

    //属于实体对象,锁对象为实体类本身,等价于synchronized(this){}
    public synchronized  void method1(){

    }
    //和method1等价,
    public void method2(){
        synchronized (this){

        }
    }

    //上面两种方式要使用其他同一锁,可以通过构造器传入锁
    public void method2Two(){
        synchronized (lock){

        }
    }

    //方法属于类,不论多少个实体对象,锁对象只有一个
    public synchronized  static void method3(){

    }
    //和method3g功能一致,锁的粒度更小,可以减少同步代码块的执行时间
    public  void method4(){
        synchronized (SyncMethodType.class){
            System.out.println(Thread.currentThread().getName());
            while (true){

            }
        }
    }
    //加static和不加功能一致
    public static void method5(){
        synchronized (SyncMethodType.class){

        }
    }
    //lock不能作为static方法的锁对象
    public static void method8(){
        /*synchronized (lock){

        }*/
    }

    public static void main(String[] args) {
        Object lock = new Object();
        SyncMethodType sd = new SyncMethodType(lock);
        SyncMethodType sd2 = new SyncMethodType(lock);
        new Thread(()->sd.method4(),"thread1").start();
        new Thread(()->sd2.method4(),"thread2").start();
    }
}

二、synchronized锁的存储方式

1、要实现多线程的互斥特性,那这把锁需要哪些因素?

  1. 锁需要有一个东西来表示,比如获得锁是什么状态、无锁状态是什么状态

  2. 这个状态需要对多个线程共享

2、对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

3、MarkWord

​ 当某个对象被synchronized 关键字当成同步锁时,那么围绕这个锁的一系列操作都和 Mark word 有关系。Mark Word 在 32 位虚拟机的长度是 32bit、在 64 位虚拟机的长度是 64bit。Mark Word 里面存储的数据会随着锁标志位的变化而变化,Mark Word 可能变化为存储以下 5 中情况:

4、为什么任何对象都可以实现锁

  • java中的每个类都派生自Object类,每个java Object在jvm中都有一个native的c++对象oop/oopDesc 进行对应。

  • 线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的

    Java 对象是天生携带 monitor。

5、锁类型

​ hotspot 虚拟机的作者经过调查发现,大部分情况下,加锁的代码不仅仅不存在多线程竞争,而且总是由同一个线程多次获得。所以基于这样一个概率,是的 synchronized 在JDK1.6 之后做了一些优化,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁、轻量级锁的概念。因此大家会发现在 synchronized 中,锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁锁的状态根据竞争激烈的程度从低到高不断升级

  • 一个线程持有锁-->偏向锁
  • 两个线程竞争锁-->偏向锁升级为轻量级锁
  • 超过两个线程竞争锁-->轻量级锁升级为重量级锁

三、偏向锁

当一个线程访问同步代码块的时候,对象头markWord会存储线程的id,后续这个进入同步代码块时,不需要再加锁和释放锁,直接比较对象头中是否存储了指向该线程的偏向锁,如果相等择说明当前偏向锁偏向于该线程,不需要再次获得锁;

1、偏向锁的获取

  • 1、首先获取锁对象的Markword,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)

  • 2、如果是可偏向状态,则通过 CAS 操作,把当前线程的ID写入到 MarkWord

    • a) 如果 cas 成功,那么 markword 就会变成这样。表示已经获得了锁对象的偏向锁,接着执行同步代码

      块;

    • 如果cas失败,说明有其他线程已经获得了偏向锁,这种情况说明当前锁存在竞争,需要撤销已获得偏向

      锁的线程,并且把它持有的锁升级为轻量级锁(这个操作需要等到全局安全点,也就是没有线程在执行字

      节码)才能执行;

  • 3、如果是已偏向状态,需要检查 markword 中存储的ThreadID 是否等于当前线程的 ThreadID

    • a) 如果相等,不需要再次获得锁,可直接执行同步代码块
    • b) 如果不相等,说明当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁

2、 偏向锁的撤销

锁只会升级,不会降级,撤销偏向锁的目的是为了加轻量级锁

偏向锁的撤销并不是把对象恢复到无锁可偏向状态(因为偏向锁并不存在锁释放的概念),而是在获取偏向锁的过程中,发现 cas 失败也就是存在线程竞争时,直接把被偏向的锁对象升级到被加了轻量级锁的状态。

对原持有偏向锁的线程进行撤销时,原获得偏向锁的线程有两种情况:

  • 1、原获得偏向锁的线程如果已经退出了临界区,也就是同步代码块执行完了,那么这个时候会把对象头设置成无锁状态并且争抢锁的线程可以基于 CAS 重新偏向当前线程
  • 2、如果原获得偏向锁的线程的同步代码块还没执行完,处于临界区之内,这个时候会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

优化

在我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过 jvm 参数UseBiasedLocking 来设置开启或关闭偏向锁

三、轻量级锁

1、基本原理

加锁和解锁逻辑:

锁升级为轻量级锁之后,对象的 Markword 也会进行相应的的变化。升级为轻量级锁的过程:

  1. 线程在自己的栈桢中创建锁记录 LockRecord。

  2. 将锁对象的对象头中的MarkWord复制到线程的刚刚创建的锁记录中。

  3. 将锁记录中的 Owner 指针指向锁对象。

  4. 将锁对象的对象头的 MarkWord替换为指向锁记录的指针。

2、自旋锁

  • 所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞,直到那个获得锁的线程释放锁之后,这个线程就可以马上获得锁的。注意,锁在原地循环的时候,是会消耗 cpu 的,就相当于在执行一个啥也没有的 for 循环。

  • 轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短的时间就能够获得锁了。

    自旋锁的使用,其实也是有一定的概率背景,在大部分同步代码块执行的时间都是很短的。所以通过看似无异议的循环反而能提升锁的性能。但是自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么这个线程不断的循环反而会消耗 CPU 资源;

  • 默认情况下自旋的次数是 10 次,可以通过 preBlockSpin 来修改

  • 在 JDK1.6 之后,引入了自适应自旋锁,自适应意味着自旋的次数不是固定不变的,而是根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。

3 、轻量级锁解锁

  • 轻量级锁的锁释放逻辑其实就是获得锁的逆向逻辑,通过CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的MarkWord 中,如果成功表示没有竞争。如果失败,表示当前锁存在竞争,那么轻量级锁就会膨胀成为重量级锁;

4、重量级锁的原理

  • 含有synchronized关键字的程序使用javap -v

​ 加了同步代码块以后,在字节码中会看到一个monitorenter 和 monitorexit。每一个 JAVA 对象都会与一个监视器 monitor 关联,我们可以把它理解成为一把锁,当一个线程想要执行一段被synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor。monitorenter 表示去获得一个对象监视器。monitorexit 表示释放 monitor 监视器的所有权,使得其他被阻塞的线程可以尝试去获得这个监视器monitor 依赖操作系统的 MutexLock(互斥锁)来实现的, 线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能

推荐阅读