http://ifeve.com/locks/
[原文链接](http://tutorials.jenkov.com/java-concurrency/locks.html "原文链接") **作者:**Jakob Jenkov **译者:**[申章](http://weibo.com/u/3051817564) **校对:**丁一
锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(*译者注:这说的是Java 5之前的情况*)。
自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了。但是你仍然需要去了解怎样使用这些锁,且了解这些实现背后的理论也是很有用处的。可以参考我对[java.util.concurrent.locks.Lock](http://tutorials.jenkov.com/java-util-concurrent/lock.html "Lock")的介绍,以了解更多关于锁的信息。
以下是本文所涵盖的主题:
1. [一个简单的锁](http://ifeve.com/locks/#simpleLock)
2. [锁的可重入性](http://ifeve.com/locks/#lockReentrance)
3. [锁的公平性](http://ifeve.com/locks/#lockFairness)
4. [在finally语句中调用unlock()](http://ifeve.com/locks/#finallyUnlock)
**一个简单的锁**
让我们从java中的一个同步块开始:
~~~
public class Counter{
private int count = 0;
public int inc(){
synchronized(this){
return ++count;
}
}
}
~~~
可以看到在inc()方法中有一个synchronized(this)代码块。该代码块可以保证在同一时间只有一个线程可以执行return ++count。虽然在synchronized的同步块中的代码可以更加复杂,但是++count这种简单的操作已经足以表达出线程同步的意思。
以下的Counter类用Lock代替synchronized达到了同样的目的:
~~~
public class Counter{
private Lock lock = new Lock();
private int count = 0;
public int inc(){
lock.lock();
int newCount = ++count;
lock.unlock();
return newCount;
}
}
~~~
lock()方法会对Lock实例对象进行加锁,因此所有对该对象调用lock()方法的线程都会被阻塞,直到该Lock对象的unlock()方法被调用。
这里有一个Lock类的简单实现:
| `01` | `public` `class` `Counter{` |
| `02` | `public` `class` `Lock{` |
| `03` | `private` `boolean` `isLocked = ``false``;` |
| `04` | |
| `05` | `public` `synchronized` `void` `lock()` |
| `06` | `throws` `InterruptedException{` |
| `07` | `while``(isLocked){` |
| `08` | `wait();` |
| `09` | `}` |
| `10` | `isLocked = ``true``;` |
| `11` | `}` |
| `12` | |
| `13` | `public` `synchronized` `void` `unlock(){` |
| `14` | `isLocked = ``false``;` |
| `15` | `notify();` |
| `16` | `}` |
| `17` | `}` |
注意其中的while(isLocked)循环,它又被叫做“自旋锁”。自旋锁以及wait()和notify()方法在[线程通信](http://ifeve.com/thread-signaling/ "Thread Signaling")这篇文章中有更加详细的介绍。当isLocked为true时,调用lock()的线程在wait()调用上阻塞等待。为防止该线程没有收到notify()调用也从wait()中返回(也称作[虚假唤醒](http://ifeve.com/thread-signaling/#spurious_wakeups "Spurious Wakeup")),这个线程会重新去检查isLocked条件以决定当前是否可以安全地继续执行还是需要重新保持等待,而不是认为线程被唤醒了就可以安全地继续执行了。如果isLocked为false,当前线程会退出while(isLocked)循环,并将isLocked设回true,让其它正在调用lock()方法的线程能够在Lock实例上加锁。
当线程完成了[临界区](http://ifeve.com/race-conditions-and-critical-sections/ "critical section")(位于lock()和unlock()之间)中的代码,就会调用unlock()。执行unlock()会重新将isLocked设置为false,并且通知(唤醒)其中一个(若有的话)在lock()方法中调用了wait()函数而处于等待状态的线程。
**锁的可重入性**
Java中的synchronized同步块是可重入的。这意味着如果一个java线程进入了代码中的synchronized同步块,并因此获得了该同步块使用的同步对象对应的管程上的锁,那么这个线程可以进入由同一个管程对象所同步的另一个java代码块。下面是一个例子:
| `1` | `public` `class` `Reentrant{` |
| `2` | `public` `synchronized` `outer(){` |
| `3` | `inner();` |
| `4` | `}` |
| `5` | |
| `6` | `public` `synchronized` `inner(){` |
| `7` | `//do something` |
| `8` | `}` |
| `9` | `}` |
注意outer()和inner()都被声明为synchronized,这在Java中和synchronized(this)块等效。如果一个线程调用了outer(),在outer()里调用inner()就没有什么问题,因为这两个方法(代码块)都由同一个管程对象(”this”)所同步。如果一个线程已经拥有了一个管程对象上的锁,那么它就有权访问被这个管程对象同步的所有代码块。这就是可重入。线程可以进入任何一个它已经拥有的锁所同步着的代码块。
前面给出的锁实现不是可重入的。如果我们像下面这样重写Reentrant类,当线程调用outer()时,会在inner()方法的lock.lock()处阻塞住。
| `01` | `public` `class` `Reentrant2{` |
| `02` | `Lock lock = ``new` `Lock();` |
| `03` | |
| `04` | `public` `outer(){` |
| `05` | `lock.lock();` |
| `06` | `inner();` |
| `07` | `lock.unlock();` |
| `08` | `}` |
| `09` | |
| `10` | `public` `synchronized` `inner(){` |
| `11` | `lock.lock();` |
| `12` | `//do something` |
| `13` | `lock.unlock();` |
| `14` | `}` |
| `15` | `}` |
调用outer()的线程首先会锁住Lock实例,然后继续调用inner()。inner()方法中该线程将再一次尝试锁住Lock实例,结果该动作会失败(也就是说该线程会被阻塞),因为这个Lock实例已经在outer()方法中被锁住了。
两次lock()之间没有调用unlock(),第二次调用lock就会阻塞,看过lock()实现后,会发现原因很明显:
| `01` | `public` `class` `Lock{` |
| `02` | `boolean` `isLocked = ``false``;` |
| `03` | |
| `04` | `public` `synchronized` `void` `lock()` |
| `05` | `throws` `InterruptedException{` |
| `06` | `while``(isLocked){` |
| `07` | `wait();` |
| `08` | `}` |
| `09` | `isLocked = ``true``;` |
| `10` | `}` |
| `11` | |
| `12` | `...` |
| `13` | `}` |
一个线程是否被允许退出lock()方法是由while循环(自旋锁)中的条件决定的。当前的判断条件是只有当isLocked为false时lock操作才被允许,而没有考虑是哪个线程锁住了它。
为了让这个Lock类具有可重入性,我们需要对它做一点小的改动:
| `01` | `public` `class` `Lock{` |
| `02` | `boolean` `isLocked = ``false``;` |
| `03` | `Thread lockedBy = ``null``;` |
| `04` | `int` `lockedCount = ``0``;` |
| `05` | |
| `06` | `public` `synchronized` `void` `lock()` |
| `07` | `throws` `InterruptedException{` |
| `08` | `Thread callingThread =` |
| `09` | `Thread.currentThread();` |
| `10` | `while``(isLocked && lockedBy != callingThread){` |
| `11` | `wait();` |
| `12` | `}` |
| `13` | `isLocked = ``true``;` |
| `14` | `lockedCount++;` |
| `15` | `lockedBy = callingThread;` |
| `16` | `}` |
| `17` | |
| `18` | `public` `synchronized` `void` `unlock(){` |
| `19` | `if``(Thread.curentThread() ==` |
| `20` | `this``.lockedBy){` |
| `21` | `lockedCount--;` |
| `22` | |
| `23` | `if``(lockedCount == ``0``){` |
| `24` | `isLocked = ``false``;` |
| `25` | `notify();` |
| `26` | `}` |
| `27` | `}` |
| `28` | `}` |
| `29` | |
| `30` | `...` |
| `31` | `}` |
注意到现在的while循环(自旋锁)也考虑到了已锁住该Lock实例的线程。如果当前的锁对象没有被加锁(isLocked = false),或者当前调用线程已经对该Lock实例加了锁,那么while循环就不会被执行,调用lock()的线程就可以退出该方法(*译者注:“被允许退出该方法”在当前语义下就是指不会调用wait()而导致阻塞)*。
除此之外,我们需要记录同一个线程重复对一个锁对象加锁的次数。否则,一次unblock()调用就会解除整个锁,即使当前锁已经被加锁过多次。在unlock()调用没有达到对应lock()调用的次数之前,我们不希望锁被解除。
现在这个Lock类就是可重入的了。
**锁的公平性**
Java的synchronized块并不保证尝试进入它们的线程的顺序。因此,如果多个线程不断竞争访问相同的synchronized同步块,就存在一种风险,其中一个或多个线程永远也得不到访问权 —— 也就是说访问权总是分配给了其它线程。这种情况被称作线程饥饿。为了避免这种问题,锁需要实现公平性。本文所展现的锁在内部是用synchronized同步块实现的,因此它们也不保证公平性。[饥饿和公平](http://tutorials.jenkov.com/java-concurrency/starvation-and-fairness.html "Starvation and Fairness")中有更多关于该内容的讨论。
**在finally语句中调用unlock()**
如果用Lock来保护临界区,并且临界区有可能会抛出异常,那么在finally语句中调用unlock()就显得非常重要了。这样可以保证这个锁对象可以被解锁以便其它线程能继续对其加锁。以下是一个示例:
| `1` | `lock.lock();` |
| `2` | `try``{` |
| `3` | `//do critical section code,` |
| `4` | `//which may throw exception` |
| `5` | `} ``finally` `{` |
| `6` | `lock.unlock();` |
| `7` | `}` |
这个简单的结构可以保证当临界区抛出异常时Lock对象可以被解锁。如果不是在finally语句中调用的unlock(),当临界区抛出异常时,Lock对象将永远停留在被锁住的状态,这会导致其它所有在该Lock对象上调用lock()的线程一直阻塞。
**原创文章,转载请注明:** 转载自[并发编程网 – ifeve.com](http://ifeve.com/)**本文链接地址:** [Java中的锁](http://ifeve.com/locks/)
- JVM
- 深入理解Java内存模型
- 深入理解Java内存模型(一)——基础
- 深入理解Java内存模型(二)——重排序
- 深入理解Java内存模型(三)——顺序一致性
- 深入理解Java内存模型(四)——volatile
- 深入理解Java内存模型(五)——锁
- 深入理解Java内存模型(六)——final
- 深入理解Java内存模型(七)——总结
- Java内存模型
- Java内存模型2
- 堆内内存还是堆外内存?
- JVM内存配置详解
- Java内存分配全面浅析
- 深入Java核心 Java内存分配原理精讲
- jvm常量池
- JVM调优总结
- JVM调优总结(一)-- 一些概念
- JVM调优总结(二)-一些概念
- VM调优总结(三)-基本垃圾回收算法
- JVM调优总结(四)-垃圾回收面临的问题
- JVM调优总结(五)-分代垃圾回收详述1
- JVM调优总结(六)-分代垃圾回收详述2
- JVM调优总结(七)-典型配置举例1
- JVM调优总结(八)-典型配置举例2
- JVM调优总结(九)-新一代的垃圾回收算法
- JVM调优总结(十)-调优方法
- 基础
- Java 征途:行者的地图
- Java程序员应该知道的10个面向对象理论
- Java泛型总结
- 序列化与反序列化
- 通过反编译深入理解Java String及intern
- android 加固防止反编译-重新打包
- volatile
- 正确使用 Volatile 变量
- 异常
- 深入理解java异常处理机制
- Java异常处理的10个最佳实践
- Java异常处理手册和最佳实践
- Java提高篇——对象克隆(复制)
- Java中如何克隆集合——ArrayList和HashSet深拷贝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常见正则表达式
- 类
- 理解java类加载器以及ClassLoader类
- 深入探讨 Java 类加载器
- 类加载器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之实现细节
- java.util.concurrent 之ConcurrentHashMap 源码分析
- HashMap的实现原理和底层数据结构
- 线程
- 关于Java并发编程的总结和思考
- 40个Java多线程问题总结
- Java中的多线程你只要看这一篇就够了
- Java多线程干货系列(1):Java多线程基础
- Java非阻塞算法简介
- Java并发的四种风味:Thread、Executor、ForkJoin和Actor
- Java中不同的并发实现的性能比较
- JAVA CAS原理深度分析
- 多个线程之间共享数据的方式
- Java并发编程
- Java并发编程(1):可重入内置锁
- Java并发编程(2):线程中断(含代码)
- Java并发编程(3):线程挂起、恢复与终止的正确方法(含代码)
- Java并发编程(4):守护线程与线程阻塞的四种情况
- Java并发编程(5):volatile变量修饰符—意料之外的问题(含代码)
- Java并发编程(6):Runnable和Thread实现多线程的区别(含代码)
- Java并发编程(7):使用synchronized获取互斥锁的几点说明
- Java并发编程(8):多线程环境中安全使用集合API(含代码)
- Java并发编程(9):死锁(含代码)
- Java并发编程(10):使用wait/notify/notifyAll实现线程间通信的几点重要说明
- java并发编程-II
- Java多线程基础:进程和线程之由来
- Java并发编程:如何创建线程?
- Java并发编程:Thread类的使用
- Java并发编程:synchronized
- Java并发编程:Lock
- Java并发编程:volatile关键字解析
- Java并发编程:深入剖析ThreadLocal
- Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
- Synchronized与Lock
- JVM底层又是如何实现synchronized的
- Java synchronized详解
- synchronized 与 Lock 的那点事
- 深入研究 Java Synchronize 和 Lock 的区别与用法
- JAVA编程中的锁机制详解
- Java中的锁
- TreadLocal
- 深入JDK源码之ThreadLocal类
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的内存泄露
- 多线程设计模式
- Java多线程编程中Future模式的详解
- 原子操作(CAS)
- [译]Java中Wait、Sleep和Yield方法的区别
- 线程池
- 如何合理地估算线程池大小?
- JAVA线程池中队列与池大小的关系
- Java四种线程池的使用
- 深入理解Java之线程池
- java并发编程III
- Java 8并发工具包漫游指南
- 聊聊并发
- 聊聊并发(一)——深入分析Volatile的实现原理
- 聊聊并发(二)——Java SE1.6中的Synchronized
- 文件
- 网络
- index
- 内存文章索引
- 基础文章索引
- 线程文章索引
- 网络文章索引
- IOC
- 设计模式文章索引
- 面试
- Java常量池详解之一道比较蛋疼的面试题
- 近5年133个Java面试问题列表
- Java工程师成神之路
- Java字符串问题Top10
- 设计模式
- Java:单例模式的七种写法
- Java 利用枚举实现单例模式
- 常用jar
- HttpClient和HtmlUnit的比较总结
- IO
- NIO
- NIO入门
- 注解
- Java Annotation认知(包括框架图、详细介绍、示例说明)