600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > Java多线程:线程同步(3)- synchronized关键字

Java多线程:线程同步(3)- synchronized关键字

时间:2024-01-18 07:02:10

相关推荐

Java多线程:线程同步(3)- synchronized关键字

🍇一、synchronized

1. 介绍

synchronizeJava中的关键字,可以用在实例方法、静态方法、同步代码块。synchronize解决了:原子性、可见性、有序性三个问题,用来保证多线程环境下共享变量的正确性。

🥇原子性:执行被synchronized修饰的方法和代码块,都必须要先获得类或者对象锁,执行完之后再释放锁,中间是不会中断的,这样就保证了原子性。

🥈可见性:执行被synchronized修饰的方法和代码块,一个线程获得了锁,执行完毕之后, 在释放锁之前,会对变量的修改同步回内存中,对其它线程是可见的。

🥉有序性synchronized保证了每个时刻都只有一个线程访问同步代码块或者同步方法,这样就相当于是有序的。

2. 使用示例

用一个示例来展示synchronized的用法,现在有两个线程,对一个变量进行自增10000000次操作,在正确的情况下,最后的结果应该是20000000。但是实际使用过程中可能会出现各种情况。

public class MainDemo extends Thread {private static int increment = 0;@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {increment++;}}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();// 阻塞主线程,直到t1和t2运行完毕t1.join();t2.join();System.out.println(increment);}}

2.1. 修饰普通方法

synchronized修饰普通方法只需要在方法上加上synchronized即可。synchronized修饰的方法,如果子类重写了这个方法,子类也必须加上synchronized关键字才能达到线程同步的效果。

public class MainDemo extends Thread {private static int increment = 0;@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {incrementMethod();}}public synchronized void incrementMethod() {increment++;}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();t1.join();t2.join();System.out.println(increment);}}

2.2. 修饰静态方法

synchronized作用于静态方法时,和实例方法类似,只需要在静态方法上面加上synchronized关键字即可。

public class MainDemo extends Thread {private static int increment = 0;@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {incrementMethod();}}public static synchronized void incrementMethod() {increment++;}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();t1.join();t2.join();System.out.println(increment);}}

2.3. 修饰同步代码块

修饰同步代码块可以使用:类对象和实例对象,但是要保证唯一性,多个线程使用的对象要是同一个对象。唯一性的意思就是说下面的objectLock必须是同一个对象,如果每个线程都新建一个对象,那么就达不到保证线程安全的效果。

public class MainDemo extends Thread {private static int increment = 0;private static Object objectLock = new Object();@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {synchronized (objectLock) {increment++;}}}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();t1.join();t2.join();System.out.println(increment);}}

3. 虚拟机标记

synchronized可以保证线程安全,那么虚拟机是如何识别synchronized的呢?

Java虚拟机层面标记synchronized修饰的代码有两种方式:

🥇ACC_SYNCHRONIZED标识位

🥈monitorentermonitorexit指令

3.1 同步代码块

synchronized修饰同步代码块的时候。

public class MainDemo extends Thread {private static int increment = 0;private static Object objectLock = new Object();@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {synchronized (objectLock) {increment++;}}}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();t1.join();t2.join();System.out.println(increment);}}

我们先编译这个类,然后再反编译反编译。

// 编译javac MainDemo.java// 反编译javap -v MainDemo.class

反编译之后输出如下内容,我们可以看到在13行和23行有monitorentermonitorexit两个指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令的时候需要去获取对象锁,执行monitorexit的时候释放对象锁。

public void run();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=4, args_size=10: iconst_01: istore_12: iload_13: ldc #2 // int 100000005: if_icmpge388: getstatic#3 // Field objectLock:Ljava/lang/Object;11: dup12: astore_213: monitorenter14: getstatic#4 // Field increment:I17: iconst_118: iadd19: putstatic#4 // Field increment:I22: aload_223: monitorexit24: goto3227: astore_328: aload_229: monitorexit30: aload_331: athrow32: iinc1, 135: goto238: return

3.2. 同步方法

synchronized修饰实例方法或者静态方法的时候。

public class MainDemo extends Thread {private static int increment = 0;@Overridepublic void run () {for (int i = 0; i < 10000000; i++) {incrementMethod();}}public synchronized void incrementMethod() {increment++;}public static void main(String[] args) throws InterruptedException {MainDemo mainDemo = new MainDemo();Thread t1 = new Thread(mainDemo);Thread t2 = new Thread(mainDemo);t1.start();t2.start();t1.join();t2.join();System.out.println(increment);}}

我们先编译这个类,然后再反编译反编译。

// 编译javac MainDemo.java// 反编译javap -v MainDemo.class

无论是实例方法还是静态方法,实现都是通过ACC_SYNCHRONIZED标识,反编译之后可以看到在方法上有ACC_SYNCHRONIZED标识,表明这是一个同步方法,线程执行这个方法的时候都会先去获取对象锁,方法执行完毕之后会释放对象锁。

public synchronized void incrementMethod();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZEDCode:stack=2, locals=1, args_size=10: getstatic#4 // Field increment:I3: iconst_14: iadd5: putstatic#4 // Field increment:I8: returnLineNumberTable:line 13: 0line 14: 8

🍉二、底层实现

1. 对象结构

实例对象在内存中的结构分了三个部分:对象头、实例数据、对齐填充。对象头又包含了:标记字段Mark Word、类型指针KlassPointer、长度Length field三个部分。

对象头:存储了锁状态标志、线程持有的锁等标志。

标记字段Mark Word:用于存储对象自身的运行时数据,他是经过轻量级锁和偏向锁的关键类型指针KlassPointer:是对象指向他的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例长度Length field:如果是数组对象,对象头还包含了数组长度。

实例数据:对象真正存储的有效信息,存放类的属性数据信息。

对齐填充:对齐填充不是必须存在的,仅仅时让对象的长度达到8字节的整数倍,其中一个原因就是为了不让对象数据跨缓存行,用空间换时间。

2. Mark Word结构

Mark word主要用于存储对象在运行时自身的一些数据,比如GC信息、锁信息等。在64位虚拟机中Mark Word的结构如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lORGkcDC-1689059958951)

3. 查看对象头结构数据

3.1. 引入相关的jar包

首先导入相关的包

<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.9</version></dependency>

3.2. 示例代码

public class SynDemo {public static void main(String[] args) {Object object = new Object();System.out.println(Integer.toString(object.hashCode(), 2));System.out.println(ClassLayout.parseInstance(object).toPrintable());}}

3.3. 对象头数据

hashcode: 10111101001111100111011000010java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE04 (object header) 01 c2 ce a7 (00000001 11000010 11001110 10100111) 44 (object header) 17 00 00 00 (00010111 00000000 00000000 00000000)84 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) 124 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

3.4. Mark Word分析

从对象头的结构可以得知,Mark word的大小为8个字节,那么我们输出的对象头数据的前两行就是``Mark word的内容,其中value就是Mark word的数据。同时我们也输出了对象头的hashcode`值,用于对比。

OFFSET SIZE TYPE DESCRIPTION VALUE04 (object header) 01 c2 ce a7 (00000001 11000010 11001110 10100111) 44 (object header) 17 00 00 00 (00010111 00000000 00000000 00000000)

Mark word值分了两种格式输出,前面是十六进制的,后面是二进制的,我们输出的hashcode值是:10111101001111100111011000010,对象头的hashcode是31位的,补全之后的hashcode值:0010111101001111100111011000010从二进制中好像是找不到对应的数据,下面做一个处理。

如下图我们将上面的二进制按照倒叙排列,图中红色方框内的数据就和hashcode值完全对应上了,再结合对象头Mark word结构,最后两位绿框就是锁的标识位。

4. 如何实现

synchronized是借助Java对象头来实现的,通过对象头的介绍,可以知道,对象头的Mark word里的数据是在变化的,不同的数据表示了不同类型的锁,而synchronized就是通过获取这些锁来实现线程安全的。

前面我们说了当我们使用synchronized来保证线程安全的时候,虚拟机在编译代码的时候,会添加标记:ACC_SYNCHRONIZEDmonitorentermonitorexit指令。

当虚拟机执行代码的时候,如果发现了这些标记,那么就会让线程去获取对象锁,也就是去修改对象头的数据,只有获取到锁的线程才能继续执行代码,其它的线程则需要等待,直到获取锁的线程释放锁。

🍏三、锁升级过程

在1.6之前,synchronized只有重量级锁,在1.6版本对synchronized锁进行了优化,有了偏向锁,轻量级锁。

由此锁升级有四种状态:无锁,偏向锁,轻量级锁,重量级锁。锁升级是不可逆的,只能升级不能降级。

锁的升级是通过对象头的Mark word的数据变化来完成的,数据会根据锁变化而变化。

1. 无锁

1.1 Mark Word结构

无锁就是没有线程来抢站对象头这个时候的Mark word的数据如下:

1.2. 对象头结构数据

public class SynDemo {public static void main(String[] args) {Object object = new Object();System.out.println(Integer.toString(object.hashCode(), 2));System.out.println(ClassLayout.parseInstance(object).toPrintable());}}

可以看到无锁的时候对象头最后八位的数据是00000001,标识锁的两位是01。

hashcode: 10111101001111100111011000010java.lang.Object object internals:OFFSET SIZE TYPE DESCRIPTION VALUE04 (object header) 01 c2 ce a7 (00000001 11000010 11001110 10100111) 44 (object header) 17 00 00 00 (00010111 00000000 00000000 00000000)84 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) 124 (loss due to the next object alignment)Instance size: 16 bytesSpace losses: 0 bytes internal + 4 bytes external = 4 bytes total

2. 偏向锁

2.1 Mark Word结构

偏向锁的Mark word锁标志位和无锁一样是01,是否偏向锁是1

2.2 什么是偏向锁

偏向锁主要是来优化同一个线程多次获取同一个锁的,有时候线程t在执行同步代码的时候先去获取锁,执行完了之后不会释放锁,然后第二次线程t第二次执行同步代码的时候先去获取锁,发现Mark word的线程ID就是它,就不需要重新加锁。

在JDK1.6之后是默认开启偏向锁的,但是我们在使用的时候是绕过偏向锁了直接进入轻量级锁,这是因为虽然默认开启了偏向锁,但是开启是有延迟的,大概是4s钟,也即是程序刚启动创建的对象是不会开启偏向锁的,4秒之后创建的对象才会开启,可以通过JVM参数来设置延迟时间。

//关闭延迟开启偏向锁-XX:BiasedLockingStartupDelay=0//禁止偏向锁-XX:-UseBiasedLocking //启用偏向锁-XX:+UseBiasedLocking

在JDK15中偏向锁已经被标记为Deprecate

2.3 加锁过程

线程获取偏向锁的过程,当线程执行被synchronized修饰的同步代码块的时候

1.检查锁标志位是否是01

2.检查是否是偏向锁

3.如果不是偏向锁,直接通过CAS替换Mark word的线程ID为当前线程ID,并修改是否偏向锁为1

4.如果是偏向锁,检查Mark word的线程ID是否是当前线程的ID,如果是的话直接就执行同步代码块,如果不是当前线程的线程ID也是通过CAS替换Mark word的线程ID为当前线程ID

5.CAS成功之后便执行同步代码

2.4 锁升级过程

上面我们说在加锁的过程中都是通过CAS操作替换Mark word的线程ID为当前线程的ID,如果CAS失败了就可能会升级为轻量级锁

1.当CAS失败的时候,原持有偏向锁到达线程安全点的时候

2.检查原持有偏向锁的线程的线程状态

3.如果原持有偏向锁的线程还没有退出同步代码块就升级为轻量级锁,并且仍然由原线程持有轻量级锁

4.如果原持有偏向锁的线程已经退出同步代码块了,偏向锁撤销Mark word的线程ID更新为空,是否偏向改为0

5.如果原线程不竞争锁,则偏向锁偏向后来的线程,如果原线程要竞争锁,则升级为轻量级锁

如果调用了对象的hashcode方法或者执行了wait和 notify方法,锁升级为重量级锁。

2.2 对象头结构数据

设置睡眠时间为5秒,这样才会进入偏向锁,只有一个线程来竞争锁,所以会转向偏向锁

public class SynDemo {public static void main(String[] args) throws InterruptedException {Thread.sleep(5000);SynDemo synDemo = new SynDemo();synchronized (synDemo) {System.out.println(ClassLayout.parseInstance(synDemo).toPrintable());}}}

这里只有一个线程在竞争锁,所以锁就是偏向锁,锁标志位是01,是否偏向是1。

OFFSET SIZE TYPE DESCRIPTION VALUE04 (object header) 05 88 80 54 (00000101 10001000 10000000 01010100) 44 (object header) c2 7f 00 00 (11000010 01111111 00000000 00000000) 84 (object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000)

3. 轻量级锁

3.1 Mark Word结构

3.2什么是轻量级锁

当偏向锁,被另外的线程访问的时候,偏向锁就会升级为轻量级锁,其它线程会通过自旋的形式尝试获取锁,不会阻塞线程,从而提高了性能。

3.3 加锁过程

1.在当前线程的栈帧中建立一个名为锁记录Lock Record空间

2.拷贝对象头的Mark word到锁记录空间,这个拷贝的过程官方称为Displaced Mark Word

3.使用CAS操作把Mark Word中的指针指向线程的锁记录空间,更新锁标志位为00

4.当线程持有偏向锁并且偏向锁升级为轻量级锁,

5.如果线程是持有偏向锁升级为轻量级锁那么不用通过CAS获取锁,而是直接持有锁

3.4 释放锁

当线程执行完同步代码块的时候,就会释放锁

1.用CAS操作把对象当前的Mark Word和线程中复制的Displaced Mark Word替换回来

2.如果替换成功,同步过程就完成了

3.如果替换不成功,说明有其它线程尝试获取该锁,就要在释放锁的同时,唤醒被挂起的线程

3.5 锁升级过程

当线程通过CAS获取轻量级锁,如果CAS的次数过多,没有获取到轻量级锁,那么锁就会升级为重量级锁。

除此之外一个线程在持有锁,一个在自旋,又有第三个线程来竞争锁,轻量级锁升级为重量级锁。

次数

3.6 对象头结构数据

public class SynDemo {public static void main(String[] args) throws InterruptedException {SynDemo synDemo = new SynDemo();Thread t1 = new Thread(() -> {synchronized (synDemo) {System.out.println(ClassLayout.parseInstance(synDemo).toPrintable());}});t1.start();}}

OFFSET SIZE TYPE DESCRIPTION VALUE04 (object header)08 49 20 11 (00001000 01001001 00100000 00010001)44 (object header)00 70 00 00 (00000000 01110000 00000000 00000000)84 (object header)05 c1 00 f8 (00000101 11000001 00000000 11111000)

4. 重量级锁

4.1 Mark word结构

4.2 什么是重量级锁

在Java1.6之前synchronized的实现只能通过重量级锁实现,在1.6之后当轻量级锁自旋一定次数后还是没有获取到锁,此时锁就会升级为重量级锁。

重量级锁在竞争锁的时候,除了持有锁的线程,其它竞争锁的线程都会在等待队列中,防止不必要的开销。

4.3 对象头数据

public class SynDemo {public static void main(String[] args) throws InterruptedException {SynDemo synDemo = new SynDemo();Thread t1 = new Thread(() -> {synchronized (synDemo){System.out.println(ClassLayout.parseInstance(synDemo).toPrintable());try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});Thread t2 = new Thread(() -> {synchronized (synDemo){System.out.println(ClassLayout.parseInstance(synDemo).toPrintable());try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}});t1.start();t2.start();}}

OFFSET SIZE TYPE DESCRIPTIONVALUE04(object header) 3a f2 6f 1c (00111010 11110010 01101111 00011100) 44(object header) 00 00 00 00 (00000000 00000000 00000000 00000000) 84(object header) 05 c1 00 f8 (00000101 11000001 00000000 11111000)

4.3 重量级锁实现原理

重量级锁是借助Monitor来实现的,在Java虚拟机中Monitor机制是基于C++实现的,每一个Monitor都有一个ObjectMonitor对象。当锁升级为重量级锁的时候Mark word中的指针就指向ObjectMonitor对象地址。通过ObjectMonitor就可以实现互斥访问同步代码。

ObjectMonitor的部分变量,用于存储锁竞争过程中的一些值。

ObjectMonitor() {// 处于wait状态的线程,会被加入到_WaitSetObjectWaiter * volatile _WaitSet;//处于等待锁block状态的线程,会被加入到该列表ObjectWaiter * volatile _EntryList;// 指向持有ObjectMonitor对象的线程void* volatile _owner;// _header是一个markOop类型,markOop就是对象头中的Mark Wordvolatile markOop _header;// 抢占该锁的线程数,约等于WaitSet.size + EntryList.sizevolatile intptr_t _count;// 等待线程数volatile intptr_t _waiters;// 锁的重入次数volatile intptr_ _recursions;// 监视器锁寄生的对象,锁是寄托存储于对象中void* volatile _object;// 操作WaitSet链表的锁volatile int _WaitSetLock;// 嵌套加锁次数,最外层锁的_recursions属性为0volatile intptr_t _recursions;// 多线程竞争锁进入时的单向链表ObjectWaiter * volatile _cxq;}

objectMoniotr源码解析和重量级锁底层实现原理参考:Java多线程:objectMonitor源码解析(4)

4.4 重量级锁加锁释放锁过程

当线程获取Monitor锁时,首先线程会被加入到_EntryList队列当中,当某个线程获取到对象的monitor锁后将ObjectMonitor中的_owner变量设置为当前线程,同时ObjectMonitor中的计数器_count加1即获得锁对象。

若持有monitor的线程调用wait()方法,将释放当前持有的monitor_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行执行完毕也将释放monitor,以便其它线程线程进入获取monitor

1.没有线程来竞争锁的时候,ObjectMonitor_ownernull

2.现在有三个线程来获取对象锁,线程首先被封装成ObjectWait对象,然后进入到_EntryList队列中,竞争对象锁。

3.当t1获取到对象锁的时候,ObjectMonitor对象的_owner指向t1,_count数量加1。

4.执行完毕之后释放锁,然后又进行下一轮锁竞争。

🍓四、锁优化

1. 适应性自旋锁

在轻量级锁中,当线程竞争不到锁的时候,是通过CAS自旋一直去获取尝试获取锁,这样就不用放弃CPU的执行时间片

这一过程有一个缺点就是如果持有锁的线程运行的时间很长的话,那么自旋的线程一直占用CPU又不能执行就很浪费资源,可以通过JVM参数设置自旋的次数,如果超过这个次数还没有获取到锁就升级为重量级锁,挂起当前线程。

// 设置自旋次数上限-XX:PreBlockSpin=10

为了优化自旋占用CPU执行时间片,JVM引入了适应性自旋,适应性自旋会根据前面获取锁的线程自旋的次数来自动调整。

如果前面的线程通过自选获取到了锁,那么JVM会自动增加自旋上限次数。

如果前面的线程自旋很少能获取到锁,那么就会挂起当前线程,升级为重量级锁。

2. 锁消除

2.1 什么是锁消除

Java代码在编译的时候,消除不可能存在共享资源竞争的锁,通过这种方式消除没必要的锁,减少无意义的请求锁时间。

锁消除的依据JIT编译的时候进行逃逸分析,如果当前对象作用域只在方法的内部,那么JVM就认为这个锁可以消除。

// 关闭锁消除-XX:-EliminateLocks// 开启逃逸分析-XX:+DoEscapeAnalysis// 开启锁消除-XX:+EliminateLocks

2.2 代码示例

虽然Demo类的synMethod是一个同步方法,但是demo类是一个局部变量,根据逃逸分析该对象只会在栈上分配

属于线程私有的,所以会自动消除锁。

public class Demo {public static void main(String[] args) {Demo demo = new Demo();demo.synMethod();}private synchronized void synMethod() {System.out.println("同步方法");}}

3. 锁粗化

3.1 什么是锁粗化

锁粗化就是把锁的范围加大到整个同步代码的外部,这样能降低频繁的获取锁,从而提升性能。

如下面这段代码,在循环体内加锁,可以把锁加到循环体的外部,这样减少了加锁的次数,提升了性能

3.2 代码示例

for(int i = 0; i < 10; i++){synchronized(lock){}}synchronized(lock) {for (int i = 0; i < 10; i++) {}}

参考资料

/post/7232524757526429756

锁升级图示

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。