锁能够保证某一块代码块在执行时候只能够有一个线程能够进入,我们常见的就是synchronized了,Java SE 1.6钱该锁是重量级锁(发生竞争关系时会主动进入睡眠状态),之后进行了各种的优化,该情况就没有那么重了。本文将详细介绍偏向锁以及轻量级锁以及锁的存储结构和升级过程。这些内容均来自于方腾飞《并发编程的艺术》以及本人做的二次加工。
先来看看利用synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为以下三种形式:
1)对于普通同步方法,锁是当前实例对象
2)对于静态同步方法,锁是当前类的Class对象
3)对于同步方法块,锁是Synchronized括号里配置的对象
一个线程是图访问同步块是,必须先得到锁,退出或者抛出异常时必须释放锁。锁到底存在哪里呢?从JVM规范可以看出来,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。代码块同步使用monitorenter 和 monitorexit指令实现的,方法同步是另一种。方法同步可以使用这两个指令来实现。可以看看这个链接的例子:http://m635674608.iteye.com/blog/1748415
锁存在哪里呢?由于每一个Java对象都有一个对象头,其中有个叫做Mark Word里面记录着锁的状态,有(轻量级锁,重量级锁,偏向锁以及GC标记)
Java SE 1.6中,锁有4种状态,级别从低到高是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级,而且只能升级,不能降级。
锁的优缺点对比
锁 | 优点 | 缺点 | 适用场景 |
偏向锁 | 加锁和解锁不需要额外的消耗, 和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争, 会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步 块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了 程序的响应速度 | 如果始终得不到锁竞争的线程, 使用自旋会消耗CPU | 追求响应时间 同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间慢 | 追求吞吐量 同步块执行速度较长 |
锁的内存语义
锁的释放-获取建立的happens-before关系
锁是Java并发编程中个最重要的同步机制。锁除了让临界区户次执行外,还可以让释放锁的现场向获取同一个锁的线程发送消息。
class MonitorExample {
int a = 0;
public synchronized void writer() { //1
a ++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
......
} //6
}
假设A线程执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类。
1)根据程序次序规则,1 happens-before 2, 2 happens-before 3; 4 happens-before 5, 5 happens-before 6 。
2)根据监视器锁规则,3 happens-before 4。
3)根据happens-before 的传递性, 2 happens-before 5。
锁的释放和获取的内存语义
锁内存语义的实现
借助ReentrantLock源代码来分析锁内存语义的具体实现机制
class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock();
try {
a++;
} finally {
lock.unlock();
}
}
public void reader() {
lock.lock();
try {
int i = a;
} finally {
lock.unlock();
}
}
}
在ReentrantLock中,调用lock()方法获取锁,调用unlock()方法解锁。使用公平锁,加锁方法lock()调用轨迹如下。
1)ReentrantLock:lock()
2)FairSync:lock
final void lock() {
acquire(1);
}
3)AbstractQueuedSynchronized: acquire(int arg)。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
4)ReentrantLock:tryAcquire(int acquires)。 /**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
在这里,state是volatile,我们能够看到加锁首先获取state变量。
在使用公平锁时,解锁方法unlock()调用轨迹如下。
1)ReentrantLock:unlock()
2)AbstractQueuedSynchronizer:release(int arg)
3)Sync:tryRelease(int releases)
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;
}