JDK 1.5 是一个大版本更新, 这个版本引入了 枚举
, 泛型
, 注解
可变参数
, 自动装箱
, for-each循环
, 还引入了基于老年代的垃圾回收器 CMS
, 最重要的是引入了并发包 java.util.concurrent
, 由著名的并发编程大师 Doug Lea 亲自操刀, 简化了 Java 开发人员在并发编程中需要考虑的种种事情
今天, 就来带你瞅瞅 Doug Lea 是如何编写代码的, 相信看完之后你会对这个人说一句 : 我草牛逼
先来看看这玩意与 synchronized 的区别, 为什么有了 synchronized , 还要出现 ReentrantLock ?
这里需要注意的是, ReentrantLock 是通过 Java 代码实现线程同步的, 并没有使用 OS 底层的互斥锁
如果是 JDK 1.6 之前, 我肯定选择的是 ReentrantLock , 他的性能远远优于 synchronized.
但是, JDK 1.6 HotSpot 对 synchronized 进行了优化, 加了自旋锁, 偏向锁, 和轻量级锁, 来避免线程数较少的情况下直接上重量级锁
经过测试, 无论是在线程竞争较少, 还是竞争激烈, 无论是代码量大, 还是代码量小, 两者的性能差异不大
但是 ReentrantLock 也有他的优势 :
public static void main(String[] args) throws InterruptedException {
CountDownLatch count = new CountDownLatch(1000);
long start1 = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
test(); // 1133
// test1(); // 1100 左右
count.countDown();
}).start();
}
count.await();
System.out.println(System.currentTimeMillis() - start1);
}
static int i;
static synchronized void test1() {
// i++;
sort(1000);
}
static Lock lock = new ReentrantLock();
static void test() {
lock.lock();
try {
// i++;
sort(1000);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
static void sort(int num) {
int[] arr = new int[num];
for (int j = 0; j < arr.length; j++) {
arr[j] = (int) (Math.random() * 100000);
}
// 冒泡排序
int tem;
for (int j = 1; j < arr.length; j++) {
for (int k = 0; k < arr.length - j; k++) {
if (arr[k] > arr[k + 1]) {
tem = arr[k];
arr[k] = arr[k + 1];
arr[k + 1] = tem;
}
}
}
}
ReentrantLock 实现了 Lock 接口, 对外暴露好几个加锁 API
public class ReentrantLock implements Lock, java.io.Serializable {
// 加锁的方式由内部类 Sync 实现, 该内部类有两个子类, 分别是公平锁和非公平锁
private final Sync sync;
// 最重要的就是他的父类, 简称 AQS (队列同步器)
abstract static class Sync extends AbstractQueuedSynchronizer {...}
static final class NonfairSync extends Sync {...}
static final class FairSync extends Sync {...}
// fair 默认为 false, 所以默认使用的 非公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
}
// AQS 内部维护了两个双向队列, 队列的节点由内部类 Node 实现
// 一个是同步队列, 存储没有获取到同步状态的线程节点
// 另一个是等待队列, 存放 Condition 正在等待的线程节点
public abstract class AbstractQueuedSynchronizer ...{
private transient volatile Node head;
private transient volatile Node tail;
// 声明为 volatile, 这是锁的标志位, 如果为 0, 说明处于无锁状态
private volatile int state;
// 一个节点有两种模式, 独占模式和共享模式
// 两者的最大区别就是同一时刻是否有多个线程同时获取到同步状态
// 独占模式 : 对文件的写操作, 只允许一个线程获取到同步状态 (同步队列节点就是独占模式)
// 共享模式 : 对文件的读操作, 就允许多个线程同时获取到同步状态 (Semaphore 就是共享模式)
static final class Node {
/** 表示该节点对应线程是获取共享资源被阻塞挂起放入队列 */
static final Node SHARED = new Node();
/**与 SHARED 相反,获取独占资源被阻塞挂起放入队列 */
static final Node EXCLUSIVE = null;
/** 指示线程已取消, 如果同步队列中等待的线程等待超时或者被打断
需要从同步队列中取消等待 */
static final int CANCELLED = 1;
/** 后继节点的线程处于等待状态, 如果当前节点的线程释放锁或者被取消, 将会通知后继节点,
调用 unpark 唤醒后继节点的线程 */
static final int SIGNAL = -1;
/** 节点在等待队列中, 节点线程等待在 Condition 上, 当前其他线程对 Condition 调用了
singal() 方法后, 该节点将会从等待队列转移到同步队列中, 加入到对锁的获取中 */
static final int CONDITION = -2;
/** 表示下一次共享式同步状态将会被无条件的传播下去 */
static final int PROPAGATE = -3;
/** 等待队列的后继节点, 因为等待队列仅在处于独占模式时才被访问 */
volatile int waitStatus;
volatile Node prev;
volatile Node next;
// 一个队列节点保存了一个线程对象
volatile Thread thread;
...
}
// 查看 FairSync 的 lock 方法
final void lock() {acquire(1);}
-----------------------------------------
// 接着查看 acquire() 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
------------------------------------------
//查看 tryAcquire()方法, 独占式获取同步状态
protected final boolean tryAcquire(int acquires) {
final Thread = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果标志位为 0, 说明此时处于无锁状态
// 接下来判断当前线程是否需要排队, 我们查看这个方法 (下面)
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 如果不需要排队且成功利用 CAS 将标志位从 0 改为 1
// 说明当前线程得到 '锁', 并保存当前线程
setExclusiveOwnerThread(current);
// 这里返回 true, 上面 acquire 方法取反, 直接返回退出
return true;
}
}
// 锁的标志位不为 0, 如果当前线程为已获得锁的线程 (这里就可以说明 ReentrantLock 具有可重入性)
else if (current == getExclusiveOwnerThread()) {
// 重入, 直接将标志位 +1, 解锁时将标志位 -1, 当标志位减为 0, 才是真正的释放锁
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
-------------------------------------
// 如果返回 false, 上面取反, 就会尝试获取锁, 所以只有这两种情况才会返回 false
// 1.如果队列为空
// 2.队列不为空, 队列至少存在两个节点且第二个节点保存的线程为当前线程
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
如果 tryAcquire
返回 false, 说明加锁失败, 接着执行 acquireQueued(addWaiter(Node), int)
// 先来看看 addWaiter(Node) 方法
// 首先创建一个新的节点, 保存当前线程对象, 这里传了一个 Node.EXCLUSIVE 进来, 说明这是一个独占式节点
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;
------------------
// 再来看看队列如何初始化的
// 初始化队列后, 队列的长度为 2, 而不是 1 (这点很重要)
// 且队列的头结点的 thread 对象永远为 null
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 利用 CAS 将一个空节点设置为队列的头结点 (该节点的 thread 对象为 null)
if (compareAndSetHead(new Node()))
tail = head;
}
else {
node.prev = t;
// 利用 CAS 将保存了当前线程对象的节点设置为队列的尾节点
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
-----------------------
// 接下来看 acquireQueued(Node, int)
队列里面的的头结点的thread属性永远为null, 持有锁的线程永远不在队列
//接着进入acquireQueued()方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 可以看到, 这里是通过自旋去获取锁
for (;;) {
// 返回前驱节点, 并检查
final Node p = node.predecessor();
// 如果前驱节点是头结点 , 则尝试去获取锁
// 只有当前线程节点在第二个位置, 才能去获取同步状态
if (p == head && tryAcquire(arg)) {
// 如果获取同步状态成功, 就会把当前节点设置为头结点
// 点进这个 setHead() 方法就会看到, 将当前节点的 thread 对象置为 null
// 并断开与前驱节点的引用
setHead(node);
// p 是头结点, 现在没有任何一个引用与之关联
p.next = null; // help GC
failed = false;
return interrupted;
}
// 如果获取锁失败, 执行 shouldParkAfterFailedAcquire 方法, 查看该方法 (下面)
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())// 调用 LockSupport#park 方法使线程休眠
// 如果被唤醒, 代码从这里开始执行
// 如果线程被打断过, 就会进入这里, 将 interrupted 改为 true, 如果获取同步状态成功
// 就会把 interrupted 返回出去, 然后调用 selfInterrupt(),
// 该方法可以防止用户主动的打断线程而造成不稳定因素
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
------------------
// 查看 shouldParkAfterFailedAcquire 方法
// 可以看到, 只有第二次进入该方法才会返回 true, 所以每个节点最多自旋两次
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;// 获取前节点的 waitStatus, 默认为 0
// 如果为 -1, 直接返回 (如果第二次进来就会进入 if, 返回 true)
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 利用 CAS 将前节点的状态改为 -1
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
在自旋两次之后才会休眠线程, 新的节点入队后, 如果无法获取到同步状态, 会把前驱节点状态值改为 -1 (新节点的状态为 0) , 尾结点所有的前驱节点状态值都为 -1
// unlock() -> 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;
}
---------------
// tryRelease()方法
protected final boolean tryRelease(int releases) {
// 每执行一次 unLock(), 就将锁标志位 -1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 锁标志位为 0, 才真正释放锁
if (c == 0) {
free = true;
// 将持有锁的线程置为 null
setExclusiveOwnerThread(null);
}
// 设置最新的锁标志
setState(c);
return free;
}
---------------------
// unparkSuccessor()
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;// 获取到头结点的状态位
if (ws < 0)
// 利用 CAS 将状态改为 0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 如果头结点存在后继节点, 则唤醒该线程
if (s != null)
LockSupport.unpark(s.thread);
}
如果感兴趣 NonFairSync 是如何实现的, 可以自己看看源码