JUC多线程 (二)

68 篇文章 0 订阅
42 篇文章 0 订阅

5 Volatile

通过前面内容我们了解了 synchronized ,虽然 JVM 对它做了很多优化,但是它还
是一个重量级的锁。而接下来要介绍的 volatile 则是轻量级的 synchronized 。如果一
个变量使用 volatile ,则它比使用 synchronized 的成本更加低,因为它不会引起线程
上下文的切换和调度。
Java 语言规范对 volatile 的定义如下:
Java 允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过
排他锁单独获得这个变量。
通俗点讲就是说一个变量如果用 volatile 修饰了,则 Java 可以确保所有线程看到这
个变量的值是一致的,如果某个线程对 volatile 修饰的共享变量进行更新,那么其他
线程可以立马看到这个更新,这就是内存可见性。
volatile 虽然看起来比较简单,使用起来无非就是在一个变量前面加上 volatile 即可,
但是要用好并不容易。

5.1 解决内存可见性问题

在可见性问题案例中进行如下修改,添加 volatile 关键词:
private volatile boolean flag = true;

线程写 Volatile 变量的过程:
1. 改变线程本地内存中 Volatile 变量副本的值;
2. 将改变后的副本的值从本地内存刷新到主内存
线程读 Volatile 变量的过程:
1. 从主内存中读取 Volatile 变量的最新值到线程的本地内存中
2. 从本地内存中读取 Volatile 变量的副本
Volatile 实现内存可见性原理:
写操作时,通过在写操作指令后加入一条 store 屏障指令,让本地内存中变量的值能
够刷新到主内存中
读操作时,通过在读操作前加入一条 load 屏障指令,及时读取到变量在主内存的值
PS: 内存屏障( Memory Barrier )是一种 CPU 指令,用于控制特定条件下的重排序和内存
可见性问题。 Java 编译器也会根据内存屏障的规则禁止重排序
volatile 的底层实现是通过插入内存屏障,但是对于编译器来说,发现一个最优布置
来最小化插入内存屏障的总数几乎是不可能的,所以, JMM 采用了保守策略。如
下:
  • StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作都已经刷新到主内存中。
  • StoreLoad屏障的作用是避免volatile写与后面可能有的volatile/写操作重排序。
  • LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。
  • LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。

5.2 原子性的问题

虽然 Volatile 关键字可以让变量在多个线程之间可见,但是 Volatile 不具备原子性
public class Demo3Volatile {
public static void main(String[] args) throws
InterruptedException {
VolatileDemo demo = new VolatileDemo();
Thread t = new Thread(demo);
t.start();
}
Thread.sleep(1000);
System.out.println(demo.count);
}
static class VolatileDemo implements Runnable {
public volatile int count;
//public volatile AtomicInteger count = new
AtomicInteger(0);
public void run() {
addCount();
}
public void addCount() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
}
以上出现原子性问题的原因是 count++ 并不是原子性操作。
count = 5 开始,流程分析:
1. 线程 1 读取 count 的值为 5
2. 线程 2 读取 count 的值为 5
3. 线程 2 1 操作
4. 线程 2 最新 count 的值为 6
5. 线程 2 写入值到主内存的最新值为 6
这个时候,线程 1 count 5 ,线程 2 count 6
如果切换到线程 1 执行,那么线程 1 得到的结果是 6 ,写入到主内存的值还是 6
现在的情况是对 count 进行了两次加 1 操作,但是主内存实际上只是加 1 一次
1. 使用 synchronized
2. 使用 ReentrantLock (可重入锁)
3. 使用 AtomicInteger (原子操作)
使用 synchronized
public synchronized void addCount() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
使用 ReentrantLock (可重入锁)
//可重入锁
private Lock lock = new ReentrantLock();
public void addCount() {
for (int i = 0; i < 10000; i++) {
lock.lock();
count++;
lock.unlock();
}
}
使用 AtomicInteger (原子操作)
public static AtomicInteger count = new AtomicInteger(0);
public void addCount() {
for (int i = 0; i < 10000; i++) {
//count++;
count.incrementAndGet();
}
}

5.3 Volatile 适合使用场景

a )对变量的写入操作不依赖其当前值
        满足:boolean 变量、直接赋值的变量等
b )该变量没有包含在具有其他变量的不变式中
        不满足:不变式 low<up
总结:变量真正独立于其他变量和自己以前的值,在单独使用的时候,适合用
volatile

5.4 synchronizedvolatile比较

a volatile 不需要加锁,比 synchronized 更轻便,不会阻塞线程
b synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,
无法保证原子性
与锁相比, Volatile 变量是一种非常简单但同时又非常脆弱的同步机制,它在某些
情况下将提供优于锁的性能和伸缩性。如果严格遵循 volatile 的使用条件( 变量真
正独立于其他变量和自己以前的值 ) 在某些情况下可以使用  volatile 
 synchronized  来优化代码提升效率。

6 J.U.CCAS

J.U.C java.util.concurrent ,是 JSR 166 标准规范的一个实现; JSR 166 以及 J.U.C
包的作者是 Doug Lea
J.U.C 框架是 Java 5 中引入的,而我们最熟悉的线程池机制就在这个包, J.U.C 框架
包含的内容有:
AbstractQueuedSynchronizer AQS 框架), J.U.C 中实现锁和同步机制的基
础;
Locks & Condition (锁和条件变量),比 synchronized wait notify 更细粒
度的锁机制;
Executor 框架(线程池、 Callable Future ),任务的执行和调度框架;
Synchronizers (同步器),主要用于协助线程同步,有 CountDownLatch
CyclicBarrier Semaphore Exchanger
Atomic Variables (原子变量),方便程序员在多线程环境下,无锁的进行原子
操作,核心操作是
CAS 原子操作,所谓的 CAS 操作,即 compare and swap
BlockingQueue (阻塞队列),阻塞队列提供了可阻塞的入队和出对操作,如 果队列满了,入队操作将阻塞直到有空间可用,如果队列空了,出队操作将阻 塞直到有元素可用; Concurrent Collections (并发容器),说到并发容器,不得不提同步容器。在 JDK1.5 之前,为了线程安全,我们一般都是使用同步容器,同步容器主要的缺 点是:对所有容器状态的访问都串行化,严重降低了并发性;某些复合操作, 仍然需要加锁来保护;迭代期间,若其它线程并发修改该容器,会抛出 ConcurrentModificationException 异常,即快速失败机制; Fork/Join 并行计算框架,这块内容是在 JDK1.7 中引入的,可以方便利用多核 平台的计算能力,简化并行程序的编写,开发人员仅需关注如何划分任务和组 合中间结果; TimeUnit 枚举, TimeUnit java.util.concurrent 包下面的一个枚举类, TimeUnit 提供了可读性更好的线程暂停操作,以及方便的时间单位转换方法;

6.1 CAS介绍

CAS Compare And Swap ,即比较并交换。同步组件中大量使用 CAS 技术实现
Java 多线程的并发操作。整个 AQS 同步组件、 Atomic 原子类操作等等都是以 CAS
实现的,甚至 ConcurrentHashMap 1.8 的版本中也调整为了 CAS+Synchronized
可以说 CAS 是整个 JUC 的基石

6.2 CAS原理剖析

再次测试之前Volatile的例子,把循环的次数调整为一亿(保证在一秒之内不能遍 历完成,从而测试三种原子操作的性能),我们发现,  AtomicInteger原子操作性能 最高,他是用的就是CAS

6.2.2 synchronized同步分析

注意,本小节是解释synchronized性能低效的原因,只要能理解synchronized 步过程其实还需要做很多事,这些逻辑的执行都需要占用资源,从而导致性能较

低,是为了对比CAS的高效。这部分分析过于深入JMM底层原理,不适合初级甚至 中级程序员学习。

我们之前讲过,  synchronized的同步操作主要是monitorentermonitorexit 两个jvm指令实现的,我们先写一段简单的代码:

public class Demo2Synchronized {
public void test2() {
synchronized (this) {
}
}
}
javac Demo2Synchronized.java
javap -c Demo2Synchronized.class

从结果可以看出,同步代码块是使用monitorentermonitorexit这两个jvm指令实 现的:

monitorentermonitorexit这两个jvm指令实现锁的使用,主要是基于 Mark Word和、 monitor

Mark Word

Hotspot虚拟机的对象头主要包括两部分数据:  Mark Word(标记字段)  Klass Pointer  (类型指针)。其中Klass Point是是对象指向它的类元数据的指针,虚拟机 通过这个指针来确定这个对象是哪个类的实例,  Mark Word用于存储对象自身的运  行时数据,它是synchronized实现轻量级锁和偏向锁的关键。

年龄、锁状态标志、线程持有的锁、偏向线ID、偏向时间戳等等。Java对象头一  般占有两个机器码(在32位虚拟机中,  1个机器码等于4字节,也就是32bit),但是 如果对象是数组类型,则需要三个机器码,因为JVM虚拟机可以通过Java对象的元数 据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用   一块来记录数组长度。下图是Java对象头的存储结构(32位虚拟机

对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的 空间效率,  Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储 尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,  Mark Word 会随着程序的运行发生变化,变化状态如下(32位虚拟机

monitor

什么是Monitor?我们可以把它理解为一个同步工具,也可以描述为一种同步机 制,它通常被描述为一个对象。与一切皆对象一样,所有的Java对象是天生的    Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每  一个Java对象都带了一把看不见的锁,它叫做内部锁或者Monitor锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record   列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor 关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时

monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个 线程占用

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record当线程 成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL

·  EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。

·  RcThis:表示blockedwaiting在该monitor record上的所有线程的个数。

   Nest:用来实现重入锁的计数。

   HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

  Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程 能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线  程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻

塞)从而导致性能严重下降。  Candidate只有两种可能的值0表示没有需要唤醒 的线程1表示要唤醒一个继任线程来竞争锁。

6.2.3 CAS原理

在上一部分,我们介绍了synchronized底层做了大量的工作,才实现同步,而同 步保证了原子操作。但是不可避免的是性能较低。  CAS如何提高性能的呢?

CAS的思想很简单:三个参数,  一个当前内存值V、旧的预期值A、即将更新的值  B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什 么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直  到成功。

CAS 先比较后修改 这个CAS过程中,根本没有获取锁,释放锁的操作,是硬件 层面的原子操作,跟JMM内存模型没有关系。大家可以理解为直接使用其他的语

言,在JVM虚拟机之外直接操作计算机硬件,正因为如此,对比synchronized的同 步,少了很多的逻辑步骤,使得性能大为提高。

JUC下的atomic类都是通过CAS来实现的,下面就是一个AtomicInteger原子操作 类的例子,在其中使用了Unsafe unsafe = Unsafe.getUnsafe() Unsafe CAS的  核心类,它提供了硬件级别的原子操作

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//操作的值也进行了volatile修饰,保证内存可见性
private volatile int value;
继续查看 AtomicInteger addAndGet() 方法:
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 +
var4));
return var5;
}
public final native boolean compareAndSwapInt(Object var1, long
var2, int var4, int var5);
Unsafe 是一个比较危险的类,主要是用于执行低级别、不安全的方法集合。尽管
这个类和所有的方法都是公开的( public ),但是这个类的使用仍然受限,你无法
在自己的 java 程序中直接使用该类,因为只有授信的代码才能获得该类的实例。可
是为什么 Unsafe native 方法就可以保证是原子操作呢?

6.3 native关键词

前面提到了 sun.misc.Unsafe 这个类,里面的方法使用 native 关键词声明本地方
法,为什么要用 native
Java 无法直接访问底层操作系统,但有能力调用其他语言编写的函数 or 方法,是
通过 JNI(Java Native Interfface) 实现。使用时,通过 native 关键字告诉 JVM 这个方法
是在外部定义的。但 JVM 也不知道去哪找这个原生方法,此时需要通过 javah 命令生
.h 文件。
示例步骤 (c 语言为例 )
1. javac 生成 .class 文件,比如 javac NativePeer.java
2. javah 生成 .h 文件,比如 javah NativePeer
3. 编写 c 语言文件,在其中 include 进上一步生成的 .h 文件,然后实现其中声明而未
实现的函数
4. 生成 dll 共享库,然后 Java 程序 load 库,调用即可
native 可以和任何除 abstract 外的关键字连用,这也说明了这些方法是有实体
的,并且能够和其他 Java 方法一样,拥有各种 Java 的特性。
native 方法有效地扩充了 jvm ,实际上我们所用的很多代码已经涉及到这种方法
了,通过非常简洁的接口帮我们实现 Java 以外的工作。

native优势:

1. 很多层次上用Java去实现是很麻烦的,而且Java解释执行的效率也差了c语言啥 的很多,纯Java实现可能会导致效率不达标,或者可读性奇差。

2. Java毕竟不是一个完整的系统,它经常需要一些底层的支持,通过JNInative    method我们就可以实现jre与底层的交互,得到强大的底层操作系统的支持,使 用一些Java本身没有封装的操作系统的特性。

 

6.4 CPUCAS处理

CAS可以保证一次的读--写操作是原子操作,在单处理器上该操作容易实现,但 是在多处理器上实现就有点儿复杂了。  CPU提供了两种方法来实现多处理器的原子   操作:总线加锁或者缓存加锁。

  总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理 器在总线上输出此信号时,其他处理器的请求将被阻塞住,么该处理器可以独 占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内  存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,

其开销有点儿大。

  缓存加锁:其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地   址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期   间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部   的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同   一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的i 时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。

CAS缺陷

CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方 法:循环时间太长、只能保证一个共享变量原子操作、  ABA问题。

  循环时间太长

如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成 功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次

数,例如BlockingQueueSynchronousQueue

  只能保证一个共享变量原子操作

看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使 用锁了。

ABA问题

CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样 一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的 时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。 对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次 改变时加1,即A —> B —> A,变成1A —> 2B —> 3A

CASABA隐患问题,Java提供了AtomicStampedReference来解决。

AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳 stamp,从而避免ABA问题。对于上面的案例应该线程1会失败。

下面我们将通过一个例子可以可以看到AtomicStampedReference

AtomicInteger的区别。我们定义两个线程,线程1负责将100 —> 110 —> 100,线 2执行 100 —>120,看两者之间的区别。

public class Demo4ABA {

private static AtomicInteger ai = new AtomicInteger(100);
private static AtomicStampedReference air = new
AtomicStampedReference(100, 1);

//ABA问题演示:
//1. 线程1先对数据进行修改 A-B-A过程
//2. 线程2也对数据进行修改 A-C的过程
public static void main(String[] args) throws
InterruptedException {

// AtomicInteger可以看到不会有任何限制随便改
// 线程2修改的时候也不可能知道要A-C 的时候,A是原来的A还是修改之后 的A
Thread at1 = new Thread(new Runnable() {
public void run() {
ai.compareAndSet(100, 110);
ai.compareAndSet(110, 100);
}
});

Thread at2 = new Thread(new Runnable() {
public void run() {
try {
//为了让线程1先执行完,等一会
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + ai.compareAndSet(100, 120));
System.out.println("执行结果:" + ai.get());
}
});

//tsf2先获取stamp,导致预期时间戳不一致
int stamp = air.getStamp();

try {
TimeUnit.MILLISECONDS.sleep(100);     
//线程tsf1执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference:" + air.compareAndSet(100, 120, stamp, stamp + 1));
int[] stampArr = {stamp + 1};
System.out.println("执行结果:" +
air.get(stampArr));


}

运行结果充分展示了AtomicIntegerABA问题和AtomicStampedReference解决 ABA问题。

7 J.U.Catomic

7.1 atomic包介绍

通过前面 CAS 的学习,我们了解到 AtomicInteger 的工作原理,它们的内部都维护
者一个对应的基本类型的成员变量 value ,这个变量是被 volatile 关键字修饰的,保
证多线程环境下看见的是同一个(可见性)。
AtomicInteger 在进行一些原子操作的时候,依赖 Unsafe 类里面的 CAS 方法,原子
操作就是通过自旋方式,不断地使用 CAS 函数进行尝试直到达到自己的目的。
除了 AtomicInteger 类以外还有很多其他的类也有类似的功能,在 JUC 中有一个包
java.util.concurrent.atomic 存放原子操作的类, atomic 里的类主要包括:
基本类型
使用原子的方式更新基本类型
AtomicInteger :整形原子类
AtomicLong :长整型原子类
AtomicBoolean :布尔型原子类
引用类型
AtomicStampedReference :原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
数组类型
使用原子的方式更新数组里的某个元素
AtomicIntegerArray :整形数组原子类
AtomicLongArray :长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
对象的属性修改类型
AtomicIntegerFieldUpdater: 原子更新整形字段的更新器
AtomicLongFieldUpdater :原子更新长整形字段的更新器
AtomicReferenceFieldUpdater :原子更新引用类形字段的更新器
JDK1.8 新增类
DoubleAdder :双浮点型原子类
LongAdder :长整型原子类
DoubleAccumulator :类似 DoubleAdder ,但要更加灵活 ( 要传入一个函数
式接口 )
LongAccumulator :类似 LongAdder ,但要更加灵活 ( 要传入一个函数式接
)
虽然涉及到的类很多,但是原理和 AtomicInteger 都是一样,使用 CAS 进行的原子
操作,其方法和使用都是大同小异的。

7.2 基本类型

使用原子的方式更新基本类型
AtomicInteger :整形原子类
AtomicLong :长整型原子类
AtomicBoolean :布尔型原子类
AtomicInteger 主要 API 如下:
getAndAdd(int) //增加指定的数据,返回变化前的数据
getAndDecrement() //减少1,返回减少前的数据
getAndIncrement() //增加1,返回增加前的数据
getAndSet(int) //设置指定的数据,返回设置前的数据
addAndGet(int) //增加指定的数据后返回增加后的数据
decrementAndGet() //减少1,返回减少后的值
incrementAndGet() //增加1,返回增加后的值
lazySet(int) //仅仅当get时才会set
compareAndSet(int, int)//尝试新增后对比,若增加成功则返回true否则返回false
AtomicLong 主要 API AtomicInteger ,只是类型不是 int ,而是 long
AtomicBoolean 主要 API 如下:
compareAndSet(boolean, boolean) //参数1为原始值,参数2为修改的新值,若
修改成功返回true,否则返回false
getAndSet(boolean)// 尝试设置新的boolean值,直到成功为止,返回设置前的数据

7.4 引用类型

AtomicReference :引用类型原子类
AtomicStampedRefrence :原子更新引用类型里的字段原子类
AtomicMarkableReference :原子更新带有标记位的引用类型
AtomicReference 引用类型和基本类型的作用基本一样,例子如下:
public class Demo5AtomicReference {
public static void main(String[] args) throws
InterruptedException {
User u1 = new User("张三", 22);
User u2 = new User("李四", 33);
AtomicReference ar = new AtomicReference(u1);
ar.compareAndSet(u1, u2);
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
AtomicStampedReference 其实它仅仅是在 AtomicReference 类的再一次包装,
里面增加了一层引用和计数器,其实是否为计数器完全由自己控制,大多数我们是
让他自增的,你也可以按照自己的方式来标示版本号。案例参考前面的 ABA 例子
AtomicMarkableReference AtomicStampedReference 功能差不多,区别的
是:它描述更加简单的是与否的关系。通常 ABA 问题只有两种状态,而
AtomicStampedReference 是多种状态。
public class Demo6AtomicMrkableReference {
public static void main(String[] args) throws
InterruptedException {
User u1 = new User("张三", 22);
User u2 = new User("李四", 33);
//和AtomicStampedReference效果一样,用于解决ABA的
//区别是表示不是用的版本号,而只有true和false两种状态。相当于未修
改和已修改
AtomicMarkableReference<User> amr = new
AtomicMarkableReference(u1, true);
amr.compareAndSet(u1, u2, false, true);
System.out.println(amr.getReference());
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}

7.3 数组类型

使用原子的方式更新数组里的某个元素
AtomicIntegerArray :整形数组原子类
AtomicLongArray :长整形数组原子类
AtomicReferenceArray :引用类型数组原子类
AtomicIntegerArray 主要 API 如下:
addAndGet(int, int)//执行加法,第一个参数为数组的下标,第二个参数为增加的
数量,返回增加后的结果
compareAndSet(int, int, int)// 对比修改,参1数组下标,参2原始值,参3修
改目标值,成功返回true否则false
decrementAndGet(int)// 参数为数组下标,将数组对应数字减少1,返回减少后的
数据
incrementAndGet(int)// 参数为数组下标,将数组对应数字增加1,返回增加后的
数据
getAndAdd(int, int)// 和addAndGet类似,区别是返回值是变化前的数据
getAndDecrement(int)// 和decrementAndGet类似,区别是返回变化前的数据
getAndIncrement(int)// 和incrementAndGet类似,区别是返回变化前的数据
getAndSet(int, int)// 将对应下标的数字设置为指定值,第二个参数为设置的值,
返回是变化前的数据
AtomicIntegerArray 主要 API AtomicLongArray ,只是类型不是 int ,而是 long
public class Demo7AtomicIntegerArray {
public static void main(String[] args) throws
InterruptedException {
int[] arr = {1, 2, 3, 4, 5};
AtomicIntegerArray aia = new AtomicIntegerArray(arr);
aia.compareAndSet(1, 2, 200);
System.out.println(aia.toString());
}
}
AtomicReferenceArray 主要 API
//参数1:数组下标;
//参数2:修改原始值对比;
//参数3:修改目标值
//修改成功返回true,否则返回false
compareAndSet(int, Object, Object)
//参数1:数组下标
//参数2:修改的目标
//修改成功为止,返回修改前的数据
getAndSet(int, Object)
AtomicReferenceArray 案例:
public class Demo8AtomicReferenceArray {
public static void main(String[] args) throws
InterruptedException {
User u1 = new User("张三", 22);
User u2 = new User("李四", 33);
User[] arr = {u1, u2};
AtomicReferenceArray<User> ara = new
AtomicReferenceArray<User>(arr);
System.out.println(ara.toString());
ara.compareAndSet(0, u1, u3);
System.out.println(ara.toString());
}
static class User {
private String name;
public volatile int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}

7.5 对象的属性修改类型

AtomicIntegerFieldUpdater: 原子更新整形字段的更新器
AtomicLongFieldUpdater :原子更新长整形字段的更新器
AtomicReferenceFieldUpdater :原子更新引用类形字段的更新器
但是他们的使用通常有以下几个限制:
限制 1 :操作的目标不能是 static 类型,前面说到的 unsafe 提取的是非 static 类型
的属性偏移量,如果是 static 类型在获取时如果没有使用对应的方法是会报错
的,而这个 Updater 并没有使用对应的方法。
限制 2 :操作的目标不能是 final 类型的,因为 final 根本没法修改。
限制 3 :必须是 volatile 类型的数据,也就是数据本身是读一致的。
限制 4 :属性必须对当前的 Updater 所在的区域是可见的,也就是 private 如果不
是当前类肯定是不可见的, protected 如果不存在父子关系也是不可见的,
default 如果不是在同一个 package 下也是不可见的。
实现方式:通过反射找到属性,对属性进行操作。
例子:
public class AtomicIntegerFieldUpdaterTest {
public static void main(String[] args) {
AtomicIntegerFieldUpdater<User> a =
AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
User user = new User("Java", 22);
System.out.println(a.get(user));
System.out.println(a.getAndAdd(user,10));
System.out.println(a.get(user));
}
}
class User {
private String name;
public volatile int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
7.6 JDK1.8 新增类
LongAdder :长整型原子类
DoubleAdder :双浮点型原子类
LongAccumulator :类似 LongAdder ,但要更加灵活 ( 要传入一个函数式接口 )
DoubleAccumulator :类似 DoubleAdder ,但要更加灵活 ( 要传入一个函数式
接口 )
LongAdder jdk1.8 提供的累加器,基于 Striped64 实现,所提供的 API 基本上可
以替换原先的 AtomicLong
LongAdder 类似于 AtomicLong 是原子性递增或者递减类, AtomicLong 已经通过
CAS 提供了非阻塞的原子性操作,相比使用阻塞算法的同步器来说性能已经很好了,
但是 JDK 开发组并不满足,因为在非常高的并发请求下 AtomicLong 的性能不能让他
们接受,虽然 AtomicLong 使用 CAS 但是 CAS 失败后还是通过无限循环的自旋锁不断
尝试。
public final long incrementAndGet() {
for (;;) {
long current = get();
long next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
在高并发下 N 多线程同时去操作一个变量会造成大量线程 CAS 失败然后处于自旋状
态,这大大浪费了 cpu 资源,降低了并发性。那么既然 AtomicLong 性能由于过多线
程同时去竞争一个变量的更新而降低的,那么如果把一个变量分解为多个变量,让
同样多的线程去竞争多个资源那么性能问题不就解决了?是的, JDK8 提供的
LongAdder 就是这个思路。下面通过图形来标示两者不同。
AtomicLong LongAdder 对比:

一段LongAdderAtomic的对比测试代码:

public class Demo9Compare {
public static void main(String[] args) {
AtomicLong atomicLong = new AtomicLong(0L);
LongAdder longAdder = new LongAdder();
for (int i = 0; i < 50; i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 1000000; j++) {
//atomicLong.incrementAndGet();
longAdder.increment();
}
}
}).start();
}
while (Thread.activeCount() > 2) {
}
System.out.println(atomicLong.get());
System.out.println(longAdder.longValue());
System.out.println("耗时:" + (System.currentTimeMillis()
- start));
}
}
不同计算机因为 CPU 、内存等硬件不一样,所以测试的数值也不一样,但是得到的
结论都是一样的

8 J.U.CAQS

8.1 AQS简介

AQS(AbstractQueuedSynchronizer ),即队列同步器。它是构建锁或者其他同
步组件的基础框架(如 ReentrantLock ReentrantReadWriteLock Semaphore
等), JUC 并发包的作者( Doug Lea )期望它能够成为实现大部分同步需求的基
础。它是 JUC 并发包中的核心基础组件。
在这里我们只是对 AQS 进行了解,它只是一个抽象类,但是 JUC 中的很多组件都是
基于这个抽象类,也可以说这个 AQS 是多数 JUC 组件的基础。
8.1.1 AQS的作用
其性能一直都是较为低下,虽然在 1.6 后,进行大量的锁优化策略,但是与 Lock 相比
synchronized 还是存在一些缺陷的:它缺少了获取锁与释放锁的可操作性,可中
断、超时获取锁,而且独占式在高并发场景下性能大打折扣。
AQS 解决了实现同步器时涉及到的大量细节问题,例如获取同步状态、 FIFO 同步
队列。基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工
作,而且也不必处理在多个位置上发生的竞争问题。
8.1.2 state状态
AQS 维护了一个 volatile int 类型的变量 state 表示当前同步状态。当 state>0 时表示
已经获取了锁,当 state = 0 时表示释放了锁。
它提供了三个方法来对同步状态 state 进行操作:
getState() :返回同步状态的当前值
setState() :设置当前同步状态
compareAndSetState() :使用 CAS 设置当前状态,该方法能够保证状态设置的
原子性
这三种操作均是 CAS 原子操作,其中 compareAndSetState 的实现依赖于 Unsafe
compareAndSwapInt() 方法
8.1.3 资源共享方式
AQS 定义两种资源共享方式:
Exclusive (独占,只有一个线程能执行,如 ReentrantLock
Share (共享,多个线程可同时执行,如 Semaphore/CountDownLatch
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要
实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取
资源失败入队 / 唤醒出队等), AQS 已经在顶层实现好了。自定义同步器实现时主要
实现以下几种方法:
isHeldExclusively() :当前同步器是否在独占式模式下被线程占用,一般该
方法表示是否被当前线程所独占。只有用到 condition 才需要去实现它。
tryAcquire(int) :独占方式。尝试获取同步状态,成功则返回 true ,失败则
返回 false 。其他线程需要等待该线程释放同步状态才能获取同步状态。
返回 false
tryAcquireShared(int) :共享方式。尝试获取同步状态。负数表示失败; 0
表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int) :共享方式。尝试释放同步状态,如果释放后允许
唤醒后续等待结点,返回 true ,否则返回 false
8.2 CLH同步队列
AQS 内部维护着一个 FIFO 队列,该队列就是 CLH 同步队列,遵循 FIFO 原则( First
Input First Output 先进先出)。 CLH 同步队列是一个 FIFO 双向队列, AQS 依赖它来
完成同步状态的管理。

当前线程如果获取同步状态失败时,  AQS则会将当前线程已经等待状态等信息构  造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步 状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。

8.2.3 入列

CLH队列入列非常简单,就是tail指向新节点、新节点的prev指向当前最后的节 点,当前最后一个节点的next指向当前节点。

代码我们可以看看addWaiter(Node node)方法:

private Node addWaiter(Node mode) {
//新建Node
Node node = new Node(Thread.currentThread(), mode);
//快速尝试添加尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
//CAS设置尾节点
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//多次尝试
enq(node);
return node;
}
在上面代码中,两个方法都是通过一个 CAS 方法 compareAndSetTail(Node
expect, Node update) 来设置尾节点,该方法可以确保节点是线程安全添加的。在
enq(Node node) 方法中, AQS 通过 死循环 的方式来保证节点可以正确添加,只有
成功添加后,当前线程才会从该方法返回,否则会一直执行下去。

8.2.4 出列

CLH 同步队列遵循 FIFO ,首节点的线程释放同步状态后,将会唤醒它的后继节点
next ),而后继节点将会在获取同步状态成功时将自己设置为首节点。 head 执行
该节点并断开原首节点的 next 和当前节点的 prev 即可,注意在这个过程是不需要使
CAS 来保证的,因为只有一个线程能够成功获取到同步状态。

9 J.U.C之锁

9.1 锁的基本概念

虽然在前面锁优化的部分已经提到过一些锁的概念,但不完全,这里是对锁的概念
补充。
9.1.1 互斥锁
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象
都对应于一个可称为 " 互斥锁 " 的标记,这个标记用来保证在任一时刻,只能有一个
线程访问该对象。
9.1.2 阻塞锁
阻塞锁,可以说是让线程进入阻塞状态进行等待,当获得相应的信号(唤醒,时
间) 时,才可以进入线程的准备就绪状态,准备就绪状态的所有线程,通过竞争,
进入运行状态。
9.1.3 自旋锁
自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线
程改变时,才能进入临界区。
由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响
应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,
占用 CPU 时间。如果线程竞争不激烈,并且保持锁的时间段。适合使用自旋锁。
9.1.4 读写锁
读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
读写锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时
有多个读者来访问共享资源,最大可能的读者数为实际的逻辑 CPU 数。写者是排他
性的,一个读写锁同时只能有一个写者或多个读者(与 CPU 数相关),但不能同时
既有读者又有写者。
9.1.5 公平锁
公平锁( Fair ):加锁前检查是否有排队等待的线程,优先排队等待的线程,先来先
非公平锁( Nonfair ):加锁时不考虑排队等待问题,直接尝试获取锁,获取不到自
动到队尾等待
非公平锁性能比公平锁高,因为公平锁需要在多核的情况下维护一个队列。

9.2 ReentrantLock

ReentrantLock ,可重入锁,是一种递归无阻塞的同步机制。它可以等同于
synchronized 的使用,但是 ReentrantLock 提供了比 synchronized 更强大、灵活的
锁机制,可以减少死锁发生的概率。
ReentrantLock 还提供了公平锁和非公平锁的选择,构造方法接受一个可选的公平
参数(默认非公平锁),当设置为 true 时,表示公平锁,否则为非公平锁。公平锁
的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低
的吞吐量。
查看 ReentrantLock 源码中的构造方法:
public ReentrantLock() {
//非公平锁
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
//公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
AQS AbstractQueuedSynchronizer ),它有两个子类:公平锁 FairSync 和非公平
NonfairSync
9.2.1 获取锁
一般都是这么使用 ReentrantLock 获取锁的:(默认非公平锁)
//非公平锁
ReentrantLock lock = new ReentrantLock();
lock.lock();
lock 方法:
public void lock() {
sync.lock();
}
加锁最终可以看到会调用方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其实底层就是使用 AQS 同步队列。
9.2.2 释放锁
获取同步锁后,使用完毕则需要释放锁, ReentrantLock 提供了 unlock 释放锁:
public void unlock() {
sync.release(1);
}
unlock 内部使用 Sync release() 释放锁, release() 是在 AQS 中定义的:
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
释放同步状态的 tryRelease() 是同步组件自己实现:
protected final boolean tryRelease(int releases) {
//减掉releases
int c = getState() - releases;
//如果释放的不是持有锁的线程,抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//state == 0 表示已经释放完全了,其他线程可以获取同步状态了
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
只有当同步状态彻底释放后该方法才会返回 true 。当同步队列的状态 state == 0
时,则将锁持有线程设置为 null free= true ,表示释放成功。
9.2.3 公平锁与非公平锁原理
公平锁与非公平锁的区别在于获取锁的时候是否按照 FIFO 的顺序来。释放锁不存
在公平性和非公平性,比较非公平锁和公平锁获取同步状态的过程,会发现两者唯
一的区别就在于公平锁在获取同步状态时多了一个限制条件:
hasQueuedPredecessors() ,定义如下:
public final boolean hasQueuedPredecessors() {
Node t = tail; //尾节点
Node h = head; //头节点
Node s;
//头节点 != 尾节点
//同步队列第一个节点不为null
//当前线程是同步队列第一个节点
return h != t &&
((s = h.next) == null || s.thread !=
Thread.currentThread());
}
该方法主要做一件事情:主要是判断当前线程是否位于 CLH 同步队列中的第一
个。如果是则返回 true ,否则返回 false
9.2.4 ReentrantLocksynchronized的区别
前面提到 ReentrantLock 提供了比 synchronized 更加灵活和强大的锁机制,那么
它的灵活和强大之处在哪里呢?他们之间又有什么相异之处呢?
1. synchronized 相比, ReentrantLock 提供了更多,更加全面的功能,具备更强
的扩展性。例如:时间锁等候,可中断锁等候,锁投票。
2. ReentrantLock 还提供了条件 Condition ,对线程的等待、唤醒操作更加详细和
灵活,所以在多个条件变量和高度竞争锁的地方, ReentrantLock 更加适合(以
后会阐述 Condition )。
3. ReentrantLock 提供了可轮询的锁请求。它会尝试着去获取锁,如果成功则继
续,否则可以等到下次运行时处理,而 synchronized 则一旦进入锁请求要么成
功要么阻塞,所以相比 synchronized 而言, ReentrantLock 会不容易产生死锁
些。
4. ReentrantLock 支持更加灵活的同步代码块,但是使用 synchronized 时,只能在
同一个 synchronized 块结构中获取和释放。注: ReentrantLock 的锁释放一定要
finally 中处理,否则可能会产生严重的后果。
5. ReentrantLock 支持中断处理,且性能较 synchronized 会好些。

9.3 读写锁ReentrantReadWriteLock

问,但是在大多数场景下,大部分时间都是提供读服务,而写服务占有的时间较
少。然而读服务不存在数据竞争问题,如果一个线程在读时禁止其他线程读势必会
导致性能降低。所以就提供了读写锁。
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性
比一般的互斥锁有了较大的提升:在同一时间可以允许多个读线程同时访问,但是
在写线程访问时,所有读线程和写线程都会被阻塞。
读写锁的主要特性:
1. 公平性:支持公平性和非公平性。
2. 重入性:支持重入。读写锁最多支持 65535 个递归写入锁和 65535 个递归读取
锁。
3. 锁降级:写锁能够降级成为读锁,遵循获取写锁、获取读锁在释放写锁的次
序。读锁不能升级为写锁。
读写锁 ReentrantReadWriteLock 实现接口 ReadWriteLock ,该接口维护了一对相
关的锁,一个用于只读操作,另一个用于写入操作。只要没有 writer ,读取锁可以
由多个 reader 线程同时保持。写入锁是独占的。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
ReadWriteLock 定义了两个方法。 readLock() 返回用于读操作的锁, writeLock()
回用于写操作的锁。 ReentrantReadWriteLock 定义如下:
/** 内部类 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return
writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return
readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {
//省略其余源代码
}
public static class WriteLock implements Lock,
java.io.Serializable{
//省略其余源代码
}
public static class ReadLock implements Lock,
java.io.Serializable {
//省略其余源代码
}

ReentrantReadWriteLock ReentrantLock 一样,其锁主体依然是 Sync ,它的读
锁、写锁都是依靠 Sync 来实现的。所以 ReentrantReadWriteLock 实际上只有一个
锁,只是在获取读取锁和写入锁的方式上不一样而已,它的读写锁其实就是两个
类: ReadLock writeLock ,这两个类都是 lock 实现。
ReentrantLock 中使用一个 int 类型的 state 来表示同步状态,该值表示锁被一个
线程重复获取的次数。但是读写锁 ReentrantReadWriteLock 内部维护着一对锁,需
要用一个变量维护多种状态。所以读写锁采用 按位切割使用 的方式来维护这个变
量,将其切分为两部分,高 16 为表示读,低 16 为表示写。分割之后,读写锁是如何
迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为 S ,那么写状态等
S & 0x0000FFFF (将高 16 位全部抹去),读状态等于 S >>> 16( 无符号补 0 右移 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;
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
9.3.1 写锁的获取
写锁就是一个支持可重入的互斥锁。
写锁的获取最终会调用 tryAcquire(int arg) ,该方法在内部类 Sync 中实现:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
//当前锁个数
int c = getState();
//写锁
int w = exclusiveCount(c);
if (c != 0) {
//c != 0 && w == 0 表示存在读锁
//当前线程不是已经获取写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//超出最大范围
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
//是否需要阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置获取锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}
该方法和 ReentrantLock tryAcquire(int arg) 大致一样,在判断重入时增加了一
项条件:读锁是否存在。因为要确保写锁的操作对读锁是可见的,如果在存在读锁
的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写
线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写
锁开始获取了,所有其他读、写线程均会被阻塞。
9.3.2 写锁的释放
获取了写锁用完了则需要释放, WriteLock 提供了 unlock() 方法释放写锁:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
写锁的释放最终还是会调用 AQS 的模板方法 release(int arg) 方法,该方法首先调
tryRelease(int arg) 方法尝试释放锁, tryRelease(int arg) 方法为读写锁内部类
Sync 中定义了,如下:
protected final boolean tryRelease(int releases) {
//释放的线程不为锁的持有者
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
//若写锁的新线程数为0,则将锁的持有者设置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
当写状态为 0 时表示 写锁已经完全释放了,从而等待的其他线程可以继续访问读写
锁,获取同步状态,同时此次写线程的修改对后续的线程可见。
9.3.3 读锁的获取
读锁为一个可重入的共享锁,它能够被多个线程同时持有,在没有其他写线程访问
时,读锁总是获取成功。
读锁的获取可以通过 ReadLock lock() 方法:
public void lock() {
sync.acquireShared(1);
}
Sync acquireShared(int arg) 定义在 AQS 中:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
9.3.4 读锁的释放
与写锁相同,读锁也提供了unlock()释放读锁
public void unlock() {
sync.releaseShared(1);
}
unlcok() 方法内部使用 Sync releaseShared(int arg) 方法,该方法也定义 AQS 中:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
读写锁有一个特性就是锁降级,锁降级就意味着写锁是可以降级为读锁的。锁降级
需要遵循以下顺序:
获取写锁 => 获取读锁 => 释放写锁
9.3.6 读写锁例子
public class Demo10ReentrantReadWriteLock {
private static volatile int count = 0;
public static void main(String[] args) {
ReentrantReadWriteLock lock = new
ReentrantReadWriteLock();
WriteDemo writeDemo = new WriteDemo(lock);
ReadDemo readDemo = new ReadDemo(lock);
for (int i = 0; i < 3; i++) {
new Thread(writeDemo).start();
}
for (int i = 0; i < 5; i++) {
new Thread(readDemo).start();
}
}
static class WriteDemo implements Runnable {
ReentrantReadWriteLock lock;
public WriteDemo(ReentrantReadWriteLock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.writeLock().lock();
count++;
System.out.println("写锁:"+count);
}
}
}
static class ReadDemo implements Runnable {
ReentrantReadWriteLock lock;
public ReadDemo(ReentrantReadWriteLock lock) {
this.lock = lock;
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
try {
TimeUnit.MILLISECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.readLock().lock();
System.out.println("读锁:"+count);
lock.readLock().unlock();
}
}
}
}

10 J.U.CCondition

10.1 Condition介绍

在没有 Lock 之前,我们使用 synchronized 来控制同步,配合 Object wait()
notify() 系列方法可以实现等待 / 通知模式。在 JDK5 后, Java 提供了 Lock 接口,相对于
Synchronized 而言, Lock 提供了条件 Condition ,对线程的等待、唤醒操作更加详
细和灵活。
下图是 Condition Object 的监视器方法的对比:

Condition提供了一系列的方法来对阻塞和唤醒线程:

1. await() :造成当前线程在接到信号或被中断之前一直处于等待状态。

2. await(longtime, TimeUnit unit) :造成当前线程在接到信号、被中断或到达 指定等待时间之前一直处于等待状态。

3. awaitNanos(long nanosTimeout) :造成当前线程在接到信号、被中断或到 达指定等待时间之前一直处于等待状态。返回值表示剩余时间,如果在

nanosTimesout之前唤醒,那么返回值 = nanosTimeout – 消耗时间,如果返 回值 <= 0 ,则可以认定它已经超时了。

4. awaitUninterruptibly() :造成当前线程在接到信号之前一直处于等待状态。 【注意:该方法对中断不敏感】。

5. awaitUntil(Date deadline) :造成当前线程在接到信号、被中断或到达指定   最后期限之前一直处于等待状态。如果没有到指定时间就被通知,则返回true 否则表示到了指定时间,返回返回false

6. signal():唤醒一个等待线程。该线程从等待方法返回前必须获得与Condition 相关的锁。

7. signal()All:唤醒所有等待线程。能够从等待方法返回的线程必须获得与 Condition相关的锁。

Condition是一种广义上的条件队列(等待队列)。他为线程提供了一种更为灵  活的等待/通知模式,线程在调用await方法后执行挂起操作,直到线程等待的某 个条件为真时才会被唤醒。  Condition必须要配合锁一起使用,因为对共享状态  变量的访问发生在多线程环境下。  一个Condition的实例必须与一个Lock绑定,

因此Condition一般都是作为Lock的内部实现。

10.2 Condition的实现

获取一个 Condition 必须通过 Lock newCondition() 方法。该方法定义在接口
Lock 下面,返回的结果是绑定到此 Lock 实例的新 Condition 实例。 Condition 为一
个接口,其下仅有一个实现类 ConditionObject ,由于 Condition 的操作需要获取相
关的锁,而 AQS 则是同步锁的实现基础,所以 ConditionObject 则定义为 AQS 的内部
类。定义如下:
public class ConditionObject implements Condition,
java.io.Serializable {
}
10.2.1 等待队列
能的关键。在队列中每一个节点都包含着一个线程引用,该线程就是在该 Condition
对象上等待的线程。源码如下:
public class ConditionObject implements Condition,
java.io.Serializable {
private static final long serialVersionUID =
1173984872572414699L;
//头节点
private transient Node firstWaiter;
//尾节点
private transient Node lastWaiter;
public ConditionObject() {
}
/** 省略方法 **/
}
从上面代码可以看出 Condition 拥有首节点( firstWaiter ),尾节点
lastWaiter )。当前线程调用 await() 方法,将会以当前线程构造成一个节点
Node ),并将节点加入到该队列的尾部。
Node 里面包含了当前线程的引用。 Node 定义与 AQS CLH 同步队列的节点使用的
都是同一个类( AbstractQueuedSynchronized.Node 静态内部类)。
Condition 的队列结构比 CLH 同步队列的结构简单些,新增过程较为简单只需要将原
尾节点的 nextWaiter 指向新增节点,然后更新 lastWaiter 即可。
10.2.2 等待状态
调用 Condition await() 方法会使当前线程进入等待状态,同时会加入到
Condition 等待队列同时释放锁。当从 await() 方法返回时,当前线程一定是获取了
10.2.3 通知
调用 Condition signal() 方法,将会唤醒在等待队列中等待最长时间的节点(条
件队列里的首节点),在唤醒节点前,会将节点移到 CLH 同步队列中。
//检测当前线程是否为拥有锁的独
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//头节点,唤醒条件队列中的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first); //唤醒
}
该方法首先会判断当前线程是否已经获得了锁,这是前置条件。然后唤醒等待队列
中的头节点。
doSignal(Node first) :唤醒头节点
private void doSignal(Node first) {
do {
//修改头结点,完成旧头结点的移出工作
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal(Node first) 主要是做两件事:
1. 修改头节点,
2. 调用 transferForSignal(Node first) 方法将节点移动到 CLH 同步队列中。
  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵然间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值