在jdk5.0之前AQS框架下的锁的性能是远远超过synchronized的,从jdk6.0开始对synchronized做出优化后二者在性能上差异已经不大了。ReentrantLock的有点在于:
- 灵活性。加锁解锁的过程是可控的,synchronized加锁解锁过程是编译完成后JVM来实现的
- 可响应中断。synchronized下无法获得锁的线程是BLOCKED的,不可以相应中断。AQS下所有的锁包括重入锁无法获得锁是WAITING的,可相应中断,可以用来解决死锁问题
- 超时获得锁。同样可以用来缓解死锁问题
- 支持公平锁避免饥饿
- 好基友Condition。虽然synchronized下同样有Object的wait notify方法,但Condition队列更灵活、更可控。
- 使用CAS来实现原子操作
- 使用LockSupport中的park unpark实现阻塞原语
1.1 基本使用
一个老生常谈的多线程自增的例子。由于i++的非原子性,在不采取任何措施的情况下最终的结果是小于等于200000的,为了使最终的结果为200000需要采取措施保障i++不可分割,在i++前后加锁即可。
虽然用synchronized可实现同样的结果,但用重入锁可以自己来加锁解锁,况且还省了一个大括号不是?
public class myThread implements Runnable { public static ReentrantLock lock = new ReentrantLock(); public static int i=0; @Override public void run() { for (int j=0;j<100000;j++){ lock.lock(); i++; lock.unlock(); } } public static void main(String[] args) throws InterruptedException { myThread thread = new myThread(); Thread t1 = new Thread(thread); Thread t2 = new Thread(thread); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
1.2 中断与死锁
首先是一个死锁的代码,线程1先获得锁1然后请求获得锁2,线程2先获得锁2然后请求获得锁1。
package ReLock; import java.util.concurrent.locks.ReentrantLock; public class deadLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public deadLock(int lock) { this.lock = lock; } @Override public void run() { if (lock == 1){ try { lock1.lockInterruptibly(); Thread.sleep(500); lock2.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); } }else { try { lock2.lockInterruptibly(); Thread.sleep(500); lock1.lockInterruptibly(); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { deadLock deadlock1 = new deadLock(1); Thread thread1 = new Thread(deadlock1); deadLock deadlock2 = new deadLock(2); Thread thread2 = new Thread(deadlock2); thread1.start(); thread2.start(); } }
执行之后没有反应且显示正在运行。用jps找到当前死锁进程的pid
然后用jstack -l pid查看进程的状态,可以看到jstack分析出了死锁,且指出了死锁的原因。
同样的可以使用jConsole图形化界面的查看死锁。
总之我们现在有了一个死锁的代码。注意到这里获取不是lock而是lockInterruptibly,意味着没有获得锁的线程可以响应中断。处于WAITING状态的线程响应中断方式是抛出异常,我们catch到异常后就可以在异常处理逻辑中释放锁。
按照这个思路修改代码,在catch中加入释放锁的逻辑。注意响应中断并不会直接释放锁,要在catch逻辑里手动释放锁。并且为了代码的健壮性,先判断当前哪些锁被当先线程持有,释放当前线程持有的锁。在主线程里加入线程中断代码,这样在中断一个线程的时候可以看到另一个线程完整的执行。
package ReLock; import java.util.concurrent.locks.ReentrantLock; public class deadLock implements Runnable { public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); int lock; public deadLock(int lock) { this.lock = lock; } @Override public void run() { if (lock == 1){ try { lock1.lockInterruptibly(); Thread.sleep(500); lock2.lockInterruptibly(); System.out.println("1执行完毕"); } catch (InterruptedException e) { if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } e.printStackTrace(); } }else { try { lock2.lockInterruptibly(); Thread.sleep(500); lock1.lockInterruptibly(); System.out.println("2执行完毕"); } catch (InterruptedException e) { if (lock1.isHeldByCurrentThread()){ lock1.unlock(); } if (lock2.isHeldByCurrentThread()){ lock2.unlock(); } e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { deadLock deadlock1 = new deadLock(1); Thread thread1 = new Thread(deadlock1); deadLock deadlock2 = new deadLock(2); Thread thread2 = new Thread(deadlock2); thread1.start(); thread2.start(); Thread.sleep(100); thread2.interrupt(); } }
但关于利用中断来解决死锁我又想到了新的思路:把lock.lockInterruptibly()换成lock.lock也是可以的,但前提是Thread.sleep要加进去。所以推广这个结论只要能够抛出InterruptedException异常的代码都可以用中断去打断。单独使用lock.lock不可以,因为单独使用lock.lock不会抛出中断异常。
比如下面这段代码,死锁的线程不会响应中断。
public class lockInter { private static ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { lock.lock(); System.out.println("解锁了"); } }); lock.lock(); thread.start(); thread.interrupt(); } }
1.3 超时获取锁与死锁
这是避免死锁的第二种思路,如果一个线程获得锁失败后等待超过一定时间就会返回,而非一直等待,又看到了乐观锁的影子。
如下所示,整体结构而言和最初的一样,这理应是一个死锁,但是经过一些等待后最终有了正确的返回结果。
package ReLock; import java.util.concurrent.locks.ReentrantLock; public class tryAcquireKillDeadLock implements Runnable{ public static ReentrantLock lock1 = new ReentrantLock(); public static ReentrantLock lock2 = new ReentrantLock(); private int num; public tryAcquireKillDeadLock(int num) { this.num = num; } @Override public void run() { if (num ==1){ while (true){ if (lock1.tryLock()){ try { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (lock2.tryLock()){ try { System.out.println(Thread.currentThread().getName()+" MyJob Done"); return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } } }else { while (true){ if (lock2.tryLock()){ try { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } if (lock1.tryLock()){ try { System.out.println(Thread.currentThread().getName()+" MyJob Done"); return; } finally { lock1.unlock(); } } } finally { lock2.unlock(); } } } } } public static void main(String[] args) { tryAcquireKillDeadLock r1 = new tryAcquireKillDeadLock(1); tryAcquireKillDeadLock r2 = new tryAcquireKillDeadLock(2); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } }
这里使用的释放锁的方法比较优雅,在finally里释放锁,虽然在finally前面有一个return语句,但finally里的释放锁的代码也会执行,并且按照从内向外的顺序,在执行完毕后再执行return。
public class testTry { public static int testT(){ try { try { int a =5; //int a = 5/0; return a; }finally { System.out.println("我执行了"); } } finally { System.out.println("我也执行了"); } } public static void main(String[] args) { testT(); } }
另外关注一下这里锁的使用与释放的代码模板,抛去sleep引入的try可以使代码简洁一点。
- 最外层要用while包裹起来,因为tryLock在未获得锁会返回false。
- 每次进入if都代表锁获取成功,把里面的代码用try finally包裹起来,这是为了避免在执行代码的时候抛出异常导致锁没有释放引发死锁,在finally里释放锁是最安全的行为
- 如果有多次获得锁的操作就在try里嵌套try finally
while (true){ if (lock1.tryLock()){ try { if (lock2.tryLock()){ try { //TO DO return; } finally { lock2.unlock(); } } } finally { lock1.unlock(); } } }
1.4 公平锁
公平与否是借助底层AQS实现的,书上说非公平的定义是每次释放锁的时候从等待队列里随机取出一个等待的线程给予锁,对次我存疑。
package ReLock; import java.util.concurrent.locks.ReentrantLock; public class FairReLock implements Runnable { private static ReentrantLock lock = new ReentrantLock(false); @Override public void run() { while (true){ try { lock.lock(); System.out.println(Thread.currentThread().getName()+" 获得锁"); } finally { lock.unlock(); } } } public static void main(String[] args) { FairReLock fairReLock = new FairReLock(); Thread t1 = new Thread(fairReLock); Thread t2 = new Thread(fairReLock); t1.start(); t2.start(); } }
当使用公平锁的时候打印结果是左边,可以看到基本上两个线程是交替获得锁;使用非公平锁的结果在右边,明显看到锁是长期被一个线程霸占后又给了另一个线程。因而公平锁最大的优点是避免饥饿产生,虽然需要付出一定的代价。