今天咱们继续学习并发。在以前咱们学习了 JMM 的知识,知道了在并发编程中,为了保证线程的安全性,须要保证线程的原子性,可见性,有序性。其中,synchronized 高频出现,由于他既保证了原子性,也保证了可见性和有序性。为何,由于 synchronized 是锁。经过锁,可让本来并行的任务变成串行。然而如你所见,这也致使了严重的性能受损。所以,不到万不得已,不要使用锁,特别是吞吐量要求特别高的 WEB 服务器。若是锁住,性能将呈几何级降低。java
但咱们仍然须要锁,在某些操做共享变量的时刻,仍然须要锁来保证数据的准确性。而Java 世界有 3 把锁,今天咱们主要说说这 3 把锁的用法。程序员
synchronized 能够说是咱们学习并发的时候第一个学习的关键字,该关键字粗鲁有效,一般是初级程序员最爱使用的,也所以会常常致使一些性能损失和死锁问题。编程
下面是 synchronized 的 3 个用法:安全
void resource1() { synchronized ("resource1") { System.out.println("做用在同步块中"); } } synchronized void resource3() { System.out.println("做用在实例方法上"); } static synchronized void resource2() { System.out.println("做用在静态方法上"); }
整理如下这个关键字的用法:服务器
synchronized 在发生异常的时候会释放锁,这点须要注意一下。多线程
synchronized 修饰的代码在生产字节码的时候会有 monitorenter 和 monitorexit 指令,而这两个指令在底层调用了虚拟机8大指令中其中两个指令-----lock 和 unlock。并发
synchronized 虽然万能,可是仍是有不少局限性,好比使用它常常会发生死锁,且没法处理,因此 Java 在 1.5版本的时候,加入了另外一个锁 Lock 接口。咱们看看该接口下的有什么。dom
JDK 在 1.5 版本新增了java.util.concurrent 包,有并发大师 Doug Lea 编写,其中代码鬼斧神工。值得咱们好好学习,包括今天说的 Lock。ide
Lock 接口函数
/** * @since 1.5 * @author Doug Lea */ public interface Lock { void lock(); void lockInterruptibly() throws InterruptedException; boolean tryLock(); boolean tryLock(long time, TimeUnit unit) throws InterruptedException; void unlock(); Condition newCondition();
void lock(); 得到锁
void lockInterruptibly() ;
boolean tryLock(); 尝试获取锁,若是获取不到,马上返回false。
boolean tryLock(long time, TimeUnit unit) 在
void unlock(); 在给定的时间里等待锁,超过期间则自动放弃
Condition newCondition(); 获取一个重入锁的好搭档,搭配重入锁使用
上面说了Lock的机构抽象方法,那么 Lock 的实现是什么呢?标准实现了 ReentrantLock, ReadWriteLock。也就是咱们今天讲的重入锁和读写锁。咱们先讲重入锁。
先来一个简单的例子:
package cn.think.in.java.lock; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class LockText implements Runnable { /** * Re - entrant - Lock * 重入锁,表示在单个线程内,这个锁能够反复进入,也就是说,一个线程能够连续两次得到同一把锁。 * 若是你不容许重入,将致使死锁。注意,lock 和 unlock 次数必定要相同,若是不一样,就会致使死锁和监视器异常。 * * synchronized 只有2种状况:1继续执行,2保持等待。 */ static Lock lock = new ReentrantLock(); static int i; public static void main(String[] args) throws InterruptedException { LockText lockText = new LockText(); Thread t1 = new Thread(lockText); Thread t2 = new Thread(lockText); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } @Override public void run() { for (int j = 0; j < 1000000; j++) { lock.lock(); try { i++; } finally { // 由于lock 若是发生了异常,是不会释放锁的,因此必须在 finally 块中释放锁 // synchronized 发生异常会主动释放锁 lock.unlock(); } } } }
在上面的代码中,咱们使用了try 块中保护了临界资源 i 的操做。能够看到, 重入锁无论是开启锁仍是释放锁都是显示的,其中须要注意的一点是,重入锁运行时若是发生了异常,不会像 synchronized 释放锁,所以须要在 finally 中释放锁。不然将产生死锁。
什么是重入锁?锁就是锁呗,为何叫重入锁?之因此这么叫,那是由于这种锁是能够反复进入的(一个线程),你们看看下面的代码:
lock.lock(); lock.lock(); tyr{ i++; } finally{ lock.unlock(); lock.unlock(); }
在这种状况下,一个线程连续两次得到两把锁,这是容许的。若是不容许这么操做,那么同一个线程咋i第二次得到锁是,将会和本身产生死锁。固然,须要注意的是,若是你屡次得到了锁,那么也要相同的释放屡次,若是释放锁的次数多了,就会获得一个 IllegalMonitorStateException 异常,反之,若是释放锁的次数少了,那么至关于这个线程尚未释放锁,其余线程也就没法进入临界区。
重入锁可以实现 synchronized 的全部功能,并且功能更为强大,咱们看看有哪些功能。
中断响应
对于 synchronized 来讲,若是一个线程在等待锁,那么结果只有2种,要么他得到这把锁继续运行,要么他就保持等待。没有第三种可能,那若是我有一个需求:须要线程在等待的时候中断线程,synchronizded 是作不到的。而重入锁能够作到,就是 lockInterruptibly 方法,该方法能够获取锁,而且在获取锁的过程种支持线程中断,也就是说,若是调用了线程中断方法,那么就会抛出异常。相对于 lock 方法,是否是更为强大?仍是写个例子吧:
package cn.think.in.java.lock; import java.util.concurrent.locks.ReentrantLock; /** * ReentrantLock(重入锁) * * Condition(条件) * * ReadWriteLock(读写锁) */ public class IntLock implements Runnable { /** * 默认是不公平的锁,设置为 true 为公平锁 * * 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程; * 使用公平锁的程序在许多线程访问时表现为很低的整体吞吐量(即速度很慢,经常极其慢) * 还要注意的是,未定时的 tryLock 方法并无使用公平设置 * * 不公平:此锁将没法保证任何特定访问顺序 * * 拾遗:1 该类的序列化与内置锁的行为方式相同:一个反序列化的锁处于解除锁定状态,无论它被序列化时的状态是怎样的。 * 2.此锁最多支持同一个线程发起的 2147483648 个递归锁。试图超过此限制会致使由锁方法抛出的 Error。 */ static ReentrantLock lock1 = new ReentrantLock(true); static ReentrantLock lock2 = new ReentrantLock(); int lock; /** * 控制加锁顺序,方便制造死锁 * @param lock */ public IntLock(int lock) { this.lock = lock; } /** * lockInterruptibly 方法: 得到锁,但优先响应中断 * tryLock 尝试得到锁,不等待 * tryLock(long time , TimeUnit unit) 尝试得到锁,等待给定的时间 */ @Override public void run() { try { if (lock == 1) { // 若是当前线程未被中断,则获取锁。 lock1.lockInterruptibly();// 即在等待锁的过程当中,能够响应中断。 try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 试图获取 lock 2 的锁 lock2.lockInterruptibly(); } else { lock2.lockInterruptibly(); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } // 该线程在企图获取 lock1 的时候,会死锁,但被调用了 thread.interrupt 方法,致使中断。中断会放弃锁。 lock1.lockInterruptibly(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock1.isHeldByCurrentThread()) { lock1.unlock(); } // 查询当前线程是否保持此锁。 if (lock2.isHeldByCurrentThread()) { lock2.unlock(); } System.out.println(Thread.currentThread().getId() + ": 线程退出"); } } public static void main(String[] args) throws InterruptedException { /** * 这部分代码主要是针对 lockInterruptibly 方法,该方法在线程发生死锁的时候能够中断线程。让线程放弃锁。 * 而 synchronized 是没有这个功能的, 他要么得到锁继续执行,要么继续等待锁。 */ IntLock r1 = new IntLock(1); IntLock r2 = new IntLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); Thread.sleep(1000); // 中断其中一个线程(只有线程在等待锁的过程当中才有效) // 若是线程已经拿到了锁,中断是不起任何做用的。 // 注意:这点 synchronized 是不能实现此功能的,synchronized 在等待过程当中没法中断 t2.interrupt(); // t2 线程中断,抛出异常,并放开锁。没有完成任务 // t1 顺利完成任务。 } }
在上面的代码种,咱们分别启动两个线程,制造了一个死锁,若是是 synchronized 是没法解除这个死锁的,这个时候重入锁的威力就出来了,咱们调用线程的 interrupt 方法,中断线程,咱们说,这个方法在线程 sleep,join ,wait 的时候,都会致使异常,这里也一羊,因为咱们使用的 lock 的 lockInterruptibly 方法,该方法就像咱们刚说的那样,在等待锁的时候,若是线程被中断了,就会出现异常,同时调用了 finally 种的 unlock 方法,注意,咱们在 finally 中用 isHeldByCurrentThread 判断当前线程是否持有此锁,这是一种预防措施,放置线程没有持有此锁,致使出现 monitorState 异常。
锁申请
除了等待通知以外,避免死锁还有另外一种方法,就是超时等待,若是超过这个时间,线程就放弃获取这把锁,这点 ,synchronized 也是不支持的。那么,如何使用呢?
package cn.think.in.java.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; public class TimeLock implements Runnable { static ReentrantLock lock = new ReentrantLock(false); @Override public void run() { try { // 最多等待5秒,超过5秒返回false,若得到锁,则返回true if (lock.tryLock(5, TimeUnit.SECONDS)) { // 锁住 6 秒,让下一个线程没法获取锁 System.out.println("锁住 6 秒,让下一个线程没法获取锁"); Thread.sleep(6000); } else { System.out.println("get lock failed"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } public static void main(String[] args) { TimeLock tl = new TimeLock(); Thread t1 = new Thread(tl); Thread t2 = new Thread(tl); t1.start(); t2.start(); } }
上面的代码中,咱们设置锁的等待时间是5秒,可是在同步块中,咱们设置了6秒暂停,锁外面的线程等待了5面发现仍是不能获取锁,就会放弃。走 else 逻辑,结束执行,注意,这里,咱们在 finally 块中依然作了判断,若是不作判断,就会出现 IllegalMonitorStateException 异常。
固然了,tryLock 方法也能够不带时间参数,若是获取不到锁,马上返回false,不然返回 true。该方法也是应对死锁的一个好办法。咱们仍是写个例子:
package cn.think.in.java.lock; import java.util.concurrent.locks.ReentrantLock; public class TryLock implements Runnable { static ReentrantLock lock1 = new ReentrantLock(); static ReentrantLock lock2 = new ReentrantLock(); int lock; public TryLock(int lock) { this.lock = lock; } @Override public void run() { // 线程1 if (lock == 1) { while (true) { // 获取1的锁 if (lock1.tryLock()) { try { // 尝试获取2的锁 if (lock2.tryLock()) { try { System.out.println(Thread.currentThread().getId() + " : My Job done"); return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } } else { // 线程2 while (true) { // 获取2的锁 if (lock2.tryLock()) { try { // 尝试获取1的锁 if (lock1.tryLock()) { try { System.out.println(Thread.currentThread().getId() + ": My Job done"); return; } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } } } /** * 这段代码若是使用 synchronized 确定会引发死锁,可是因为使用 tryLock,他会不断的尝试, 当第一次失败了,他会放弃,而后执行完毕,并释放外层的锁,这个时候就是 * 另外一个线程抢锁的好时机。 * @param args */ public static void main(String[] args) { TryLock r1 = new TryLock(1); TryLock r2 = new TryLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } }
这段代码若是使用 synchronized 确定会引发死锁,可是因为使用 tryLock,他会不断的尝试, 当第一次失败了,他会放弃,而后执行完毕,并释放外层的锁,这个时候就是另外一个线程抢锁的好时机。
公平锁和非公平锁
大多数状况下,为了效率,锁都是不公平的。系统在选择锁的时候都是随机的,不会按照某种顺序,好比时间顺序,公平锁的一大特色:他不会产生饥饿现象。只要你排队 ,最终仍是能够获得资源的。若是咱们使用 synchronized ,获得的锁就是不公平的。所以,这也是重入锁比 synchronized 强大的一个优点。咱们一样写个例子:
package cn.think.in.java.lock; import java.util.concurrent.locks.ReentrantLock; public class FairLock implements Runnable { // 公平锁和非公平锁的结果彻底不一样 /* * 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 10 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 9 得到锁 ======================下面是公平锁,上面是非公平锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到锁 9 得到锁 10 得到 * * */ static ReentrantLock unFairLock = new ReentrantLock(false); static ReentrantLock fairLock = new ReentrantLock(true); @Override public void run() { while (true) { try { fairLock.lock(); System.out.println(Thread.currentThread().getId() + " 得到锁"); } finally { fairLock.unlock(); } } } /** * 默认是不公平的锁,设置为 true 为公平锁 * * 公平:在多个线程的争用下,这些锁倾向于将访问权授予等待时间最长的线程; * 使用公平锁的程序在许多线程访问时表现为很低的整体吞吐量(即速度很慢,经常极其慢) * 还要注意的是,未定时的 tryLock 方法并无使用公平设置 * * 不公平:此锁将没法保证任何特定访问顺序,可是效率很高 * */ public static void main(String[] args) { FairLock fairLock = new FairLock(); Thread t1 = new Thread(fairLock, "cxs - t1"); Thread t2 = new Thread(fairLock, "cxs - t2"); t1.start(); t2.start(); } }
重入锁的构造函数有一个 boolean 参数,ture 表示公平,false 表示不公平,默认是不公平的,公平锁会下降性能。代码中由运行结果,能够看到,公平锁的打印顺序是彻底交替运行,而不公平锁的顺序彻底是随机的。注意:若是没有特殊需求,请不要使用公平锁,会大大下降吞吐量。
到这里,咱们总结一下重入锁相比 synchronized 有哪些优点:
固然,你们会说, synchronized 能够经过 Object 的 wait 方法和 notify 方法实现线程之间的通讯,重入锁能够作到吗?楼主告诉你们,固然能够了! JDK 中的阻塞队列就是用重入锁加 他的搭档 condition 实现的。
重入锁的好搭档-----Condition
还记的刚开始说 Lock 接口有一个newCondition 方法吗,该方法就是获取 Condition 的。该 Condition 绑定了该锁。Condition 有哪些方法呢?咱们看看:
public interface Condition { void await() throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; long awaitNanos(long nanosTimeout) throws InterruptedException; boolean await(long time, TimeUnit unit) throws InterruptedException; void awaitUninterruptibly(); boolean awaitUntil(Date deadline) throws InterruptedException; void signal(); void signalAll(); }
看着是否是特别属性,Condition 为了避免和 Object 类的 wait 方法冲突,使用 await 方法,而 signal 方法对应的就是 notify 方法。signalAll 方法对应的就是 notifyAll 方法。其中还有一些时间限制的 await 方法,和 Object 的 wait 方法的做用相同。注意,其中有一个 awaitUninterruptibly 方法,该方法从名字能够看出,并不会响应线程的中断,而 Object 的 wait 方法是会响应的。而 awaitUntil 方法就是等待到一个给定的绝对时间。除非调用了 signal 或者中断了。如何使用呢?来一段代码吧:
package cn.think.in.java.lock.condition; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 重入锁的好搭档 * * await 使当前线程等待,同时释放当前锁,当其余线程中使用 signal 或者 signalAll 方法时,线程会从新得到锁并继续执行。 * 或者当线程被中断时,也能跳出等待,这和 Object.wait 方法很类似。 * awaitUninterruptibly() 方法与 await 方法基本相同,可是它并不会在等待过程当中响应中断。 * singal() 该方法用于唤醒一个在等待中的线程,相对的 singalAll 方法会唤醒全部在等待的线程,这和 Object.notify 方法很相似。 */ public class ConditionTest implements Runnable { static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); @Override public void run() { try { lock.lock(); // 该线程会释放 lock 的锁,也就是说,一个线程想调用 condition 的方法,必须先获取 lock 的锁。 // 不然就会像 object 的 wait 方法同样,监视器异常 condition.await(); System.out.println("Thread is going on"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { ConditionTest t = new ConditionTest(); Thread t1 = new Thread(t); t1.start(); Thread.sleep(1000); // 通知 t1 继续执行 // main 线程必须获取 lock 的锁,才能调用 condition 的方法。不然就是监视器异常,这点和 object 的 wait 方法是同样的。 lock.lock(); // IllegalMonitorStateException // 从 condition 的等待队列中,唤醒一个线程。 condition.signal(); lock.unlock(); } }
能够说,condition 的使用方式和 Object 类的 wait 方法的使用方式很类似,不管在哪个线程中调用 await 或者 signal 方法,都必须获取对应的锁,不然会出现 IllegalMonitorStateException 异常。
到这里,咱们能够说, Condition 的实现比 Object 的 wait 和 notify 仍是强一点,其中就包括了等待到指定的绝对时间,而且还有一个不受线程中断影响的 awaitUninterruptibly 方法。所以,咱们说,只要容许,请使用重入锁,尽可能不要使用无脑的 synchronized 。虽然在 JDK 1.6 后, synchronized 被优化了,但仍然建议使用 重入锁。
伟大的 Doug Lea 不只仅创造了 重入锁,还创造了 读写锁。什么是读写锁呢?咱们知道,线程不安全的缘由来自于多线程对数据的修改,若是你不修改数据,根本不须要锁。咱们彻底能够将读写分离,提升性能,在读的时候不使用锁,在写的时候才加入锁。这就是 ReadWriteLock 的设计原理。
那么,如何使用呢?
package cn.think.in.java.lock; import java.util.Random; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockDemo { static Lock lock = new ReentrantLock(); static ReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); static Lock readLock = reentrantReadWriteLock.readLock(); static Lock writeLock = reentrantReadWriteLock.writeLock(); int value; public Object handleRead(Lock lock) throws InterruptedException { try { lock.lock(); // 模拟读操做,读操做的耗时越多,读写锁的优点就越明显 Thread.sleep(1000); return value; } finally { lock.unlock(); } } public void handleWrite(Lock lock, int index) throws InterruptedException { try { lock.lock(); Thread.sleep(1000); // 模拟写操做 value = index; } finally { lock.unlock(); } } public static void main(String[] args) { final ReadWriteLockDemo demo = new ReadWriteLockDemo(); Runnable readRunnable = new Runnable() { @Override public void run() { try { demo.handleRead(readLock); // demo.handleRead(lock); } catch (InterruptedException e) { e.printStackTrace(); } } }; Runnable writeRunnable = new Runnable() { @Override public void run() { try { demo.handleWrite(writeLock, new Random().nextInt()); // demo.handleWrite(lock, new Random().nextInt()); } catch (InterruptedException e) { e.printStackTrace(); } } }; /** * 使用读写锁,这段程序只须要2秒左右 * 使用普通的锁,这段程序须要20秒左右。 */ for (int i = 0; i < 18; i++) { new Thread(readRunnable).start(); } for (int i = 18; i < 20; i++) { new Thread(writeRunnable).start(); } } }
使用 ReentrantReadWriteLock 的 readLock()方法能够返回读锁,writeLock 能够返回写锁,咱们使用普通的的重入锁和读写锁进行测试,怎么测试呢?
两个循环:一个循环开启18个线程去读数据,一个循环开启两个线程去写。若是使用普通的重入锁,将耗时20秒,由于普通的重入锁在读的时候依然是串行的。而若是使用读写锁,只须要2秒,也就是写的时候是串行的。读的时候是并行的,极大的提升了性能。
注意:只要涉及到写都是串行的。好比读写操做,写写操做,都是串行的,只有读读操做是并行的。
读写锁 ReadWriteLock 接口只有 2个方法:
Lock readLock(); 返回一个读锁 Lock writeLock(); 返回一个写锁
他的标准实现类是 ReentrantReadWriteLock 类,该类和普通重入锁同样,也能实现公平锁,中断响应,锁申请等特性。由于他们返回的读锁或者写锁都实现了 Lock 接口。
到这里,咱们已经将 Java 世界的三把锁的使用弄清楚了,从分析的过程当中咱们知道了,JDK 1.5 的重入锁彻底能够代替关键字 synchronized ,能实现不少 synchronized 没有的功能。好比中断响应,锁申请,公平锁等,而重入锁的搭档 Condition 也比 Object 的wait 和notify 强大,好比有设置绝对时间的等待,还有忽略线程中断的 await 方法,这些都是 synchronized 没法实现的。还有优化读性能的 读写锁,在读的时候彻底是并行的,在某些场景下,好比读不少,写不多,性能将是几何级别的提高。
因此,之后,能不用 synchronzed 就不要用,用的很差就会致使死锁。
今天的Java 三把锁就介绍到这里。
good luck !!!!