Java并发编程与技术内幕:聊聊锁的技术内幕(上)

一、基础知识

       在Java并发编程里头,锁是一个非常重要的概念。就如同现实生活一样,如果房子上了锁。别人就进不去。Java里头如果一段代码取得了一个锁,其它地方再想去这个锁(或者再执行这个相同的代码)就都得等待锁释放。锁其实分成非常多。比如有互斥锁、读写锁、乐观锁、悲观锁、自旋锁、公平锁、非公平锁等。包括信号量其实都可以认为是一个锁。

 1、什么时需要锁呢?

     其实非常多的场景,如共享实例变量、共享连接资源时以及包括并发包中BlockingQueue、ConcurrentHashMap等并发集合中都大量使用了锁。基体上使用同步的地方都可以改成锁来用,但是使用锁的地方不一定能改成同步来用。

2、 锁和同步的对比

1)同步synchronized算是一个关键词,是来来修饰方法的,但是锁lock是一个实例变量,通过调用lock()方法来取得锁

2)、只能同步方法,而不能同步变量和类,锁也是一样

3)、同步无法保证线程取得方法执行的先后顺序。锁可以设置公平锁来确保。
4)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。锁也是一样。
6)、线程睡眠时,它所持的任何锁都不会释放。
7)、线程可以获得多个锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步锁。
8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)、在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要获取哪个对象的锁。例如:

最后,还需要说的一点是。如果使用锁,那么一定的注意编写代码,但不很容易出现死锁!避免方法后文后讲。

 3、简单实例

在看锁的源码时,首先来看个锁的实例,从而对锁有一个简单的理解。由线程A输出1、2、3.接着线程B输出4、5、6.最后线程A再输出7、8、9

 

 
  1. package com.func.axc.reentrantlock;

  2.  
  3. import java.util.concurrent.locks.Condition;

  4. import java.util.concurrent.locks.Lock;

  5. import java.util.concurrent.locks.ReentrantLock;

  6.  
  7. /**

  8. * 功能概要:

  9. *

  10. * @author linbingwen

  11. * @since 2016年5月27日

  12. */

  13. public class ReenTrantLockTest {

  14.  
  15. static class NumberWrapper {

  16. public int value = 1;

  17. }

  18.  
  19. public static void main(String[] args) {

  20. // 初始化可重入锁

  21. final Lock lock = new ReentrantLock();

  22.  
  23. // 第一个条件当屏幕上输出到3

  24. final Condition reachThreeCondition = lock.newCondition();

  25. // 第二个条件当屏幕上输出到6

  26. final Condition reachSixCondition = lock.newCondition();

  27.  
  28. // NumberWrapper只是为了封装一个数字,一边可以将数字对象共享,并可以设置为final

  29. // 注意这里不要用Integer, Integer 是不可变对象

  30. final NumberWrapper num = new NumberWrapper();

  31. // 初始化A线程

  32. Thread threadA = new Thread(new Runnable() {

  33. @Override

  34. public void run() {

  35. // 需要先获得锁

  36. lock.lock();

  37. try {

  38. System.out.println("threadA start write");

  39. // A线程先输出前3个数

  40. while (num.value <= 3) {

  41. System.out.println(num.value);

  42. num.value++;

  43. }

  44. // 输出到3时要signal,告诉B线程可以开始了

  45. reachThreeCondition.signal();

  46. } finally {

  47. lock.unlock();

  48. }

  49. lock.lock();

  50. try {

  51. // 等待输出6的条件

  52. reachSixCondition.await();

  53. System.out.println("threadA start write");

  54. // 输出剩余数字

  55. while (num.value <= 9) {

  56. System.out.println(num.value);

  57. num.value++;

  58. }

  59.  
  60. } catch (InterruptedException e) {

  61. e.printStackTrace();

  62. } finally {

  63. lock.unlock();

  64. }

  65. }

  66.  
  67. });

  68.  
  69. Thread threadB = new Thread(new Runnable() {

  70. @Override

  71. public void run() {

  72. try {

  73. lock.lock();

  74.  
  75. while (num.value <= 3) {

  76. // 等待3输出完毕的信号

  77. reachThreeCondition.await();

  78. }

  79. } catch (InterruptedException e) {

  80. e.printStackTrace();

  81. } finally {

  82. lock.unlock();

  83. }

  84. try {

  85. lock.lock();

  86. // 已经收到信号,开始输出4,5,6

  87. System.out.println("threadB start write");

  88. while (num.value <= 6) {

  89. System.out.println(num.value);

  90. num.value++;

  91. }

  92. // 4,5,6输出完毕,告诉A线程6输出完了

  93. reachSixCondition.signal();

  94. } finally {

  95. lock.unlock();

  96. }

  97. }

  98.  
  99. });

  100.  
  101. // 启动两个线程

  102. threadB.start();

  103. threadA.start();

  104. }

  105. }


输出结果:

 

 

 

这个题目用同步的方法也做其实也可以。但是用锁可能更好一点。在上面笔者使用了锁和条件从而完成 了要求。

 

二、说说源码

最基础的我们先来看看lock方法

 

 
  1. package java.util.concurrent.locks;

  2. import java.util.concurrent.TimeUnit;

  3.  
  4. public interface Lock {

  5.  
  6. //取得锁,但是要注意lock()忽视interrupt(), 拿不到锁就 一直阻塞

  7. void lock();

  8.  
  9. //同样也是取得锁,但是lockInterruptibly()会响应打扰 interrupt()并catch到InterruptedException,从而跳出阻塞

  10. void lockInterruptibly() throws InterruptedException;

  11.  
  12. //尝试取得锁,成功返回true

  13. boolean tryLock();

  14.  
  15. //在规定的时间等待里,如果取得锁就返回tre

  16. boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

  17.  
  18. //释放锁

  19. void unlock();

  20. //条件状态,非常有用,Blockingqueue阻塞队列就是用到它了

  21. Condition newCondition();

  22. }

1、接下来看看它最常见的实现类,ReentrantLock可重入锁。

 

 
  1. public class ReentrantLock implements Lock, java.io.Serializable {

  2. private static final long serialVersionUID = 7373984872572414699L;

  3. private final Sync sync; //就只有一个Sync变量,ReentrantLock的所有方法基本都是调用Sync的方法

 

2、构造函数

 

 
  1. public ReentrantLock() {

  2. sync = new NonfairSync(); //默认非公平锁

  3. }

  4.  
  5. public ReentrantLock(boolean fair) {

  6. sync = (fair)? new FairSync() : new NonfairSync();//公平锁

  7. }

其里的公平锁的意思是哪个线程先来等待,谁就先获得这个锁。而非公平锁则是看操作系统的调度,有不确定性。一般设置成非公平锁的性能会好很多。

 

 

 

 

3、然后看看lock方法

 
  1. public void lock() {

  2. sync.lock();

  3. }

还有这个

 

 

 
  1. public void lockInterruptibly() throws InterruptedException {

  2. sync.acquireInterruptibly(1);

  3. }


发现都 是调用 sync这个变量的方法,它其实是一个ReentrantLock的内部类。真实起作用的其实是它,所以直接看它源码:

 

首先是非公平锁:

 

 
  1. final static class NonfairSync extends Sync {

  2. private static final long serialVersionUID = 7316153563782823691L;

  3.  
  4. final void lock() {

  5. if (compareAndSetState(0, 1)) //0未获取,1已经获取

  6. setExclusiveOwnerThread(Thread.currentThread());//设置独占模式,则一个锁只能被一个线程持有,其他线程必须要等待。

  7. else

  8. acquire(1);//如果没有取得锁,尝试使用信号量的方式

  9. }

  10.  
  11. protected final boolean tryAcquire(int acquires) {

  12. return nonfairTryAcquire(acquires);

它使用到的方法如下:

 

 

 
  1. //设置状态

  2. protected final boolean compareAndSetState(int expect, int update) {

  3. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

  4. }

  5.  
  6. //可以看到, compareAndSwapInt不是用Java实现的, 而是通过JNI调用操作系统的原生程序.注意它是原子方法(C++写的)

  7. public final native boolean compareAndSwapInt(Object o, long offset,int expected, int x);

最终取得锁的方法其实在java Unsafe类的compareAndSwap方法。compareAndSwap是个原子方法,原理是cas.就是说如果他是xx,那么就改为xxx. 这个是高效,而且是原子的,不用加锁. 也不用但是其他值改了而产生误操作,应为会先判断当前值,符合期望才去改变. 

 

4、tryLock()方法

上面是lock方法是的调用,如果是tryLock呢?

 

 
  1. public boolean tryLock() {

  2. return sync.nonfairTryAcquire(1);

  3. }

再看sync的方法

 

 

 
  1. final boolean nonfairTryAcquire(int acquires) {

  2. final Thread current = Thread.currentThread();

  3. int c = getState();//取得状态

  4. if (c == 0) {//0表示未获取锁

  5. if (compareAndSetState(0, acquires)) {//CAS设置状态

  6. setExclusiveOwnerThread(current);//设置独占线程

  7. return true;

  8. }

  9. }

  10. else if (current == getExclusiveOwnerThread()) {//当前线程已有这个锁了

  11. int nextc = c + acquires;//设置重入的次数,如果是一个线程在有锁的情况下多次调用tryLock就有可能进入这个方法

  12. if (nextc < 0) // 重入数溢出了

  13. throw new Error("Maximum lock count exceeded");

  14. setState(nextc);

  15. return true;

  16. }

  17. return false;//如果到这里就是没有取到锁了,

  18. }


其中getState()方法是在AbstractQueuedSynchronizer类的就方法,取得就是下面这个变量

 

 

private volatile int state;

 

在互斥锁中它表示着线程是否已经获取了锁,0未获取,1已经获取了,大于1表示重入数。同时AQS提供了getState()、setState()、compareAndSetState()方法来获取和修改该值:

 
  1. protected final int getState() {

  2. return state;

  3. }

  4. protected final void setState(int newState) {

  5. state = newState;

  6. }

  7. protected final boolean compareAndSetState(int expect, int update) {

  8. return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

  9. }

这些方法需要java.util.concurrent.atomic包的支持,采用CAS操作,保证其原则性和可见性。

 

 

 

5、tryLock(long timeout, TimeUnit unit)方法

 

 
  1. public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {

  2. return sync.tryAcquireNanos(1, unit.toNanos(timeout));

  3. }


带有超时时间等待获取锁的方法。真正调用 的其实是Sync父类AbstractQueuedSynchronizer的方法

 

 

 
  1. public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {

  2. if (Thread.interrupted())//检测到当前线程的中断标志为true

  3. throw new InterruptedException();

  4. return tryAcquire(arg) ||

  5. doAcquireNanos(arg, nanosTimeout);

  6. }


这里调用 了两个方法tryAcquire和doAcquireNanos,其实tryAcquire调用的方法就是Lock()调用的方法

 

 

 
  1. protected final boolean tryAcquire(int acquires) {

  2. return nonfairTryAcquire(acquires);

  3. }

这样就不再说明。下面直接来看doAcquireNanos方法,它才是一直在等待循环获取锁的方法。

 

 

 
  1. private boolean doAcquireNanos(int arg, long nanosTimeout)

  2. throws InterruptedException {

  3. long lastTime = System.nanoTime();

  4. final Node node = addWaiter(Node.EXCLUSIVE);//放入等待的节点,会组成 一个链表

  5. try {

  6. for (;;) { //死循环,时间到了才会跳出

  7. final Node p = node.predecessor();

  8. if (p == head && tryAcquire(arg)) { //当前节点是头节点。然后尝试获得锁

  9. setHead(node);

  10. p.next = null; // 把当前节点去掉

  11. return true;

  12. }

  13. if (nanosTimeout <= 0) { //超出等待时间

  14. cancelAcquire(node);

  15. return false;

  16. }

  17. if (nanosTimeout > spinForTimeoutThreshold &&

  18. shouldParkAfterFailedAcquire(p, node))

  19. LockSupport.parkNanos(this, nanosTimeout);//还在等待时间内

  20. long now = System.nanoTime();

  21. nanosTimeout -= now - lastTime;

  22. lastTime = now;

  23. if (Thread.interrupted())//检测到中断信号,直接跳出

  24. break;

  25. }

  26. } catch (RuntimeException ex) {

  27. cancelAcquire(node);

  28. throw ex;

  29. }

  30. cancelAcquire(node);//检测 到中断信号时才会执行到这里

  31. throw new InterruptedException();

  32. }

锁的介绍就到这里了,下文再来接着讲~

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值