ReZero's Utopia.

Thread

Word count: 3.7kReading time: 13 min
2020/09/19 Share

线程生命周期

happens-before

  1. 程序顺序规则:一个线程中的每一个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start规则:如果线程A执行操作ThreadB.start()启动线程B,那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作、
  6. join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

内存屏障

  1. 如果第一个操作是volatile读,那无论第二个操作是什么,都不能重排序;

  2. 如果第二个操作是volatile写,那无论第一个操作是什么,都不能重排序;

  3. 如果第一个操作是volatile写,第二个操作是volatile读,那不能重排序。

多线程

原子类

1
2
3
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

c++ 实现 native method: compareAndSwapInt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
int mp = os::is_MP();
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
}

LOCK_IF_MP multi processor

cmpxchg 不具有原子性,原子性 是通过加lock来保障

lock cmxchg 指令,lock保证 cmpchg操作某块内存时不允许其他 cpu 对该块做出修改

tips: lock在执行后面指令时锁定一个北桥信号而不采用锁总线的方式

JOL java object layout

1
System.out.println(ClassLayout.parseInstance(o).toPrintable());

markword 8个字节, class pointer 4个字节, instance data, padding

指针长度 默认取决你的JVM,比如 64 bit, 即8个字节

1
2
3
4
5
java -XX:+PrintCommandLineFlags -version
-XX:InitialHeapSize=267602688 -XX:MaxHeapSize=4281643008 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)

但是由于开启了 UseCompressed*, 会被压缩为4个字节

Oop 指的是 ordinary object pointer, 指成员变量

markword 记录的信息

tips: 轻量级锁, 自旋锁, 无锁【傻逼叫法】 指的是同一种锁,只不过是各种花哨叫法

偏向锁:写 threadId 代表加锁进行占用,当发生竞争(即有线程来争抢该锁,一个线程即可)便会撤销偏向锁并开始升级为自旋锁,升级过程:竞争该锁的线程在各自的线程栈中生成 lock record, 然后各自通过自旋(即CAS:先读出锁内指针,然后将其改为自己 lock record 的地址,如果改回的过程中发现读出的状态没有改变,那么就成功抢到)的方式进行争抢,争抢成功后就会如上图,将原先的当前线程指针改为了指向 lock record 的指针。

上面明显看出一个问题,就是严重自旋,即如果某线程长时间持有锁那他人就会一直自旋(就是一个衡量标准,称为竞争加剧,比如线程超过了10次自旋, PreBlockSpin, 自旋线程数超过cpu核数的一半,1.6后加入自适应自旋:Adaptive Self Spinning,即由JVM自己抉择 ),这时候就会选择升级为重量级锁

严重自旋的问题:自旋就是while(), 旋就耗 cpu,所以可能会把 cpu 拉满

重量级锁会开个队列让线程等,等(也就是堵塞态)是不耗cpu的,操作系统会主动通知

  1. 0 0 1 未加锁

  2. hashCode

  3. synchronized(o)00 -> 轻量级锁,-XX:BiasedLockingStartupDelay=4 , JVM 启动 4 秒后才会采用偏向锁模式,因为JVM启动需要执行很多 sync,那必然很多竞争,所以直接先用轻量锁跑(从而可以防一手大量的偏向锁的撤销和升级,白瞎资源)。

关于 epoch: 批量锁

锁降级:GC的时候,此时该锁除了GC线程已经不被其他线程访问了,没有意义

锁消除 lock eliminate: 比如局部变量 StringBuffer, 因为堆栈封闭,本身就没有线程安全考虑,所以会对其 append 方法进行锁消除

锁粗化:比如循环对 StringBuffer 进行 append,JVM会将锁拿到循环外部

JIT: just in time

将热点代码直接转成机器码,避免解释从而提高执行效率

hsdis JVM反汇编插件

java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T

C1, C2优化

synchronized 过程

  1. class 文件: monitorenter monitorexit
  2. 执行过程自动升级
  3. lock comxchg

volatile

保证线程可见性

超线程:一个ALU对应多个PC|Registers,所谓得的四核八线程

Context switch:上下文切换:当仅有ALURegisterPC三个组件时,同一时间仅能有一个线程执行,其他线程执行时需要将当前线程在这Register, ALU组件中的相关数据保存起来,然后才可以执行

Cache Line 64 字节:cpu 层的数据一致性是以 cache line 为单位的

缓存行对齐:disruptor 强行塞无意义变量来对齐缓存行避免过频繁而无意义的一致性追求

乱序执行

JVM 层级:内存屏障, 操作系统层级:Lock指令

系统底层实现一致性:MESI,如果不能(数据过大超出缓存行)的话就锁总线(万能方式)

系统底层实现有序性:内存屏障,sfence mfence lfence 等系统原语 或者 锁总线

单例模式:DCL, double check lock

1
2
3
4
5
0 new #2 <java/lang/Object>                   # new 出内存布局
3 dup
4 invokespecial #1 <java/lang/Object.<init>> # 构造方法
7 astore_1 # 连接引用和内存区
8 return

如果 4 7 发生了指令重排序,那么就会出现引用判断不为 NULL 但实际没有构造成功,即线程使用了半初始化对象

如何实现禁止重排

  1. 字节码:ACC_volatile 标记

  2. 内存屏障:屏障两边的指令不可重排,保障有序性。JSR 内存屏障:LL,SS,LS,SL 按例解释 LL:L1,L2 不能重排,以此类推

  3. hotspot 实现

    1. bytecodeinterpreter.cpp
    1
    2
    3
    4
    5
    6
    int field_offset = cache->f2_as_index();
    if (cache->is_volatile()) {
    if(support_iriw...){
    OrderAccess::fence();
    }
    }
    1. orderAccess_bsd_x86.inline.hpp
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    inline void OrderAccess::fence() {
    if (os::is_MP()) {
    // always use locked addl since mfence is sometimes expensive
    #ifdef AMD64
    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
    #else
    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    #endif
    }
    }

    可见并非采用了系统原语,而是使用了 lock, 这是兼容保证,因为 mfence等并非所有系统都支持

强软弱虚

强引用:就是平常使用的引用,普通引用。引用存在就不会被回收

软引用:new SoftReferenceM<>(new byte[1024])

- Xmx=20m 最大堆内存 当堆内存占用不够的时候软引用就会被回收
- 应用:缓存,不用的时候释放掉,大不了再从数据库里取一次

弱引用:WeakReference

  • 特点:垃圾回收器看到就会回收
  • 应用:一次性,防止内存泄漏

虚引用:PhantomReferernce

  • 特点:永远 get 不到
  • 应用:管理堆外内存,给堆外内存对象挂一个虚引用,这样对象被gc时会将其相关信息放到队列中,特有的gc线程会监听这个队列,以此来管理堆外内存

ThreadLocal

应用:

  • @Transactional 同一线程的多个方法调用时拿到同一个 connection

源码: Thread 里有个 map ThreadLocal<ThreadLocal, Object> 调用 ThreadLocal.set 方法时会取当前线程的 大map, 然后把自己和value放进去转成一个 entry<key, value>

1
Entry extends WeakReference { super{k}} // 弱引用指向threadLocal

防止内存泄露,如果强引用,那么t1没了也仍然不会被回收,因为entry key仍然引用它,但还是有个问题,就是value无法被访问了(key被指成null),因此必须调用一次 threadLocl.remove 方法

程序抛出异常时,默认锁会被释放,这样就会被原来的那些个准备拿到这把锁的程序乱冲进来,程序乱入。

ReentrantLock

优于synchronized的地方:

  1. tryLock进行锁定,锁定与否都会继续执行方法(synchronized锁不到就阻塞了)
  2. lock.lockInterruptibly: A线程持有B线程想要的锁,B如果拿不到就会一直等,这时候可以用 interrupt 方法打断等待
  3. 公平锁:队列优先
  4. condition特性:本质上是多个等待队列

CountdownLatch

灵活版的join, await 插门闩,countdown拉门闩,拉多了就下来了

CycleBarrier

你加一我加一,加满就重来

Phaser 多阶段栅栏

重写onAdvance方法,前进,线程抵达这个栅栏的时候,所有的线程都满足了这个第一个栅栏的条件了onAdvance会被自动调用,目前我们有好几个阶段,这个阶段是被写死的,必须是数字0开始,onAdvance会传来两个参数phase是第几个阶段,registeredParties是目前这个阶段有几个人参加,每一个阶段都有一个打印,返回值false,一直到最后一个阶段返回true,所有线程结束,整个栅栏组,Phaser栅栏组就结束了

1
2
arriveAndAwaitAdvance  // 下个阶段
phaser.arriveAndDeregister // 停止,不再进入下个阶段

ReadWriteLock

读锁读共享,写锁写独占,读不放锁写别动,写不放锁都别动

Semaphore

传一个 permits, 每次acquire就减一,支持公平锁

Exchanger

两个线程交换信息,通信常用,或者游戏交换装备


Reentrantlock、CountDownLatch、CyclicBarrier、Phaser、ReadWriteLock、Semaphore,Exchanger都是用同一个队列,同一个类来实现的,这个类叫AQS


lock support

park,unpark原理:通过一个变量标识,变量在0 1之间切换(park unpark)大于0时就可继续执行

面试题

生产者消费者

为什么用while而不是用if? 因为当LinkedList集合中“馒头”数等于最大值的时候,if在判断了集合的大小等于MAX的时候,调用了wait()方法以后,它不会再去判断一次,方法会继续往下运行,假如在你wait()以后,另一个方法又添加了一个“馒头”,你没有再次判断,就又添加了一次,造成数据错误,就会出问题,因此必须用while。

AQS

共享一个state, state为0就可以抢,CAS抢到了就设置成1,重入就在1的基础上加1,以此递增

除了监控state外,还维护一个双向链表,这个链表的节点就是线程


acquire 方法:假如你要往一个链表上添加尾巴,尤其是好多线程都要往链表上添加尾巴,我们仔细想想看用普通的方法怎么做?第一点要加锁这一点是肯定的,因为多线程,你要保证线程安全,一般的情况下,我们会锁定整个链表(Sync),我们的新线程来了以后,要加到尾巴上,这样很正常,但是我们锁定整个链表的话,锁的太多太大了,现在呢它用的并不是锁定整个链表的方法,而是只观测tail这一个节点就可以了,怎么做到的呢?compareAndAetTail(oldTail,node),中oldTail是它的预期值,假如说我们想把当前线程设置为整个链表尾巴的过程中,另外一个线程来了,它插入了一个节点,那么仔细想一下Node oldTail = tail;的整个oldTail还等于整个新的Tail吗?不等于了吧,那么既然不等于了,说明中间有线程被其它线程打断了,那如果说却是还是等于原来的oldTail,这个时候就说明没有线程被打断,那我们就接着设置尾巴,只要设置成功了OK,compareAndAetTail(oldTail,node)方法中的参数node就做为新的Tail了,所以用了CAS操作就不需要把原来的整个链表上锁,这也是AQS在效率上比较高的核心


读acquireQueued()这个方法,这个方法的意思是,在队列里尝试去获得锁,在队列里排队获得锁,那么它是怎么做到的呢?我们先大致走一遍这个方法,首先在for循环里获得了Node节点的前置节点,然后判断如果前置节点是头节点,并且调用tryAcquire(arg)方法尝试一下去得到这把锁,获得了头节点以后,你设置的节点就是第二个,你这个节点要去和前置节点争这把锁,这个时候前置节点释放了,如果你设置的节点拿到了这把锁,拿到以后你设置的节点也就是当前节点就被设置为前置节点,如果没有拿到这把锁,当前节点就会阻塞等着,等着什么?等着前置节点叫醒你,所以它上来之后是竞争,怎么竞争呢?如果你是最后节点,你就下别说了,你就老老实实等着,如果你的前面已经是头节点了,说明什么?说明快轮到我了,那我就跑一下,试试看能不能拿到这把锁,说不定前置节点这会儿已经释放这把锁了,如果拿不着阻塞,阻塞以后干什么?等着前置节点释放这把锁以后,叫醒队列里的线程,我想执行过程已经很明了了,打个比方,有一个人,他后面又有几个人在后面排队,这时候第一个人是获得了这把锁,永远都是第一个人获得锁,那么后边来的人干什么呢?站在队伍后面排队,然后他会探头看他前面这个人是不是往前走了一步,如果走了,他也走一步,当后来的这个人排到了队伍的第二个位置的时候,发现前面就是第一个人了,等这第一个人走了就轮到他了,他会看第一个人是不是完事了,完事了他就变成头节点了,就是这么个意思。


VarHandle除了可以完成普通属性的原子操作,还可以完成原子性的线程安全的操作

在JDK1.9之前要操作类里边的成员变量的属性,只能通过反射完成,用反射和用VarHandle的区别在于VarHandle的效率要高的多,反射每次用之前要检查,VarHandle不需要,VarHandle可以理解为直接操纵二进制码,所以VarHandle反射高的多

并发容器

CopyOnWriteList

适合读多写少

BlockingQueue

CATALOG
  1. 1. 线程生命周期
  2. 2. happens-before
  3. 3. 内存屏障
  • 多线程
    1. 原子类
    2. JOL java object layout
      1. JIT: just in time
      2. synchronized 过程
    3. volatile
      1. 保证线程可见性
      2. 乱序执行
        1. 1. 如何实现禁止重排
    4. 强软弱虚
      1. ThreadLocal
      1. ReentrantLock
      2. CountdownLatch
      3. CycleBarrier
      4. Phaser 多阶段栅栏
      5. ReadWriteLock
      6. Semaphore
      7. Exchanger
      8. lock support
    5. 面试题
      1. 生产者消费者
    6. AQS
    7. 并发容器
      1. CopyOnWriteList
      2. BlockingQueue