前几天被问到ReentrantLock锁的用法和作用,竟然一时没答上来,太可惜了,ReentrantLock锁明明见了很多次,一直没留意。其实在前面分析UIL框架的时候,就有ReentrantLock锁的案例
参考 Universal-Image-Loader系列2-源码分析 防止同一时间点的重复请求
是时候好好总结一下了
synchronized锁机制
synchronized,有两个重要后果,通常是指该代码具有 原子性(atomicity)和 可见性(visibility)。原子性意味着一个线程一次只能执行由一个指定监控对象(lock)保护的代码,从而防止多个线程在更新共享状态时相互冲突。可见性则更为微妙,它要对付内存缓存和编译器优化的各种反常行为
public class Test {
private static boolean stopRequest;
public static void main(String args[]) {
Thread myThread = new Thread(new Runnable() {
public void run() {
int count = 0;
while (!stopRequest) { //访问类属性,共享的
System.out.println("count: " + count++);
ThreadUtils.sleep(1000);
}
}
});
myThread.start();
ThreadUtils.sleep(2000);
stopRequest = true;
}
}
这段代码真的没问题吗? 答案是否定的,当然有问题,基本类型的读写是原子的 boolean stopRequest;
虽然是基本类型,读写是原子性的,但是并不能确保某个线程的对该变量的修改,另一个线程可以看得见。所以主线程修改了stopRequest,但是并不能保证myThread对这个修改可见,所以myThread可能一直运行下去
解决方案1. volatile修饰
private static volatile boolean stopRequest;
volatile保证了任何一个线程读取该域多都将看到最近被写入的值,不保证互斥访问,所以有更好的性能 轻量级的实现,比synchronized有更好的性能
解决方案2:使用AtomicBoolean,也有良好的性能,java推荐使用方式,java.util.concurrent
包中的类,专为同步设计
public class Test {
private static AtomicBoolean stopRequest;
public static void main(String args[]) {
Thread myThread = new Thread(new Runnable() {
public void run() {
int count = 0;
while (!stopRequest.get()) { //访问类属性,共享的
System.out.println("count: " + count++);
ThreadUtils.sleep(1000);
}
}
});
myThread.start();
ThreadUtils.sleep(2000);
stopRequest.set(true);;
}
}
类似的还有AtomicInteger等
解决方案3:synchronized修饰
public class Test {
private static boolean stopRequest;
private synchronized static void requestStop() {
stopRequest = true;
}
private synchronized static boolean getRequestStop() {
return stopRequest;
}
public static void main(String args[]) {
Thread myThread = new Thread(new Runnable() {
public void run() {
int count = 0;
while (!getRequestStop()) {
System.out.println("count: " + count++);
ThreadUtils.sleep(1000);
}
}
});
myThread.start();
ThreadUtils.sleep(2000);
requestStop();
}
}
此时是最低效的,因为requestStop和getRequestStop方法并不需要互斥访问,因为只有一条读和写的语句,同时读写stopRequest是boolean类型,基本类型读写是原子的,所以requestStop和getRequestStop方法必然是互斥的,所以就不需要重量型的synchronized,使用volatile/AtomicBoolean有更好的性能.
ReentrantLock锁机制
ReentrantLock锁应用
public void lock ()
Acquires the lock.
Acquires the lock if it is not held by another thread and returns immediately, setting the lock hold count to one.
If the current thread already holds the lock then the hold count is incremented by one and the method returns immediately.
If the lock is held by another thread then the current thread becomes disabled for thread scheduling purposes and lies dormant until the lock has been acquired, at which time the lock hold count is set to one.
请求锁,1. 如果没有任何一个线程持有该锁那么直接返回,同时设置holdcount为0 2. 如果被当前线程持有该锁,那么holdcount执行+1,然后返回 3. 如果当锁被其它线程持有,那么当前线程不能被调度运行,处于休眠状态直到该锁的holdcount为0那么会重新请求锁public boolean isLocked ()
Queries if this lock is held by any thread. This method is designed for use in monitoring of the system state, not for synchronization control.
查询这个锁是否被线程持有public void unlock ()
Attempts to release this lock.
If the current thread is the holder of this lock then the hold count is decremented. If the hold count is now zero then the lock is released. If the current thread is not the holder of this lock then IllegalMonitorStateException is thrown.
尝试释放锁,假如这个锁的持有者是当前线程,那么holdcount执行-1,如果holdcount等于0的时候,这个锁才被释放,假如这个锁的持有者不是当前线程,抛出IllegalMonitorStateException
代码实例
public class Test {
ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Test test = new Test();
new MyThread("T-one", test.lock).start();
new MyThread("T-two", test.lock).start();
new MyThread("T-three", test.lock).start();
}
}
class MyThread extends Thread {
String name;
ReentrantLock lock;
public MyThread(String name, ReentrantLock lock) {
this.name = name;
this.lock = lock;
}
@Override
public void run() {
if (lock.isLocked()) {
System.out.println(name + " wait as lock");
}
lock.lock();
try {
for (int i = 0; i < 2; i++) {
System.out.println(name + " index:" + i);
}
} finally {
lock.unlock();
}
}
}
打印结果: //T-1首先获得该锁,然后T-2/T-3发现该锁已经被持有了,所以休眠,当线程T-1执行完,此时lock的holdcount为1,finally中unlock释放锁,holdcount为0了,所以T-2/T-3重新请求锁,T-2抢到了,执行,最后执行T-3
T-1 index:0
T-2 wait as lock
T-3 wait as lock
T-1 index:1
T-2 index:0
T-2 index:1
T-3 index:0
T-3 index:1
再来看看变形
try {
for (int i = 0; i < 2; i++) {
if (i == 1) {
lock.lock();
}
System.out.println(name + " index:" + i);
}
} finally {
lock.unlock();
//lock.unlock();
}
输出结果:
T-2 wait as lock
T-3 wait as lock
T-1 index:0
T-1 index:1
T-2/T-3没得到执行,因为T-1中导致lock的holdcount为2,但是只调用了一次unlock,所以T-2/T-3一直在休眠状态
//lock.unlock(); 取消注释,那么就正常啦
再看另一种变形
try {
for (int i = 0; i < 2; i++) {
System.out.println(name + " index:" + i);
}
} finally {
lock.unlock();
lock.unlock();
}
输出结果:
java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(Unknown Source)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(Unknown Source)
at java.util.concurrent.locks.ReentrantLock.unlock(Unknown Source)
at MyThread.run(Test.java:44)
很好理解了,T-1执行第一次unlock,那么此时lock的holdcount为0了,所以T-2/T-3可能得到了调度持有了该锁,那么在执行T-1执行第二次lock.unlock();此时lock的持有者已经不是当前线程了,所以抛出IllegalMonitorStateException异常
UIL中的ReentrantLock锁方案
使用UIL库加载图片,3个view同时请求同一个url,我们知道UIL中每请求一次会new一个runnable,然后放在线程池中去运行,那么此时就有3个线程在同时运行了,那么怎么去实现,第一个请求做网络上获取,然后之后的直接在硬盘缓存或者内存缓存中取呢?
1. synchronized锁方案
Object getLockForUri(String uri) {
Object lock = uriLocks.get(uri);
if (lock == null) {
lock = new Object();
uriLocks.put(uri, lock);
}
return lock;
}
//加载和显示图片的Runnable
final class LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener {
@Override
public void run() {
.......
Object loadFromUriLock = imageLoadingInfo.loadFromUriLock;
synchronized(loadFromUriLock){
.......
}
}
如果使用synchronized来实现锁机制,那么此时没有办法查询到loadFromUriLock对象的锁是否被某个线程持有的,只有傻傻的阻塞在那里了,得不到任何log日志,但是使用ReentrantLock的话就可以做到,高级特性
2. ReentrantLock锁方案
//ImageLoaderEngine.java
ReentrantLock getLockForUri(String uri) {
ReentrantLock lock = uriLocks.get(uri);
if (lock == null) {
lock = new ReentrantLock();
uriLocks.put(uri, lock);
}
return lock;
}
//加载和显示图片的Runnable
final class LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener {
@Override
public void run() {
.......
ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
if (loadFromUriLock.isLocked()) { //此时可以看到该锁是否被其它线程所持有,如果是的话,返回true,那么就可以打印消息了,告诉开发人员,该url的图片正在请求了
L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
}
loadFromUriLock.lock(); //如果loadFromUriLock锁被其它线程持有了,那么一直阻塞,直到loadFromUriLock.unlock()
try {
........
} finally {
loadFromUriLock.unlock(); //必须在finally中手动释放锁
}
}
}
ReentrantLock源码分析
待定
ReentrantLock和synchronized两种锁定机制的对比
参考 Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制
对 synchronized 的改进
如此看来同步相当好了,是么?那么为什么 JSR 166 小组花了这么多时间来开发 java.util.concurrent.lock 框架呢?答案很简单-同步是不错,但它并不完美。它有一些功能性的限制 —— 它无法中断一个正在等候获得锁的线程,也无法通过轮询得到锁,如果不想等下去,也就没法得到锁。同步还要求锁的释放只能在与获得锁所在的堆栈帧相同的堆栈帧中进行,多数情况下,这没问题(而且与异常处理交互得很好),但是,确实存在一些非块结构的锁定更合适的情况。
ReentrantLock 类
java.util.concurrent.lock 中的 Lock 框架是锁定的一个抽象,它允许把锁定的实现作为 Java 类,而不是作为语言的特性来实现。这就为 Lock 的多种实现留下了空间,各种实现可能有不同的调度算法、性能特性或者锁定语义。 ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语义,但是添加了类似轮询锁tryLock方法实现、定时锁等候和可中断锁等候lockInterruptibly方法实现的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
lock 必须在 finally 块中释放。否则,如果受保护的代码将抛出异常,锁就有可能永远得不到释放! synchronized锁方案代码块结束自动释放锁
条件变量
ReentrantLock还提供了Condition,对线程的等待和唤醒等操作更加灵活,一个ReentrantLock可以有多个Condition实例,所以更有扩展性。
根类 Object 包含某些特殊的方法,用来在线程的 wait() 、 notify() 和 notifyAll() 之间进行通信。这些是高级的并发性特性,许多开发人员从来没有用过它们 —— 这可能是件好事,因为它们相当微妙,很容易使用不当。幸运的是,随着 JDK 5.0 中引入 java.util.concurrent ,开发人员几乎更加没有什么地方需要使用这些方法了。
通知与锁定之间有一个交互 —— 为了在对象上 wait 或 notify ,您必须持有该对象的锁。就像 Lock 是同步的概括一样, Lock 框架包含了对 wait 和 notify 的概括,这个概括叫作 条件(Condition) 。 Lock 对象则充当绑定到这个锁的条件变量的工厂对象,与标准的 wait 和 notify 方法不同,对于指定的 Lock ,可以有不止一个条件变量与它关联。这样就简化了许多并发算法的开发。例如, 条件(Condition) 的 Javadoc 显示了一个有界缓冲区实现的示例,该示例使用了两个条件变量,“not full”和“not empty”,它比每个 lock 只用一个 wait 设置的实现方式可读性要好一些(而且更有效)。 Condition 的方法与 wait 、 notify 和 notifyAll 方法类似,分别命名为 await 、 signal 和 signalAll ,因为它们不能覆盖 Object 上的对应方法。
什么时候选择用ReentrantLock代替synchronized
既然如此,我们什么时候才应该使用 ReentrantLock 呢?答案非常简单 —— 在确实需要一些 synchronized 所没有的特性的时候,比如时间锁等候、可中断锁等候、无块结构锁、多个条件变量或者轮询锁。 ReentrantLock 还具有可伸缩性的好处,应当在高度争用的情况下使用它,但是请记住,大多数 synchronized 块几乎从来没有出现过争用,所以可以把高度争用放在一边。我建议用 synchronized 开发,直到确实证明 synchronized 不合适,而不要仅仅是假设如果使用 ReentrantLock “性能会更好”。请记住,这些是供高级用户使用的高级工具。(而且,真正的高级用户喜欢选择能够找到的最简单工具,直到他们认为简单的工具不适用为止。)。一如既往,首先要把事情做好,然后再考虑是不是有必要做得更快。