Java多线程 Lock接口,ReentranctLock,ReentrantReadWriteLock

Posted 小灯笼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程 Lock接口,ReentranctLock,ReentrantReadWriteLock相关的知识,希望对你有一定的参考价值。

  在JDK5里面,提供了一个Lock接口。该接口通过底层框架的形式为设计更面向对象、可更加细粒度控制线程代码、更灵活控制线程通信提供了基础。实现Lock接口且使用得比较多的是可重入锁(ReentrantLock)以及读写锁(ReentrantReadWriteLock)。

1. ReentrantLock

  在Java多线程(二) 多线程的锁机制 里面,已经总结过通过使用Synchronized关键字实现线程内的方法锁定。但使用Synchronized关键字有一些局限性,上锁和释放锁是由JVM决定的,用户没法上锁和释放进行控制。那么问题就来了:假如有一个线程业务类管理某一全局变量的读和写。对于每条线程,在读的时候数据是共享的可以让多个线程同时去读。但有某个线程在对该全局变量进行写的时候,其他的线程都不能够对变量进行读或者写(对应数据库内的读共享写互斥)。可能会有如下伪代码:

 1 package com.scl.thread.lock;
 2 
 3 public class MyCounter
 4 {
 5     public int count;
 6 
 7     public int readCount()
 8     {
 9         return this.count;
10     }
11 
12     public void writeCount()
13     {
14         synchronized(this)
15         {
16             count++;
17         }
18     }
19 }

  尽管对写操作进行了空值,但是在写的时候,线程还是能够进行读操作!由此,JDK5并发库内提供了Lock接口。程序员可以通过实现Lock接口对代码块进行更灵活的锁控制.

 JDK5通过使用AbstractQueuedSynchronizer(简写为AQS)抽象类把Lock接口的功能实现了一大部分功能,如果程序员需要编写一套跟有自身逻辑的"锁"时,可以简单地通过实现public boolean tryAcquire(int acquire) 及 public boolean tryRelease(int releases) 进行加锁及释放锁功能。AQS为整个并发内容的核心框架,类似synchronized的锁(ReentrantLock :可重入锁)就是使用了AQS框架进行构建。ReentrantLock提供了一个可中断、拥有并发竞争机制[指线程对锁的竞争方式:公平竞争或不公平竞争]的方式,该部分的内容的源码分析可以查看: ReentrantLock 实现原理深入探究

        正如ReentrantLock跟Synchronized关键字所使用的功能基本一样,而且Synchronized还能自己释放锁,那什么时候使用ReentrantLock?

  ① 在中断线程的时候,可以使用ReentrantLock进行控制

    如线程1有一个耗时很大的任务在执行,执行时线程2必须进行等待。当线程1执行的任务时间实在太长了,线程2放弃等待进行线程后续的操作。该情况下如果使用Synchronized,只能通过抛出异常的形式进行异常操作。

  ② 多条件变量通讯

    如有3条线程,线程1完成任务后通知线程2执行,线程2执行完业务逻辑以后通知线程3执行,线程3执行完通知线程1继续执行。用Synchronized关键字很难处理这种问题。用Lock却可以很好的处理这些内容。当然,线程1 、2、3 同样地可以换由一个线程组去执行这些任务。  

   1.1  可中断的线程控制

       1.1.1  Java的线程中断机制

  Java中断线程可以通过实例方法: stop 或 interrupt 进行线程中断,两者有什么区别?先查看以下两段代码及运行结果。

  1 package com.scl.thread.interrupt;
  2 
  3 public class TestInterrupt
  4 {
  5     // 各线程可见的线程状态标志位
  6     public static volatile boolean isStop = false;
  7 
  8     public static void main(String[] args) throws InterruptedException
  9     {
 10         // 创建三条线程,线程1使用stop方法中断,线程2使用interrupt方法中断,线程3与线程2比较使用了interrupt后是否因中断退出
 11         Thread th1 = new Thread(new SubThread1(), "SubThread1");
 12         Thread th2 = new Thread(new SubThread2(), "SubThread2");
 13         Thread th3 = new Thread(new SubThread3(), "SubThread3");
 14 
 15         System.out.println("==============subThread1 code block result==============");
 16         System.out.println("Main Thread call subThread1 to start");
 17         th1.start();
 18         Thread.sleep(3000);
 19         System.out.println("Main Thread start to stop subThread1");
 20         th1.stop();
 21         System.out.println("subThread1 was stopped by Main Thread");
 22         // 等待子线程进行stop,让子线程有充分时间处理相关业务
 23         Thread.sleep(20);
 24         System.out.println("===================================================");
 25         Thread.sleep(20);
 26 
 27         System.out.println("==============subThread2 code block result==============");
 28         System.out.println("Main Thread call subThread2 to start");
 29         th2.start();
 30         Thread.sleep(3000);
 31         System.out.println("Main Thread start to interrupt subThread2");
 32         // 设置标志位,令子线程2可以按顺序退出
 33         isStop = true;
 34         th2.interrupt();
 35         // 等待子线程进行interrupt,让子线程有充分时间处理相关业务
 36         Thread.sleep(20);
 37         System.out.println(" subThread2 was interruptted by Main Thread");
 38         System.out.println("===================================================");
 39         Thread.sleep(20);
 40 
 41         System.out.println("==============subThread3 code block result==============");
 42         System.out.println("Main Thread call subThread3 to start");
 43         th3.start();
 44         Thread.sleep(3000);
 45         System.out.println("Main Thread start to interrupt subThread3");
 46         th2.interrupt();
 47         // 等待子线程进行interrupt,让子线程有充分时间处理相关业务
 48         Thread.sleep(20);
 49         System.out.println("subThread3 was interrupted by Main Thread");
 50         System.out.println("===================================================");
 51         Thread.sleep(20);
 52 
 53         System.out.println("Main Thread end");
 54     }
 55 }
 56 
 57 class SubThread1 implements Runnable
 58 {
 59     @Override
 60     public void run()
 61     {
 62         while (!TestInterrupt.isStop)
 63         {
 64             try
 65             {
 66                 // 子线程1进行睡眠
 67                 Thread.sleep(2000);
 68             }
 69             catch (InterruptedException e)
 70             {
 71                 e.printStackTrace();
 72             }
 73 
 74             System.out.println(Thread.currentThread().getName() + " is running...");
 75         }
 76         // 调用stop方法,该语句不会被执行,因为线程整个退出了
 77         System.out.println(Thread.currentThread().getName() + " is ready to cancle");
 78     }
 79 }
 80 
 81 class SubThread2 implements Runnable
 82 {
 83     @Override
 84     public void run()
 85     {
 86         while (!TestInterrupt.isStop)
 87         {
 88             try
 89             {
 90                 // 子线程1进行睡眠
 91                 Thread.sleep(200);
 92             }
 93             catch (InterruptedException e)
 94             {
 95                 e.printStackTrace();
 96             }
 97 
 98             System.out.println(Thread.currentThread().getName() + " is running...");
 99         }
100         // 使用interrupt方法,在发现线程2被阻塞或休眠(sleep)的情况下,会收到一个interrupt的异常。但不会终止线程,仅设置线程是否可以中断的标志位
101         System.out.println(Thread.currentThread().getName() + " is ready to cancle");
102     }
103 }
104 
105 // 调用interrupt方法,对比子线程2,发现使用interrupt方法根本没有中断整个线程,设置后线程也没有进行退出。一直运行
106 class SubThread3 implements Runnable
107 {
108     @Override
109     public void run()
110     {
111         // 使用true代替标志位,判断调用interrupt方法后是否正常中断线程
112         while (true)
113         {
114             try
115             {
116                 Thread.sleep(2000);
117             }
118             catch (InterruptedException e)
119             {
120                 e.printStackTrace();
121             }
122             System.out.println(Thread.currentThread().getName() + " is running...");
123         }
124     }
125 }
stop方法及interrupt方法对比
 1 ==============subThread1 code block result==============
 2 Main Thread call subThread1 to start
 3 SubThread1 is running...
 4 Main Thread start to stop subThread1
 5 subThread1 was stopped by Main Thread
 6 ===================================================
 7 ==============subThread2 code block result==============
 8 Main Thread call subThread2 to start
 9 SubThread2 is running...
10 SubThread2 is running...
11 SubThread2 is running...
12 SubThread2 is running...
13 SubThread2 is running...
14 SubThread2 is running...
15 SubThread2 is running...
16 SubThread2 is running...
17 SubThread2 is running...
18 SubThread2 is running...
19 SubThread2 is running...
20 SubThread2 is running...
21 SubThread2 is running...
22 SubThread2 is running...
23 Main Thread start to interrupt subThread2
24 SubThread2 is running...
25 SubThread2 is ready to cancle
26 java.lang.InterruptedException: sleep interrupted
27     at java.lang.Thread.sleep(Native Method)
28     at com.scl.thread.interrupt.SubThread2.run(TestInterrupt.java:91)
29     at java.lang.Thread.run(Thread.java:744)
30  subThread2 was interruptted by Main Thread
31 ===================================================
32 ==============subThread3 code block result==============
33 Main Thread call subThread3 to start
34 SubThread3 is running...
35 Main Thread start to interrupt subThread3
36 subThread3 was interrupted by Main Thread
37 ===================================================
38 Main Thread end
39 SubThread3 is running...
40 SubThread3 is running...
运行结果

  从代码运行结果上面,可以看到两个函数的差异:stop把线程给结束了,而interrupt方法没有结束线程,因此两者总结如下:

  ① 在线程实例调用stop 方法后把线程给终结了,在子线程1内while以外的代码块将不会被执行

  ② 在线程实例调用interrupt 方法后,线程没有被终结且该方法通过线程间协作的关系,可以有序地退出线程。且实例线程在被中断后,若线程被阻塞或者休眠的情况下,将收到一个InterruptedException。

  ③ 子线程3的运行结果充分证实了线程实例调用interrupt方法根本没有把线程给中断,只是把线程的标志状态进行更改。让线程实例在适当的时机进行退出。

    由此可见public void interrupt( )并非马上对线程进行中断(强行结束线程),而是通过协作的方法把线程的状态设置为可中断告知阻塞中的线程在特定的时刻可以对进行终结。同时,Java提供了两个检验线程中断方法① 实例方法 public boolean isInterrupted() ②静态方法 public static boolean interrupted();

     两个方法的区别是:静态方法会去清理线程状态信息。实例方法不会清理状态标志。即当线程实例调用interrupt 的前提下,若再调用静态方法,第一次会返回true,后面全返回false;若在前提下,调用实例方法isInterrupt,如果线程正常退出,会一直返回false [判断线程Thread对象是否已经是终止状态,与线程状态无关]。静态方法偏向于判断线程状态。而实例方法更关心线程是否存活。

  需要特别注意的是当线程调用interrupt方法时,假如线程在等待锁或者被休眠了。中断状态会被设置为false。JDK的API里面明确指出了这一点。

下面开始查看Lock与synchronized关键字与ReentrantLock的一些区别。

  1.1.2 ReentrantLock对线程中断的控制

        首先,单纯地使用synchronized关键字不能进行锁中断控制. 在synchronized关键字控制的代码块内,不会因为线程中断而做出相关处理。

 先查看使用synchronized关键字在处理线程中断时的结果。

业务逻辑主要为:开辟两条线程,一条线程对文件进行读操作,另一条线程对文件进行写操作。写操作内容需要时间较长,且先执行。读操作后执行,若读线程等待超过4秒。让读线程中断,进行格式化文件。

① 使用接口,区分使用synchronized关键字及Lock方式控制线程中断的业务逻辑

 1 package com.scl.thread.interrupt;
 2 
 3 //文件读写接口,使用Synchronized关键字控制线程中断以及使用Lock控制线程中断都实现该接口
 4 public interface IFileHandler
 5 {
 6     boolean isGetReadLock = false;
 7 
 8     void read();
 9 
10     void write();
11 
12     void formatFile();
13 }
控制文件接口

② 在synchronized关键字控制代码块的前提下,对线程进行中断的业务逻辑代码。

 1 package com.scl.thread.interrupt;
 2 
 3 public class SyncFileHandler implements IFileHandler
 4 {
 5     private volatile boolean isGetReadLock = false;
 6 
 7     public boolean isGetReadLock()
 8     {
 9         return isGetReadLock;
10     }
11 
12     public void read()
13     {
14         synchronized (FileHandlerByThreads.class.getClass())
15         {
16             System.out.println(Thread.currentThread().getName() + " start");
17             // 能进来则设置变量标志位
18             isGetReadLock = true;
19         }
20     }
21 
22     // 模拟运行时间比较久的写操作
23     public void write()
24     {
25         try
26         {
27             synchronized (FileHandlerByThreads.class.getClass())
28             {
29                 System.out.println(Thread.currentThread().getName() + " start");
30                 long startTime = System.currentTimeMillis();
31                 // 模拟一个耗时较长的操作
32                 for (;;)
33                 {
34                     if (System.currentTimeMillis() - startTime > Integer.MAX_VALUE)
35                     {
36                         break;
37                     }
38                 }
39             }
40 
41             System.out.println("Writer has writered down everything! bravo");
42         }
43         catch (Exception e)
44         {
45             e.printStackTrace();
46         }
47 
48     }
49 
50     public void formatFile()
51     {
52         System.out.println("begin to format the file");
53         // format the file
54     }
55 }
synchronized控制下的线程中断

③ 客户端测试代码

 1 package com.scl.thread.interrupt;
 2 
 3 public class TestLockInterruptibly
 4 {
 5     public static void main(String[] args) throws Exception
 6     {
 7         // 1. 根据lock控制中断
 8         // FileHandlerByThreads fileControl = new FileHandlerByThreads();
 9         // Thread readthr = new Thread(new ReadThread(fileControl), "reader");
10         // Thread writethr = new Thread(new WriteThread(fileControl), "writer");
11 
12         // 2. 使用synchronized关键字控制中断线程
13         SyncFileHandler sync = new SyncFileHandler();
14 
15         Thread readthr = new Thread(new ReadThread(sync), "reader");
16         Thread writethr = new Thread(new WriteThread(sync), "writer");
17         writethr.start();
18         readthr.start();
19 
20         long startTime = System.currentTimeMillis();
21         // 循环判是否有线程获取到了读锁断
22         while (!sync.isGetReadLock())
23         {
24             long endTime = System.currentTimeMillis();
25             // 如果4秒后读线程仍然没有等到读锁,离开等待
26             if (endTime - startTime > 4000)
27             {
28                 readthr.interrupt();
29                 System.out.println("4 seconds have passed,try to interrupt reader Thread");
30                 break;
31             }
32         }
33 
34     }
35 }
36 
37 class ReadThread implements Runnable
38 {
39     private IFileHandler fileControl;
40 
41     public ReadThread(IFileHandler fileControl)
42     {
43         this.fileControl = fileControl;
44     }
45 
46     @Override
47     public void run()
48     {
49         fileControl.read();
50         // 测试单纯使用synchronized关键字控制线程中断
51         System.out.println("reader thread end");
52         fileControl.formatFile();
53     }
54 }
55 
56 class WriteThread implements Runnable
57 {
58     private IFileHandler fileControl;
59 
60     public WriteThread(IFileHandler fileControl)
61     {
62         this.fileControl = fileControl;
63     }
64 
65     @Override
66     public void run()
67     {
68         fileControl.write();
69     }
70 }
客户端测试代码

代码运行结果:线程未中断,控制台输出如下

1 writer start
2 4 seconds have passed,try to interrupt reader Thread
synchronized代码运行结果

    如上面的结果,我们期望的是在读线程在运行4秒后能够被中断,且去运行格式化代码的任务。但是在读线程在调用interrupt方法后,读方法后面的代码并没有执行。反而是一直等待。控制台并没有输出"reader thread end",以及格式化代码的操作。由此可见synchronized关键字不会去响应线程中断。

   查看了大部分博客后,发现大家写的都是synchronized并不响应中断。但使用synchronized是否不能完成可中断线程的响应呢?

   要接收到中断信息,无非有两种方法 ①等待锁(使用wait、join等方法) ②进入休眠。这两个做法都需要使用循环,让程序等待。第一种方法完全不可行,我现在就是想要什么时候能够获取到锁,JDK通过synchronized没有提供方法让程序员知道:"我的代码获取到锁了吗"这个条件,其次等待对象wait方法,需要在synchrnized里面。synchronized (FileHandlerByThreads.class.getClass())这个条件本来就进不去,更别谈里面的wait方法了。第二种方法,让程序进入休眠。因此有以下代码

 1     public void read()
 2     {
 3         try
 4         {
 5 System.out.println(Thread.currentThread().getName() + " start");        
 6                 while (true)
 7                 {
 8                     Thread.sleep(100);
 9                     if (Thread.currentThread().isInterrupted())
10                     {
11                         break;
12                     }
13                 }
14                 synchronized (FileHandlerByThreads.class.getClass())
15                 {
16                     System.out.println(Thread.currentThread().getName() + " start");
17                 }
18             
19 
20         }
21         catch (InterruptedException e)
22         {
23             e.printStackTrace();
24             System.out.println("reader Thread leave the file and going to format the file");
25         }
26         finally
27         {
28             // lock.unlock();
29         }
30 
31     }
未卜先知型 sleep

    这样写,终于能够获取到interrupt发送过来的信息,并且捕获到异常了。但是,read方法一直都在休眠。这做法不是未卜先知了吗,因为你都知道了read方法肯定是得不到锁的,不断地在休眠。

  &

以上是关于Java多线程 Lock接口,ReentranctLock,ReentrantReadWriteLock的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程学习之Lock与ReentranLock详解

java多线程知识总结

Java多线程-简单使用Lock(锁)

Java-进阶:多线程2

多线程18:Lock锁

Java深入学习12:线程按目标顺序执行以及Lock锁和Condiiton接口