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 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

发布了51 篇原创文章 · 获赞 31 · 访问量 14万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览