深入学习并发编程中的synchronized

  • Post author:
  • Post category:其他




synchronized不可中断性

synchronized是不可中断,处于阻塞状态的线程会一直等待锁。

package com.sj.thread;

/*
目标:演示synchronized不可中断
1.定义一个Runnable
2.在Runnable定义同步代码块
3.先开启一个线程来执行同步代码块,保证不退出同步代码块
4.后开启一个线程来执行同步代码块(阻塞状态)
5.停止第二个线程
*/
public class DemoUninterruptible {
    private static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 1.定义一个Runnable
        Runnable run = () -> {
// 2.在Runnable定义同步代码块
            synchronized (obj) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "enter synchronized code");
// 保证不退出同步代码块
                try {
                    Thread.sleep(888888);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
// 3.先开启一个线程来执行同步代码块
        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
// 4.后开启一个线程来执行同步代码块(阻塞状态)
        Thread t2 = new Thread(run);
        t2.start();
// 5.停止第二个线程
        System.out.println("before stop thread");
        t2.interrupt();
        System.out.println("after stop thread");
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }
}

ReentrantLock可中断演示

public class DemoUninterruptibleForLock {
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        test01();
//        test02();
    }

    // 演示Lock可中断
    public static void test02() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            boolean b = false;
            try {
                b = lock.tryLock(3, TimeUnit.SECONDS);
                if (b) {
                    System.out.println(name + " required lock success, run code");
                    Thread.sleep(88888);
                } else {
                    System.out.println(name + " more than the maximum time to wait for the lock, no any operation!");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (b) {
                    lock.unlock();
                    System.out.println(name + "Releases the lock");
                }
            }
        };
        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();
    }

    // 演示Lock不可中断
    public static void test01() throws InterruptedException {
        Runnable run = () -> {
            String name = Thread.currentThread().getName();
            try {
                lock.lock();
                System.out.println(name + " Acquires the lock success, run code");
                Thread.sleep(88888);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println(name + " Releases the lock.");
            }
        };
        Thread t1 = new Thread(run);
        t1.start();
        Thread.sleep(1000);
        Thread t2 = new Thread(run);
        t2.start();
        System.out.println("before Interrupts this thread.");
        t2.interrupt();
        System.out.println("after Interrupts this thread.");
        Thread.sleep(1000);
        System.out.println(t1.getState());
        System.out.println(t2.getState());
    }

}


Java面试热点问题,synchronized原理剖析与优化



五、Synchronized实现原理



javap 反汇编



目标

通过javap反汇编学习synchronized的原理

我们编写一个简单的synchronized代码,如下:

public class Demo01 {

    private static Object obj = new Object();

    public static void main(String[] args) {
        synchronized (obj){
            System.out.println("1");
        }
    }

    public synchronized void test(){
        System.out.println("2");
    }

}

我们要看synchronized的原理,但是synchronized是一个关键字,看不到源码。我们可以将class文件

进行反汇编。

JDK自带的一个工具: javap ,对字节码进行反汇编,查看字节码指令。

在DOS命令行输入:

javap -p -v -c D:\code\hsa-mbs-nation-bj-svc\hsa-mbs-nation-biz\target\classes\cn\hsa\mbs\medtrtcomquery\bo\impl\Demo01.class

反汇编后的效果如下:

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field obj:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String 1
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 14
        line 11: 24
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      25     0  args   [Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 19
          locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
    MethodParameters:
      Name                           Flags
      args

  public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String 2
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 14: 0
        line 15: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcn/hsa/mbs/medtrtcomquery/bo/impl/Demo01;

在这里插入图片描述



monitorenter

首先我们来看一下JVM规范中对于monitorenter的描述:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.monitorenter

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

翻译过来: 每一个对象都会和一个监视器monitor关联。监视器被占用时会被锁住,其他线程无法来获

取该monitor。 当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象对应

的monitor的所有权。其过程如下:

  1. 若monior的进入数为0,线程可以进入monitor,并将monitor的进入数置为1。当前线程成为

    monitor的owner(所有者)
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程已经占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直

    到monitor的进入数变为0,才能重新尝试获取monitor的所有权。

monitorenter小结:



monitorexit

首先我们来看一下JVM规范中对于monitorexit的描述:

The thread that executes monitorexit must be the owner of the monitor associated with the

instance referenced by objectref. The thread decrements the entry count of the monitor

associated with objectref. If as a result the value of the entry count is zero, the thread

exits the monitor and is no longer its owner. Other threads that are blocking to enter the

monitor are allowed to attempt to do so.

翻译过来:

  1. 能执行monitorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1。当monitor的进入数减为0时,当前线程退出

    monitor,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个

    monitor的所有权

monitorexit释放锁。

monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit。

面试题synchroznied出现异常会释放锁吗?



同步方法

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10

可以看到同步方法在反汇编后,会增加 ACC_SYNCHRONIZED 修饰。会隐式调用monitorenter和

monitorexit。在执行同步方法前会调用monitorenter,在执行完同步方法后会调用monitorexit



小结

通过javap反汇编我们看到synchronized使用编程了monitorentor和monitorexit两个指令.每个锁对象

都会关联一个monitor(监视器,它才是真正的锁对象),它内部有两个重要的成员变量owner会保存获得锁

的线程,recursions会保存线程获得锁的次数,当执行到monitorexit时,recursions会-1,当计数器减到0时

这个线程就会释放锁



面试题:synchronized与Lock的区别

  1. synchronized是关键字,而Lock是一个接口。
  2. synchronized会自动释放锁,而Lock必须手动释放锁。
  3. synchronized是不可中断的,Lock可以中断也可以不中断。
  4. 通过Lock可以知道线程有没有拿到锁,而synchronized不能。
  5. synchronized能锁住方法和代码块,而Lock只能锁住代码块。
  6. Lock可以使用读锁提高多线程读效率。
  7. synchronized是非公平锁,ReentrantLock可以控制是否是公平锁。



深入JVM源码



monitor监视器锁

源码下载:

https://hg.openjdk.java.net/jdk8/jdk8/hotspot/

\hotspot-87ee5ee27509\src\share\vm\runtime\objectMonitor.hpp

 // initialize the monitor, exception the semaphore, all other fields
  // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;  // 线程的重入次数
    _object       = NULL;  // 存储该monitor的对象
    _owner        = NULL; // 标识拥有该monitor的线程
    _WaitSet      = NULL; // 处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;  // 多线程竞争锁时的单向列表
    FreeNext      = NULL ; // 处于等待锁block状态的线程,会被加入到该列表
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }
  1. _owner:初始时为NULL。当有线程占有该monitor时,owner标记为该线程的唯一标识。当线程

    释放monitor时,owner又恢复为NULL。owner是一个临界资源,JVM是通过CAS操作来保证其线

    程安全的。
  2. _cxq:竞争队列,所有请求锁的线程首先会被放在这个队列中(单向链接)。_cxq是一个临界资

    源,JVM通过CAS原子指令来修改_cxq队列。修改前_cxq的旧值填入了node的next字段,_cxq指

    向新值(新线程)。因此_cxq是一个后进先出的stack(栈)。
  3. _EntryList:_cxq队列中有资格成为候选资源的线程会被移动到该队列中。
  4. _WaitSet:因为调用wait方法而被阻塞的线程会被放在该队列中。

在这里插入图片描述

会释放锁

synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个

同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量owner:拥有

这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待

线程在获取锁的时候,实际上就是获得一个监视器对象(

monitor

) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带 monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了

monitorenter



monitorexit

指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

线程执行到monitorenter指令时,会尝试

获取对象所对应的monitor所有权

,也就是尝试获取对象的锁,而执行monitorexit,就是

释放monitor的所有权

对象在内存中的布局

在 Hotspot 虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)。一般而言,synchronized使用的锁对象是存储在Java对象头里。它是轻量级锁和偏向锁的关键。

1、Java对象头

首先,我们要知道对象在内存中的布局:

已知对象是存放在堆内存中的,对象大致可以分为三个部分,分别是对象头、实例变量和填充字节。

对象头主要是由MarkWord和Klass Point(类型指针)组成,其中Klass Point是是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例,Mark Word用于存储对象自身的运行时数据。

如果对象是数组对象,那么对象头占用3个字宽(Word),如果对象是

非数组对象

,那么对象头占用2个字宽。(1word = 2 Byte = 16 bit)

在这里插入图片描述

上图中的偏向锁和轻量级锁都是在java6以后对锁机制进行优化时引进的,下文的锁升级部分会具体讲解,Synchronized关键字对应的是重量级锁,接下来对重量级锁在Hotspot JVM中的实现锁讲解。



六、JDK6 synchronized优化



CAS



CAS概述和作用

CAS的全成是: Compare And Swap(比较相同再交换)。是现代CPU广泛支持的一种对内存中的共享数

据进行操作的一种特殊指令。

CAS的作用:CAS可以将比较和交换转换为原子操作,这个原子操作直接由CPU保证。CAS可以保证共

享变量赋值时的原子操作。CAS操作依赖3个值:内存中的值V,旧的预估值X,要修改的新值B,如果旧

的预估值X等于内存中的值V,就将新的值B保存到内存



CAS和volatile实现无锁并发

public class DemoSyncNoLock {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger atomicInteger = new AtomicInteger();
        Runnable mr = () -> {
            for (int i = 0; i < 1000; i++) {
                atomicInteger.incrementAndGet();
            }
        };
        List<Thread> ts = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(mr);
            t.start();
            ts.add(t);
        }
        for (Thread t : ts) {
            // 使创建它的线程等待他执行完在执行。子线程按照创建顺序依次执行
            t.join();
        }
        System.out.println("number = " + atomicInteger.get());
    }
}



CAS原理

通过刚才AtomicInteger的源码我们可以看到,Unsafe类提供了原子操作。



Unsafe类介绍

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使

用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Unsafe对

象不能直接调用,只能通过反射获得。



Unsafe实现CAS

在这里插入图片描述

在这里插入图片描述



乐观锁和悲观锁


悲观锁

从悲观的角度出发:

总是假设最坏的情况,每次操作共享变量时都认为会被其他线程修改,所以每次在操作数据的时候都会上锁,这

样线程想拿这个数据就会阻塞。因此synchronized我们也将其称之为悲观锁。JDK中的ReentrantLock

也是一种悲观锁。性能较差!


乐观锁

从乐观的角度出发:

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,就算改了也没关系,再重试即可。所

以不会上锁,但是在

更新的时候会判断

(保证原子性)一下在此期间别人有没有去修改这个数据,如何没有人修改则更

新,如果有人修改则重试。

CAS这种机制我们也可以将其称之为乐观锁。综合性能较好!

CAS获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。结合CAS和volatile可以

实现无锁并发,适用于竞争不激烈、多核 CPU 的场景下

  1. 因为没有使用 synchronized,所以线程不会陷入阻塞,这是效率提升的因素之一。
  2. 但如果竞争激烈,可以想到重试必然频繁发生,反而效率会受影响。



小结

CAS的作用? Compare And Swap,CAS可以将比较和交换转换为原子操作,这个原子操作直接由处理

器保证。

CAS的原理?CAS需要3个值:内存地址V,旧的预期值A,要修改的新值B,如果内存地址V和旧的预期值

A相等就修改内存地址值为B


静态内存

是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。

  • 类名.class 该对象的类型是Class
  • 静态的同步方法使用的锁是该方法所在类的字节码文件对象。 类名.class

深入JVM源码

目标

通过JVM源码分析synchronized的原理



synchronized锁升级过程

高效并发是从JDK 5到JDK 6的一个重要改进,HotSpot虛拟机开发团队在这个版本上花费了大量的精力

去实现各种锁优化技术,包括偏向锁( Biased Locking )、轻量级锁( Lightweight Locking )和如适应性

自旋(Adaptive Spinning)、锁消除( Lock Elimination)、锁粗化( Lock Coarsening )等,这些技术都是为

了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。

无锁–》偏向锁–》轻量级锁–》重量级锁



Java对象的布局



目标

学习Java对象的布局

术语参考: http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

在这里插入图片描述



对象头

当一个线程尝试访问synchronized修饰的代码块时,它首先要获得锁,那么这个锁到底存在哪里呢?是

存在锁对象的对象头中的。

HotSpot采用

instanceOopDesc



arrayOopDesc

来描述对象头,arrayOopDesc对象用来描述数组类

型。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp 文件中,另外,arrayOopDesc

的定义对应 arrayOop.hpp 。

class instanceOopDesc : public oopDesc {
public:
// aligned header size.
static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
// If compressed, the offset of the fields of the instance may not be aligned.
static int base_offset_in_bytes() {
// offset computation code breaks if UseCompressedClassPointers
// only is true
return (UseCompressedOops && UseCompressedClassPointers) ?
klass_gap_offset_in_bytes() :
sizeof(instanceOopDesc);
}
static bool contains_field_offset(int offset, int nonstatic_field_size) {
int base_in_bytes = base_offset_in_bytes();
return (offset >= base_in_bytes &&
(offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
}
};

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot

源码中的 oop.hpp 文件中。

class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
// Fast access to barrier set. Must be initialized.
static BarrierSet* _bs;
// 省略其他代码
};

在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark 和 _metadata

_mark 表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有

关的信息

_metadata 表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示

普通指针、 _compressed_klass 表示压缩类指针。

对象头由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指

针,及对象指向它的类元数据的指针。



Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、

线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类

型是 markOop 。源码位于 markOop.hpp 中。



版权声明:本文为qq_43183527原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。