Java多线程 -- JUC包源码分析4 -- 各种锁与无锁
来源:互联网 发布:js给span标签赋值 编辑:程序博客网 时间:2024/05/19 19:14
说到锁,想必很多人就会被如下的各种专业名词绕晕了。本文试图厘清关于锁的各种概念,从而对锁有一个全面而深入的理解。
–自旋锁/阻塞锁
–独占锁(排它锁)/共享锁(读写锁)
–公平锁/非公平锁
–偏向锁/非偏向锁
–可重入锁
–悲观锁/乐观锁
–ReentrantLock源码分析
–AbstractQueuedSynchronized(队列同步器) –源码分析
–ReentrantReadWriteLock源码分析
–无锁
–总结:synchronized关键字 与 Lock的区别
自旋锁与阻塞锁
自旋锁: 线程拿不到锁的时候,CPU空转,不放弃CPU,等待别的线程释放锁。前面所讲的CAS乐观锁,即是自旋锁的典型例子。
很显然,自旋锁只有在多CPU情况下,才可能使用。如果单CPU,占着不放,其他程序就没办法调度了。
阻塞锁: 线程拿不到锁的时候,放弃CPU,进入阻塞状态。等别的线程释放锁之后,再幻醒此线程,调度到CPU上执行。
synchonized关键字,Lock,都是阻塞锁(注:这里说synchonized关键字是阻塞锁,不准确,后面会再详细阐述)
自旋锁相比阻塞锁,少了线程切换的时间,因此性能可能更高。但如果自旋很多次,也拿不到锁,则会浪费CPU的资源。
在Java中,有1个 -XX:UseSpining 参数, 缺省为10。此参数用来指定,在JVM内部,自旋锁的cpu空转次数。
独占锁与共享锁(读写锁)
独占锁:Synchronized/ReentrantLock都是独占锁,或者叫“排他锁”。1个线程拿到锁之后,其他线程只能等待。
“读”与“读”互斥,“读”与“写”互斥,“写”与“写”互斥
共享锁:在笔者看来,共享锁和“读写锁”就是一个概念,读写分离。
“读”与“读”不互斥,“读”与“写”互斥,“写”与“写”互斥
因为“读”与“读”不互斥,所以1个线程拿到“读”锁之后,其他线程也可以拿到“读”锁,因此“读”锁是共享的。
但1个线程拿到”读“锁之后,另1个线程,拿不到”写“锁;反之亦然!
公平锁与非公平锁
大家都知道,ReentrantLock的构造函数,有个参数,指定锁是公平的,还是非公平的,缺省是非公平的。
- 1
- 2
- 3
- 1
- 2
- 3
那到底什么叫公平/非公平呢?
打个比喻:你去火车站买票,买票窗口前排了1个长队。
非公平:去了之后,直奔窗口,问有没有你要的票,有的话,买了走人;没有,再排队。
公平:去了之后,排在队伍末尾,直到前面的人都买完之后,你再买。
而Lock的公平/非公平,就是指当前线程去拿锁(去火车站买票)的时候的策略。关于Lock的源码,后面会详细分析。
偏向锁与非偏向锁
后面结合源码讲述
可重入锁
所谓可重入,就是指某个线程在拿到锁之后,在锁的代码块内部,再次去拿该锁,仍可拿到。
synchronized关键字, Lock都是可重入锁。
因此你可以在synchronized方法内部,调用另外一个synchronized方法;在lock.lock()的代码块里面,再次调用lock.lock()。
悲观锁与乐观锁
前面已讲
ReentrantLock的lock源码分析
队列同步器基本原理
在深入分析源码之前,先分别看一下的类图:
ReentrantLock类图:
ReentrantReadWriteLock类图:
从类图中,可以看到2点:
1. 公平锁/非公平锁的实现,分别用了FairSync/NonFairSync
2. 使用了一个共同的部件 AbstractQueuedSynchronizer,队列同步器。这是整个锁实现的核心部件,它的基本原理如下:
(1) 内部有一个state变量 + Thread exclusiveOwnerThread 变量
state = 0 //表示没有线程持有该锁
state = 1 //表示有1个线程,持有该锁
state > 1 //表示同1个线程,多次持有该锁,也就是”可重入“。
每一次lock(),state++; unlock(), state–;
(2) exclusiveOwnerThread变量,记录当前持有该锁的Thread
(3)当某个线程要拿锁,却发现state > 0时,进入阻塞队列。等待state = 0的时候,队列中某个线程被唤起。
关于”队列同步器“的源码,后面会详细分析
公平锁与非公平锁实现上的差别
先从构造函数入手:
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
再看lock()函数:
- 1
- 2
- 3
- 1
- 2
- 3
接下来,比较一下2者的lock()有何差异:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
接下来比较一下acquire()函数的差异:acquire()是定义在基类AbstractQueuedSynchronizer里面的,里面调用的tryAcquire()是abstract的
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
接下来比较2者在tryAcquire()实现上的差别:
- 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
- 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
- 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
- 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
小结:tryAcquire()主要做了2件事:
(1)没人持有锁的情况下,通过CAS去抢锁。公平/非公平,抢法不一样:非公平,直接抢;公平,自己排在队列第1个,才去抢。
如果已经有人持有锁,函数之间返回false;
(2)实现了锁的可重入性:发现state > 0,并且锁是自己持有的,再次lock(),不走整个的lock()流程,直接state++就可以了。
接下来就是要讲的第3件事:拿不到锁的时候,如何排队?回到上面的代码:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
接下来就涉及到“队列同步器”的核心部分:
AbstractQueuedSynchronized(AQS 队列同步器)
park/unpark 操作原语
在分析上面的acquireQueued函数之前,先来看一对关键的操作原语:
LockSupport.park/unpark
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
park()/unpark()的底层,是native代码,用pthread_mutex实现的,在此不再深究。再此主要想讲一下它的有趣的特性:
你可能注意到,它是一对static函数,并且传入的参数是一个Thread。不同于synchrnozied/lock,作用在数据上面,间接的让线程阻塞; 它是直接作用在线程上面,可以精确控制让某个Thread阻塞,唤醒。
得益于LockSupport的这对强大特性,线程同步器,可以让所有阻塞的线程形成一个链表(也就是阻塞队列),然后有选择性的阻塞、唤醒某个线程。
屏蔽中断
我们知道lock()是不响应中断的,lockInterruptibly才会响应中断,那它内部是如何做到的呢?
答案就在下面的死循环里面:
- 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
- 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
至此,我们完整分析了ReentrantLock.lock()函数。总结下来,有以下几个关键点:
(1)公平/非公平的实现
(2)park/unpark原语
(3)线程组成的阻塞队列 – 无锁链表
(4)死循环,实现中断的屏蔽
ReentrantLock的unlock源码分析
接下来,分析ReentrantLock.unlock(),unlock的实现比较简单
- 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
- 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
ReentrantLock的lockInterruptibly源码分析
从上面代码可以看出,lockInterruptibly至所以可以实现响应中断,是因为LockSupport.park是会响应中断的。
- 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
- 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
无锁链表
至此,关于RetrantLock的3个核心函数:lock/unlock/lockInteruptibly分析完了,接下来分析一下AQS中的那个阻塞线程所组成的队列 – 无锁链表
- 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
- 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
ReentrantReadWriteLock源代码分析
如上面类图所示,其内部有2把锁, ReadLock 和 WriteLock。但2把锁,引用的是一个共同的Sync,还是通过一个AQS里面的state变量来实现。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
下面分别看一下,ReadLock.lock(), WriteLock.lock()分别是如何实现的:
lock
- 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
- 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
- 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
- 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
关键点:
(1)从上面2种lock()代码可以看出,”读”与“读”是不互斥的,“读”与“写”互斥,“写”与“写”互斥。
(2)只有1种case,读锁与写锁不互斥。就是同1个线程,同时获得读锁和写锁,是允许的。
关于unlock,原理和ReentrantLock的类似,在此不再详述。
总结:synchronized关键字与Lock的区别
经过上述分析,我们可以总结出,Lock的实现,有以下几个关键原理:
(1)通过CAS操作state变量,实现加锁功能
对于ReentrantLock,state取0,1,>1 3类值,分别代码了锁的3种状态。
对于ReentrantReadWriteLock, state的低16位,代表写锁的状态;高16位,代表读锁的状态
(2)park/unpark原语,实现线程的阻塞/唤醒
(3)无锁链表实现的队列,把所有阻塞的线程串在一起
(4)对中断的处理:通过死循环,被中断唤醒的线程,再次加锁,再次阻塞。。
那它和synchronized关键字有什么区别呢?
虽然synchronized是语言层面的,Lock是上层应用层面的一个库,其实从底层实现原理来讲,是一样的,都是pthread mutex。但在使用层面,Lock更灵活:
(1)有公平/非公平策略
(2)有tryLock(),非阻塞方式。Synchronized只有阻塞方式
(3)有lockInterruptibly,可以响应中断。Synchronized不能响应中断
(4)Lock对应的Condition,其使用方式要比Synchronized对应的wait()/notify()更加灵活。
除此之外,2者加锁后,thread所处的状态是不一样的:
synchronized加锁,thread是处于Blocked状态;
Lock枷锁,thread是处于Waiting状态!
另外一个不一样的是,synchronized关键字内部有自旋机制,先自旋,超过次数,再阻塞;Lock里面没有用自旋机制。
- Java多线程 -- JUC包源码分析4 -- 各种锁与无锁
- Java多线程 -- JUC包源码分析4 -- 各种锁与无锁
- Java多线程 -- JUC包源码分析17 -- 弱一致性与无锁队列
- Java多线程 -- JUC包源码分析1 -- CAS/乐观锁
- Java多线程 -- JUC包源码分析1 -- CAS/乐观锁
- Java多线程 -- JUC包源码分析15 -- SynchronousQueue与CachedThreadPool
- Java多线程 -- JUC包源码分析18 -- ConcurrentSkipListMap(Set)/TreeMap(Set)/无锁链表
- Java多线程 -- JUC包源码分析10 -- ConcurrentLinkedQueue源码分析
- Java多线程 -- JUC包源码分析11 -- CyclicBarrier源码分析
- Java多线程 -- JUC包源码分析12 -- ThreadPoolExecutor源码分析
- Java多线程 -- JUC包源码分析16 -- Exchanger源码分析
- Java多线程 -- JUC包源码分析14 -- ScheduledThreadPoolExecutor与DelayQueue源码分析
- Java多线程 -- JUC包源码分析9 -- AbstractQueuedSynchronizer深入分析-- Semaphore与CountDownLatch
- Java多线程 -- JUC包源码分析3-- volatile/final语义
- Java多线程 -- JUC包源码分析6 -- ConcurrentHashMap
- Java多线程 -- JUC包源码分析19 -- ForkJoinPool/ForkJoinTask
- Java多线程 -- JUC包源码分析3-- volatile/final语义
- Java多线程 -- JUC包源码分析13 -- Callable/FutureTask源码分析
- js 定时器 倒计时
- 集合的类型与各自的特性
- AO+Java实现比例尺、指北针、图名、图例的添加(ArcGIS Add-in for Java)
- 关于controller层向前段输出json数据
- Cython初探
- Java多线程 -- JUC包源码分析4 -- 各种锁与无锁
- indexPathForSelectedRow returning nil
- SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)
- C# 三种动态调用WebService的方法,测试通过
- tnsnames.ora的目录
- 聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障
- Python 是如何找模块的呢?
- 上传文件-java.lang.NoSuchFieldException: resourceEntries
- java.lang.IllegalStateException: Optional int parameter 'pageSize' is present but cannot be translat