本文非原创,为转载文章,原文链接:https://www.jianshu.com/p/2344a3e68ca9
一、Lock
synchronized是Java语言的关键字,是内置特性,而ReentrantLock是一个类(实现Lock接口的类),通过该类可以实现线程的同步。Lock是一个接口,源码很简单,主要是声明了四个方法:
1 2 3 4 5 6 7 8 public interface Lock { void lock () ; void lockInterruptibly () throws InterruptedException ; boolean tryLock () ; boolean tryLock (long var1, TimeUnit var3) throws InterruptedException ; void unlock () ; Condition newCondition () ; }
1.Lock一般的使用如下: 1 2 3 4 5 6 7 8 9 Lock lock= ...; lock.lock(); try { }catch (Exception e){ }finally { lock.unlock(); }
lock()
、tryLock()
、tryLock(long time, TimeUnit unit)
和lockInterruptibly()
是用来获取锁的,unLock()
方法是用来释放锁的,其放在finally块里执行,可以保证锁一定被释放,newCondition
方法下面会做介绍(通过该方法可以生成一个Condition对象,而Condition是一个多线程间协调通信的工具类)。
2.Lock接口的主要方法介绍:
lock()
:获取不到锁就不罢休,否则线程一直处于block
状态。
tryLock()
:尝试性地获取锁,不管有没有获取到都马上返回,拿到锁就返回true
,不然就返回false
。
tryLock(long time, TimeUnit unit)
:如果获取不到锁,就等待一段时间,超时返回false。
lockInterruptibly()
:该方法稍微难理解一些,在说该方法之前,先说说线程的中断机制,每个线程都有一个中断标志,不过这里要分两种情况说明:
线程在sleep
、wait
或者join
, 这个时候如果有别的线程调用该线程的 interrupt()
方法,此线程会被唤醒并被要求处理InterruptedException
。
如果线程处在运行状态, 则在调用该线程的interrupt()
方法时,不会响应该中断。lockInterruptibly()
和上面的第一种情况是一样的, 线程在获取锁被阻塞时,如果调用lockInterruptibly()
方法,该线程会被唤醒并被要求处理InterruptedException
。下面给出一个响应中断的简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Test { public static void main (String[] args) { MyRunnable myRunnable = new Test().new MyRunnable(); Thread thread1 = new Thread(myRunnable,"thread1" ); Thread thread2 = new Thread(myRunnable,"thread2" ); thread1.start(); thread2.start(); thread2.interrupt(); } public class MyRunnable implements Runnable { private Lock lock=new ReentrantLock(); @Override public synchronized void run () { try { lock.lockInterruptibly(); System.out.println(Thread.currentThread().getName() +"获取了锁" ); Thread.sleep(5000 ); }catch (InterruptedException e) { e.printStackTrace(); System.out.println(Thread.currentThread().getName() +"响应中断" ); }finally { lock.unlock(); System.out.println(Thread.currentThread().getName() +"释放了锁" ); } } } }
执行结果如下:
1 2 3 thread1获取了锁 thread1释放了锁 thread2响应中断
thread2在响应中断后,在finally
块里执行unlock
方法时,会抛出java.lang.IllegalMonitorStateException
异常(因为thread2并没有获取到锁,只是在等待获取锁的时候响应了中断,这时再释放锁就会抛出异常)。
3.newCondition()方法 上面简单介绍了ReentrantLock的使用,下面具体介绍使用ReentrantLock的中的newCondition方法实现一个生产者消费者的例子。 生产者、消费者 例子:两个线程A、B,A生产牙刷并将其放到一个缓冲队列中,B从缓冲队列中购买(消费)牙刷(说明:缓冲队列的大小是有限制的),这样就会出现如下两种情况。
当缓冲队列已满时,A并不能再生产牙刷,只能等B从缓冲队列购买牙刷;
当缓冲队列为空时,B不能再从缓冲队列中购买牙刷,只能等A生产牙刷放到缓冲队列后才能购买。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 public class ToothBrushDemo { public static void main (String[] args) { final ToothBrushBusiness toothBrushBusiness = new ToothBrushDemo().new ToothBrushBusiness(); new Thread(new Runnable() { @Override public void run () { executeRunnable(toothBrushBusiness, true ); } }, "牙刷生产者1" ).start(); new Thread(new Runnable() { @Override public void run () { executeRunnable(toothBrushBusiness, false ); } }, "牙刷消费者1" ).start(); } public static void executeRunnable (ToothBrushBusiness toothBrushBusiness, boolean isProducer) { for (int i = 0 ; i < 50 ; i++) { if (isProducer) { toothBrushBusiness.produceToothBrush(); } else { toothBrushBusiness.consumeToothBrush(); } } } public class ToothBrushBusiness { private GoodQueue<ToothBrush> toothBrushQueue = new GoodQueue<ToothBrush>(new ToothBrush[10 ]); private int number = 1 ; private final ReentrantLock lock = new ReentrantLock(); private final Condition notEmpty = lock.newCondition(); private final Condition notFull = lock.newCondition(); public ToothBrushBusiness () { } public void produceToothBrush () { lock.lock(); try { while (toothBrushQueue.isFull()) { notFull.await(); } ToothBrush toothBrush = new ToothBrush(number); toothBrushQueue.enQueue(toothBrush); System.out.println("生产: " + toothBrush.toString()); number++; notEmpty.signal(); } catch (InterruptedException e) { e.printStackTrace(); } catch (GoodQueueException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void consumeToothBrush () { lock.lock(); try { while (toothBrushQueue.isEmpty()) { notEmpty.await(); } ToothBrush toothBrush = toothBrushQueue.deQueue(); System.out.println("消费: " + toothBrush.toString()); notFull.signal(); } catch (InterruptedException e) { e.printStackTrace(); } catch (GoodQueueException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class ToothBrush { private int number; public ToothBrush (int number) { this .number = number; } @Override public String toString () { return "牙刷编号{" + "number=" + number + '}' ; } } }
这里缓冲队列的大小设成了10,定义了一个可重入锁lock,两个状态标记对象notEmpty,notFull,分别用来标记缓冲队列是否为空,是否已满。
当缓冲队列已满时,调用notFull.await方法用来阻塞生产牙刷线程。
当缓冲队列为空时,调用notEmpty.await方法用来阻塞购买牙刷线程。
notEmpty.signal用来唤醒消费牙刷线程,notFull.signal用来唤醒生产牙刷线程。
4.Object和Conditon对应关系如下:
Object
Condition
休眠
wait
await
唤醒特定线程
notify
signal
唤醒所有线程
notifyAll
signalAll
对于同一个锁,我们可以创建多个Condition,就是多个监视器的意思。在不同的情况下使用不同的Condition,Condition是被绑定到Lock上的,要创建一个Lock的Condition必须用newCondition()方法。
二、ReadWriteLock ReentrantLock(可重入锁)是唯一实现了Lock接口的类,并且ReentrantLock提供了更多的方法。
synchronized和ReentrantLock都是可重入锁,可重入性举个简单的例子,当一个线程执行到某个synchronized方法时,比如说method1,而在method1中会调用另外一个synchronized方法method2,此时线程不必重新去申请锁,而是可以直接执行方法method2。
ReentrantReadWriteLock简介 上面的响应中断的例子已经地使用到了ReentrantLock,下面来介绍另外一种锁,可重入读写锁ReentrantReadWriteLock,该类实现了ReadWriteLock接口,该接口的源码如下:
1 2 3 4 public interface ReadWriteLock { Lock readLock () ; Lock writeLock () ; }
ReentrantReadWriteLock会使用两把锁来解决问题,一个读锁,一个写锁。
线程进入读锁的前提条件:
没有其他线程的写锁
没有写请求,或者有写请求但调用线程和持有锁的线程是同一个线程
进入写锁的前提条件:
没有其他线程的读锁
没有其他线程的写锁
需要提前了解的概念:
锁降级:从写锁变成读锁;
锁升级:从读锁变成写锁。
读锁是可以被多线程共享的,写锁是单线程独占的。也就是说写锁的并发限制比读锁高,这可能就是升级/降级名称的来源。
ReadWriteLock接口只有获取读锁和写锁的方法,而ReentrantReadWriteLock是实现了ReadWriteLock接口,接着对其应用场景做简单介绍。
应用场景: 假设一个共享的文件,其属性是可读,如果某个时间有100个线程在同时读取该文件,如果通过synchronized或者Lock来实现线程的同步访问,那么有个问题来了,当这100个线程的某个线程获取到了锁后,其它的线程都要等该线程释放了锁才能进行读操作,这样就会造成系统资源和时间极大的浪费,而ReentrantReadWriteLock正好解决了这个问题。下面给一个简单的例子,并根据代码以及输出结果做简要说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Test { public static void main (String[] args) { MyRunnable myRunnable = newTest().new MyRunnable(); Thread thread1 = new Thread(myRunnable, "thread1" ); Thread thread2 = new Thread(myRunnable, "thread2" ); Thread thread3 = new Thread(myRunnable, "thread3" ); thread1.start(); thread2.start(); thread3.start(); } public class MyRunnable implements Runnable { private ReadLock lock = new ReentrantReadWriteLock().readLock(); @Override public synchronized void run () { try { lock.lock(); int i = 0 ; while (i < 5 ) { System.out.println(Thread.currentThread().getName() + "正在进行读操作" ); i++; } System.out.println(Thread.currentThread().getName() + "读操作完毕" ); } finally { lock.unlock(); } } } }
输出结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 thread1正在进行读操作 thread1正在进行读操作 thread1正在进行读操作 thread1正在进行读操作 thread1正在进行读操作 thread1读操作完毕 thread3正在进行读操作 thread3正在进行读操作 thread3正在进行读操作 thread3正在进行读操作 thread3正在进行读操作 thread3读操作完毕 thread2正在进行读操作 thread2正在进行读操作 thread2正在进行读操作 thread2正在进行读操作 thread2正在进行读操作 thread2读操作完毕
从输出结果可以看出,三个线程并没有交替输出,这是因为这里只是读取了5次,但将读取次数i的值改成一个较大的数值如100000时,输出结果就会交替的出现。
看了好多人的博文,在我看来,这个Lock的用处就是可以细化加锁和解锁的操作,使锁操作更加直观,可控