您的位置:首页 > 编程语言 > Java开发

Java中常用的锁分析总结

2017-05-10 23:30 274 查看
http://www.tuicool.com/articles/NnQjyq


Java中常用的锁分析总结

Java中常用的锁分析总结

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 ReentrantLock lock = 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

                                          public void 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 ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

       /**

        * 读锁

        */

       private ReadLock readLock = readWriteLock .readLock();

       /**

        * 写锁

        */

       private WriteLock writeLock = readWriteLock .writeLock();

       /**

        * 共享资源

        */

       private String shareData = " 寂寞等待中..." ;

       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();

              }

              return shareData ;

       }

       public static void main(String[] args) {

              final ReadWriteLockTest shareData = new ReadWriteLockTest();

              /**

               * 起50 条读线程

               */

              for ( int i = 0; i < 50; i++) {

                     new Thread( new Runnable() {

                            public void 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() {

                            public void 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 class SynchronizedTest1 {

    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

分享 
 
 

 收藏
  纠错





推荐文章

1. Java元编程及其应用

2. 查JVM参数就找JVMPocket(JVM口袋)小程序

3. 无视社区担忧,JPMS(Jigsaw)将被提交公开预览

4. 【MyBatis源码分析】insert方法、update方法、delete方法处理流程(..

5. Java常见面试题之Forward和Redirect的区别

6. Java String 探索

相关推刊

by
针尖上的舞者

《java》6

by
swqwer

《锁》3

by
Luxe
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: