1. 线程安全
多个线程对公共资源进行非原子操作,就会存在线程安全问题
- 多线程环境
- 多个线程共享一个资源
- 对资源进行非原子性操作
2. Lock 接口介绍
Java中锁的实现可以由synchronized关键字来完成,jdk1.5之后出现了一种新的方式来实现——Lock接口。
/**
* 1.采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。 <br>
* 2.因此一般来说,使用Lock必须在try{}catch{}块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生
*/
public interface Lock {
/**
* 获取锁。 如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
*/
void lock();
/**
* 可中断获取锁,当通过这个方法去获取锁时,如果线程正在等待获取锁(休眠中),则这个线程能够响应中断,即中断线程的等待状态<br>
* 注意:当一个线程获取了锁之后,是不会被interrupt()方法中断的
*/
void lockInterruptibly() throws InterruptedException;
/**
* 如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false。
*/
boolean tryLock();
/**
* 如果锁可用,则此方法将立即返回值 true。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在发生以下三种情况之一前,该线程将一直处于休眠状态:
* <ul>
* <li>1、锁由当前线程获得;
* <li>2、或者 其他某个线程中断当前线程,并且支持对锁获取的中断;
* <li>3、或者 已超过指定的等待时间
* </ul>
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁。
*/
void unlock();
/**
* 返回绑定到此 Lock 实例的新 Condition 实例。<br>
* 在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前重新获取锁。
*/
Condition newCondition();
}
3. ReentrantLock重入锁
ReentrantLock,意思是“可重入锁”,Lock接口有三个实现类分别是ReentrantLock, ReentrantReadWriteLock.ReadLock, ReentrantReadWriteLock.WriteLock。后面两个是内部类。
首先让我们先简单的看看ReentrantLock类中都有哪些方法,不喜欢可以直接跳过
/**
* 除了实现 Lock 接口,此类还定义了 isLocked 和 getLockQueueLength 方法,以及一些相关的 protected
* 访问方法,这些方法对检测和监视可能很有用。
*/
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
/**
* 为了将此类用作同步器的基础,需要适当地重新定义以下方法。<br>
*
* <li>{@link #tryAcquire}
* <li>{@link #tryRelease}
* <li>{@link #tryAcquireShared}
* <li>{@link #tryReleaseShared}
* <li>{@link #isHeldExclusively}
*
* 这是通过使用 getState()、setState(int)或 compareAndSetState(int, int)
* 方法来检查或修改同步状态来实现的
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
重写的父类方法tryRelease
protected final boolean tryRelease(int releases) {
return false;
}
}
static final class NonfairSync extends Sync {
重写的父类方法tryAcquire
protected final boolean tryAcquire(int acquires) {
return false;
}
}
static final class FairSync extends Sync {
重写的父类方法tryAcquire
protected final boolean tryAcquire(int acquires) {
return false;
}
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
以下是核心实现Lock接口的方法,我们可以看出,以下方法都是调用了内部类Sync
/**
* 获取锁,成功直接返回(计数加 1),失败则线程阻塞
*/
public void lock() {
sync.acquire(1);
}
/**
* 中断获取锁,成功直接返回(计数加 1)<br>
* 失败则当前线程进入休眠状态,直到以下两个事件发生
* <li>1. 当前线程获取锁成功
* <li>2. 当前线程被其他线程中断
*/
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
/**
* 尝试非阻塞的获取锁,调用该方法立即返回,true表示获取到锁(计数加1)<br>
* 同时这个锁是不遵守公平设置的,如果想遵守公平设置获取锁可以使用 tryLock(0, TimeUnit.SECONDS)}
*/
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
/**
* 超时获取锁,同时这个方法遵守公平设置 。 以下情况会返回:
* <li>时间内获取到了锁,
* <li>时间内被中断,
* <li>时间到了没有获取到锁。
*/
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
* 释放锁,如果当前线程是锁持有者,则计数器减1,如果计数器为0则释放锁,如果当前线程不是锁持有者,抛出异常
*/
public void unlock() {
sync.release(1);
}
/**
* 返回用来与此 Lock 实例一起使用的 Condition 实例。
*/
public Condition newCondition() {
return sync.newCondition();
}
/* 删除了用于 监控系统,测试调试的方法*/
}
我们可以看到内部所有的方法都是调用了内部类 Sync,Sync继承于AQS(AbstractQueuedSynchronizer ),同时Sync有两个子类分别是非公平NonfairSync,和公平FairSync
5.1 模板模式
模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。
根据定义可以和明显的看出来 AQS使用了模板模式,
5.2 装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
根据装饰器模式的介绍,感觉 ReentrantLock 就像对Sync的包装
5.3 策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
根据策略模式介绍,ReentrantLock的构造器根据参数使用了简单的策略选择
以上是对锁类型,Lock接口,ReentrantLock做了一个简单的介绍,以及个人对于设计模式的猜想吧,下面将以ReentrantLock.lock(), ReentrantLock.unlock()进行一步一步的深入分析源码,首先让我们先简单的了解下AQS
6. AbstractQueuedSynchronizer(AQS 略讲)
官方介绍:
为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。子类必须定义更改此状态的受保护方法,并定义哪种状态对于此对象意味着被获取或被释放。假定这些条件之后,此类中的其他方法就可以实现所有排队和阻塞机制。
- 先进先出 (FIFO) 等待队列 (通过字段head,tail来表示)
- 原子 int 值(volatile int state)
- 需要适当地重新定义以下方法(以下方法默认实现都是直接抛出异常)
- tryAcquire 试图在独占模式下获取对象状态。失败将线程加入队列
- tryRelease 试图设置状态来反映独占模式下的一个释放。
- tryAcquireShared 试图在共享模式下获取对象状态
- tryReleaseShared 试图设置状态来反映共享模式下的一个释放。
- isHeldExclusively 如果对于当前线程,同步是以独占方式进行的,则返回 true
简单来说,AQS内部一共有三个字段,head和tail构造FIFO等待队列,state表示状态,ReentrantLock的内部类Sync和子类就是重写了tryAcquire 和 tryRelease 实现了锁
/**
* 等待队列的头部,懒加载初始化,只能通过setHead修改,如果头存在,它的等待状态是 保证不被取消。
*/
private transient volatile Node head;
/**
* 等待队列的尾部,懒加载初始化,只有通过new 一个等待node才能修改
*/
private transient volatile Node tail;
/**
* 同步状态值
*/
private volatile int state;
6.1 AQS内部的node
之所以贴出来node的代码,因为有几个状态转换,大家可以下面注意下
static final class Node {
/* 标识节点当前在共享模式下 */
static final Node SHARED = new Node();
/** 标识节点当前在独占模式下 */
static final Node EXCLUSIVE = null;
/** 因为超时或者中断,节点会被设置为取消状态,被取消的节点时不会参与到竞争中的,他会一直保持取消状态不会转变为其他状态; */
static final int CANCELLED = 1;
/**
* 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行,
* 在AQS.shouldParkAfterFailedAcquire(pred,node)方法中会把前驱节点的状态设置为此状态,表示队列此节点后面有线程等待,可以理解为这个状态是后面线程的状态
*/
static final int SIGNAL = -1;
/**
* 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()后,改节点将会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享式同步状态获取将会无条件地传播下去 表示当前场景下后续的acquireShared能够得以执行
*/
static final int PROPAGATE = -3;
/* 值为0,表示当前节点在sync队列中,等待着获取锁。 */
volatile int waitStatus;
/** 前驱节点,比如当前节点被取消,那就需要前驱节点和后继节点来完成连接。 */
volatile Node prev;
/** 当前节点点后继节点 */
volatile Node next;
/** 入队列时的当前线程。 */
volatile Thread thread;
/** 存储condition队列中的后继节点 */
Node nextWaiter;
/**
* 共享模式返回true
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 返回前驱节点
*/
final Node predecessor() {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { }
Node(Node nextWaiter) {
this.nextWaiter = nextWaiter;
THREAD.set(this, Thread.currentThread());
}
Node(int waitStatus) {
WAITSTATUS.set(this, waitStatus);
THREAD.set(this, Thread.currentThread());
}
}
7. ReentrantLock.lock() (非公平)调用过程分析
7.1 线程1第一次调用
sync 是非公平锁 NonfairSync extend Sync 的实例,核心的实现就是AQS.acquire(1)调用NonfairSync .tryAcquire(1),调用方法链
- Lock.lock()
直接调用内部类Sync的acquire(1),既AQS.acquire(1) ,acquire是sync从AQS继承来的public final 方法
- AQS.acquire(1)
acquire直接调用了tryAcquire(1),此方法是需要子类实现,既NonfairSync.tryAcquire(1)
- NonfairSync.tryAcquire(1)
直接调用了父类的Sync.nonfairTryAcquire(1)
- Sync.nonfairTryAcquire(1)
- 获取同步状态值volatile int state;
- state==0 cas比较替换
- 替换成功设置当前线程返回true 获取锁成功
public class ReentrantLock implements Lock, java.io.Serializable {
/** 可以看到此方法直接调用了 AQS的 public final方法 */
public void lock() {
sync.acquire(1);
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer {
// acquire 直接调用了 tryAcquire ,tryAcquire是抽象方法需要子类NonfairSync 实现, acquireQueued暂时忽略它, 我们假设第一次调用tryAcquire 返回true获取到锁
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
static final class NonfairSync extends Sync {
// 直接调用父类Sync的非公平nonfairTryAcquire
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/** */
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
1、获取当前线程
final Thread current = Thread.currentThread();
2、获取 AQS volatile state 的状态值
int c = getState();
3、状态是否等于0 等于说明此锁暂时无线程占有
if (c == 0) {
4、乐观锁设置状态 比较替换状态值 新的api利用了java9 的句柄替换了Unsafe实现
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
3.1、进入这个else if分支,说明是重入了,需要操作:state=state+1
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;
}
}
public abstract class AbstractQueuedSynchronizer {
// VarHandle mechanics java9利用句柄 提供了一系列标准的内存屏障操作,
// 用于更加细粒度的控制内存排序。在安全性、可用性、性能上都要优于现有的API。
// VarHandle 可以与任何字段、数组元素或静态变量关联,支持在不同访问模型下对这些类型变量的访问,
// 包括简单的 read/write 访问,volatile 类型的 read/write 访问,和 CAS(compare-and-swap)等。
private static final VarHandle STATE;
protected final boolean compareAndSetState(int expect, int update) {
return STATE.compareAndSet(this, expect, update);
}
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
STATE = l.findVarHandle(AbstractQueuedSynchronizer.class, "state", int.class);
}
}
7.2 线程2调用lock()
假如线程1和线程2同时调用lock()方法,线程1优先获取到锁,线程2的执行方法链如下
- Lock.lock() --> AQS.acquire(1) --> NonfairSync.tryAcquire(1) --> Sync.nonfairTryAcquire(1)
他们几个方法依次调用,nonfairTryAcquire方法判断 state!=0,不是当前线程return false,然后AQS.acquire(1) 方继续执行acquireQueued(addWaiter(Node.EXCLUSIVE)
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
2、addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
Node node2 = new Node(mode); 1、包装当前线程为node2(此处是我改为的node2方便理解)
for (;;) {
Node oldTail = tail; 2、获取tail
if (oldTail != null) {
node2.setPrevRelaxed(oldTail); 3、设置node2 的prev指向tail
if (compareAndSetTail(oldTail, node2)) { 4、乐观锁使tail指向node2
oldTail.next = node2; 5、老tail 的next指向node2
return node2;
}
} else {
initializeSyncQueue(); 0、初始化AQS队列如下图
}
}
}
private final void initializeSyncQueue() {
Node h;
if (HEAD.compareAndSet(this, null, (h = new Node())))
tail = h;
}
- 包装当前线程为node(取个别名为node2)
- 获取到tail节点
- 如果tail节点等于null(因为是第二个线程所以此处tail节点等于null)初始化队列
- 循环再来,如果tail节点不等于null(上面已经初始化)
- 4.1 设置当前线程node2的prev指向tail(head的node1)
- 4.2 乐观锁比较替换node2为tail(既tail指向node2)
- 4.3 老tail(node1)的next指向当前node2 返回当前node2
3、AQS.acquireQueued(node2, 1)
- 获取到当前node2节点的前驱节点
- 判断是head -- return true(此时线程2上面的情况前驱节点是head)
- tryAcquire(1) 尝试去获取锁(假如这个时候,线程3来了获取到了,或者线程1还没执行完毕)return false
- 执行 shouldParkAfterFailedAcquire(p, node)
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor(); 1. 获取到当前node2节点的前驱节点
2. 判断是head -- return true(此时线程2上面的情况前驱节点是head)
3. tryAcquire(1) 尝试去获取锁(假如这个时候,线程3来了获取到了,或者线程1还没执行完毕)return false
if (p == head && tryAcquire(arg)) {
** 假如线程2获取到锁,为啥设置自己为head,
** 因为线程3来了,他的前驱节点是线程2,所以线程2把自己设置为head,线程3才能进入此方法
setHead(node);
p.next = null; // help GC
return interrupted;
}
4. 执行 shouldParkAfterFailedAcquire(p, node)
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
if (interrupted)
selfInterrupt();
throw t;
}
}
- shouldParkAfterFailedAcquire 判断此节点的前驱节点状态,因为前驱节点为head节点,状态为0,所以只会执行设置 前驱节点状态为 Node.SIGNAL 返回false
- 因为返回false所以再次for循环,假如再次没有获取到锁,第二次进入shouldParkAfterFailedAcquire ,因为prev的状态刚刚被设置为signal,所以返回true
- 所以执行 interrupted |= parkAndCheckInterrupt(); 阻塞当前线程返回是否中断
(此处分析的是lock()方法只是返回,lockInterruptibly()会主动抛出 InterruptedException)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL) // 后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后继节点,使后继节点的线程得以运行
return true;
if (ws > 0) { // 因为超时或者中断,节点会被设置为取消状态
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
*** 设置前驱节点 等待状态
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
7.3 线程3调用lock()
假如线程2处于等待状态,那么线程3会包装成node3,在QAS.addWaiter(),会加入到Node2队列后面,然后在AQS.acquireQueued(node,1),会设置前驱节点状态为 Node.SIGNAL阻塞当前线程,
8. ReentrantLock.unlock() 调用过程分析
1、 ReentrantLock.unlock()直接调用了 内部类Sync继承来的方法AQS.release(1),
public void unlock() {
sync.release(1);
}
2、AQS.release(1) 调用子类实现的tryRelease(1)
public final boolean release(int arg) {
1、释放锁成功 true
if (tryRelease(arg)) {
Node h = head;
2、我们知道线程2改变了head的状态为Node.SIGNAL = -1
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); 3、执行unpark
return true;
}
return false;
}
3、Sync.tryRelease(1), 执行完毕后回到上一步看注释
- 获取状态减去1的值
- 如果不是当前线程拥有者抛出异常
- 如果值等于0,表示释放锁
- 设置状态,不使用cas是因为当前方法必须是在当前线程锁定下调用的
protected final boolean tryRelease(int releases) {
1、获取状态减去1的值
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
2、如果值等于0,表示释放锁
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
3、设置状态,不使用cas是因为当前方法必须是在当前线程锁定下调用的
setState(c);
return free;
}
4、unparkSuccessor(head)
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)
node.compareAndSetWaitStatus(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.
* 获取当前节点的下一个节点,如果==null,或者>0 节点取消了
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
从后往前遍历,找到临近当前节点的正常状态节点
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
1、唤醒下一个节点
if (s != null)
LockSupport.unpark(s.thread);
}
5、for循环为啥从后往前遍历
一般不会出现倒叙全遍历,因为每次节点的中断取消都会清理一遍
- 我们在回顾下,addWaite(), 先设置当前node的前驱节点为tail
- 然后设置当前节点为tail,
这样在上面遍历的时候,可以保证一旦节点加入到队列,可以立马获取遍历到他,如果是从前往后遍历,这边加入队列的时候,先设置tail(已经成功加入队列),然后设置next,会出现并发问题,可能遍历不到最后一个新加入的节点
private Node addWaiter(Node mode) {
Node node2 = new Node(mode); 1、包装当前线程为node2(此处是我改为的node2方便理解)
for (;;) {
Node oldTail = tail; 2、获取tail
if (oldTail != null) {
node2.setPrevRelaxed(oldTail); 3、设置node2 的prev指向tail
if (compareAndSetTail(oldTail, node2)) { 4、乐观锁使tail指向node2
oldTail.next = node2; 5、老tail 的next指向node2
return node2;
}
} else {
initializeSyncQueue(); 0、初始化AQS队列如下图
}
}
}
6、如果线程2刚刚加入到队列还没调用LockSupport.park(this);,而线程1正好执行完毕,优先调用了线程2的unpark(),LockSupport.unpark(s.thread);
LockSupport 类似于信号量Semaphore,如果在park之前调用了unpark,那么再调用park的时候不会阻塞,直接返回
9. 公平锁
我们可以看到公平锁在获取锁的过程中,比非公平锁多了一个条件,hasQueuedPredecessors()判断队列中是否存在等待节点,jdk10公平锁,非公平锁就这一点区别,和jdk8还是不太一样的
static final class FairSync extends Sync {
@ReservedStackAccess
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
1、注意这里的 hasQueuedPredecessors()
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;
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h !=t 说明队列不为空
// (s = h.next) == null 不知道在什么情况下 h.next==null
// s.thread != Thread.currentThread() 如果有而且不能是当前线程
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
9. 总结
本篇文章主要是介绍了,可重入锁ReentrantLock的lock和unlock过程,至于ReentrantLock中的其他方法lockInterruptibly(),tryLock(),tryLock(long time, TimeUnit unit)就不做过多的啰嗦了,几乎都是没有什么差别的。下篇文章会对Condition做一个详细的解读
番外篇
1. Java锁保证可见性的具体实现
Happens-before规则
- 从JDK 5开始,JSR-133定义了新的内存模型,内存模型描述了多线程代码中的哪些行为是合法的,以及线程间如何通过内存进行交互。
- 新的内存模型语义在内存操作(读取字段,写入字段,加锁,解锁)和其他线程操作上创建了一些偏序规则,这些规则又叫作Happens-before规则。
- 它的含义是当一个动作happens before另一个动作,这意味着第一个动作被保证在第二个动作之前被执行并且结果对其可见。我们利用Happens-before规则来解释Java锁到底如何保证了可见性。
Java内存模型一共定义了八条Happens-before规则,和Java锁相关的有以下两条:
- 内置锁(synchronized)的释放锁操作发生在该锁随后的加锁操作之前
- 一个volatile变量的写操作发生在这个volatile变量随后的读操作之前
2. 锁释放和获取的内存语义
- 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
- 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量
锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。
3. 下面对锁释放和锁获取的内存语义做个总结:
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
- 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
4. Lock与synchronized的区别:
- Lock是一个接口,是代码层面的实现;synchronized是关键字,是内置的语言实现(JVM层面)。
- Lock是显示地获取释放锁,扩展性更强;synchronized是隐式地获取释放锁,更简捷。
- Lock在发生异常时,如果没有主动通过unlock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
- Lock可以让等待锁的线程响应中断;而使用synchronized时等待锁的线程会一直等待下去,不能响应中断;
- Lock可以尝试非阻塞、可中断、超时地获取锁;synchronized不可以。
- Lock可以知道是否成功获取锁;synchronized无法知道。