Lock、Synchronized锁解析

  上篇博文在讲解 ConcurrentHashMap 时说到 1.7 中 put 方法实现同步的方式是使用继承了 ReentrantLock 类的 segment 内部类调用 lock 方法实现的,而在 1.8 中是使用 synchronied 锁住要添加数据对应数组的第一个值实现的,关于这两种锁的区别时留了一个坑,现在来补下。众所周知,在多线程下,对共享数据的操作需要格外小心,因为多线程下的各个线程执行的顺序是无法预料的,所以对一个共享数据的操作可能会产生不同的结果,这时我们就需要让线程对共享数据地操作按我们预期的方式去执行,得到预期的结果,实现这一方法就是使用锁来限制,当然也可以直接使用同步容器,就比如 ConcurrentHashMap、Vector等,但是这些容器同步的实现还是靠 lock 或 synchronized 锁。

 

监视器(monitor)

  在了解这两种锁之前,先要知道一个概念,"监视器"。

  监视器是操作系统实现同步的概念,一个监视器往往一个对象引用相关联,当一个监视器开始监视某一段代码时,其他的线程就需要拥有这个监视器对应的对象,监视器确认后才能让这个线程放行,继续执行后面的代码。可以说  java 中的 synchronized、Lock 锁这些就是监视器,是 "监视器" 这个概念的实现。

 

synchronized

  synchronized 是 java 的关键字,它可以修饰方法,代码块,下面先简单说一下它的用法。

用法

1、修饰实例方法  

 1   public synchronized void test() {
 2         if(sum<0) {
 3             n=false;
 4             return ;
 5         }
 6         try {
 7             Thread.sleep(200);
 8         } catch (InterruptedException e) {
 9             // TODO 自动生成的 catch 块
10             e.printStackTrace();
11         }
12         System.out.println(Thread.currentThread().getName()+sum--);
13     }

  这种方式锁住的是当前这个类的对象,如果两个线程创建了不同的对象,那么这个方法是锁不住的,只有这两个线程拥有同一个对象,然后拿这个对象作为钥匙抢夺CPU然后进入方法执行。

 1 public class safe1{
 2     public static void main(String[] args) {
 3         Ticket ticket = new Ticket(); 
 4         new Thread(ticket,"张三").start();    // 如果这里的 ticket 换成了 new Ticket(),那么就不能实现同步作用了
 5         new Thread(ticket,"李四").start();
 6         new Thread(ticket,"王五").start();
 7 
 8     }    
 9 
10 }
11 class Ticket implements Runnable{
12     private int sum = 100;
13     private boolean n = true;
14     
15     public synchronized void sale() {
16         if(sum < 0) {
17             n = false;
18             return ;
19         }
20         try {
21             Thread.sleep(200);
22         } catch (InterruptedException e) {
23             // TODO 自动生成的 catch 块
24             e.printStackTrace();
25         }
26         System.out.println(Thread.currentThread().getName()+sum--);
27     }
28     public void run() {
29         while(n) {
30             sale();
31         }
32         
33     }
34 
35 }

执行结果:

 

 上面这个例子是模拟抢票功能,可以看到三个线程在拥有同一个类对象时会实现同步,那么如果把 “张三” 线程对象换成新 new 的对象,结果会怎样呢?

 可以看到关于李四的票就会出现票数混乱,数据不能同步。

 

2、修饰类方法(静态方法)

  还是拿上面购票的例子来讲解,如果 synchronized 修饰静态方法,那么锁住的就是当前类,也就是 class 信息,因为 class 信息是在当前类加载时就被加载到方法区的,不同的对象都会拥有同一个该对象的类信息,所以在多线程下即使是不同对象,最后的结果也能实现同步

 1 public class safe1{
 2     public static void main(String[] args) {
 3         new Thread(new Ticket(),"张三").start();    
 4         new Thread(new Ticket(),"李四").start();
 5         new Thread(new Ticket(),"王五").start();
 6 
 7     }    
 8 
 9 }
10 class Ticket implements Runnable{
11     private static int sum = 100;
12     private static boolean n = true;
13     
14     public synchronized static void sale() {
15         if(sum < 0) {
16             n = false;
17             return ;
18         }
19         try {
20             Thread.sleep(200);
21         } catch (InterruptedException e) {
22             // TODO 自动生成的 catch 块
23             e.printStackTrace();
24         }
25         System.out.println(Thread.currentThread().getName()+sum--);
26     }
27     public void run() {
28         while(n) {
29             sale();
30         }
31         
32     }
33 
34 }

结果:

 

3、修饰代码块

  这个其实和前两种方式差不多,只不过它修饰的变成了某一段代码块,而前面两种修饰的是整个方法的代码块,并且修饰代码块可以自定义 “钥匙” ,这样使得实现更加灵活,所以一般是推荐使用 synchronized 修饰代码块实现线程同步的。同样还是以上面购票为例

 1 public class safe1{
 2     public static void main(String[] args) {
 3         new Thread(new Ticket(),"张三").start();    
 4         new Thread(new Ticket(),"李四").start();
 5         new Thread(new Ticket(),"王五").start();
 6 
 7     }    
 8 
 9 }
10 class Ticket implements Runnable{
11     private static int sum = 100;
12     private static boolean n = true;
13     
14     public void sale() {
15         synchronized (Ticket.class) {
16             if(sum < 0) {
17                 n = false;
18                 return ;
19             }
20             try {
21                 Thread.sleep(200);
22             } catch (InterruptedException e) {
23                 // TODO 自动生成的 catch 块
24                 e.printStackTrace();
25             }
26             System.out.println(Thread.currentThread().getName()+sum--);
27         }
28     }
29     public void run() {
30         while(n) {
31             sale();
32         }
33         
34     }
35 
36 }

可以看到这次是在实例方法里,如果修饰在方法上锁住的就是当前类对象,不同线程必须拥有同一个对象才能实现同步,而在这个例子里 synchronized 锁住的是 .class 类信息,所以最后还是能实现同步

 

原理

  因为 synchronized 是关键字,没有具体的类实现,所以我们只能在指令集上查看,先上代码

    public void aa(){
        synchronized (this){
            int gg = 4;
        }
    }

    public void bb(){
        int gg = 4;
    }

在使用 jclass Bytecode viewer 工具将编译后的 class 文件转成可视化的指令集后,可以看到指令集如下:

  aa 方法:

  

   bb 方法:

  

  异常表:

  

   可以看到在 aa 方法中多了一些指令,其中比较重要的就是 "monitorenter"、"monitorexit"。这两个指令对应的着 "解锁" 和 "加锁" ,也就是线程 "获取到锁" 以及代码执行完成后的 "释放锁"。"monitor" 就对应着文章开头说的监视器,因为 synchronized 就是 java 中的 "监视器"。指令中有一次 "monitorenter" 代表线程得到 CPU 调度(也叫做线程得到了锁),但是指令里却有两次 "monitorexit",这是为了防止线程发生异常没有执行 第一次的 "monitorexit",而导致其他线程永远无法得到线程,所以在4-8指令之间或者11-14指令之间发生任何异常都会去执行从11指令开始执行,执行到第二个 "monitorexit"。 

 

Synchronized 锁升级机制

  在 JDK 早期的版本,synchronized 锁的效率是非常低的,它的效率远低于 lock 锁,但是 sychronized 毕竟是 java 的关键词,它不应该就此淘汰。所以在 JDK1.6 中对它进行优化,其实优化内容不仅仅是与 synchronized 有关的,还有 "自适应自旋锁"、"锁消除"、"锁粗化"  等。关于 synchronized 的优化就是它的升级机制。synchronized 也因为这个优化效率变得能和 Lock 锁效率不相上下。

 

  1.6 之前的 synchronized 都是 "重量级锁",什么是重量级锁呢?就是一个线程在获取到 CPU 调度后,开始执行 synchronized 修饰的代码块,这时其他执行到这里的线程必须进行一次 "上下文切换"(下面有解释)(其实在进行上下文切换前会先尝试获取锁资源,失败才会进行"上下文切换",这是非公平锁的特性,下面 Lock 部分有讲解,这里比较的是 synchronized 效率问题,所以忽略一开始就抢夺到锁资源的情况)和 "加锁 "、"解锁" 操作。这就是 "重量级锁",这样的锁有严重的弊端。"上下文切换" 和 "加锁"、 "解锁" 这些动作虽然在单线程下消耗的时间并不算多,但是在一些高并发场景,例如百万、千万并发的场景,那么这些动作消耗的总时间就比较大了;另外一种情况就是某段代码可能发生多个线程抢占执行的情况,但是实际并没有发生这种情况,都是一个线程执行完后下一个线程才执行到这段代码,这样 "加锁"、解锁" 消耗的时间就浪费了。那么有什么方法去解决这个问题呢,这就是锁升级机制带来的好处。

上下文切换:线程之间的切换需要前一个线程先保存当前的状态,然后进入 "睡眠" 状态,然后下一个线程 "启动",执行,等到下一次前一个线程获取到 CPU 调度时,再去读取上次保存的状态,然后 "启动"。我们把一个线程从保存当前状态到下一次"启动"完成称作这个线程的一次 "上下文切换"。

 

  synchronized 锁升级机制是从 偏向锁->轻量级锁->重量级锁 ,这个过程是不可逆的。

  在具体说这三种锁时,先要了解对象头的 Mark Word 部分,我们都知道对象上存储着这个对象的一切信息,包括它的地址、内部方法、属性等信息,前面说过监视器,就是一个锁对应着一个对象,所以在对象上也存储着这个对象所关联锁的信息。关于锁的信息就存储在对象对象头的 Mark Word 部分上。下面是 Mark Word 结构示意图:

下面说得偏向锁、轻量级锁、重量级锁都会用到这上面的字段。 

1、偏向锁

  

  首先是偏向锁,偏向锁是指一段代码同一时间内只有一个线程执行(这是在开启了重偏向,如果没有开启重偏向则是一段代码一直只有一个线程执行)。当不满足条件时就会升级成轻量级锁。偏向锁的执行逻辑是:

1、判断 对象头的 Mark Word 部分的锁标志位,01表示为偏向锁,00轻量级锁,10重量级锁

2、判断是否偏向锁

  1、0,升级为轻量级锁,然后执行相关策略

  2、1,检查线程ID位是否是当前线程ID。

    1、是,获得偏向锁,执行代码

    2、否,尝试进行CAS写入当前线程ID

      1、成功。获得锁,执行

      2、失败。说明已经存在线程ID了,会在安全的时间点暂停当前持有该偏向锁的线程,然后判断该线程是否存活

        1、存活,判断该线程是否正在执行锁住的代码

          1、正在执行,升级为轻量级锁,然后执行轻量级锁的相关策略(为该线程的栈中开启一片区域来保存复制的 mark work 记作 lock record,然后将锁对应的对象对象头的 mark word 部分的指针指向该线程,然后唤醒该线程继续执行,在此期间当前线程也会在栈中拷贝一份 mark word然后使用自旋锁+CAS乐观锁尝试将该对象的 mark word 指针指向当前的 lock record,执行完轻量级锁后 mark word 指针会删除,以便后面的线程重新指向)

          2、没有执行。检查是否开启重偏向。

            1、开启了,先设置为匿名偏向状态,然后将 mark word 的 threadId 写入当前的线程ID位置,然后再唤醒线程,继续执行

            2、没有开启,先撤销偏向锁,将 mark word 设置为无锁状态,然后升级轻量级锁,执行轻量级锁的执行策略

        2、没有存活,检查是否开启重偏向。

 

从上面的执行策略来看,偏向锁下是没有加锁、释放锁的操作的,这样就加快了对某段一段时间内只有一个线程执行的代码的执行效率。上面还提到自旋锁,乐观锁。简单说一下。

自旋锁:由于线程切换需要进行 "上下文切换",这个过程一次两次可能不算耗时,但是在多线程下,特别是在高并发场景下大量线程频繁地进行线程切换,就会出现大量的 "上下文切换",这中间消耗的时间是非常长的,所以对于这部分代码就使用 "自旋锁",它的特点是不会保存当前线程状态,也不会进入 "睡眠状态",而是一直尝试获取 CPU 调度,保持一种 "运行" 状态,这样就省去了 "上下文切换" 的时间,当然,这只适用于多核 CPU ,单核 CPU 是不能发挥 "自旋锁" 的作用的,因为它在一直尝试,这个尝试的过程也会占用 CPU 。

乐观锁先保存一个参考数据,然后修改当前线程空间的变量,然后准备更新到主内存中去,在更新之前检查主内存对应的参考数据是否与之前保存的参考数据一致,如果一致更新到主内存,如果不一样那么此次更新作废。

 

2、轻量级锁

 

  轻量级锁适用于线程数量少且执行时间短的代码块。在线程还未得到CPU调度时,首先会在该线程的栈中开启一块区域作为lock record,然后将对象头的 Mark Word 部分拷贝到 lock record 位置,然后尝试将对象对象头 Mark Word 轻量级锁部分的指向栈的指针指向自己线程的lock record,如果成功就表明该线程得到了锁,CPU就会调度。详细的执行过程是:

  1.如果这个对象锁是刚刚升级到轻量级锁且锁对应对象的mark word的偏向锁部分存储的 threadId 对应的线程没有执行完当前对应的代码,那么系统就会先将CPU交给 threadId 对应的线程,让他先执行完。过程就是先在该线程的栈中开启一块区域作为lock record,然后将mark word拷贝到 lock record,再将轻量级锁部分的指针指向 lock record。随后开始执行锁修饰的代码块,执行完毕后会进行两次检查:1.对象头的Mark Word中锁记录指针是否还是指向当前线程的lock record部分  2.lock record是否还与对象头的Mark Word一致。如果一致,就释放锁资源。如果不一致就将锁升级为重量级锁,然后释放。

  2.如果是普通的线程,那么首先还是在当前线程的栈中开启一块区域作为lock record,然后将对象头的 Mark Word 部分拷贝到 lock record 位置,然后尝试将对象对象头 Mark Word 轻量级锁部分的指向栈的指针指向自己线程的lock record,

    1.如果成功,就继续执行后面代码,

    2.如果失败就以自旋锁方式继续尝试,

      1.如果一定次数还是没有获取到锁,那么就将锁膨胀为重量级锁。

      2.如果成功执行锁修饰的代码,执行完会再进行两个检查,如果符合就释放锁。不符合就膨胀成重量级锁,然后再释放。

 

3、重量级锁

  重量级锁前面也说过了,就是一个线程在执行时,其他线程就先保存当前线程状态,然后进入 "休眠" 状态,乖乖等待CPU分配,得到CPU后才会读取上一次保存的状态,然后继续执行。它的执行逻辑还是先判断Mark Word部分的锁标志位,是10就说明是重量级锁,然后先来的尝试获取,得到CPU,继续执行,后面的线程就需要等待进行一次上下文切换。

 

总结:

  正是因为 synchronized 锁升级机制的存在,使得 synchronized 的效率不再那么低。

 

等待-通知模型

   在一个线程执行 synchronized 修饰的代码块时,其他线程并不是必须等到该线程执行完才可能得到 CPU 调度,对于某些业务场景,需要我们在一段代码中进行线程地来回切换。这就需要说到 "等待-通知" 模型了,在说这个模型前,要先了解 Object 的 wait() 方法

1 public final void wait() throws InterruptedException {
2         wait(0);
3 }
4 
5 
6 
7 
8 
9 public final native void wait(long timeout) throws InterruptedException;

可以看到 wait(long timeout) 是使用 native 修饰的,是一个本地方法,用 C、C++实现的,这个方法作用就是让当前线程释放掉这个对象资源,进入 "休眠",让其他线程去进行对象资源的争夺、执行。与其对应的就是 notify() 方法,它也是 Object 类的方法,这个方法会随机让该对象对应锁的一个 "休眠" 的线程 "苏醒",然后参与CPU的竞争中。还有一个 notifyAll() 是让对象对应锁的所有 "休眠" 的线程 "苏醒"。下面来看一个例子

 1 public class CoTest01 {
 2     public static void main(String[] args) {
 3         SynContainer container = new SynContainer();
 4         new Productor(container).start();
 5         new Consumer(container).start();
 6     }
 7 }
 8 //生产者
 9 class Productor extends Thread{
10     SynContainer container  ;    
11     public Productor(SynContainer container) {
12         this.container = container;
13     }
14 
15     public void run() {
16         //生产
17 //        synchronized (container) {
18             for(int i=0;i<100;i++) {
19                 container.push(new Steamedbun(i));
20             }
21 //        }
22     }
23 }
24 //消费者
25 class Consumer extends Thread{
26     SynContainer container  ;    
27     public Consumer(SynContainer container) {
28         this.container = container;
29     }
30     public void run() {
31 //        synchronized (container) {
32             //消费
33             for(int i=0;i<100;i++) {
34                 container.pop();
35             }
36 //        }
37     }
38 }
39 //缓冲区
40 class SynContainer{
41     List<Steamedbun> list=new ArrayList<>(); //存储容器
42     //存储 生产
43     public synchronized void push(Steamedbun bun) {
44         //何时能生产  容器存在空间
45         //不能生产 只有等待    需要注意,这里的wait在范围内判断调用时在并发量小的情况下使用if不会有问题,但是在高并发场景就需要使用while来代替,负责有线程安全问题
46         while(list.size() == 10) {
47             try {
48                 this.wait(); //线程阻塞  消费者通知生产解除
49             } catch (InterruptedException e) {
50                 System.out.println("push 异常");
51             }
52         }
53         //存在空间 可以生产
54         list.add(bun);
55         //存在数据了,可以通知消费了
56         this.notifyAll();
57         System.out.println("生产-->"+list.size()+"个馒头");
58     }
59     //获取 消费
60     public synchronized void pop() {
61         //何时消费 容器中是否存在数据
62         //没有数据 只有等待
63         while(list.size() == 0) {
64             try {
65                 this.wait(); //线程阻塞  生产者通知消费解除
66             } catch (InterruptedException e) {
67             }
68         }
69         //存在数据可以消费
70         list.remove(list.size()-1);
71         this.notifyAll(); //存在空间了,可以唤醒对方生产了
72         System.out.println("消费第" + list.size() + "个馒头");
73     }
74 }
75 //馒头
76 class Steamedbun{
77     int id;
78     public Steamedbun(int id) {
79         this.id = id;
80     }
81     
82 }

上面这个例子就是典型的 "等待-通知" 模型。通过 wait() 和 notifyAll() 来控制线程之间的切换。

执行结果:

 

 

Lock

   lock 是 java 核心类库中的一个接口,它是 jdk 维护的实现同步的一个接口,常用的实现类是 RenntrantLock、ReadWriteLock 等。相比于 synchronized,它更灵活,效率也更高(jdk6之前)。下面就先以 ReeentrantLock 为例,来说一下Lock 接口中常用的构造器和实现方法。在说构造器前先要了解什么是公平锁,非公平锁。它不能修饰方法,只能修饰代码块。

 

公平锁

  公平锁指的是一个线程在执行到一段锁包裹的代码前,发现已经有其他线程到了,并且已经排成了一个 "等待队伍" (这个属于AQS定义的线程执行规则,会单独开一篇AQS的博客中说),排队等待CPU调度,那么公平锁的策略是直接加入这个 "等待队伍" 的最后面,保证锁资源的公平性分配。

 

非公平锁  

  非公平锁指的是在遇到 "等待队列" 时,会先尝试获取锁资源,如果获取到直接 "插队" 执行,如果没有获取到就乖乖到最后面 "排队"。虽然这种锁看起来不 "文明",但是它的总体效率是比 "公平锁" 要高的,如果在 "队头" 的线程发生异常停止运行,那么后面的线程就需要一直等待影响效率。但是这样也会导致优先级低的线程在和高优先级的线程竞争时一直没有获取到CPU,从而一直无法执行造成 "活锁"。所以总结下来就是非公平锁会使系统整体的效率提高但是可能会导致 "活锁" 的情况发生。 上面的 synchronized 就是非公平锁。

构造器 

   /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

  可以看到, ReetrantLock 有两个构造方法,这也是Lock 实现类通有的,我们重点看一下第二种构造器,因为第一种的实现代码只不过是第二种构造器中代码的一种。参数 fair 表示是否是公平锁?如果是 true ,那么创建的 ReetrantLock 对象就是公平锁对象,如果 false 或者没有指定参数那么创建出来的对象都是非公平锁对象。而 synchronized 只可能是非公平锁。

 

常用方法:

  下面就以ReentranLock 为例,说一下lock的常用实现方法。

1、Lock()

    lock() 方法其实和 synchronized 的重量级锁执行策略是一样的,当然如果在对象创建时指定公平锁,那么会直接进入 “等待队列” ,如果没有指定或者是指定为非公平锁那么会先尝试获取锁资源。然后没有获取到就会进行一次上下文切换。

 2、tryLock()

    尝试获取锁资源,获取到就直接执行后面的代码并返回 true,如果没有获取到直接退出不进入 "等待队列" 并返回 false 。

3、tryLock(long time,TimeUnit unit)

    尝试在一段时间内获取锁资源,获取到就执行后面的代码并返回 true , 否则退出返回 false 。可以通过 interrupt() 方法中断阻塞状态。

4、lockInterruptibly()

    和 lock()一样,只不过可以调用该线程的 interrupt() 方法去中断,而 Lock() 方法不会被中断,只能获取到锁资源的线程调用了 unlock方法才会中断等待状态。

 

同步,等待通知模型实现

  同步实现:lock 的加锁的对象就是 lock 本身的对象,所以我们只需要调用 lock()方法就可以实现加锁操作,但是与 synchronized 不同的是 lock 锁需要手动的去释放,也就是调用 unlock() 方法去释放当前对象的锁,所以unlock()方法一般是在 finally 修饰的代码块中,防止上面发生异常而没有释放锁导致死锁。

  等待-通知模式实现:lock 的 "等待-通知模式" 是通过 Condition 类实现的,调用 lock 对象的 newCondition() 方法去创建与之对应的 Condition 对象,然后调用 Condition 对象的 await 方法阻塞当前线程并释放资源给其他线程,等到其他线程执行完再调用 Condition 的 signal() 方法(对应Object中的 notify()方法)去随机唤醒一个阻塞的线程,而 signalAll()(对应notifyAll())则是唤醒 lock 对象对应的所有阻塞线程。 

下面就用一个例子来实现。

 1 public class lock2{
 2     
 3     static lock2 ll=new lock2();
 4     ReentrantLock lock=new ReentrantLock();
 5     Condition cc=lock.newCondition();
 6     
 7     public static void main(String[] args) {
 8         new Thread(new qq()).start();
 9         new Thread(new qq2()).start();
10     }
11     public static class qq implements Runnable{
12 
13         @Override
14         public void run() {
15             ll.aa();
16         }
17         
18     }
19     public static class qq2 implements Runnable{
20         
21         @Override
22         public void run() {
23             ll.bb();
24         }
25         
26     }
27     
28     public void aa() {
29         lock.lock();
30         try {
31             System.out.println("aa方法开始了"+Thread.currentThread().getName());
32             Thread.sleep(2000);
33             cc.await();
34             System.out.println("aa方法结束了"+Thread.currentThread().getName());
35         } catch (InterruptedException e) {
36             e.printStackTrace();
37         }finally {
38             lock.unlock();
39         }
40     }
41     public void bb() {
42         lock.lock();
43         try {
44             System.out.println("bb方法开始了"+Thread.currentThread().getName());
45             Thread.sleep(2000);
46             System.out.println("bb方法结束了"+Thread.currentThread().getName());
47             cc.signal();
48         } catch (InterruptedException e) {
49             e.printStackTrace();
50         }finally {
51             lock.unlock();
52         }
53     }
54     
55 }

结果:

 在 "aa方法开始了Thread-0" 输出后,等待了两秒后,通过 await 方法阻塞当前线程,然后把锁资源让给 "Thread-1",输出 "bb方法开始了Thread-1" ,两秒后再输出 "bb方法结束了Thread-1",然后通过signal唤醒,因为这里只有一个 "Thread-0" 线程阻塞所以直接唤醒 "Thread-0",最后输出 "aa方法结束了Thread-0" 执行完毕。

同时可以看到,Lock 的唤醒和阻塞是通过 Condition 对象实现的,而 Condition 对象是通过 Lock 对象调用方法生成的,也就是一个 Lock 对象可以生成多个 Condition 对象,所以我们可以指定具体的某一个 Condition 对象对应的线程进行阻塞和唤醒,而 wait 只能随机唤醒某一个,不能实现精准的唤醒。 

 

ReentrantReadWriteLock

  ReentrantReadWriteLock 是一种特殊的 Lock实现类,它除了可以实现上面提到的所有功能外,还能实现 "共享锁" 和 "排他锁" 。它的读锁就是 "共享锁" ,写锁是 "排他锁" 。

  

共享锁

    共享锁就是不同线程可以同时执行,相当于没有加锁。那么问题来了,既然多线程可以同时获取共享锁,那么共享锁的意义是什么呢?答案就是为了和 "排他锁" 互斥。下面先看 ReentrantReadWriteLock 共享锁的例子。

 1 public class RWLRTest {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final RWLRTest test = new RWLRTest();
 6         new Thread(){
 7             public void run() {
 8                 test.get(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 test.get(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void get(Thread thread) {
18         rwl.readLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在进行读操作");
23             }
24             System.out.println(thread.getName()+"读操作完毕");
25         } finally {
26             rwl.readLock().unlock();
27         }
28     }
29 }

结果:

 可以看到,在调用 readLock的lock()方法后,两个线程依然能交叉执行,这就是共享锁的特点

排它锁

  排它锁就是我们常见的锁,同一时间锁资源只能被一个线程所占用,它和 "共享锁" 是互斥关系。下面还是以上面的代码改成 "写锁"试试。

 1 public class RWLWTest {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final RWLWTest test = new RWLRTest();
 6         new Thread(){
 7             public void run() {
 8                 test.get(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 test.get(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void get(Thread thread) {
18         rwl.writeLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在进行写操作");
23             }
24             System.out.println(thread.getName()+"写操作完毕");
25         } finally {
26             rwl.writeLock().unlock();
27         }
28     }
29 }

结果:

可以看到,两个线程是互斥关系。

 

  为了严谨,再比较一下 "读锁" 与 "写锁" 的互斥关系

 1 public class RWLTest {
 2     private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
 3     
 4     public static void main(String[] args)  {
 5         final RWLTest rwlTest = new RWLTest();
 6         new Thread(){
 7             public void run() {
 8                 rwlTest.getWrite(Thread.currentThread());
 9             };
10         }.start();
11         new Thread(){
12             public void run() {
13                 rwlTest.getRead(Thread.currentThread());
14             };
15         }.start();
16     }  
17     public void getWrite(Thread thread) {
18         rwl.writeLock().lock();
19         try {
20             long start = System.currentTimeMillis();
21             while(System.currentTimeMillis() - start <= 1) {
22                 System.out.println(thread.getName()+"正在进行写操作");
23             }
24             System.out.println(thread.getName()+"写操作完毕");
25         } finally {
26             rwl.writeLock().unlock();
27         }
28     }
29     public void getRead(Thread thread) {
30         rwl.readLock().lock();
31         try {
32             long start = System.currentTimeMillis();
33             while(System.currentTimeMillis() - start <= 1) {
34                 System.out.println(thread.getName()+"正在进行读操作");
35             }
36             System.out.println(thread.getName()+"读操作完毕");
37         } finally {
38             rwl.readLock().unlock();
39         }
40     }
41 }

 结果:   

 

总结  

  Lock锁synchronized锁区别

  1. Lock接口实现的类锁是核心类库中的代码,是Java编写的;synchronized是关键字,属于JVM,也就是Java原生的,使用其他语言实现。
  2. Lock实现类锁有更多方法,比如可以选择是公平锁还是非公平锁;一段时间获取不到资源可以退出等待队列;以及共享锁排它锁;而后者功能就比较单一了。
  3. synchronized 可以修饰静态方法、实例方法、代码块;Lock 实现的锁只能修饰代码块
  4. synchronized不需要释放锁,Lock锁需要手动释放。
  5. Lock可以实现精准唤醒,synchronized不可以。

  相同点:

  1. 都可以修饰代码块
  2. 都是可重入锁
  3. 效率差不多(jdk1.6优化以后)

 

posted on 2020-10-17 15:57  萌新J  阅读(804)  评论(0编辑  收藏  举报