【开发宝典】Java并发系列教程(四)

京东云开发者
• 阅读 377

作者:京东零售 刘跃明

Monitor概念

Java对象的内存布局

对象除了我们自定义的一些属性外,还有其它数据,在内存中可以分为三个区域:对象头、实例数据、对齐填充,这三个区域组成起来才是一个完整的对象。

对象头:在JVM中需要大量存储对象,存储时为了实现一些额外的功能,需要在对象中添加一些标记字段用于增强对象功能,这些标记字段组成了对象头。

实例数据:存放类的属性数据信息,包括父类的属性信息。

对齐填充:由于虚拟机要求对象其实地址必须是8字节的整数倍,需要存在填充区域以满足8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。



【开发宝典】Java并发系列教程(四)



图1

Java对象头

JVM中对象头的方式有以下两种(以32位虚拟机为例):

普通对象

Object Header (64 bits)
Mark Word (32 bits) Klass Word (32 bits)

数组对象

Object Header (96 bits)
Mark Word(32bits) Klass Word(32bits) array length(32bits)

Mark Word

这部分主要用来存储对象自身的运行数据,如hashcode、gc分带年龄等,Mark Word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark Word为32位,64位JVM为64位。为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
lock:2 Marked for GC

其中各部分的含义如下:

lock: 2位的锁状态标记位,该标记的值不同,整个Mark Word表示的含义不同。

biased_lock lock 状态
0 01 无锁
1 01 偏向锁
0 00 轻量级锁
0 10 重量级锁
0 11 GC标记

biased_lock: 对象是否启用偏向锁标记,只占1个二进制位,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age: 4位的Java对象年龄,在GC中,如果对象再Survivor区复制一次,年龄增加1,当对象达到设定的阈值时,将会晋升到老年代,默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6,由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode: 25位的对象表示Hash码,采用延迟加载技术,调用方法System.idenHashcode()计算,并会将结果写到该对象头中,当对象被锁定时,该值会移动到管程Monitor中。

thread: 持有偏向锁的线程ID。

epoch: 偏向时间戳。

ptr_to_lock_record: 指向栈中锁记录的指针。

ptr_to_heavyweight_monitor: 指向管程Monitor的指针。

Klass Word

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例,该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

array length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM长度为32位,64位JVM则为64位。

Monitor原理

Monitor被翻译为监视器管程

每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针。

Monitor结构如下:



【开发宝典】Java并发系列教程(四)



图2

•刚开始Monitor中Owner为null

•当Thread-2执行synchronized(obj)就会将Monitor的所有者Owner置为Thread-2,Monitor中只能有一个Owner

•在Thread-2上锁的过程中,如果Thread-3、Thread-4、Thread-5也来执行synchronized(obj),就会进入EntryList BLOCKED

•Thread-2执行完同步代码块的内容,然后唤醒EntryList中等待的线程来竞争锁,竞争是非公平的,也就是先进并非先获取锁

•图2中WaitSet中的Thread-0、Thread-1是之前获得过锁,但条件不满足进入WAITING状态的线程,后面讲wait-notify时会分析

注意:

•synchronized必须是进入同一个对象的Monitor才有上述的效果

•不加synchronized的对象不会关联监视器,不遵从以上规则

synchronized原理

static final Object lock = new Object();
static int counter = 0;

public static void main(String[] args) {
    synchronized (lock) {
        counter++;
    }
}

对应的字节码为:

public static main([Ljava/lang/String;)V

TRYCATCHBLOCK L0 L1 L2 null

TRYCATCHBLOCK L2 L3 L2 null

L4

LINENUMBER 6 L4

GETSTATIC MyClass03.lock : Ljava/lang/Object;

DUP

ASTORE 1

MONITORENTER //注释1

L0

LINENUMBER 7 L0

GETSTATIC MyClass03.counter : I

ICONST_1

IADD

PUTSTATIC MyClass03.counter : I

L5

LINENUMBER 8 L5

ALOAD 1

MONITOREXIT //注释2

L1

GOTO L6

L2

FRAME FULL [[Ljava/lang/String; java/lang/Object] [java/lang/Throwable]

ASTORE 2

ALOAD 1

MONITOREXIT //注释3

L3

ALOAD 2

ATHROW

L6

LINENUMBER 9 L6

FRAME CHOP 1

RETURN

L7

LOCALVARIABLE args [Ljava/lang/String; L4 L7 0

MAXSTACK = 2

MAXLOCALS = 3

注释1

MONITORENTER的意思为:每个对象都有一个监视锁(Monitor),当Monitor被占用时就会处于锁定状态,线程执行MONITORENTER指令时尝试获取Monitor的所有权,过程如下:

•如果Monitor的进入数为0,则该线程进入Monitor,并将进入数设置为1,该线程即为Monitor的所有者(Owner)

•如果该线程已经占用Monitor,只是重新进入Monitor,则进入Monitor的进入数加1

•如果其它线程已经占用Monitor,则该线程进入阻塞状态,直到Monitor进入数为0,再重新尝试获取Monitor的所有权

注释2

MONITOREXIT的意思为:执行指令时,Monitor的进入数减1,如果减1后进入数为0,该线程退出Monitor,不再是这个Monitor的所有者,其它被Monitor阻塞的线程重新尝试获取Monitor的所有权。

总结

通过注释1和注释2可知,synchronized的实现原理,底层是通过Monitor的对象来完成,其实wait和notify等方法也依赖Monitor,这就是为什么wait和notify方法必须要在同步方法内调用,否则会抛出java.lang.IllegalMonitorStateException的原因。

如果程序正常执行则按上述描述即可完成,如果程序在同步方法内发生异常,代码则会走注释3,在注释3可以看到MONITOREXIT指令,也就是synchronized已经处理异常情况下的退出。

注:方法级别的synchronized不会在字节码指令中有所体现,而是在常量池中增加了ACC_SYNCHRONIZED标识符,JVM就是通过该标识符来实现同步的,方法调用时,JVM会判断方法的ACC_SYNCHRONIZED是否被设置,如果被设置,线程执行方法前会先获取Monitor所有权,执行完方法后再释放Monitor所有权,本质是一样的。

synchronized原理进阶

轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。

轻量级锁对使用者是透明的,即语法仍然是synchronized

假设有两个方法同步块,利用同一个对象加锁

static final Object obj = new Object();

public static void method1() {
    synchronized (obj) { // 同步块 A
        method2();
    }
}

public static void method2() {
    synchronized (obj) { // 同步块 B
    }
}

创建锁记录(Lock Record)对象,每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word



【开发宝典】Java并发系列教程(四)



图3

让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录



【开发宝典】Java并发系列教程(四)



图4

如果cas替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁,这是图示如下



【开发宝典】Java并发系列教程(四)



图5

如果cas失败,有两种情况

•如果是其它线程已经持有了该Object的轻量级锁,这是表明有竞争,进入锁膨胀过程

•如果是自己线程执行了synchronized锁重入,那么再添加一条Lock Record作为重入的技术



【开发宝典】Java并发系列教程(四)



图6

当退出synchronized代码块(解锁时),如果有取值为null的锁记录,表示由重入,这是重置锁记录,表示重入技术减一



【开发宝典】Java并发系列教程(四)



图7

当退出synchronized代码块(解锁时),锁记录的值不为null,这时使用cas将Mark Word的值回复给对象头

•成功,则解锁成功

•失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

锁膨胀

如果在尝试加轻量级锁的过程中,CAS操作无法成功,这是一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这是需要进行锁膨胀,将轻量级锁变为重量级锁。

当Thread-1进行轻量级加锁时,Thread-0已经对该对象加了轻量级锁



【开发宝典】Java并发系列教程(四)



图8

这是Thread-1加轻量级锁失败,进入锁膨胀流程

•即为Object对象申请Monitor锁,让Object指向重量级锁地址

•然后自己进入Monitor的EntryList BLOCKED



【开发宝典】Java并发系列教程(四)



图9

当Thread-0退出同步块解锁时,使用cas将Mark Word的值恢复给对象头,失败,这是会进入重量级解锁流程,即按照Monitor地址找到Monitor对象,设置Owner为null,唤醒EntryList 中BLOCKED线程

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步,释放了锁),这是当前线程就可以避免阻塞。

自旋重试成功的情况

线程1(core 1上) 对象Mark 线程2(core 2 上)
- 10(重量锁) -
访问同步块,获取Monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取Monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行完毕 10(重量锁)重量锁指针 自旋重试
成功(解锁) 01(无锁) 自旋重试
- 10(重量锁)重量锁指针 成功(加锁)
- 10(重量锁)重量锁指针 执行同步块
-

自旋重试失败的情况

线程1(core 1上) 对象Mark 线程2(core 2 上)
- 10(重量锁) -
访问同步块,获取Monitor 10(重量锁)重量锁指针 -
成功(加锁) 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 -
执行同步块 10(重量锁)重量锁指针 访问同步块,获取Monitor
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 自旋重试
执行同步块 10(重量锁)重量锁指针 阻塞
-

•自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。

•在Java 6之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

•Java 7之后不能控制是否开启自旋功能。

偏向锁

轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行CAS操作。

Java 6中引入了偏向锁做进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。

注:

Java 15之后废弃偏向锁,默认是关闭,如果想使用偏向锁,配置-XX:+UseBiasedLocking启动参数。

启动偏向锁之后,偏向锁有一个延迟生效的机制,这是因为JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。这个延时的时间大概为4s左右,具体时间因机器而异。当然我们也可以设置JVM参数 -XX:BiasedLockingStartupDelay=0 来取消延时加载偏向锁。

例如:

static final Object obj = new Object();

public static void m1() {
    synchronized (obj) { // 同步块 A
        m2();
    }
}

public static void m2() {
    synchronized (obj) { // 同步块 B
        m3();
    }
}

public static void m3() {
    synchronized (obj) {
    }
}

如果关闭偏向锁,使用轻量锁情况:



【开发宝典】Java并发系列教程(四)



图10

开启偏向锁,使用偏向锁情况:



【开发宝典】Java并发系列教程(四)



图11

偏向状态

回忆一下对象头格式

Mark Word (32 bits) State
identity_hashcode:25 age:4 biased_lock:1 lock:2 Normal
thread:23 epoch:2 age:4 biased_lock:1 lock:2 Biased
ptr_to_lock_record:30 lock:2 LightweightLocked
ptr_to_heavyweight_monitor:30 lock:2 HeavyweightLocked
lock:2 Marked for GC

一个对象创建时:

•如果开启了偏向锁(默认开启),那么对象创建后,Mark Word值为0x05,也就是最后是3位为101,这是它的thread、epoch、age都为0

•如果没有开启偏向锁,那么对象创建后,Mark Word值为0x01,也就是最后3位为001,这时它的hashcode、age都为0,第一次用到hashcode时才会赋值

我们来验证下,使用jol第三方工具,以及对工具打印对象头做了一个处理,让对象头开起来更简便:

测试代码

public synchronized static void main(String[] args){
    log.info("{}", toSimplePrintable(object));
}

开启偏向锁的情况下

打印的数据如下(由于Java15之后偏向锁废弃,因此打开偏向锁打印会警告)

17:15:17 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

最后为101,其他都为0,验证了上述第一条。

可能你又要问了,我这也没使用synchronized关键字呀,那不也应该是无锁么?怎么会是偏向锁呢?

仔细看一下偏向锁的组成,对照输出结果红色划线位置,你会发现占用 thread 和 epoch 的 位置的均为0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,准备好进行偏向了!你也可以理解为此时的偏向锁是一个特殊状态的无锁

关闭偏向锁的情况下

打印的数据如下

17:18:32 [main] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

最后为001,其它都是0,验证了上述第二条。

接下来验证加锁的情况,代码如下:

private static Object object = new Object();
public synchronized static void main(String[] args){
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

开启偏向锁的情况,打印数据如下

17:24:05 [t1] c.MyClass03 - synchronized前

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:24:05 [t1] c.MyClass03 - synchronized中

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

17:24:05 [t1] c.MyClass03 - synchronized后

17:24:05 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001110 00000111 01001000 00000101

使用了偏向锁,并记录了线程的值(101前面的一串数字),但是处于偏向锁的对象解锁后,线程id仍存储于对象头中。

关闭偏向锁的情况,打印数据如下

17:28:24 [t1] c.MyClass03 - synchronized前

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

17:28:24 [t1] c.MyClass03 - synchronized中

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01110000 00100100 10101001 01100000

17:28:24 [t1] c.MyClass03 - synchronized后

17:28:24 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

使用轻量锁(最后为000),并且记录了占中存储的锁信息地址(000前面一串数字),同步块结束后恢复到原先状态(因为没有使用hashcode,所以hashcode值为0)。

偏向锁撤销

在真正讲解偏向撤销之前,需要和大家明确一个概念——偏向锁撤销和偏向锁释放是两码事。

•撤销:笼统的说就是多个线程竞争导致不能再使用偏向模式的时候,主要是告知这个锁对象不能再用偏向模式

•释放:和你的常规理解一样,对应的就是 synchronized 方法的退出或 synchronized 块的结束

何为偏向撤销?

从偏向状态撤回原有的状态,也就是将 MarkWord 的第 3 位(是否偏向撤销)的值,从 1 变回 0

如果只是一个线程获取锁,再加上「偏心」的机制,是没有理由撤销偏向的,所以偏向的撤销只能发生在有竞争的情况下

撤销-hashcode调用

调用了对象的hashcode会导致偏向锁被撤销:

•轻量级锁会在锁记录中记录hashcode

•重量级锁会在Monitor中记录hashcode

测试代码如下

private static Object object = new Object();
public synchronized static void main(String[] args){
    object.hashCode();//调用hashcode
    new Thread(()->{
        log.info("{}", "synchronized前");
        log.info("{}", toSimplePrintable(object));
        synchronized (object){
            log.info("{}", "synchronized中");
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", "synchronized后");
        log.info("{}", toSimplePrintable(object));
    },"t1").start();
}

打印如下:

17:36:05 [t1] c.MyClass03 - synchronized前

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

17:36:06 [t1] c.MyClass03 - synchronized中

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01101110 00010011 11101001 01100000

17:36:06 [t1] c.MyClass03 - synchronized后

17:36:06 [t1] c.MyClass03 - 00000000 00000000 00000000 01011111 00100001 00001000 10110101 00000001

撤销-其它线程使用对象

当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁。

测试代码如下

private static void test2() {
    Thread t1 = new Thread(() -> {
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        synchronized (MyClass03.class) {
            MyClass03.class.notify();//t1执行完之后才通知t2执行
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (MyClass03.class) {
            try {
                MyClass03.class.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
        }
        log.info("{}", toSimplePrintable(object));
    }, "t2");
    t2.start();
}

打印数据如下

17:51:38 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01000111 00000000 11101000 00000101

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000001 01111000 00100000 01101001 01010000

17:51:38 [t2] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001

可以看到线程t1是使用偏向锁,线程t2使用锁之前是一样的,但是一旦使用了锁,便升级为轻量级锁,执行完同步代码之后,恢复成撤销偏向锁的状态。

撤销-调用wait/notify

代码如下

private static void test3(){
    Thread t1 = new Thread(() -> {
        log.info("{}", toSimplePrintable(object));
        synchronized (object) {
            log.info("{}", toSimplePrintable(object));
            try {
                object.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.info("{}", toSimplePrintable(object));
        }
    }, "t1");
    t1.start();
    new Thread(() -> {
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        synchronized (object) {
            log.debug("notify");
            object.notify();
        }
    }, "t2").start();
}

打印数据如下

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000101

17:57:57 [t1] c.MyClass03 - 00000000 00000000 00000000 00000001 00001111 00001100 11010000 00000101

17:58:02 [t2] c.MyClass03 - notify

17:58:02 [t1] c.MyClass03 - 00000000 00000000 01100000 00000000 00000011 11000001 10000010 01110010

调用wait和notify得是用Monitor,所以会从偏向锁升级为重量级锁。

批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这是偏向了线程t1的对象仍然有机会重新偏向t2,重偏向会重置对象的Thread ID。

当撤销偏向锁阈值超过20次后,JVM会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程。

代码如下

public static class Dog{}

private static void test4() {
    Vector<Dog> list = new Vector<>();
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 30; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
        }
        synchronized (list) {
            list.notify();
        }
    }, "t1");
    t1.start();
    Thread t2 = new Thread(() -> {
        synchronized (list) {
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        log.debug("===============> ");
        for (int i = 0; i < 30; i++) {
            Dog d = list.get(i);
            log.info("{}", i+"\t"+toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i+"\t"+toSimplePrintable(d));
            }
            log.info("{}", i+"\t"+toSimplePrintable(d));
        }
    }, "t2");
    t2.start();
}

打印如下



【开发宝典】Java并发系列教程(四)



图12

另外我在测试的是否发现一个线程,当对象是普通类(如Dog)时,重偏向的阈值就是20,也就是第21次开启了偏向锁,但是如果把普通类替换成Object时,重偏向的阈值就是9,也就是第10次开启了偏向锁并重偏向(如图13),这是怎么回事儿,有了解的同学可以评论交流下。



【开发宝典】Java并发系列教程(四)



图13

批量撤销

当撤销偏向锁阈值超过40次后,JVM会这样觉得,自己确实偏向错了,根本不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

代码如下

static Thread t1, t2, t3;

private static void test6() throws InterruptedException {
    Vector<Dog> list = new Vector<>();
    int loopNumber = 40;
    t1 = new Thread(() -> {
        for (int i = 0; i < loopNumber; i++) {
            Dog d = new Dog();
            list.add(d);
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
        }
        LockSupport.unpark(t2);
    }, "t1");
    t1.start();
    t2 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
        LockSupport.unpark(t3);
    }, "t2");
    t2.start();
    t3 = new Thread(() -> {
        LockSupport.park();
        log.debug("===============> ");
        for (int i = 0; i < loopNumber; i++) {
            Dog d = list.get(i);
            log.info("{}", i + "\t" + toSimplePrintable(d));
            synchronized (d) {
                log.info("{}", i + "\t" + toSimplePrintable(d));
            }
            log.info("{}", i + "\t" + toSimplePrintable(d));
        }
    }, "t3");
    t3.start();
    t3.join();
    log.info("{}", toSimplePrintable(new Dog()));
}

打印如下



【开发宝典】Java并发系列教程(四)



图14

点赞
收藏
评论区
推荐文章
秋招已经开始准备了!【Java面试题】最新Java开发岗面试知识笔记
在最近两个月不断的面试中,我分类总结了Java开发岗位面试中的一些知识点。主要包括以下几个部分:1.Java基础知识点2.Java常见集合3.高并发编程(JUC包)4.JVM内存管理5.Java8知识点6.网络协议相关7.数据库相关8.MVC框架相关9.大数据相关10.Linux命令相关面试,
Wesley13 Wesley13
3年前
java锁学习(一)
作用能够保证同一时刻,最多只有一个线程执行该段代码,以达到并发安全的效果主要用于同时刻对线程间对任务进行锁地位synchronized是JAVA的原生关键字,是JAVA中最基本的互斥手段,是并发编程中的元老角色不使用并发的后果不使用并发会导致多线程情况下,同一个数据被多个线程同时更改,造成结果和预期不一致
Wesley13 Wesley13
3年前
Java 并发编程之美
一、前言并发编程相比Java中其他知识点学习门槛较高,从而导致很多人望而却步。但无论是职场面试,还是高并发/高流量的系统的实现,却都离不开并发编程,于是能够真正掌握并发编程的人成为了市场迫切需求的人才。二、学习并发编程Java并发编程作为Java技术栈中的一块顶梁柱,其学习成本还是比较大的,很多人学习起来感到没有头
Wesley13 Wesley13
3年前
Java 开发岗面试知识点解析
在不断的面试中,分类总结了Java开发岗位面试中的一些知识点。主要包括以下几个部分:1.Java基础知识点2.Java常见集合3.高并发编程(JUC包)4.JVM内存管理5.Java8知识点6.网络协议相关7.数据库相关8.MVC框架相关9.大数据相关10.Linux命令相
可莉 可莉
3年前
2021 程序员修炼内功必备:阿里新产 Java 并发编程原理笔记(全彩版)限时开源!
写在前面:近年来在大厂的面试中,高并发不但占比较多,而且已经不局限于并发工具的使用,更多的会深入到底的层实现原理,这样能考察候程序员的内功,看其是否能知其所以然。关于市面上关于Java并发编程的资料感觉有些知识点不是很清晰,于是展开了对Java并发编程原理的讨论。在这收集整理了这些Java并发编程原理整理成书籍,分享给大家。目录
Wesley13 Wesley13
3年前
Java并发概述之安全
Java并发的学习内容主要来自《Java并发编程实战》一书,本文为一概述。并发最简单的解释应该是不同任务的执行时间区间存在交集。由于时间上的交集共享变量,并发会带来安全问题。从任务的角度而言,任务的执行需要得到正确的效果;从对象的角度而言,对象需要被正确的访问。所谓正确,或常说的线程安全,包括了一个对象操作,或者一个任务执行的三个方面:前置条件
Stella981 Stella981
3年前
2021 程序员修炼内功必备:阿里新产 Java 并发编程原理笔记(全彩版)限时开源!
写在前面:近年来在大厂的面试中,高并发不但占比较多,而且已经不局限于并发工具的使用,更多的会深入到底的层实现原理,这样能考察候程序员的内功,看其是否能知其所以然。关于市面上关于Java并发编程的资料感觉有些知识点不是很清晰,于是展开了对Java并发编程原理的讨论。在这收集整理了这些Java并发编程原理整理成书籍,分享给大家。目录