Java内存模型规定了所有公共变量都必须存储在主存之中,每个工作线程还都有自己的工作内存,当线程执行任务过程中是从主存获取变量到自己的工作内存进行操作,且线程之间不能互相访问其他线程的工作内存。jmm简单来说它是一种规范,确保多线程环境下,保证线程安全,保障共享变量的可见性、操作的原子性、指令执行的顺序性等,具体的实现有synchronized、lock等。
java中要实现线程同步有两种方式:(显性锁)锁和(隐性锁)synchorized。
synchnorized关键字:包括两种方式同步方法和同步代码块,它包括两种加锁方式,锁对象和锁类。
(1)如果一个synchnorized的锁作用域是一个实例对象,假如该实例对象被多个synchorized方法或代码块使用,只要一个线程访问了该对象的一个synchorized方法(代码块),其它线程就不再能访问任意一个同步方法(代码块),但可以运行其它非synchorized修饰的代码,并且其它线程可以访问同一个类其它实例的的同步方法(代码块)。
public synchronized void test(){
System.out.println("test synchronized");
}
public void test_1(){
synchronized (this){
System.out.println("test synchronized");
}
}
(2)如果一个synchorized的锁作用范围是一个类,可以防止多个线程同时访问这个类中的synchronized方法,对该类所有实例对象起作用。
public static synchronized void test_2(){
System.out.println("test synchronized");
}
public void test_3() {
synchronized (test.class) {
System.out.println("test synchronized");
}
}
synchorized的实现:对于synchronized锁定的对象或类而言,在它的头中都有一个指向monitor对象的指针,monitor它是一个监视器锁,当线程想要获取某一个对象的使用权时就需要获取monitor的使用权。monitor对象的内部结构如下,主要是entrySet,waitSet,owner,count字段。
(1)线程想要进入被synchorized修饰的代码段中,就必须要获取该作用域下的monitor锁,如果owner为null,说明当前没有其它线程在运行该同步代码,那么owner赋值为当前线程,同时count+1,synchorized是可重入的,每进入一次count+1当count为0时才说明释放了该锁;
(2)如果当用户想要执行同步代码时发现有其他线程获取了monitor对象的使用权时,就需要进入entrySet进行等待,当monitor被释放后会通知entrySet中的线程,去竞争monitor锁的使用权,由此可以看出synchorized是一个非公平锁,不能保证先到来的线程先获取代码执行权限
(3)当前持有锁的线程,如果调用了wait方法,就会释放锁进入到waitSet中,等待被唤醒,但是需要注意的是,waitSet中的线程被唤醒后并不会马上获得锁的使用权,而是进入了block状态,等到monitor被释放后与entrySet中阻塞线程线程共同争夺锁的使用权。
(4)当一个线程真正执行完后就会释放锁,退出临界区。
需要强调的是synchorized是依赖jvm实现的。
java中锁的实现:
核心类是aqs,即AbstractQueuedSynchronizer(队列同步器)抽象类,它使用了一个volatile型的int数据status来标记状态,提供了三种status的访问方式
getStatus();//获取当前锁的状态
setStatus(int newStatus);//设置新值
compareAndSetStatus(int except, int update);//尝试获取锁
除了上述方法外里面还有acquire、tryAcquire、release方法等,下面以ReenTranLock为例介绍下java中锁是怎么实现的。
ReenTrantLock中包含了公平锁和非公平锁,默认是非公平锁,但可以通过ReenTranLock(Bollean fair)方法指定锁的类型。
//默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
//可以指定使用公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReenTranLock实现了Lock接口,通过lock和unlock方法来获取和释放锁,实际是通过Sync类型的sync对象来调用其中的lock、release方法
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
....
public void lock() {
sync.lock();
}
...
public void unlock() {
sync.release(1);
}
...
}
ReenTranLock中包含一个抽象静态类Sync继承了AbstractQueuedSynchronized,同时利用静态类NoFairSync、FairSync继承了Sync类实现了非公平锁和公平锁,它们都对Sync类的lock方法和AQS抽象类的tryAcquire方法提供了自己的实现方式,下面以非公平锁NofairSync为例介绍下加锁的方式,之后再讨论下公平锁和非公平锁的不同,以及它们释放锁的方式。
非公平锁 NofairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
1.ReenTranLock lock = new ReenTranLock(); lock.lock()实际就是调用的NofairSync的lock方法,使用非公平锁时线程在调用lock方法时会立即调用一次compareAndSetStatus方法尝试获取锁,如果成功,则会将锁持有者设置为当前线程,然后继续自己的业务操作
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
2.获取锁失败后则会调用aqs的acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2.1进入tryAcquire方法尝试再次获取锁,NofairSync实现了自己的tryAcquire方法,在其中调用了父类Sync的nofairTryAcquire方法。首先会获取aqs中status的值,当status为0时说明当前没有线程持有锁,再次使用cas尝试获取锁,成功后将本线程设置为锁的持有者;当status不为0时,则判断当前线程是否已经持有了锁,是的话将status+acquires作为新的status,由此可以看出ReenTranLock是可重入的
//NofairSync tryAcquire实现
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//Sync nonfairTryAcquire实现
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
2.2当tryAcquire返回false即获取锁失败时,会调用aqs的addWaiter(Node.EXCLUSIVE)方法,将线程放入阻塞列表中。Node.EXCLUSIVE标明创建的node锁是独占的,获取等待队列尾指针tail,当tail不为null时,尝试将新node作为新的尾节点,这里要使用cas的方式,避免同时有多个线程创建node将自己作为新的尾节点;当尾指针指向为null,或者将当前节点作为新尾节点竞争失败时,调用enq方法。
/***
* 先解释下Node的waitStatus字段不同属性代表的含义
* SIGNAL -1 表示本节点的next节点进入了block状态,当前节点调用了release方法释放锁或注销放弃获取锁的时候需要调用unpark方法唤醒后续节点中的等待线程
*
* CANCELLED 1 由于超时或中断node中线程注销,不再等待
*
* CONDITION -2 说明当前node正处于condition(条件)队列中,正在等待条件满足
*
* PROPAGATE 标明node处于可传播状态
*
* 0: 不属于上述状态,一般作为新创建Node的默认状态
* */
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
2.3 enq 当尾指针指向null时说明当前阻塞列表还未初始化,此时需要创建头节点Node将首尾指针指向刚创建的默认node;当tail不指向null时会采用无限循环将当前线程node插入阻塞列表中作为新的尾节点,刚初始化的阻塞列表和插入线程阻塞node的列表如下图所示
![]() | ![]() |
初始阻塞列表 | 插入等待线程后 |
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
2.4 acquireQueued 在acquireQueue中会有一个死循环,当node前节点是头节点时,他会有尝试获得锁的机会,再次调用tryAcquire方法,当获取成功,将当前节点作为新的头节点,在setHead中会将其pre前指针、thread字段置为null,头指针指向此新的头节点,将原头节点的next置为null;否则的话进入shouldParkAfterFailedAcquire方法中
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(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
2.5 shouldParkAfterFailedAcquire从名字就可以看出来它是当尝试acquire获取锁失败后当前线程应该park,但park前需要进行些处理,该方法会保证当前节点的前置节点状态被置为SIGNAL,从而确保前节点获取锁后释放锁操作release后该节点可以被唤醒,当它的前置节点不为-1时需要将其置为-1即SIGNAL,但此时并不立即将线程执行park,而是再尝试进入循环看能不能acquire到锁,这次再次失败则会进入parkAndCheckInterrupt方法,此时执行LockSupport.park当前线程阻塞
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
至此为止当前线程被放入阻塞列表当中,可以看到在整个lock方法的执行过程中,线程会有好几次acquire机会,并不是立即被放入到阻塞列表中的。
3.当前持有锁的线程执行完毕之后,需要调用lock.unlock方法释放锁,实际是调用了sync.release方法,release是aqs中的方法,首先会执行tryRelease,tryRelease在Sync中被定义。当tryRelease释放锁成功后会获取当前头指针指向node,指向unparkSuccessor方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
3.1 tryRelease 将aqs中的status - release 即释放的锁数量,当status为0时说明锁已释放完毕,将持有锁的线程置为null,返回true锁已完全释放,否则返回false
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
3.2unparkSuccessor 当头节点的waitStatus小于0时将其置为0说明已执行完毕,获取头节点的nxet节点,当其不为null且waitStatus不为1已取消获取锁时执行unpark将其唤醒,在acquireQueue的for循环中继续获取锁;否则的话从阻塞链表的尾节点开始向前遍历,找到第一个非cancelled节点唤醒
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
到此为止非公平锁的加锁和释放锁的过程已经介绍完了,下面说一些公平锁
公平锁 FairSync
公平锁也实现了Sync的lock接口和aqs的tryAcquire方法,不过不同的是在lock方法中不会再一开始就尝试使用cas获取锁了,而是直接进入进入了aqs的acquire方法中
final void lock() {
acquire(1);
}
并且在acquire调用的tryAcquire方法和NofairSync的实现逻辑也不同,不能直接进行cas尝试获取锁了,它会先判断一下当前阻塞链表中是否有等待线程,不存在时才会进行cas;此外在acquireQueue中调用的tryAcquire也是同样的逻辑
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
由此我们可以看出Reentranlock中公平锁和非公平锁的区别,在于当一个线程调用lock方法时,非公平锁允许新进入的线程可以抢占锁的使用权,老线程需要排队等待获取锁;而公平锁无论新老线程进行都需要排队等待获取锁,除非等待队列中没有其他线程在等待获取锁,那么新进入的线程可以尝试利用cas获取锁或新进入的线程与当前持有锁的线程是同一个,那么直接state+1。
synchronized和ReentrantLock区别
(1)synchronized是由jvm来支持实现的,而ReenTranLock是由jdk提供的实现
(2)synchronized提供的是非公平锁,而ReenTranLock既支持公平锁也支持非公平锁,默认是非公平的,区别是在调用lock方法时非公平锁允许新进入线程抢占锁的使用权,而老线程需要排队等待,公平锁只有位于等待队列头部的节点可以获取锁,除非等待队列不存在其他线程在等待获取锁,或新进入线程就是当前持有锁的线程
(3)synchronized的等待线程只能被随机唤醒或全部唤醒,而ReenTranLock可以使用condition绑定一组线程,支持唤醒特定线程,condition.signal()用于唤醒这条件队列中首个线程,condition = lock.newCondition(),condition.await(),condition.signal()
(4)ReenTranLock的阻塞线程可中断,当持有锁的线程长时间不释放锁,正在等待的线程可以终止等待,通过lock.lockInterruptlibly()来实现
ReenTranLock的condition
// 锁和条件变量
Lock lock = new ReentrantLock();
// 条件
Condition condition1 = lock.newCondition();
ReentranLock中使用newCondition()方法获取一个condition对象,实际创建了aqs中ConditionObject实例,ConditionObject实现了Condition接口,这里面比较重要的是await、signal、signalAll方法
1.await方法
当线程获取到锁,发现自己不满足某些条件时,可以调用await方法,如下
if(xxx条件不满足) {
condition1.await();
}
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 调用await后执行1、2、3步
// 1. 创建node放入条件队列
Node node = addConditionWaiter();
// 2. 释放线程持有的锁,唤醒同步队列head指向的下一节点
int savedState = fullyRelease(node);
int interruptMode = 0;
// 3. 判断是否处于同步队列中,不在的话一直处于阻塞状态
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//被signal唤醒后
// 使用acquireQueue尝试获取锁,获取成功将该线程node作为同步队列head,否则继续陷入阻塞
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
此时在await方法中执行如下操作:
(1)创建一个node节点,设置waitStatus状态为Node.CONDITION,将node放入条件队列尾部
(2)调用release(int arg)将线程持有的锁全部释放,将aqs的status置为0,唤醒同步(阻塞)队列head的下一个节点
(3)循环判断node是否在同步队列中,如果不在则一直将线程置为阻塞状态,LockSupport.park(this)
至此为止已经唤醒其他线程去争夺锁了
2.condition1.signal方法
当其他线程调用了signal()方法后
(1).将条件队列的头节点放入同步队列的尾部,将waitStatus置为0
(2).将同步队列原tail节点的waitStatus置为Signal
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
/*
* If cannot change waitStatus, the node has been cancelled.
*/
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
/*
* Splice onto queue and try to set waitStatus of predecessor to
* indicate that thread is (probably) waiting. If cancelled or
* attempt to set waitStatus fails, wake up to resync (in which
* case the waitStatus can be transiently and harmlessly wrong).
*/
Node p = enq(node);
int ws = p.waitStatus;
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
3.原条件队列中的线程await恢复后调用acquireQueued尝试获取锁,获取成功则将该线程node节点作为新的head,否则进入阻塞状态
同步队列中 指向下个节点的指针叫next 条件队列中指向下个节点的指针交nextWaiter,调用了condition.await的线程会释放所有锁,进入条件队列,被signal会唤醒条件队列首节点,将其放入同步队列中