Java基础总结笔记-JMM 并发 线程锁

667 阅读23分钟

参考

内存模型 JMM

Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model, JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果。

主流程序(如C/C++)直接使用物理硬件和操作系统的内存模型,因此对不同平台就必须编写不同的程序。

JMM与Java内存区域的划分是不同的概念层次:

  • JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式
  • JMM围绕原子性、有序性、可见性展开
  • 内存模型可以理解为在特定的操作协议下,对特定内存或高速缓存进行读写访问的过程抽象

硬件效率与一致性

犹豫计算机存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓存:

  • 将运算中需要使用到的数据复制到缓存中让运算能快速进行
  • 当运算结束后,再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写

虽然解决了处理器与内存的速度矛盾,但也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:

  • 缓存一致性(Cache Coherence)

主内存与工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。

此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数组的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。

为了获得较好的执行效率,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

  • 主内存:主要存储Java实例对象(成员变量、局部变量等),共享的类信息、常量、静态变量,存在多线程访问的安全问题。可与前面介绍的物理硬件的主内存类比,但此处仅是虚拟机内存的一部分
  • 工作内存:主要存储当前方法的所有本地变量信息(存储着主内存的变量副本拷贝),每个线程只能访问自己的工作内存。不存在线程安全问题。可与前面讲的处理器的高速缓存类比

线程、内存、工作内存三者之间的交互关系:

若两个线程同时调用一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,如下图示意:

内存间交互操作

  • 一个变量如何从主内存拷贝到工作内存
  • 如何从工作内存同步回主内存

Java内存模型定义了8种操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的(double、long变量来说,load/store/read/write在某些平台允许有例外):

操作作用范围说明
lock(锁定)主内存把变量标识为一条线程独占的状态
unlock(解锁)主内存释放锁定状态的变量
read(读取)主内存把变量的值从主内存传输到线程的工作内存,以便后续的load操作
load(载入)工作内存read操作从主内存中得到的变量值放入工作内存的变量副本之中
use(使用)工作内存把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时,将会执行这个操作
assign(赋值)工作内存把一个执行引擎接到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作
store(存储)工作内存把工作内存中一个变量值传送给主内存,以便后续write操作使用
write(写入)主内存主内存store操作从工作内存中得到的变量值放入主内存变量中

Java线程与硬件处理器

  • Java线程实现是基于一对一的线程模型:即我们在使用Java线程时,Java虚拟机内部转而调用当前操作系统的内核线程来完成当前任务

Java内存模型与硬件内存架构关系

  • JMM只是一种抽象的概念,一组规则,并不实际存在

多线程遇到的问题

原子性

  • 原子性指一个操作不可终端

指令重排

重排优化可能会导致程序出现内存可见性问题,分以下三种:

  • 编译器优化的重排:编译器在不改变单线程程序语义的前提下,重新排队语句执行顺序
  • 指令并行的重排:处理器采用指令级并行技术将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应的机器指令执行顺序
  • 内存系统的重排:由于处理器使用缓存和读写缓冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差

可见性

  • 当一个线程修改了某个共享变量值,其他线程是否可以马上得知这个修改的值

有序性

  • 对于单线程的执行代码,执行是按照顺序依次执行的
  • 对于多线程,因为指令重排,可能出现乱序现象

JMM解决多线程问题

happens-before原则(先行发生原则)

这个原则非常重要,它是判断数据是否存在数据是否存在竞争,线程是否安全的主要依据

  • 程序顺序原则:即一个线程内必须保证语义串行性
  • 锁规则:解锁(unlock)操作必然发生同一个锁加锁(lock)之前
  • volatile规则volatile变量在每次线程被访问时,都强迫从主内存中读该变量的值,而当改变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值
  • 线程启动规则:线程start方法先于它的每一个动作。在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见
  • 线程终止规则:线程所有的操作先于线程的终止。在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见
  • 线程中断规则:对线程interrupt方法调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断
  • 传递规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C
  • 对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法

基础概念

线程安全性

CAS

  • 乐观锁:假设对共享资源的访问没有冲突,无需加锁,使用CAS技术
  • 悲观锁:假设每次访问共享资源时总会发生冲突,需要加锁

CAS全程Compare And Swap,核心方法

function: CAS(V, E N)

参数:

  • V表示要更新的变量
  • E表示预期值
  • N表示新值

  • CPU指令对CAS的支持:CAS是一条CPU的原子指令,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,执行过程中不允许中断,不会造成不一致的问题

CAS的ABA问题

重入锁

公平锁 VS 非公平锁

  • 公平锁:多个线程按照申请锁的顺序获取锁(FIFO)
  • 非公平锁:多个线程直接尝试获取锁,如果获取不到则会到等待队列的队尾等待。
    • 非公平锁有点是减少唤起线程的开销(线程有几率不阻塞,直接获取锁),整体吞吐效率高
    • 缺点是等待队列中的线程可能会饿死,或者等待很久才能获取锁

ReentrantLock中的实现对比,两者的区别在公平锁在获取同步状态时,多了一个限制条件:hasQueuedPredecessors:主要判断当前线程是否位于同步队列中的第一个

独享锁 VS 共享锁

  • 独享锁:也叫排他锁,指该锁一次只能被一个线程所持有。synchronized和JUC中的Lock实现类就是互斥锁。

  • 共享锁:该锁可以被多个线程所持有。获得共享锁的线程只能读数据,不能修改数据。

ReentrantReadWriteLock中有读写两把锁,所以需要在一个整形变量state上分别描述读锁和写锁的数量,于是将state变量切割为两个部分:

写锁加锁:

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

protected final boolean tryAcquire(int acquires) {
	Thread current = Thread.currentThread();
	int c = getState();
	// 获得写锁个数
	int w = exclusiveCount(c);
	// 存在读锁或写锁
	if (c != 0) {
		// (Note: if c != 0 and w == 0 then shared count != 0)
		// 1. 如果读锁非零(写锁为0),或者写锁非零并且当前线程不是持有锁的线程,获取锁失败!
		if (w == 0 || current != getExclusiveOwnerThread())			
			return false;
		// 2. 如果写入锁的数量超过上限,抛出异常
		if (w + exclusiveCount(acquires) > MAX_COUNT)
			throw new Error("Maximum lock count exceeded");
		// Reentrant acquire
		// 获取锁成功,更新state
		setState(c + acquires);
		return true;
	}
	// 如果没有线程持有锁,并且当前线程需要阻塞那么就返回失败;
	// 或者如果通过CAS增加写线程数失败也返回失败。
	if (writerShouldBlock() ||
		!compareAndSetState(c, c + acquires))
		return false;
	// 如果c=0,w=0或者c>0,w>0(重入),则设置当前线程或锁的拥有者
	setExclusiveOwnerThread(current);
	return true;
}

读锁加锁:

protected final int tryAcquireShared(int unused) {
	Thread current = Thread.currentThread();
	int c = getState();
	// 如果写锁已经被另外的线程获取,直接返回失败
	if (exclusiveCount(c) != 0 &&
		getExclusiveOwnerThread() != current)
		return -1;
	int r = sharedCount(c);
	// 如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。
	// 读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是“1<<16”。
	// 所以读写锁才能实现读读的过程共享,而读写、写读、写写的过程互斥
	if (!readerShouldBlock() &&
		r < MAX_COUNT &&
		compareAndSetState(c, c + SHARED_UNIT)) {
		if (r == 0) {
			firstReader = current;
			firstReaderHoldCount = 1;
		} else if (firstReader == current) {
			firstReaderHoldCount++;
		} else {
			HoldCounter rh = cachedHoldCounter;
			if (rh == null || rh.tid != getThreadId(current))
				cachedHoldCounter = rh = readHolds.get();
			else if (rh.count == 0)
				readHolds.set(rh);
			rh.count++;
		}
		return 1;
	}
	return fullTryAcquireShared(current);
}

读写锁可以实现读读过程中共享,而读写、写读、写写的过程中互斥。

AQS

AQS是AbstractQueuedSynchronizer的简称。

原理

  • AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架
  • AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配

CLH:Craig、Landin and Hagersten队列,单向链表,AQS中队列是CLH变体的虚拟双向队列(FIFO)。

  1. AQS使用一个volatileint型成员变量表示同步状态
  2. 通过内置的FIFO队列来完成资源获取排队工作
  3. 通过CAS完成对state值的修改

Node类结构:

方法和属性类型说明
waitStatusint当前节点在队列中状态
SIGNAL(-1): 线程已经准备好,就等待资源释放
CANCELLED(1):线程获取锁的请求已经被取消(超时或中断)
CONDITION(-2):节在等待队列中,线程等待唤醒
PROPAGATE(-3):当前线程处于SHARED状态下,该字段才会使用
0: 初始状态
threadThread处于该节点的线程
preNode指向前一个节点
nextNode指向下一个节点
nextWaiterNode指向下一个处于CONDITION状态的节点

AQS框架

锁优化

同步的性能问题

  • 互斥同步对性能最大的影响是阻塞实现,挂起线程和恢复线程的操作都需要转入内核态中完成,这些操作给系统并发性能带来了很大的压力
  • 共享数据的锁定状态只会持续很短的一段时间,为了这段很短时间去挂起和恢复线程并不值得

优化的处理

自旋锁

让线程执行一个忙循环(自旋)

  • 自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器的时间
  • 如果锁被占用的时间很短,则自旋的效果非常好
  • 反之,如果锁被占用时间过长,则自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而带来性能的浪费
  • 自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数(默认10次,用户可以修改参数-XX:PreBlockSpin),仍然没有获得锁,则应当使用传统方式挂起线程

自适应自旋锁

自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定

  • 如果再同一个锁对象上,自旋等待刚刚获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间
  • 如果对于某个锁,自旋很少获得成功,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源

锁消除

  • 锁消除是指虚拟机即时编译在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。
  • 锁消除的主要判定依据来源于逃逸分析的数据支持
    • 如果判断一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它是线程私有的,同步加锁就无需进行

锁粗化

如果虚拟机探测到有一串零碎的操作都对同一个对象加锁,将会把锁同步的范围扩展(粗化)到整个操作序列的外部。这样就避免了频繁地进行互斥同步操作导致不必要的性能损耗

轻量级锁

轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量

Hotspot虚拟机对象头Mark Word(64位JVM)

标志位锁状态存储内容
01未锁定对象哈希码(31)对象分代年龄(4)
00轻量级锁栈中锁记录的指针(64)
10重量级锁monitor的指针(64)
11GC标记空,不需要记录信息
01偏向锁偏向线程ID(54)、偏向时间戳(2)、对象分代年龄(4)

MarkWord最后两位用来表示该对象的锁状态

执行过程

  1. 在代码块进入同步块的时候,如果此时同步对象没有被锁定(锁标记为01状态),JVM首席在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word拷贝(Displaced Mark Word)

  1. 然后,虚拟机将使用CAS操作尝试对对象的Mark Word更新为指向Lock Record的指针。
    • 如果这个更新操作成功了,那么这个线程就拥有了该对象的锁,锁标记转变为“00”(轻量级锁),如下图
    • 如果更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧
      • 如果是说明当前线程已经拥有这个对象的锁,难就可以直接进入同步块继续执行
      • 否则说明这个锁对象已经被其他线程抢占了。如果两条以上的

偏向锁

偏向锁的目的是消除数据在无竞争的情况下的同步原语(连CAS都不做),进一步提高程序的运行性能。

这个锁会偏向第一个获得它的线程,如果再接下来执行过程中,该锁没有被其他线程获取,则持有偏向锁的线程将永远不需要再进行同步。

步骤:

  1. 如果当前虚拟机启用了偏向锁(-XX:+UseBiasedLocking),那么锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式
  2. 同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中
  3. 如果CAS成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(如Locking Unlocking及对Mark Word的Update等)
  4. 当有另外一个线程尝试去获取这个锁时,偏向模式宣告结束。根据锁对象目前是否处于被锁定的状态,偏向撤销(Revoke Bias)后恢复到未锁定(“01”)或轻量级锁定(“00”),后续的同步操作如上面的轻量级锁那样执行。

偏向锁、轻量级锁的状态转化及对象Mark Word的关系

线程状态

  • 新建(New):创建后尚未启动的线程
  • 运行(Runable):Runable包括了操作系统线程状态中的Running和Ready,也就是处于此状态的线程可能正在执行,也有可能正在等待着CPU为它分配执行时间
  • 无限期等待(Waiting):处于这种状态的线程不会被分配CPU执行时间,它们要等待被其他线程显式地唤醒。方法如下:
    • 没有设置Timeout参数的Object.wait()方法
    • 没有设置Timeout参数的Thread.join()方法
    • LockSupport.park()方法
  • 限期等待(Timed Waiting):处于这种状态的线程也不会被分配CPU执行时间,在一定的时间后它们会自动唤醒。方法如下:
    • Thread.sleep()方法
    • 设置了Timeout参数的Object.wait()方法
    • 设置了Timeout参数的Thread.join()方法
    • LockSupport.parkNanos()方法
    • LockSupport.packUntil()方法
  • 阻塞(Blocked):“阻塞状态”与“等待状态”的区别是
    • “阻塞状态”在等待着获取到一个排他锁,这个事件在另外一个线程放弃这个锁的时候发生
    • “等待状态”是在等待一段时间,或者唤醒动作发生。在程序等待进入同步区域的时候,线程进入这种状态
  • 结束(Terminated):已终止的线程状态

实现

volatile

volatile修饰的变量具有两种特性:

  1. 保证此变量对所有线程的可见性

但不符合以下两种仍要通过加锁来保证原子性:

  • 运算结果并不依赖于变量的当前值,或者能够确保只有单一线程修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

如下面的代码就不符合,无法保证原子性

public class VoltatileTest {
    public static final int THREADS_COUNT = 20;
    public static volatile int race = 0;
    public static void increase() {
        race++;
    }
    
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
            threads[i].start();
        }

        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println(race);
    }
}

问题出在race++之中:

  • getstatic指令把race值取到操作数栈顶时,volatile关键字保证了race的值在此时是正确的
  • 但是在执行iconst_1iadd指令的时候,其他的线程已经把race的值加大了
  • 而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存之中
public static void increase();
    Code:
       0: getstatic     #2                  // Field race:I
       3: iconst_1
       4: iadd
       5: putstatic     #2                  // Field race:I
       8: return
  1. 禁止指令重排优化

synchronized

修饰实例方法

进入同步代码前,需要获取当前实例的锁

  • 方法级的同步是隐式的,即无需通过字节码指令来控制,它实现在方法的调用和返回操作之中
  • JVM可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否是同步方法
  • 当方法调用时,如果设置了ACC_SYNCHRONIZED,执行线程将先持有monitor
  • 如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛出到同步方法之外自动释放
public synchronized void increase4Obj(){
    i++;
}

反编译字节码

public synchronized void increase4Obj();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED     // 同步方法标志
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field race:I
         8: return
      LineNumberTable:
        line 49: 0
        line 50: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LMain;

修饰静态方法

进入同步代码前,需要获取当前类对象的锁

// 作用于静态方法,使用当前class对象锁
public static synchronized void increase(){
    i++;
}

反编译字节码

public static synchronized void increase();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field race:I
         3: iconst_1
         4: iadd
         5: putstatic     #2                  // Field race:I
         8: return
      LineNumberTable:
        line 20: 0
        line 21: 8

修饰代码块

指定加锁对象,进入同步代码块前,需要获取给定对象的锁

  • 同步语句块使用的实现使用的是monitorentermonitorexit指令
  • 当执行monitorenter指令时,当前线程试图获取objectref(对象锁)所对应的monitor的持有权
  • objectrefmonitor进入时计数器为0,那么线程可以成功取得monitor并将计数器置为1,获取锁成功
  • 如果当前线程已经拥有objectrefmonitor的持有权,那么它可以重入这个monitor重入时计数器也会加1
  • 倘若其他线程已经拥有objectrefmonitor的所有权,那当前线程将被阻塞,知道正在执行的线程完毕(即monitorexit),执行线程将释放monitor锁并设置计数器值为0,其他线程将有机会持有monitor

this当前实例对象锁

public void increaceSyncBlock() {
    synchronized(this){
        for(int i=0; i<1000000; i++) {
            race++;
        }
    }
}

反编译字节码

public void increaceSyncBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter                    // 进入同步方法
         4: iconst_0
         5: istore_2
         6: iload_2
         7: ldc           #23                 // int 1000000
         9: if_icmpge     26
        12: getstatic     #2                  // Field race:I
        15: iconst_1
        16: iadd
        17: putstatic     #2                  // Field race:I
        20: iinc          2, 1
        23: goto          6
        26: aload_1
        27: monitorexit                     // 退出同步方法
        28: goto          36
        31: astore_3
        32: aload_1
        33: monitorexit                     // 异常时,退出同步方法
        34: aload_3
        35: athrow
        36: return

从反编译字节码可以看出来,为了保证在方法异常时,仍然可以monitorentermonitorexit指令正确配对,编译器会自动产生一个异常处理器(可以处理所有的异常),它的目的就是为了用来执行monitorexit指令

class对象锁

public void increateSyncClass() {
    synchronized(Other.class){
        for(int i=0; i<1000000; i++) {
            race++;
        }
    }
}

反编译字节码

public void increateSyncClass();
	descriptor: ()V
	flags: ACC_PUBLIC
	Code:
	  stack=2, locals=4, args_size=1
		 0: ldc           #24                 // class Other   获取class锁
		 2: dup
		 3: astore_1
		 4: monitorenter                    // 进入同步方法
		 5: iconst_0
		 6: istore_2
		 7: iload_2
		 8: ldc           #23                 // int 1000000
		10: if_icmpge     27
		13: getstatic     #2                  // Field race:I
		16: iconst_1
		17: iadd
		18: putstatic     #2                  // Field race:I
		21: iinc          2, 1
		24: goto          7
		27: aload_1
		28: monitorexit                     // 退出同步方法
		29: goto          37
		32: astore_3
		33: aload_1
		34: monitorexit                     // 异常时,退出同步方法
		35: aload_3
		36: athrow
		37: return


底层语义原理

synchronized基于进入和退出管程(Monitor)对象实现。

  • 同步代码块:有明确的monitorentermonitorexit指令
  • 方法同步:方法调用指令读取运行时常量池中方法的ACC_SYNCHRONIZED标志来隐式实现

Java对象头与Monitor

对象在内存中的布局:

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐
  • 填充数据:由于虚拟机要求对象的其实地址必须是8字节的整数倍,仅仅是为了字节对齐
  • 对象头:synchronized的锁对象的基础

synchronized重量级锁

当多个线程同时请求某个监控器时,转换实现

  • Contention List: 所有请求锁的线程将被首先放置到该竞争队列
  • Entry List: Contention List中那些有资格成为候选人的线程被移到此处
  • Wait Set: 那些调用wait方法被阻塞的线程放置到Wait Set
  • OnDeck: 任何时刻最多只能有一个线程正在竞争锁,该线程成为OnDeck
  • Owner: 获得锁的线程
  • !Owner: 释放锁的线程

wait()notity, notifyAll()方法依赖于ObjectMonitor模式实现

ObjectMonitor() {
    _header     = NULL;
    _count      = 0;    // 记录个数
    waiters     = 0,
    _recursions = 0;
    _object     = NULL;
    _owner      = NULL;
    _WaitSet    = NULLL; // 处于wait状态的线程,会被加入
    _WaitSetLock= 0;
    _Responsible= 0;
    _succ       = NULL;
    _cxq        = NULL;
    FreeNext    = NULL;
    _EntryList  = NULL; // 处于等待锁block状态的线程,会被加入
    _SpinFreq   = 0;
    _SpinClock  = 0;
    OwnerIsThread = 0;
}
  • 当线程获取到对象的monitor后进入_Owner区域,并把monitor中的owner变量设置为当前线程,同时monitor中的技术器count加1
  • 若线程调用wait()方法,失望当前持有monitorowner变量变为null,count自减1,同时该线程进入WaitSet集合中等待被唤醒
  • 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor

monitor对象存在于每个Java对象的对象头中(存储的引用),synchronized锁便是通过这种方式获取锁的。

JVM对synchronized的优化

最初synchronized是重量级锁,随着后来的优化,synchronized可以从偏向锁 -> 轻量级锁 -> 重量级锁

CAS与Unsafe类及并发报Atomic

Unsafe

  • Unsafe类位于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存。Java官方不建议直接使用Unsafe类,但Java中的CAS操作的执行依赖Unsafe类的方法:
/**
 * 
 * @param o 给定对象
 * @param offset 对象内存偏移,可以迅速定位字段并设置或获取该字段的值
 * @param expected 表示期望值
 * @param set 要设置的值
 * @return 通过CAS原子指令执行操作
 */
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object set);

public final native boolean compareAndSwapInt(Object o, long object, int expected, int set);

public final native boolean compareAndSwapLong(Object o, long expected, long expected, long set);

public final int getAndAddInt(Object o, long offset, int delta);
public final int getAndSetInt(Object o, long offset, int newValue);
public final long getAndAddLong(Object o, long offset, long delta);
public final long getAndSetLong(Object o, long offset, long newValue);
public final Object getAndSetObject(Object o, long offset, Object newValue) {
    Object v;
    do {
        v = getObjectVolatile(o, offset);
    } while (!compareAndSwapObject(o, offset, v, newValue));
    return v;
}
  • 挂起与恢复:LockSupport.park最终调用的底层的Unsafe.packUnsafe.unpack
  • 内存屏障:定义内存屏障,避免代码重排序(loadFence storeFence fullFence)

Atomic

位于java.util.concurrent.atomic包下

  • 更新基本类型AtomicBoolean AtomicInteger AtomicLong,底层都是通过Unsafe的CAS操作方法实现
/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

  • 原子更新引用AtomicReference
  • 原子更新数组AtomicIntegerArray AtomicLongArray AtomicReferenceArray,原理同上 ?数组元素的内存地址 = 起始地址 + 下标 * 每个元素占用内存空间?
  • 原子更新属性:更新某个类里面的某个字段,AtomicIntegerFieldUpdater AtomicLongFieldUpdater AtomicReferrenceFieldUpdater,限制条件较多:
    • 操作的字段不能是static或final
    • 操作的字段必须是volatile修饰
    • 属性必须对当前的Updater可见(proteced,public)

示例

public static void main(String[] args) throws Exception   {
    AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(Person.class, String.class,"name");
    Person person = new Person();
    System.out.println(referenceFieldUpdater.compareAndSet(person,"person","personXXX"));
    System.out.println(referenceFieldUpdater.getAndSet(person, "getAndSet"));

    AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
    integerFieldUpdater.incrementAndGet(person);
}

static class Person {
    volatile String name = "person";
    volatile int age = 10;
}
  • 解决ABA问题
    • AtomicStampedReference:带有时间戳的对象引用,在每次修改后,不仅会设置新值,而且还会记录更改时间。当设置对象时,对象值及时间戳都必须满足期望方可写入成功,解决了反复读写,无法预知值是否修改的ABA问题
    • AtomicMarkableReference:维护一个boolean值的标识,在truefalse两种状态切换,并不能完全防止ABA出现,但可以减少出现的概率

JUC中的应用场景

ReentrantLock

synchronized对比

ReentrantLocksynchronized
锁实现机制依赖AQS监视器模式
灵活性支持中断、超时、尝试获取锁不灵活
锁释放必须显示调用unlock自动释放监视器
锁类型公平锁&非公平锁非公平锁
条件队列可关联多个条件队列关联一个条件队列
可重入可以可以

JUC中的锁

同步工具与AQS关联
ReentrantLock使用AQS保存锁重复持有的次数,当一个线程获取锁时,ReentrantLock记录当前获得锁的线程标识,用于检测是否重复获取,以及错误线程试图解锁操作时,异常情况处理
Semaphore使用AQS同步状态来保存信号量当前计数
CountDownLatch使用AQS同步状态表示计数,计数为0时,所有的Acquire操作(wait方法)才可以通过
ReantrantReadWriteLock使用AQS同步状态中的低16位保存写锁持有次数,高16位保存读锁持有次数
ThreadPoolExecutorWoker利用AQS同步状态实现对独占线程变量的设置

COW(Copy-On-Write)

Copy-On-Write: 写时复制容器:

  • 当添加元素是,先将当前容器就行复制(Copy),然后在新的容器中条件元素,添加完成后**,再将原容器的引用指向新容器**。
  • 好处:我们可以对COW容器进行并发的读,而不需要加锁,是一种读写分离的思想

COW缺点:

  • 内存占用较大:写操作的时候,会同事存在两份对象内存
  • 数据一致性问题:COW容器只能保证数据的最终一致性,不能保证数据的实时一致性,如果希望写入的数据,马上就能读到,不要使用COW

CopyOnWriteArrayList实现代码解析

  • get方法:直接获取,无需锁
private E get(Object[] a, int index) {
    return (E) a[index];
}
  • add方法:使用ReentrantLock锁,比如在队列末尾添加数据:
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // 使用锁
    lock.lock();
    try {
        // 获取旧数据引用
        Object[] elements = getArray();
        int len = elements.length;
        // 创建新的数组,并将旧数据拷贝到新数组中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        // 在新容器中添加数据
        newElements[len] = e;
        // 将引用指向新的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
  • replace更新:使用ReentrantLock
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            // 创建新的数组,并将老的数据拷贝到新数组
            Object[] newElements = Arrays.copyOf(elements, len);
            // 更新数据
            newElements[index] = element;
            // 将引用指向新的数组
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}