Java中常用的锁分析总结
1. ReentrantLock、ReentrantReadWriteLock及Sychronized简介
(a) 类继承结构
ReentrantLock类继承结构:
ReentrantReadWriteLick类继承结构:
简述:通过类的继承结构可以看出ReentrantLock 和 ReentrantReadWriteLock是拥有者两个不同类继承结构的体系,两者并无关联。
Ps:Sychronized是一个关键字
(b) 几个相关概念
什么是可重入锁:可重入锁的概念是自己可以再次获取自己的内部锁。举个例子,比如一条线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的(如果不可重入的锁的话,此刻会造成死锁)。说的更高深一点可重入锁是一种递归无阻塞的同步机制。
什么叫读写锁:读写锁拆成读锁和写锁来理解。读锁可以共享,多个线程可以同时拥有读锁,但是写锁却只能只有一个线程拥有,而且获取写锁的时候其他线程都已经释放了读锁,而且该线程获取写锁之后,其他线程不能再获取读锁。简单的说就是写锁是排他锁,读锁是共享锁。
获取锁涉及到的两个概念即 公平和非公平:公平表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得的FIFO顺序。而非公平就是一种获取锁的抢占机制,和公平相对就是先来不一定先得,这个方式可能造成某些线程饥饿(一直拿不到锁)。
(c) ReentrantLock,ReentrantReadWriteLock,Sychronized用法即作用
ReentrantLock: 类ReentrantLock实现了Lock,它拥有与Sychronized相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断等候的一些特性。此外,它还提供了在与激烈争用情况下更佳的性能(说白了就是ReentrantLock和Sychronized差不多,线程间都是完全互斥的,一个时刻只能有一个线程获取到锁,执行被锁住的代码,但ReentrantLock相对于Sychronized提供了更加丰富的功能并且在线程调度上做了优化,JVM调度使用ReentrantLock的线程会更快)
代码示例:ReentrantLockTest.java
/**
* ReentrantLock DEMO
* @author jianying.wcj
* @date 2013–5–20
*/
public class ReetrantLockTest {
/**
* 一个可重入锁成员变量
*/
private ReentrantLocklock =new ReentrantLock();
public static void main(String[] args) {
ReetrantLockTestdalt = new ReetrantLockTest();
dalt.testLock();
}
public void testLock(){
for(int i = 0; i < 5; i++) {
Threadthread = new Thread(new Runnable(){
@Override
publicvoid run() {
sayHello();
}
},“thread”+i);
thread.start();
}
}
public void sayHello() {
/**
* 当一条线程不释放锁的时候,第二个线程走到这里的时候就阻塞掉了
*/
try {
lock.lock();
System.out.println(Thread.currentThread().getName()+” locking …”);
System.out.println(“Hello world!”);
System.out.println(Thread.currentThread().getName()+” unlocking …”);
}finally {
lock.unlock();
}
}
}
执行结果:
简述:首先要操作ReentrantLock的加锁(lock)和解锁(unlock)必须是针对同一个ReentrantLock对象,要是new 两个ReetrantLock来分别完成对同一资源的加锁和解锁是没有意义的。比如LockA对象对 resource 加锁,让后LockB对象对Resource解锁,这个是不对的,没有意义的)。通过执行结果可以看出,当一个线程去lock资源的时候,必须是上一个线程对资源完成了unlock,这个和syncronized关键字启动的作用是一样的。 另外在使用时一个需要格外主意的点是 unlock方法的调用要放在finally代码块里,来保证锁一定会释放,否则可能造成某一个资源一直被锁死,排查问题比较困难。
ReentrantReadWriteLock:类ReentrantReadWriteLock实现了ReadWirteLock接口。它和ReentrantLock是不同的两套实现,在类继承结构上并无关联。和ReentrantLock定义的互斥锁不同的是,ReentrantReadWriteLock定义了两把锁即读锁和写锁。读锁可以共享,即同一个资源可以让多个线程获取读锁。这个和ReentrantLock(或者sychronized)相比大大提高了读的性能。在需要对资源进行写入的时候在会加写锁达到互斥的目的。话不多说看DEMO:
ReentrantReadWriteLock.java:
public class ReadWriteLockTest {
/**
* 一个可重入读写锁
*/
private ReentrantReadWriteLockreadWriteLock =new ReentrantReadWriteLock();
/**
* 读锁
*/
private ReadLockreadLock =readWriteLock.readLock();
/**
* 写锁
*/
private WriteLockwriteLock =readWriteLock.writeLock();
/**
* 共享资源
*/
private StringshareData =“寂寞等待中…”;
public void write(String str) throws InterruptedException {
writeLock.lock();
System.err.println(“ThreadName:”+Thread.currentThread().getName()+“locking…”);
try {
shareData = str;
System.err.println(“ThreadName:” + Thread.currentThread().getName()+“修改为”+str);
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
System.err.println(“ThreadName:” + Thread.currentThread().getName()+” unlock…”);
writeLock.unlock();
}
}
public String read() {
readLock.lock();
System.out.println(“ThreadName:” + Thread.currentThread().getName()+“lock…”);
try {
System.out.println(“ThreadName:”+Thread.currentThread().getName()+“获取为:”+shareData);
Thread.sleep(1);
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println(“ThreadName:” + Thread.currentThread().getName()+“unlock…”);
readLock.unlock();
}
returnshareData;
}
public static void main(String[] args) {
final ReadWriteLockTest shareData =new ReadWriteLockTest();
/**
* 起50条读线程
*/
for(int i = 0; i < 50; i++) {
new Thread(new Runnable() {
publicvoid run() {
try {
Thread.sleep(1);
}catch (InterruptedException e) {
e.printStackTrace();
}
shareData.read();
}
},“get Thread-read”+i).start();
}
for(int i = 0; i < 5; i++) {
new Thread(new Runnable() {
publicvoid run() {
try {
Thread.sleep(1);
}catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
shareData.write(new Random().nextLong()+“”);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
},“wirte Thread-write”+i).start();
}
}
}
运行结果:
简述:Demo读锁和写锁都是ReentrantReadWriteLock类定义的内部公开类,要想让读锁和读锁或者读锁跟写锁产生共享或者互斥关系,必须要求读锁和写锁是有同一个ReentrantReadWriteLock产生的,否则是没有意义的。从运行结果中可以看出读锁之间的共享,写锁和写锁,写锁和读锁之间的互斥关系。
Synchronized关键字:
public class SychronizedTest implements Runnable{
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+“synchronized loop “ + i);
}
}
}
public static void main(String[] args) {
SychronizedTest t1 = new SychronizedTest();
Thread ta = new Thread(t1,“A”);
Thread tb = new Thread(t1,“B”);
ta.start();
tb.start();
}
}
运行结果:
·
简述:从运行记过来看,被sychronized包围的代码是原子的。这个不多说,这个关键字大家应该都很熟悉。
2. ReentrantLock、ReentrantReadWriteLock及Sychronized实现原理(源码级别)
(a) 锁机制的内部实现
ReentrantLock内部锁机制实现相关类图:
简述:ReentrantLock锁机制的实现是基于它的一个成员变量sync,这个Sync是AbstractQueuedSynchronized(AQS)的一个子类(ps:sync类是ReentrantLock自己定义的一个内部类)。另外在ReentrantLock内部还定义了另外两个类,分别是FairSync和NonFairSync,这两个类就是分别对应的锁公平分配和不公平分配的两个实现,它们都继承自Sync(类图已经清晰的描述出来了继承结构)。有关锁的分配和释放逻辑都是封装在了AQS里面的(AQS是AbstractQueuedSynchronized的简称,是JSR166规范中提出的一个基础的同步中心类或者说是同步框架,其在内部实现了大量的同步操作,而且用户还可以在此类的基础上自定义自己的同步类),可见Sync和AQS是锁机制实现的核心类(AQS详述见下文)。
ReentrantLock当中的部分实例代码:
1. 两个构造函数(可见默认使用的非公平锁的分配机制):
2. Lock方法的实现其实就是直接代理了Sync lock的实现:
3. TryLock方法也是一样的,都是代理自Sync
4. 解锁方法
Ps:说白了ReentrantLock就是基于Sync的,而Sync就是一种AQS,其中核心机制AQS都实现好了。
ReentrantReadWriteLock内部实现机制实现类图:
ReentrantReadWriteLock的类图和ReentrantLock的类图感觉是一摸一样的,唯一的区别就是Sync、FairSync、NonSync是ReentrantReadWriteLock自己定义的。因为ReentrantReadWriteLock要实现读写锁机制,所以这里的Sync和ReentrantLock的Sync肯定不会相同。其他的和ReentrantLock都是一样的,核心的实现都是基于AQS的子类Sync(AQS分析见下文)
部分示例代码如下:
1.构造函数(内部定义了ReadLock和WriteLock,默认也采用锁非公平分配的实现)
2. WriteLock当中的Lock方法:
Ps:上文简单的贴了两行代码主要为了说明一点,ReentrantLock和ReentrantReadWriteLock的实现是基于AQS的。下文再从源码角度分析一下具体实现。
Synchronized关键字:
简述:Synchronized实现的同步和上面提到的AQS的方式是不同的,AQS实现了一套自己的算法来实现共享资源的合理控制(具体算法实现,下文分析),而Synchronized实现的同步控制是基于java 内部的对象锁的。
Java内部对象锁:JVM中每个对象和类实际上都与一把锁与之相关联,对于对象来说,监视的是这个对象变量,对于类来说,监视的是类变量。当虚拟机装载类时,会创建一个Class类的实例,锁住的实际上是这个类对应的Class累的实例。对象锁是可重入的,也就是说一个对象或者类上的锁是可以累加的。
Ps:java中的同步是通过监视器模型来实现的,Java中的监视器实际上是一个代码块.
Synchronized实现分析:这么说还是有点抽象,那么从代码角度来分析一下Synchronized是怎么实现的。
(a) 先看看Synchronized代码快的方式:
SynchronizedTest1.java:
package test9;
/**
* @author jianying.wcj
* @date 2013–5–22
*/
public classSynchronizedTest1 {
public void sayHello(){
synchronized(this){
System.out.println(“hello world!”);
}
}
}
先用javac编译成.class 然后再用javap–verbose SynchronizedTest1 查看自己码的汇编码如下图所示:
简述:红色标记出来的是两条JVM命令,用来标识进入同步代码块,和退出同步代码块,由此可见Synchronized已经上升到JVM指令的级别和AQS的实现还是有很大差别的。上面这个是Synchronized代码块的形式,Synchronized还有另一种使用方式就是同步方法。
(b) Synchronized同步方法的方式:
SynchronizedTest2.java:
package test9;
/**
* @author jianying.wcj
* @date 2013–5–22
*/
public class SychronizedTest{
public synchronized void sayHello(){
System.out.println(“hello world!”);
}
}
同样通过javap命令查看汇编码如下:
简述:通过看这段汇编码,并没有发现JVM的同步块指令,可见同步方法和代码同步块采用的是不同的实现方式。同步方法的实现是JVM定义了方法的访问标志 ACC_SYNCHRONIZED 在方法表中,JVM后将同步方法前面设置这个标志,用于标识这个是一个同步方法。
3. Sync及AQS的核心实现(源码级别)
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源的设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
那么首先看一下CLH队列锁的数据结构及实现算法。
(a)CLH队列的数据结构(如图):
简述:CLH队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配的。具体构建队列的算法是这样的:
假设: 有共享资源S目前正被L3线程占用,此时有L1、L2线程分别对资源S进行lock操作以及获取锁后进行unlock操作。具体的流程如下:
(1)由于目前资源S被占用,所以将线程L1包装成一个CLH队列的Node,将这个Node的前驱(prev)指向当前对列里的队尾,放入队尾这个操作采用了CAS原语(原子操作)。如果当前的队尾为NULL,那么就建一个虚拟的Header,然后将T1线程挂载到虚拟Header下。核心代码如下:
Ps: addWaiter就是放入队列的操作。
Ps:采用CAS将节点加入到队尾,如果队尾为null进入enq操作。
Ps:创建了一个虚拟的Header
(2) L2线程请求资源S,那么它和L1线程一样将自己加入到队尾,L2的prev指向L1,L1.next指向L2(双向队列嘛)。
(3) 当L3释放资源即unlock的时候,唤醒与L3关联的下一个节点,同时释放当前节点。关键代码:
(b)每个结点类的属性及方法信息:
属性简述:CANCELLED:表示因为超时或者中断,结点被设置为取消状态,被取消的状态结点不应该去竞争锁。SIGNAL:表示这个结点的继任结点被阻塞了,因为等待某个条件而被阻塞。CONDITION:表示这个结点在队列中,因为等待某个条件而被阻塞。这几个是常量属性默认值为:
这几个常量用来设置waitStatus属性。
Thread属性表示关联到这个结点的线程。Prev和next就是关联前后结点的索引变量。NextWaiter 记录的是这个结点是独占式还是可共享的属性。
4. 几种锁的性能比较及使用场景(应用级别)
对于性能的对比这篇博客介绍的比较好:
http://blog.csdn.net/lantian0802/article/details/8948696