管程

管程

1、Monitor

2、保证同一时刻,只有一个进程在管程内活动,即管程内定义的操作在同一时刻,只被一个进程调用(由编译器实现),但不能保证进程以设计的顺序执行

3、JVM 中同步是基于进入、退出管程对象实现

(1)每个对象都会有一个管程对象

(2)管程随着 Java 对象一同创建、销毁

4、执行线程首先要持有管程对象,然后才能执行方法,当方法完成后释放管程,方法在执行时持有管程,其他线程无法再获取同一个管程

 

线程安全

1、临界区

(1)Critical Section

(2)一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区

2、竞态条件

(1)Race Condition

(2)多个线程在临界区内执行,由于代码的执行序列不同,而导致结果无法预测,称之为发生竞态条件

3、避免临界区的竞态条件发生

(1)阻塞式的解决方案:synchronized,Lock

(2)非阻塞式的解决方案:原子变量

 

1、将多个线程对共享数据的并发访问,转换为串行访问,即一个共享数据一次只能被一个线程访问

2、锁具有排他性(Exclusive),即一个锁一次只能被一个线程持有,这种锁称为排它锁 / 互斥锁(Mutex)

3、JVM 把锁分为内部锁、显示锁

(1)内部锁:synchronized 关键字

(2)显示锁:java.concurrent.locks.Lock 接口的实现类

4、作用

(1)通过互斥保障原子性:一个锁只能被一个线程持有,保证临界区的代码一次只能被一个线程执行

(2)通过写线程冲刷处理器的缓存、读线程刷新处理器缓存,实现可见性:在 Java 平台中,锁的获得隐含刷新处理器缓存的动作,锁的释放隐含冲刷处理器缓存的动作

(3)有序性:写线程在临界区所执行的,在读线程所执行的临界区中,是完全按照源码顺序执行

5、使用锁必须满足以下条件

(1)多个线程在访问共享数据时,必须使用同一个锁

(2)即使是读取共享数据的线程,也需要使用同步锁

 

synchronized

1、对象锁

(1)采用互斥的方式,让同一时刻至多只有一个线程,能持有对象锁

(2)其它线程再想获取该对象锁时,就会阻塞住

(3)保证拥有锁的线程,可以安全的执行临界区内的代码,不用担心线程上下文切换

2、Java 中互斥、同步都可以采用 synchronized 来完成

(1)互斥:保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码

(2)同步:由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

3、synchronized 实际是用对象锁,保证临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断

4、面向对象的改进:创建锁类

(1)存放共享资源

(2)方法封装同步代码

5、语法

synchronized(锁对象) {
    临界区;
}

6、方法上的 synchronized

(1)加在成员方法上,锁住当前实例对象

class Test{
    public synchronized void test() {

    }
}
//等价于
class Test{
    public void test() {
        synchronized(this) {

        }
    }
}

(2)加在静态方法上,锁住所属 Class 对象

class Test{
    public synchronized static void test() {
    }
}
//等价于
class Test{
    public static void test() {
        synchronized(Test.class) {

        }
    }
}

7、使用一个常量对象作为锁对象,不同方法中的同步代码块也可以同步

 

成员变量、静态变量是否线程安全

1、如果它们没有共享,则线程安全

2、如果它们被共享,根据它们的状态是否能够改变,分两种情况

(1)如果只有读操作,则线程安全

(2)如果有读写操作,则这段代码是临界区,需要考虑线程安全

 

局部变量是否线程安全

1、局部变量是线程安全的

(1)因为局部变量存放在栈帧中,栈帧是线程私有的

(2)多个栈帧,就有多个局部变量,不被多个线程共享

2、局部变量引用的对象未必安全

(1)如果该对象没有逃离方法的作用访问,它是线程安全的

(2)如果该对象逃离方法的作用范围,需要考虑线程安全

 

暴露局部变量

1、只把方法的访问修饰符,将 private 修改为 public,不会暴露局部变量

2、例:子类重写方法,并创建新线程,引用父类方法局部变量,线程不安全

(1)private、final:父类方法不能被子类重写,防止暴露局部变量

(2)fianl:引用不可变,但不约束堆对象

 

常见线程安全类

1、String

2、Integer

3、StringBuffer

4、Random

5、Vector

6、Hashtable

7、java.util.concurrent 包下的类

8、线程安全指,多个线程调用它们同一个实例的某个方法时,是线程安全

(1)每个方法都是原子

(2)注意:其多个方法组合非原子

 

Java 对象头

1、32 位

(1)普通对象:Object Header(64 bits):Mark Word(32 bits)+ Klass Word(32 bits,类型指针)

(2)数组对象:Object Header(96 bits):Mark Word(32 bits)、Klass Word(32 bits,类型指针)、array length(32bits) 

2、Mark Word 结构(32 位)

(1)无锁(Normal):hashcode(25 bits)+ age(4 bits)+ biased_lock:0 + 01

(2)偏爱锁(Biased):thread(23 bits)+ epoch(2 bits)+ age(4 bits)biased_lock:1 + 01

(3)轻量锁(Lightweight Locked):ptr_to_lock_record(30 bits)+ 00

(4)重量锁(Heavyweight Locked):ptr_to_heavyweight_monitor(30 bits)+ 10

(5)Marked for GC:11

3、Mark Word 结构(64 位)

(1)无锁(Normal): unused(25 bits)+ hashcode(31 bits)+ unused(1 bits)+ age(4 bits)+ biased_lock:0 + 01

(2)偏爱锁(Biased):thread(23 bits)+ epoch(2 bits)+ age(4 bits)+ biased_lock:1 + 01

(3)轻量锁(Lightweight Locked):ptr_to_lock_record(62 bits)+ 00

(4)重量锁(Heavyweight Locked):ptr_to_heavyweight_monitor(62 bits)+ 10

(5)Marked for GC:11

4、组成

(1)锁标志位(lock):区分锁状态,11 时表示对象待GC回收状态,只有最后 2 位锁标识(11)有效

(2)biased_lock:是否偏向锁,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位

(3)分代年龄(age):表示对象被 GC 的次数,当该次数到达阈值的时候,对象就会转移到老年代

(4)对象的 hashcode(hash):运行期间调用 System.identityHashCode() 来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果 31 位不够表示,在偏向锁,轻量锁,重量锁,hashcode 会被转移到 Monitor 中

(5)偏向锁的线程 ID(JavaThread):偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的 ID。在后面的操作中,就无需再进行尝试获取锁的动作

(6)epoch:偏向锁在 CAS 锁操作过程中,偏向性标识,表示对象更偏向哪个锁

(7)ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM 使用原子操作而不是 OS 互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM 通过 CAS 操作在对象的标题字中设置指向锁记录的指针

(8)ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器 Monitor 的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到 Monitor 以管理等待的线程。在重量级锁定的情况下,JVM 在对象的ptr_to_heavyweight_monitor 设置指向 Monitor 的指针

 

对象结构

1、对象头

(1)Mark Word:存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等

(2)Klass Word:类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例

2、实例数据

(1)如果对象有属性字段,则这里会有数据信息

(2)如果对象无属性字段,则这里就不会有数据

(3)根据字段类型的不同占用不同的字节

3、对齐填充

(1)对象可以有对齐数据也可以没有

(2)默认情况下,JVM 堆中对象的起始地址需要对齐至 8 的倍数

(3)如果一个对象用不到 8 * N 个字节,则需要通过对齐数据来对其填充,以此来补齐对象头和实例数据占用内存之后剩余的空间大小

(4)如果对象头和实例数据已经占满了JVM所分配的内存空间,那么就不用再进行对齐填充了

(5)字段内存对齐的其中一个原因,是让字段只出现在同一 CPU 的缓存行中。如果字段不是对齐的,那么就有可能出现跨缓存行的字段。也就是说,该字段的读取可能需要替换两个缓存行,而该字段的存储也会同时污染两个缓存行。这两种情况对程序的执行效率而言都是不利的。其实对其填充的最终目的是为了计算机高效寻址

 

Monitor 原理

1、Monitor:监视器 / 管程

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

(1)开始时 Monitor 中的 Owner 为 null

(2)当 T1 进入临界区,将 Monitor Owner 设置为 T1,上锁成功,Monitor 中同一时刻只能有一个 Owner

(3)当 T2 占据锁时,如果 T3、T4 尝试进入临界区,则进入 EntryList(阻塞队列) 中,变成 BLOCKED(阻塞)状态

(4)T2 执行完同步代码块,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平的

3、synchronized 必须是进入同一个对象的 Monitor,才有上述的效果;不加 synchronized 的对象不会关联监视器,不遵从以上规则

4、Monitor 对象不可视,在 JVM 底层使用 C / C++ 实现

 

轻量级锁

1、使用

(1)如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的,即没有人竞争,则可以使用轻量级锁进行优化

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

2、每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word、Object reference(对象引用)

3、让锁记录中的 Object reference 指向锁对象,并且尝试使用 CAS(Compare And Sweep),两者互换:锁对象的 Mark Word <-> 锁记录的地址、状态 00

4、如果 CAS 替换成功,则锁对象的对象头,储存锁记录的地址和状态 00(表示轻量级锁)

5、如果 CAS 失败

(1)如果是其它线程,已经持有该对象的轻量级锁,则表示有竞争,首先会进行自旋锁,自旋一定次数后,如果还是失败,则进入锁膨胀阶段

(2)如果是自身线程执行 synchronized 锁重入,则再添加一条 Lock Record 作为重入的计数

6、当线程退出 synchronized 代码块时

(1)当前线程有多个 Lock Record,表示发生锁重入,只有一个 Lock Record 拥有锁对象的 Mark Word,其余 Lock Record 为 null

(2)如果获取的锁记录取值为 null 的锁记录,表示发生锁重入,这时清除该锁记录,表示重入计数减 1

7、当线程退出 synchronized 代码块时,如果获取的锁记录取值不为 null(即为锁对象的 Mark Word),则使用 CAS 将 Mark Word 值恢复到锁对象

(1)成功,则解锁成功

(2)失败,则说明轻量级锁进行锁膨胀,或已经升级为重量级锁,进入重量级锁解锁流程

 

锁重入

1、同一线程,对同一锁对象,执行 synchronized

2、锁重入的 Lock Record,CAS 失败,只引用锁对象,记录的 Mark Word 为 null

 

锁膨胀

1、如果在尝试加轻量级锁的过程中,CAS 操作无法成功,因为其它线程已经为这个对象加上轻量级锁,需要进行锁膨胀,将轻量级锁变成重量级锁

2、当 T1 进行轻量级加锁时,T0 已经对该对象加轻量级锁

(1)此时 T1 加轻量级锁失败,进入锁膨胀流程,即为对象申请 Monitor 锁,让锁对象指向重量级锁地址

(2)然后 T1 进入 Monitor 的 EntryList,变成 BLOCKED 状态

3、当 T0 退出 synchronized 同步块时

(1)使用 CAS 将 Mark Word 值恢复到对象头,锁对象的对象头指向 Monitor,则进入重量级锁的解锁过程

(2)按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null ,唤醒 EntryList 中的 T1

 

自旋优化

1、重量级锁竞争时,可以使用自旋来进行优化

(1)线程反复检查锁变量是否可用

(2)由于线程在这一过程中保持执行,因此是一种忙等待

(3)如果当前线程自旋成功,即在自旋时,持锁的线程释放锁,则当前线程可以不用进行上下文切换,就获得锁

2、自旋会占用 CPU 时间片,单核 CPU 自旋无效,多核 CPU 自旋才有效

3、Java 6 后,自旋锁是自适应

(1)如:对象刚自旋操作成功过,则认为本次自旋成功可能性很高,则自旋多次

(2)反之,就少自旋,甚至不自旋

4、Java 7 后,不能控制是否开启自旋功能

 

偏向锁

1、在轻量级的锁中,如果同一个线程,对同一个对象进行重入锁时,也需要执行 CAS 操作,需要消耗性能

2、Java 6 引入偏向锁

(1)只有第一次使用 CAS 时,将对象的 Mark Word 头设置为偏向线程 ID

(2)入锁线程再进行重入锁时,发现线程 ID 属于自己,则不用再进行 CAS

(3)只要不发生竞争,这个对象就归该线程所有

3、一个对象创建时

(1)如果开启偏向锁(默认开启),则创建对象后,Mark Word 值为 0x05(16 进制),即最后 3 位为 101(2 进制),此时它的 thread、epoch、age 都为 0

(2)偏向锁是默认延迟生效,不会在程序启动时立即生效

(4)如果没有开启偏向锁,则创建对象后,Mark Word 值为 0x01(16 进制),即最后 3 位为 001(2 进制),此时它的 hashcode、age 都为 0,第一次用到 hashcode 时,才会赋值

(5)处于偏向锁的对象解锁后,线程 ID 仍存储于对象头中

4、JVM 参数

(1)禁用延迟:-XX:BiasedLockingStartupDelay=0

(2)禁用偏向锁:-XX:-UseBiasedLocking

5、撤销偏向:使对象的偏向锁失效

(1)对象调用 hashCode(),偏向锁 -> 无锁

(2)多个线程使用该对象,偏向锁 -> 轻量锁

(3)调用 wait / notify,只有重量级锁才有等待-通知机制,使用该机制都会升级为重量级锁

6、场景:适用于冲突竞争少,不适用于多线程竞争

7、synchronized 使用锁的优先级:偏向锁 -> 轻量锁 -> 重量级

 

hashCode()

1、对象调用 hashCode()

(1)导致偏向锁被撤销,Basied -> Normal

(2)调用一次 hashCode() 后,该对象不能再设置偏向锁,如果可以,则 Mark Word 中的 identity hash code,必然会被偏向线程 id 覆盖,造成同一个对象,前后两次调用 hashCode(),得到结果不一致

2、hashCode

(1)正常状态对象一开始是没有 hashCode,第一次调用才生成

(2)偏向锁的对象 MarkWord 中存储的是线程 ID,没有额外空间存储 hashCode

(3)轻量级锁:在线程栈帧的锁记录中,记录 hashCode

(4)重量级锁:在 Monitor 中,记录 hashCode

 

撤销偏向锁

1、此处的撤销阈值,指同属一个类中,多个不同对象(实例、Class)被撤销,即不限于撤销一个偏向锁

2、批量重偏向

(1)如果对象被多个线程访问,但没有竞争,这时偏向线程 T1 的对象,仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID

(2)当撤销偏向锁超过阈值(默认 20 次)后,JVM 在给这些对象加锁时,重新偏向至加锁线程

3、批量撤销

(1)当撤销偏向锁阈值超过 40 次后,JVM 会认为不该偏向

(2)使得整个类的所有对象,都会变为不可偏向,包括新建对象也是不可偏向的

 

synchronized 优化

1、减少上锁时间:同步代码块中尽量短

2、减少锁的粒度:将一个锁拆分为多个锁提高并发度

 

锁粗化

1、对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化,这不同于之前讲的细分锁的粒度

2、JVM 在遇到一连串不断对同一个锁进行请求和释放操作时,会把所有的锁整合成对锁的一次请求,从而减少对锁的请求次数

3、例

(1)多次循环进入同步块,不如同步块内多次循环

(2)另外 JVM 可能会做如下优化,把多次 append 的加锁操作粗化为一次,因为都是对同一个对象加锁,没必要重入多次

new StringBuffer().append("a").append("b").append("c");

 

锁消除

1、JVM 会进行代码的逃逸分析

(1)JIT 两种编译器:C1、C2

(2)C2 优化策略之一:同步省略 / 锁消除

2、例如:某个加锁对象是方法内局部变量,不会被其它线程所访问到,这时就会被即时编译器忽略掉所有同步操作

 

wait、notify

1、Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet,变为 WAITING 状态

2、BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片,只要它们一直不唤醒,调度器就一直不会考虑调度它们

(1)BLOCKED 线程,等待锁

(2)WAITING 线程,获得锁后,又释放锁

3、唤醒时机

(1)BLOCKED 线程会在 Owner 线程释放锁时唤醒

(2)WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争

4、Object 类下的 API

(1)唤醒正在等待对象 Monitor 的单个线程,如果多个线程正在等待该对象,随机选择其中一个被唤醒

public final void notify()

(2)唤醒正在等待对象 Monitor 的所有线程

public final void notifyAll()

(3)导致当前线程等待,直到另一个线程调用该对象的 notify() 或 notifyAll(),底层为 wait(0)

public final void wait()
                throws InterruptedException

(4)导致当前线程等待,直到另一个线程调用此对象的 notify(),或 notifyAll(),或指定的时间已过

public final void wait(long timeout)
                throws InterruptedException

(5)导致当前线程等待,直到另一个线程调用此对象的 notify(),或 notifyAll(),或其他一些线程中断当前线程,或已过指定时间

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
            "nanosecond timeout value out of range");
    }

    //不能精确到纳秒,只在timeout基础上加一毫秒
    if (nanos > 0) {
        timeout++;
    }

    //底层仍调用wait(timeout)
    wait(timeout);
}

5、sleep(long n)、wait(long n) 区别

(1)sleep 是 Thread 方法,wait 是 Object 方法

(2)sleep 不需要强制和 synchronized 配合使用,wait 需要和 synchronized 一起用

(3)sleep 在睡眠时,不会释放对象锁,wait 在等待时,会释放对象锁

(4)使用 wait 一般需要搭配 notify 或 notifyAll 使用,否则会让线程一直等待

6、正确使用 wait、notify

(1)虚假唤醒:notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程

(2)解决:使用 notifyAll

synchronized (lock) {
    //不满足唤醒条件,则一直等待,避免虚假唤醒
    while(唤醒条件) {
        lock.wait();
    }
    //条件为false后,即唤醒后,再运行
}

synchronized (lock) {
    //唤醒所有等待线程
    lock.notifyAll();
}

(3)线程持有的锁对象,去调用 Object 类下相关 API

 

同步模式:保护性暂停

1、Guarded Suspension

2、应用场景:一个线程等待另一个线程的执行结果

3、要点

(1)有一个结果需要从一个线程传递到另一个线程,让他们共同关联一个 GuardedObject

(2)如果有结果不断从一个线程,到另一个线程,则可以使用消息队列(生产者 / 消费者)

(3)JDK 中,采用的就是此模式实现 join、Future

(4)因为要等待另一方的结果,所以归类到同步模式 

4、控制超时、解决虚假唤醒

class GuardedObject {
    
    //结果
    private Object response;
    
    //锁对象
    private final Object lock = new Object();
    
    //获取执行结果
    public Object get(long millis) {
        synchronized (lock) {
            //初始时间
            long begin = System.currentTimeMillis();
            //已等待时间
            long timePassed = 0;
            while (response == null) {
                //剩余等待时间 = 总等待时间 - 已等待时间
                long waitTime = millis - timePassed;
                //剩余等待时间 <= 0,则break
                if (waitTime <= 0) {
                    break;
                }
                try {
                    //等待指定时间
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //等待过程中被notifyAll,可能为虚假唤醒,需要计算已等待时间,以进行下一次等待
                timePassed = System.currentTimeMillis() - begin;
            }
            //break出循环,返回结果
            return response;
        }
    }
    
    //产生结果
    public void complete(Object response) {
        synchronized (lock) {
            //条件满足,通知等待线程
            this.response = response;
            lock.notifyAll();
        }
    }
}

5、解耦:等待、生产

(1)结果类

class GuardedObject {
    
    //标识GuardedObject
    private int id;
    
    public GuardedObject(int id) {
        this.id = id;
    }
    
    public int getId() {
        return id;
    }
    
    //结果
    private Object response;
    
    //获取结果
    public Object get(long timeout) {
        synchronized (this) {
            //初始时间
            long begin = System.currentTimeMillis();
            //已等待时间
            long passedTime = 0;
            while (response == null) {
                //此轮循环应等待时间
                long waitTime = timeout - passedTime;
                //经历的时间超过了最大等待时间时,退出循环
                if (waitTime <= 0) {
                    break;
                }
                try {
                    this.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace()
                    }
                //等待过程中被notifyAll,可能为虚假唤醒,需要计算已等待时间,以进行下一次等待
                passedTime = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }
    
    //产生结果
    public void complete(Object response) {
        synchronized (this) {
            //给结果成员变量赋值
            this.response = response;
            this.notifyAll();
        }
    }
}

(2)中间解耦类

class Mailboxes {
    
    //联系id、结果
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    
    //起始id
    private static int id = 1;
    
    //产生唯一id
    private static synchronized int generateId() {
        return id++;
    }
    
    //根据id获取结果
    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }
    
    //生产结果,并与id联系
    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }
    
    //获取id的Set集合
    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}

(3)等待端

class People extends Thread{
    @Override
    public void run() {
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        //等待指定时间,获取结果
        Object mail = guardedObject.get(5000);
    }
}

(4)生产端

class Postman extends Thread {
    
    //结果id
    private int id;
    
    //id对应结果
    private String mail;
    
    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }
    
    @Override
    public void run() {
        //根据id获取结果对象
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        //完成结果
        guardedObject.complete(mail);
    }
}

 

join

1、源码

(1)Thread 类中方法

(2)原理:保护性暂停模式

public final synchronized void join(long millis)
    throws InterruptedException {
    //记录初始时间
    long base = System.currentTimeMillis();
    //记录已等待时间
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        //调用者轮询检查线程alive状态
        while (isAlive()) {
            //底层仍调用wait
            wait(0);
        }
    } else {
        while (isAlive()) {
            //delay(剩余等待时间)= mills(最大等待时间)- now(已等待时间)
            long delay = millis - now;
            //剩余等待时间<=0,则不需要再等待
            if (delay <= 0) {
                break;
            }
            //否则继续等待
            wait(delay);
            //等待过程中被notifyAll,可能为虚假唤醒,需要计算已等待时间,以进行下一次等待
            now = System.currentTimeMillis() - base;
        }
    }
}

2、保护性暂停、join 区别

(1)join:必须等待返回结果的线程结束;等待结果必须为全局变量

(2)保护性暂停:线程返回结果后,仍可以执行其他代码;等待结果可以为局部变量

 

异步模式:生产者 / 消费者

1、要点

(1)与保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应

(2)消费队列:平衡生产、消费的线程资源

(3)生产者仅负责产生结果数据,不关心数据该如何处理,消费者只处理结果数据

(4)消息队列有容量限制,满时不会再加入数据,空时不会再消耗数据

(5)JDK 中各种阻塞队列,采用此模式实现

2、示例

class Message {
    private int id;
    private Object message;
    public Message(int id, Object message) {
        this.id = id;
        this.message = message;
    }
    public int getId() {
        return id;
    }
    public Object getMessage() {
        return message;
    }
}

class MessageQueue {
    
    //消息队列
    private LinkedList<Message> queue;
    
    //队列容量
    private int capacity;
    
    //构造器
    public MessageQueue(int capacity) {
        this.capacity = capacity;
        queue = new LinkedList<>();
    }
    
    //获取消息
    public Message take() {
        synchronized (queue) {
            //循环检测队列是否为空,因为notifyAll也会唤醒其他消费者,所以被唤醒的消费者需要重新进入等待
            while (queue.isEmpty()) {
                //消息队列为空
                try {
                    //消费者进入等待
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //获取并返回队列头部消息
            Message message = queue.removeFirst();
            //唤醒所有线程,即队列已空,需要唤醒生产者,存入消息
            queue.notifyAll();
            return message;
        }
    }
    
    //存入消息
    public void put(Message message) {
        synchronized (queue) {
            //循环检测队列是否已满,因为notifyAll也会唤醒其他生产者,所以被唤醒的生产者需要重新进入等待
            while (queue.size() == capacity) {
                //消息队列已满
                try {
                    //生产者进入等待
                    queue.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //将消息存入队列尾部
            queue.addLast(message);
            //唤醒所有线程,即队列已满,需要唤醒消费者,消耗消息
            queue.notifyAll();
        }
    }
}

 

park、unpark

1、LockSupport 类中的 static 方法

(1)为给定的线程提供许可证(如果尚未提供),许可证最多持有一个。如果线程在 park 被阻塞,那么它将被解除阻塞。否则,其下一次调用 park 保证不被阻止。如果给定的线程尚未启动,则此操作无法保证完全没有任何影响

public static void unpark(Thread thread)

(2)禁止当前线程进行线程调度,直到许可证可用

public static void park()

(3)禁用当前线程进行线程调度,直到指定的等待时间,或许可证可用

public static void parkNanos(long nanos)

(4)禁用当前线程进行线程调度,直到指定的截止日期,或许可证可用

public static void parkUntil(long deadline)

2、与 Object 的 wait、notify 相比

(1)wait、notify、notifyAll 必须配合 Monitor 一起使用,而 park、unpark 不需要

(2)park、unpark 是以线程为单位,精确阻塞、唤醒线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程

(3)park 之前可先 unpark,而 wait 之前不能先 notify / notifyAll

 

park、unpark 原理

1、每个线程都私有一个 Parker 对象

(1)三部分组成:_counter、_cond、_mutex

(2)不可视对象,底层由 C / C++ 实现

2、当前线程正常运行

(1)当前线程调用 Unsafe.park() 方法

(2)检查 _counter,此情况为 0,这时获得 _mutex 互斥锁

(3)线程进入 _cond 条件变量阻塞

(4)设置 _counter = 0

3、当前线程已调用 park()

(1)调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

(2)唤醒 _cond 条件变量中的 Thread_0

(3)Thread_0 恢复运行

(4)设置 _counter 为 0 

  

4、当前线程正常运行

(1)调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1

(2)当前线程调用 Unsafe.park() 方法

(3)检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行

(4)设置 _counter 为 0

 

线程状态转换

1、t 线程调用 start() 时,从 NEW -> RUNNABLE

2、t 线程使用 synchronized(obj),获取对象锁后

(1)调用 obj.wait() 时,t 线程从 RUNNABLE -> WAITING

(2)调用 obj.notify() / obj.notifyAll() / t.interrupt() 时:竞争锁成功,t 线程从 WAITING -> RUNNABLE;竞争锁失败,t 线程从 WAITING -> BLOCKED

3、当前线程调用 t.join() 方法时,从 RUNNABLE -> WAITING

(1)注意:是当前线程在 t 线程对象的 Monitor上等待

(2)t 线程运行结束,或调用当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

4、LockSupport,无限时

(1)当前线程调用 LockSupport.park(),使当前线程从 RUNNABLE -> WAITING

(2)当前线程调用 LockSupport.unpark(目标线程),或调用目标线程的 interrupt(),使目标线程从 WAITING -> RUNNABLE

5、t 线程使用 synchronized(obj) 获取了对象锁后

(1)调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE -> TIMED_WAITING

(2)t 线程等待时间超过 n 毫秒,或调用 obj.notify() / obj.notifyAll() / t.interrupt() 时:竞争锁成功,t 线程从 TIMED_WAITING -> RUNNABLE;竞争锁失败,t 线程从 TIMED_WAITING -> BLOCKED

6、当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE -> TIMED_WAITING

(1)注意:是当前线程在 t 线程对象的 Monitor 上等待

(2)当前线程等待时间超过 n 毫秒,或 t 线程运行结束,或调用当前线程的 interrupt() 时,当前线程从 TIMED_WAITING -> RUNNABLE

7、sleep

(1)当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE -> TIMED_WAITING

(2)当前线程等待时间超过 n 毫秒,当前线程从 TIMED_WAITING -> RUNNABLE

8、LockSupport,有限时

(1)当前线程调用 LockSupport.parkNanos(long nanos),或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE -> TIMED_WAITING

(2)调用 LockSupport.unpark(目标线程),或调用目标线程的 interrupt() ,或等待超时,使目标线程从 TIMED_WAITING -> RUNNABLE

9、RUNNABLE <-> BLOCKED

(1)t 线程用 synchronized(obj) 获取对象锁时,如果竞争失败,从 RUNNABLE -> BLOCKED

(2)持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED -> RUNNABLE ,其它失败的线程仍然 BLOCKED

10、当前线程所有代码运行完毕,RUNNABLE -> TERMINATED

 

多把锁

1、将锁的粒度细分

2、优点:增强并发度

3、缺点:如果一个线程需要同时获得多把锁,则容易发生死锁

 

活跃性问题

1、线程没有按预期结束,无法继续执行的情况

2、分类

(1)活锁

(2)死锁

(3)饥饿

 

活锁

1、出现在两个线程互相改变对方的结束条件,最后两者都无法结束

2、解决

(1)交错线程的执行时间

(2)开发中使用随机 sleep 时间

3、示例

public class TestLiveLock {
    
    static volatile int count = 10;
    
    static final Object lock = new Object();
    
    public static void main(String[] args) {
        
        new Thread(() -> {
            //期望减到0,退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过20,退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

 

死锁

1、一个线程需要同时获取多把锁,此时容易发生死锁

2、定位死锁

(1)jconsole

(2)jps 定位进程 id,再用 jstack 定位死锁

3、解决

(1)顺序加锁

(2)引出新的活跃性问题:饥饿

 

饥饿

1、一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

2、读写锁时会涉及饥饿问题

 

ReentrantLock

1、相对于 synchronized

(1)可中断

(2)可以设置超时时间

(3)可以设置为公平锁

(4)支持多个条件变量

(5)与 synchronized 一样,都支持可重入

(6)synchronized 为关键字级别,ReentrantLock 为对象级别

2、基本语法

(1)必须保证 lock()、unlock() 成对出现

(2)获取锁的代码,在 try 块内外的作用相同

(3)finally 保证不论是否异常,都能释放锁

//获取锁
reentrantLock.lock();
try {
    //临界区
} finally {
    //释放锁
    reentrantLock.unlock();
}

3、可重入

(1)同一个线程如果首次获得锁,则为该锁的拥有者,因此有权利再次获取这把锁

(2)如果是不可重入锁,则第二次获得锁时,自己也会被该锁阻塞

4、可打断

(1)获取锁,被该所阻塞的线程可被打断

public void lockInterruptibly() throws InterruptedException

(2)获得锁,被该所阻塞的线程不可被打断

public void lock()

5、锁超时

(1)如果锁被释放并且被当前线程获得,或者锁已经被当前线程持有,则返回 true;否则返回false

public boolean tryLock()

(2)如果锁未被任何线程持有,并且被当前线程获取,或锁已经被当前线程持有,则为 true;如果在锁被获取之前,等待时间已经过去,则为 false

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

6、公平锁

(1)公平:阻塞线程在释放锁时,WaitSet 中先阻塞的线程,先获得锁

(2)ReentrantLock 默认不公平

public ReentrantLock() {
    sync = new NonfairSync();
}

(3)根据给定的公平政策创建一个 ReentrantLock 实例

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

(4)公平锁会降低并发度,一般不设置为公平锁

7、条件变量

(1)synchronized 中的条件变量,即 WaitSet,当条件不满足时进入 WaitSet 等待,有且只有一个

(2)ReentrantLock 支持多个条件变量,有多个 WaitSet

(3)返回一个 Condition 实例,与调用该方法的 Lock 实例一起使用

public Condition newCondition() {
    return sync.newCondition();
}
final ConditionObject newCondition() {
    return new ConditionObject();
}

(4)ConditionObject 实现 Condition

(5)await、signal 可代替 wait、notify

 

Condition 接口

1、ReentrantLock 的 ConditionObject 使用要点

(1)await 前需要获得锁

(2)await 执行后,会释放锁,进入 ConditionObject 实例等待

(3)await 的线程被唤醒 / 被打断 / 超时,去重新竞争 lock 锁

(4)竞争 lock 锁成功后,从 await 后继续执行

2、源码

public interface Condition {

    //导致当前线程等待,直到被signal / signalAll / interrupted
    void await() throws InterruptedException;

    //导致当前线程等待,直到被signal / signalAll
    void awaitUninterruptibly();

    //导致当前线程等待,直到被signal / signalAll / interrupted / 等待指定时间
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    //导致当前线程等待,直到被signal / signalAll / interrupted / 等待指定时间
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    //导致当前线程等待,直到被signal / signalAll / interrupted / 到达指定日期
    boolean awaitUntil(Date deadline) throws InterruptedException;

    //如果任何线程正在等待此条件,则随机选择一个线程进行唤醒
    void signal();

    //唤醒等待此条件的所有线程
    void signalAll();
}

 

同步模式:顺序控制

1、执行顺序需求:先 2 后 1

2、wait、notify

(1)需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒,因此使用运行标记,来判断是否应该 wait

(2)干扰线程唤醒 wait 线程,条件不满足时还要重新等待,使用 while 循环来解决虚假唤醒

(3)唤醒对象上的 wait 线程需要使用 notifyAll,因为同步对象上的等待线程可能不止一个

//同步锁对象
static Object obj = new Object();

//t2运行标记,表示t2是否已执行
static boolean t2runed = false;

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        synchronized (obj) {
            while (!t2runed) { 
                try {
                    //若t2没有执行,t1等待
                    obj.wait(); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        //若t2已执行,则t1执行
        System.out.println(1);
    });
    
    Thread t2 = new Thread(() -> {
        //t2直接执行
        System.out.println(2);
        synchronized (obj) {
            //修改运行标记
            t2runed = true;
            //通知obj上等待的线程,可能有多个,因此需要使用notifyAll
            obj.notifyAll();
        }
    });
    
    t1.start();
    t2.start();
}

2、park、unpark 优化

(1)park、unpark 灵活,无调用顺序

(2)以线程为单位进行暂停、恢复,不需要同步对象、运行标记 

Thread t1 = new Thread(() -> {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) { }
    //当没有许可时,当前线程暂停运行;有许可时,使用该许可,当前线程恢复运行
    LockSupport.park();
    System.out.println("1");
});

Thread t2 = new Thread(() -> {
    System.out.println("2");
    //给线程t1发放许可
    LockSupport.unpark(t1);
});

t1.start();
t2.start();

 

交替输出

1、输出需求

(1)线程 1 输出 5 次 a,线程 2 输出 5 次 b,线程 3 输出 5 次 c

(2)要求输出:abcabcabcabcabc

2、wait、notify

class WaitNotify {
    
    //线程标记,表示当前应该输出的线程
    private int flag;
    
    //循环次数
    private int loopNumber;
    
    public WaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    
    public void print(int waitFlag, int nextFlag, String str) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != waitFlag) {
                    try {
                        //若当前线程的标记,与当前应该输出的线程标记不同,则等待
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                //若当前线程的标记,与当前应该输出的线程标记相同,则输出
                System.out.print(str);
                //并将flag修改为下一个应该输出的线程标记
                flag = nextFlag;
                //唤醒所有等待线程
                this.notifyAll();
            }
        }
    }
}
public static void main(String[] args) {

    WaitNotify waitNotify = new WaitNotify(1, 5);

    new Thread(() -> {
        WaitNotify.print(1, 2, "a");
    }).start();

    new Thread(() -> {
        WaitNotify.print(2, 3, "b");
    }).start();

    new Thread(() -> {
        WaitNotify.print(3, 1, "c");
    }).start();
}

3、ReentrantLock

class AwaitSignal extends ReentrantLock {
    
    //循环次数
    private int loopNumber;
    
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    
    //启动初始线程
    public void start(Condition first) {
        this.lock();
        try {
            //主线程唤醒初始线程
            first.signal();
        } finally {
            this.unlock();
        }
    }
    
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                //开始时,先wait
                current.await();
                //被唤醒时,输出
                System.out.print(str);
                //并唤醒下一个线程
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                //释放锁
                this.unlock();
            }
        }
    }
}
public static void main(String[] args) {

    AwaitSignal as = new AwaitSignal(5);
    Condition aWaitSet = as.newCondition();
    Condition bWaitSet = as.newCondition();
    Condition cWaitSet = as.newCondition();

    new Thread(() -> {
        as.print("a", aWaitSet, bWaitSet);
    }).start();

    new Thread(() -> {
        as.print("b", bWaitSet, cWaitSet);
    }).start();

    new Thread(() -> {
        as.print("c", cWaitSet, aWaitSet);
    }).start();

    as.start(aWaitSet);
}

4、park、unpark

class ParkAndUnPark {

    public void run(String str, Thread nextThread) {
        for(int i = 0; i < loopNumber; i++) {
            //线程先park
            LockSupport.park();
            //被唤醒后输出
            System.out.print(str);
            //唤醒下一线程
            LockSupport.unpark(nextThread);
        }
    }

    //循环次数
    private int loopNumber;

    public ParkAndUnPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}
public static void main(String[] args) {
    
    ParkAndUnPark obj = new ParkAndUnPark(5);
    
    t1 = new Thread(() -> {
        obj.run("a", t2);
    });

    t2 = new Thread(() -> {
        obj.run("b", t3);
    });

    t3 = new Thread(() -> {
        obj.run("c", t1);
    });
    
    t1.start();
    t2.start();
    t3.start();

    //主线程唤醒初始线程
    LockSupport.unpark(t1);
}

 

ThreadLocal

1、实现资源对象的线程隔离,让每个线程使用私有的资源对象,避免争用引发的线程安全问题

(1)同时实现线程内的资源共享

(2)与方法内的局部变量相比,局部变量不能跨越方法

2、原理

(1)每个线程内有一个 ThreadLocalMap 类型的成员变量,存储资源对象

static class ThreadLocalMap

(2)ThreadLocalMap 起到隔离作用,ThreadLocal 作为 key 关联 value

(3)第一次使用 ThreadLocal 时,才创建 ThreadLocalMap

(4)在不同的 ThreadLocalMap 中,ThreadLocal 可为同一对象

(5)创建 ThreadLocal 时,就为其分配 hash 值

3、set

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        //以 ThreadLocal 作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 中
        map.set(this, value);
    else
        //ThreadLocalMap懒惰创建
        createMap(t, value);
}
private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    //计算key的桶下标
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         //开放寻址法,解决hash冲突
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            //启发式扫描,GC临近所有节点
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextIndex(int i, int len) {
    //若有hash冲突,即不同key在同一位置,则后一key尝试加入下一位置
    return ((i + 1 < len) ? i + 1 : 0);
}

4、get

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //以ThreadLocal作为key,到当前线程中查找关联的value
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

5、remove

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        //以ThreadLocal作为key,移除当前线程关联的value
        m.remove(this);
}

6、ThreadLocalMap 中的 key(ThreadLocal)设计为弱引用,value 仍为强引用

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }

    //负载因子 2/3
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }
    
    //初始容量 16
    private static final int INITIAL_CAPACITY = 16;
}

(1)Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在 GC 时释放其占用的内存

(2)GC 仅是让 key 的内存释放,后续要根据 key 是否为 null,进一步释放 value 的内存

7、释放 value 时机

(1)get,获取 key,发现 key == null,GC 相应 value

(2)set,使用启发式扫描,清除临近 key == null 的 value,启发次数与元素个数,是否发现 key == null 有关

8、一般使用 static 修饰 ThreadLocal

(1)即使 ThreadLocal 在 ThreadLocalMap 中为弱引用,但 static 使其变为强引用

(2)此时 7 中两种 GC value 方法失效

(3)建议:主动调用 remove

posted @   半条咸鱼  阅读(128)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示