打赏

Java 并发系列之五:java 锁

1. Lock接口

2. 队列同步器AQS

3. 重入锁 ReentrantLock

4. 读写锁 ReentrantReadWriteLock

5. LockSupport工具

6. Condition接口

7. CAS

8. synchronized

9. 锁的内存语义

10. txt

  1   2     Lock接口
  3         锁是用来控制多个线程访问共享资源的方式。一般来说一个锁可以防止多个线程同时访问共享资源(但有些锁可以允许多个线程访问共享资源,如读写锁)。
  4         在Lock接口出现前,java使用synchronized关键字实现锁的功能,但是在javaSE5之后,并发包中提供了Lock接口(以及其实现类)用来实现锁的功能。
  5         Lock V.S. synchronized 
  6             Lock接口有而synchronized不具备的主要特性
  7                 尝试非阻塞地获取锁
  8                     当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  9                 超时获取锁
 10                     在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回
 11                 能被中断地获取锁
 12                     与synchronized不同,获取到所得线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
 13             Lock提供了与synchronized相似的功能,但必须显示的获取锁与释放锁,虽然不及隐式操作方便,但是拥有了锁获取与释放的可操作性、可中断的锁获取与超时获取锁等多重功能。
 14         Lock API
 15             void lock()
 16                 获取锁,调用该方法当前线程将会获取锁,当获取锁后,从该方法返回
 17             void lockInterruptibly() throws InterruptedException
 18                 可中断地获取锁,会响应中断(在锁的获取中可以中断当前线程,如果中断则出现异常)
 19             boolean tryLock()
 20                 尝试非阻塞的获取锁,调用该方法后立即返回,如果能够获取则返回true,否则返回false
 21             boolean tryLock(long time, TimeUnit unit) throws InterruptedException
 22                 超时的获取锁,当前线程在以下3种情况下会返回
 23                     当前线程在超时时间内获得了锁
 24                     当前线程在超时时间内被中断
 25                     超时时间结束,返回false
 26             void unlock()
 27                 释放锁
 28             Condition newCondition()
 29                 获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用后,当前线程将释放锁
 30     队列同步器AQS
 31         队列同步器AbstractQueuedSynchronizer(以下简称同步器), 是用来构建锁或者其他同步组件(继承lock)的基础框架,
 32             它使用了一个int成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
 33             同步框架,提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列
 34             基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch和FutureTask(任务)
 35         使用方式
 36             继承
 37                 同步器的主要使用方法是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
 38             静态内部类
 39                 子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放方法来供自定义同步组件使用
 40             独占式共享式获取同步状态
 41                 同步器即可以支持独占式获取同步状态,也可以支持共享式地获取同步状态,这样方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock、CountDownLatch等)。
 42         锁 VS 同步器
 43             联系
 44                 同步器是实现锁(也可以是任何同步组件)的关键:在锁中聚合同步器,利用同步器实现锁的语义。lock方法内部调用实现了AQS的内部类的require方法
 45             区别
 46                 锁是面向使用者的,他定义了使用者与锁交互的接口(比如允许两个线程并行访问),隐藏了实现细节;
 47                 同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步管理状态、线程的排队、等待与唤醒等底层操作。
 48                 锁让使用者仅仅是调用其方法既可以实现同步效果、同步器让实现者通过重写抽象方法进行了队列的底层操作。他们两个是使用者和实现者关注不同的领域实现了相同的效果。
 49         AQS API
 50             同步器基于模板设计模式实现的,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义的同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法会调用使用者重写的方法。
 51             自定义同步组件(extends Lock)将使用同步器 (静态内部类) 提供的模板方法来实现自己的同步语义
 52             访问或修改同步状态
 53                 重写同步器指定方法时需要使用同步器提供的如下三个方法来访问或修改同步状态
 54                 getState():获取当前同步状态
 55                 setState(int newState):设置当前同步状态
 56                 compareAndState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。
 57             同步器可重写的方法
 58                 protected boolean tryAcquire(int arg)
 59                     独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态。
 60                 protected boolean tryRelease(int arg)
 61                     独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态
 62                 protected int tryAcquireShared(int arg)
 63                     共享式获取同步状态,返回大于等于0的值,表示获取成功,反之,获取失败
 64                 protected boolean tryReleaseShared(int arg)
 65                     共享式释放同步状态
 66                 protected boolean isHeldExclusively()
 67                     当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占
 68             同步器提供的模板方法
 69                 独占式获取与释放同步状态
 70                     void acquire(int arg)
 71                         独占式获取同步状态, 如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用重写的tryAcquire(int arg)
 72                     void acquireInterruptibly(int arg)
 73                         响应中断,当前线程未获取到同步状态而进入同步队列中,如果当前线程被中断,会抛出中断异常并返回
 74                     boolean tryAcquireNanos(int arg, long nanos)
 75                         响应中断+超时控制
 76                     boolean release(int arg)
 77                         独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒
 78                 共享式获取与释放同步状态
 79                     void acquireShared(int arg)
 80                         共享式获取同步状态,如果当前线程未获取到同步状态,这将会进入同步队列等待,与独占式获取的主要区别是在同一时刻可以有多个线程获取到同步状态
 81                     void acquireSharedInterruptibly(int arg)
 82                     void tryAcquireSharedNanos(int arg,long nanos)
 83                     boolean releaseShared(int arg)
 84                 查询同步队列中的等待线程情况
 85                     Collection<Thread> getQueuedThreads()
 86                         获取等待在同步队列上的线程集合
 87         队列同步器的实现分析
 88             同步器是如何实现线程同步的
 89             同步队列
 90                 原理:同步器依赖于内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构成一个节点(Node)并将其加入同步队列,同时阻塞当前线程,当同步状态释放时,会将首节点中的线程唤醒,使其再次尝试获取同步状态。
 91                 同步队列的基本结构插图
 92                 同步器
 93                     head: 首节点引用
 94                     tail:尾节点引用
 95                 FIFO双向同步队列:first in first output,先入先出。
 96                     没有成功获取同步状态的线程将会成为节点(Node)加入该队列的尾部
 97                     设置尾节点
 98                         线程加入队列的过程必须保证线程安全,同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),保证线程安全。
 99                     设置首节点
100                         同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点
101                         设置首节点是通过获取同步状态成功的线程完成的,由于只有一个线程能够获取到同步状态,因此设置头节点的方法并不需要CAS来保障,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
102                 节点(Node)AQS内部类
103                     用来保存获取同步状态失败的线程引用、等待状态以及前驱和后继节点信息。
104                     节点属性和名称
105                         int waitStatus
106                             等待状态
107                                 INITIAL: 值为0,初始状态
108                                 CANCELLED: 值为1,同步队列中等待的线程等待超时或者中断,需要从同步队列中取消等待
109                                 SIGNAL: 值为1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,将会通知后续节点,使得后续节点得以运行
110                                 CONDITION: 值为2,节点在等待队列中,节点等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入对同步状态的获取中
111                                 PROPAGATE: 值为3,表示下一次共享同步状态获取将会无条件地被传播下去
112                         Node prev
113                             前驱结点,当节点加入同步队列时被设置(尾部添加)
114                         Node next
115                             后继节点
116                         Node nextWaiter
117                             等待队列的后继节点,如果当前线程是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点公用同一个字段
118                         Thread thread
119                             获取同步状态的线程
120             独占式同步状态获取与释放
121                 核心
122                     在获取同步状态时,同步器会维持一个同步队列,获取失败的线程都会被加入到同步队列中,并在同步队列中自旋(判断自己前驱节点为头节点)。
123                     移出队列(停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
124                 实现
125                     acquire(int arg)方法可以获取同步状态,该方法对中断不敏感,也就是说由于线程获取同步状态失败后进入同步队列中,后继对线程进行中断操作时,线程不会从同步队列移除。
126                     tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,enq(final Node node)中,同步器通过死循环的方式来确保节点的添加,在死循环中只有通过CAS将当前节点设置为尾节点之后,当前线程才能从该方法返回,否则的话当前线程不断地尝试设置。最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
127                     当前线程获取同步状态完成相应逻辑后,需要释放同步状态,通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。unparkSuccerssor(Node node)方法使用LcokSupport来唤醒处理等待状态的线程。
128                 获取流程
129                     第一步,获取同步状态成功,退出;失败则生成节点,加入到同步队列尾部
130                     第二步,CAS判断前驱节点是否为头节点,是的话获取同步状态;不是则进入等待状态,之后如果线程被中断或者前驱节点被释放,继续第二步CAS判断
131                     第三步,前驱节点是头节点,且本节点获取同步状态成功,将当前节点设置成为头节点;失败则进入等待状态,之后如果线程被中断或者前驱节点被释放,继续第二步CAS判断
132             共享式同步状态获取与释放
133                 共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
134                 共享是可以在同一时刻所有共享线程对资源进行访问的,而独占的话是在同一时刻只有一个线程能够访问。
135                 读写为例:写操作要求对资源的独占,而读操作是可以共享式的访问。
136                 实现
137                     acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,
138                         doAcquireShared(int arg)方法的自旋过程中,如果当前节点的前驱为头节点时,尝试获取同步状态,如果返回值大于等于0,表示该次获取同步状态成功,并从自旋过程中退出。
139                     tryAcquireShared(int arg)方法返回值为int型,当返回值大于等于0时,表示能够获取到同步状态。
140                     共享式获取也需要释放同步状态,通过调用releaseShared(int arg)方法可以释放同步状态,该方法释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件,它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般都是通过CAS和循环来保证的,因为释放同步状态的操作会同时来自多个线程。
141             独占式超时获取同步状态
142                 可以被看做响应中断获取同步状态过程的增强版
143                     响应中断+超时控制
144                 核心
145                     该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上不同。
146                     如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于0表示超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker, long nanos)方法返回)
147                 实现
148                     通过调用同步器的tryAcquireNanos(int arg,long nanosTimeout)方法可以超时获取同步状态,即在指定的时间内获取同步状态,如果获取到同步状态则返回true,否则,返回false。
149                     doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上,增加了超时获取的特性。为了针对超时获取,主要需要计算出还需要睡眠的时间间隔nanosTimeout,为了防止过早通知,nanosTimeout计算公式为:nanosTimeout=nowlastTime,其中now为当前唤醒时间,lastTime为上次唤醒时间,nowlastTime为已经睡眠的时间,如果nanosTimeout大于0表示超时时间未到,需要继续睡眠nanosTimeout纳秒,反之,表示已经超时。如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自旋过程。原因:非常短的超时等待无法做到十分精确,如果这时再进行超时等待,相反会让nanosTimeout的超时从整体上表现的不精确。因此在超时非常短的场景下,同步器会进入无条件的快速自旋。
150                 获取流程
151                     第一步,加入同步队列尾部
152                     第二步,判断前驱节点是否为头节点,是则获取同步状态,获取成功将当前节点设置成头节点;前驱节点不是头节点或者获取失败则计算时间间隔nanosTimeout
153                     第三步,如果nanosTimeout大于0表示超时时间未到,需要继续睡眠nanosTimeout纳秒,线程被通知后,重新计算超时nanosTimeout; 如果小于0,表示已经超时,直接退出
154                     第四步,判断当前线程是否中断,如果是,抛出中断异常并退出; 如果不是,转回第三步
155             自定义同步组件TwinsLock
156                 同步组件功能:该组件同一时刻最多只允许两个线程访问,超过两个线程的访问将被阻塞,我们将这个同步工具命名为TwinsLock。
157                 核心步骤
158                     实现Lock
159                         TwinsLock实现了Lock接口,提供了面向使用者的接口,使用者调用lock()方法获取锁,使用unlock()释放锁,而且同一时刻只能有两个线程获取到锁
160                     定义同步器AQS
161                         自定义同步组件通过组合自定义同步器来完成同步功能,一般情况下自定义同步器会被定义为自定义同步组件的内部类。
162                     确定访问模式
163                         TwinsLock能够在同一时刻支持多个线程的访问,这是共享式访问。
164                         同步器应该提供acquireShared(int args)方法与Shared相关的方法。
165                         这就要求TwinsLock必须重写tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,这样才能保证同步器的共享式同步状态的获取与释放方法的执行。tryAcquireShared(int args) 返回值大于等于0时,当前线程才能获取同步状态,对于上层的TwinsLock而言,则表示当前线程获取锁 lock()。
166                     定义资源数
167                         TwinsLock在同一时刻允许至多两个线程的同时访问,同步资源数为2。设置初始状态status为2,当一个线程进行获取时,status减1,该线程释放,则status加1,状态的合法范围0、1、2。其中0代表两个线程已经获取了同步资源,此时再有其他线程对同步状态进行获取,该线程只能被阻塞。在同步状态变更时,需要使用compareAndSet(int expect, int update)方法做原子性保障
168     重入锁
169         ReentrantLock
170             排他锁
171             可重入
172                 可重入锁表示锁能够支持一个线程对资源的重复加锁
173                     调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞
174                 实现需要解决两个问题
175                     1. 线程再次获得锁
176                         锁需要去识别获取获取锁的线程是否为当前线程,如果是,则再次成功获取
177                     2. 锁的最终释放
178                         线程重复N次获取了锁,随后在第N次释放该锁后,其他线程能够获取到该锁。锁的最终释放要求锁对于获取进行计数自增,计数表示当前锁被重复获取的次数,当锁被释放时,计数自减,当计数等于0时表示锁已经成功释放。
179             比synchronized(隐式的支持重进入)更强大,灵活的锁机制,可以减少死锁发生的概率
180             分为公平锁和非公平锁
181                 公平锁
182                     锁的获取顺序按照FIFO原则,代价是大量的线程切换
183                     公平的锁机制往往没有非公平的效率高,
184                     公平锁能减少饥饿发生的概率,等待越久的请求越是容易得到优先满足
185                 非公平锁
186                     CAS设置同步状态成功,表示当前线程成功获得锁; 在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待
187                     非公平锁可能使线程饥饿,但是极少的线程切换,保证了较大的吞吐量
188                     获取锁的抢占机制,是随机获得锁的
189                 默认是非公平锁
190             底层采用AQS实现,通过内部Sync继承AQS,通过组合自定义同步器AQS来实现锁的同步和释放
191                 同步状态表示锁被一个线程重复获取的次数
192             API
193                 void lock()
194                     获取锁,调用该方法当前线程将会获取锁,当获取锁后,从该方法返回,持有了对象监视器,其他线程只有等待锁被释放时再次争抢
195                 getHoldCount()
196                     查询当前线程保持此锁的个数,也就是调用lock()方法的次数
197                         注意 当前线程
198                 getQueueLength()
199                     返回正等待获取此锁的线程估计数,比如都在等待sleep()
200                 hasQueuedThread(Thread thread)
201                     查询指定的线程是否正在等待获取锁
202                 hasQueuedThreads()
203                     查询是否有线程正在等待获取此锁
204                 getWaitQueueLength(Condition condition)
205                     返回等待与此锁相关的condition的线程估计数,比如condition.await()方法
206                 hasWaiters(Condition condition)
207                     查询是否有线程正在等待与此锁有关的condition条件
208                 isFair()
209                     判断是不是公平锁
210                 isHeldByCurrentThread()
211                     当前线程是否保持此锁
212                 isLocked()
213                     查询此锁是否由任意线程保持
214                 等待获得锁的线程
215                 等待condition的线程
216     读写锁
217         ReentrantReadWriteLock
218             读写锁,两把锁:共享锁读锁、排它锁写锁; 
219             背景
220                 没有读写锁之前,读写需要用等待通知机制,写操作依赖Synchronized关键字进行同步,防止出现脏读(jdk1.5之前)
221                 读写锁通过读写分离,提升了并发性和写操作对读操作的可见性
222                 读写锁的性能都会比排它锁好,因为大多数场景读是多于写的,在读多于写的情况下,读写锁能够提供比排它锁更好的并发量和吞吐量。
223                 Java提供读写锁的实现是ReentrantReadWriteLock
224             特性
225                 公平性选择
226                     支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平
227                 重进入
228                     支持重进入,比如:读线程获得读锁之后,能够再次获取读锁;写线程获取写锁之后,能够再次获取写锁,同时也可以获取读锁
229                 锁降级
230                     遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
231             父类接口ReadWriteLock
232                 readLock()
233                 writeLock()
234             主要函数
235                 int getReadLockCount()
236                     返回当前读锁被获取的次数。该次数不等于获取读锁的线程数
237                 int getReadHoldCount()
238                     返回当前线程获取读锁的次数
239                 boolean isWriteLocked()
240                     判断写锁是否被获取
241                 int getWriteHoldCount()
242                     返回当前写锁被获取的次数
243                 lock.readLock().lock()
244                 lock.readLock().unlock()
245                 lock.writeLock().lock()
246                 lock.writeLock().unlock()
247                 读读共享,写写互斥,读写互斥,写读互斥
248             实现分析
249                 读写状态的设计
250                     依赖自定义同步器AQS来实现同步功能
251                     同步状态(一个整型变量,高16表示读,低16表示写)上维护多个读线程和一个写线程的状态,按位切割使用同步状态
252                     假设当前的同步状态值为S
253 写状态=S & 0x0000FFFF;  读状态= S >>>16
254 当写状态加1= S+1;  当读状态加1 = S + (1<<16)
255                 写锁的获取与释放
256                     支持重进入的排它锁
257                     如果当前线程已经获取了写锁,则增加写状态。
258                     如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,保证了写锁的操作对读锁的可见性
259                     每次释放时均减少写状态,当写状态为0时,表示写锁已经被释放
260                 读锁的获取与释放
261                     支持重进入的共享锁
262                     如果当前线程已经获取了读锁,则增加读状态
263                     如果当前线程在获取读锁时,写锁已经被其他线程获取,则进入等待状态
264                     如果当前线程已经获取了写锁或者写锁未被获取,则当前线程增加(线程安全,依靠CAS保证)读状态,成功获取读锁
265                     读锁的每次释放均减少读状态(线程安全,可能有多个读线程同时释放读锁)
266                 锁降级
267                     锁降级指的是写锁降级为读锁
268                     遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁
269                     锁降级中读锁的获取是有必要的,可以保证数据的可见性
270                     不支持锁升级也是为了保证数据的可见性
271     LockSupport工具
272         阻塞和唤醒一个线程
273         方法
274             park()
275                 阻塞当前线程,如果调用unpark(Thread thread)方法或者当前线程被中断,才能从park()方法返回
276             parkNanos(long nanos)
277                 阻塞当前线程,最长不超过nanos纳秒,返回条件为park()基础+超时返回
278             parkUntil(long deadline)
279                 阻塞当前线程,直到deadline时间,从1970年到deadline时间的毫秒数
280             unpark(Thread thread)
281                 唤醒处于阻塞状态的线程
282             park(Object blocker)
283                 blocker标识当前线程在等待的对象,该对象主要用于问题排查和系统监控
284             parkNanos(Object blocker, long nanos)
285             parkUntil(Object blocker, long deadline) 
286             方便问题定位,问题排查和系统监控
287         对于线程的状态而言,java.concurrent.Lock为   等待状态,因为Lock接口对于阻塞的实现(AQS  require, release)使用了LockSupport类中的相关方法
288     Condition接口
289         Lock提供条件Condition,对线程的等待、唤醒操作更加详细和灵活,依赖于lock
290         背景
291             synchronized+wait/notify实现等待通知模式
292                 被通知的线程是JVM随机选择的
293                 所有的线程都会注册在一个对象上
294                 伪代码
295                     synchronized(对象) {
296     while(条件满足) {
297           对象.wait();
298     }
299     对应的处理逻辑
300     对象.notifyAll();
301 }
302             condition + lock 实现等待通知模式
303                 支持选择性通知,调度线程上更加灵活
304                 Lock对象里面可以创建多个Condition实例(对象监视器)
305                 支持单独唤醒部分制定线程
306                 伪代码
307                         lock.lock();
308     while(条件满足) {
309           condition.await();
310     }
311     对应的处理逻辑
312     condition.signalAll();
313 }
314         Object Monitor Methods  V.S. Condition
315             前置条件
316                 1. 获取对象的锁synchronized(obj)
317                 2. 调用Lock.lock()获取锁
318 调用Lock.newCondition()获取Condtion对象
319 调用 Lock.unlock()释放锁
320             调用方式
321                 1. 直接调用 object.wait() object.notify()
322                 2. 直接调用 condition.await() condition.signal()
323             等待队列个数
324                 1个; 多个
325             当前线程释放锁并进入等待状态
326                 支持;支持
327             当前线程释放锁并进入等待状态,在等待状态中不响应中断
328                 不支持;支持
329             当前线程释放锁并进入超时等待状态
330                 支持;支持
331             当前线程释放锁并进入等待状态到将来某一个时刻
332                 不支持;支持
333             唤醒等待队列中的一个线程
334                 支持;支持
335             唤醒等待队列中的全部线程
336                 支持;支持
337         主要方法
338              void await() throws InterruptedException;
339                 线程进入等待状态直到被通知 (获得condition对象锁对应的锁) 或者中断
340              void awaitUninterruptibly();
341                 线程进入等待状态直到被通知,对中断不敏感
342              long awaitNanos(long nanosTimeout) throws InterruptedException;
343                 线程进入等待状态直到被通知、中断或者超时,返回值表示剩余的时间,返回值<=0可认为超时了
344             boolean await(long time, TimeUnit unit) throws InterruptedException;
345              boolean awaitUntil(Date deadline) throws InterruptedException;
346                 线程进入等待状态直到被通知、中断或者到某个时间,未到时间点就被通知,返回true;到了指定时间,返回false
347             void signal();
348                 唤醒一个等待在condition上的线程,该线程从等待方法返回前必须获得与condition相关联的锁lock
349             void signalAll();
350                 唤醒所有等待在condition上的线程,该线程从等待方法返回前必须获得与condition相关联的锁lock
351         实现分析
352             Condition的实现类ConditonObject是同步器AQS的内部类(ConditionObject implements Condition),
353             等待队列
354                 等待队列是一个FIFO的队列
355                 每个Condition对象都包含者一个队列(等待队列)
356                     首节点
357                         firstWalker
358                     尾节点
359                         lastWalker
360                             每次新增节点,更新指向
361                     节点
362                         Node(ASQ的内部类)
363                             包含了一个线程引用 nextWalker
364                 新增节点
365                     当前线程调用await()方法,线程将当前线程构造一个节点(Node),并将节点加入到该等待队列的尾部进入等待状态,更新引用指向如lastWaiter,最后释放锁。
366                 在Object的监视器模型上,一个对象拥有一个同步队列和等待队列; 而并发包中的Lock(同步器)拥有一个同步队列和多个等待队列
367             等待
368                 await()
369                     线程进入等待状态直到被通知 (获得condition对象锁对应的锁) 或者中断
370                 如果从队列的角度看此方法,相当于同步队列的首节点(获得了锁的节点)重构成一个新的节点加入到condition的等待队列中了
371                 加入等待队列后,释放同步状态,唤醒同步队列的后继节点,然后当前线程进入等待状态
372             通知
373                 signal()
374                     唤醒一个等待在condition上等待时间最长的线程|首节点,该线程从等待方法返回前必须获得与condition相关联的锁lock
375                 获取锁的线程获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程
376                 signalAll()
377                     相当于将等待队列中的所有节点全部移动到同步队列中,并唤醒每个节点的线程
378         注意
379             在调用condition.await()方法之前必须调用lock.lock()代码获得同步监视器,否则会报错IllegalMonitorStateException异常
380             使用wait()、notify()、notifyAll()方法都需要先对调用对象加锁。如果没有持有适当的锁,也会抛出IllegalMonitorStateException
381         等待通知模式
382     锁的内存语义
383         锁的功能
384             让临界区互斥执行
385             锁的内存语义
386         锁的释放与获取的内存语义
387             释放锁的线程向获取同一个锁的线程发送消息
388             线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存
389             线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从内存中读取共享变量
390             volatile的写读与 锁的释放获取 有相同的内存效果:volatile写和锁的释放有相同的内存语义,volatile读和锁的获取有相同的内存语义。这也是增强volatile的原因,提供了一种比锁更轻量级的线程之间的通信机制。
391         锁的释放与获取的内存语义两种实现
392             1. 利用volatile 变量的 写读 锁具有的内存语义
393             2. 利用CAS(AQS里面的一个调用本地的方法,调用处理器cmpxchg指令添加lock前缀)所附带的volatile读和volatile写的内存语义
394         公平锁和非公平锁的内存语义
395             公平锁和非公平锁释放时,最后都要写一个volatile变量state
396             公平锁获取时,首先会去读volatile变量。
397             非公平锁获取时,首先会用CAS更新volatile变量,CAS同时具有volatile读和volatile写的内存语义
398         JUC 包的实现
399             通用化的实现模型
400                 1. 声明共享变量为volatile
401                 2. 使用CAS的原子条件更新来实现线程之间的同步
402                 3. 同时配合volatile的读写和CAS具有的volatile读写的内存语义来实现线程之间通信
403                 4. 基础类基于123:AQS、非阻塞结构、原子变量类
404                 5. 高层类基于4:Lock、同步器、阻塞队列、Executor、并发容器
405         锁的升级与对比
406             JAVA1.6中,锁一共有4中状态,级别从低到高依次是:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态
407             这几种状态会随着竞争情况的逐渐升级,锁可以升级但是不能降级,这种策略是为了提高获得锁和释放锁的效率
408             无锁
409             偏向锁
410                 背景
411                     大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
412                     偏向锁使用了一种等到竞争出现才释放锁的机制。当其他线程尝试竞争偏向锁时持有偏向所的线程才会释放锁。
413                 重要操作
414                     获得偏向锁
415                         1. 一个线程访问同步块并获得锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
416                         2. 以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要看对象头Mark Word里面是否存储着指向当前线程的偏向锁,如果有,表示线程已经获得了锁,如果没有,下一步
417                         3. 判断Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有,使用CAS竞争锁; 如果有,则尝试使用CAS将对象头的偏向锁线程ID指向当前线程
418                     撤销偏向锁
419                         其他线程访问同步块,尝试获得偏向锁,失败,启动撤销偏向锁
420                         1. 首先暂停持有偏向锁的线程
421                         2. 解锁,将线程ID设为空
422                         3. 恢复线程
423                     关闭偏向锁
424                         java 7里默认是启用的
425                         关闭延迟
426                             XX:BiasedLockingStartupDelay=0
427                         关闭偏向锁,程序默认会进入轻量级锁
428                             XX:UseBiasedLocking=false
429                 应用
430                     优点
431                         加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差别
432                     缺点
433                         如果线程间存在锁竞争,会带来额外的锁撤销的消耗
434                     使用场景
435                         适用于只有一个线程访问同步块场景
436             轻量级锁
437                 注意,轻量级锁解锁失败,锁就会膨胀成为重量级锁,就不会恢复到轻量级锁状态,当线程处于这个状态,其他线程试图获取锁时,会被阻塞住,当持有所得线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新一轮的夺锁之争。
438                 重要操作
439                     轻量级加锁
440                         1. 访问同步块,分配空间并复制Mark Word到栈 Displaced Mark Word
441                         2. CAS尝试将对象头中Mark Word替换为指向锁记录的指针,成功则获得锁,执行同步体;失败表示其他线程竞争锁,当前线程通过使用自旋来获取锁
442                     轻量级解锁
443                         1. 解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头
444                         2. 如果成功,表示没有竞争发生,如果失败,表示当前锁存在竞争(因为别的线程在争夺锁),锁就会膨胀成为重量级锁(别的线程阻塞),释放锁并唤醒等待的线程
445                 应用
446                     优点
447                         竞争的线程不会阻塞,提高了程序的响应速度
448                     缺点
449                         如果始终得不到锁竞争的线程,使用自旋会消耗CPU
450                     使用场景
451                         追求响应时间
452                         同步块执行速度非常快
453             重量级锁
454                 应用
455                     优点
456                         线程竞争不使用自旋,不会消耗CPU
457                     缺点
458                         线程阻塞,响应时间缓慢
459                     使用场景
460                         追求吞吐量
461                         同步块执行速度较长
462     Sychronized
463         同步、重量级锁
464         只有使用Synchronized线程处于阻塞,其他Lock, AQS, LockSupport等线程都是处于等待状态
465         原理
466             synchronized可以保证方法或者代码块在运行时,同一时刻只有一个方法可以进入到临界区,同时它还可以保证变量的内存可见性。
467         锁对象
468             1. 普通同步方法锁,是当前实例对象
469             2. 静态同步方法,锁是当前类的class对象
470             3. 同步方法块,锁是括号里面的对象
471             java中的每一个对象都可以作为锁
472         实现机制
473             Java对象头
474                 synchronized用的锁是保存在Java对象头里的
475                 包括两部分数据
476                     Mark Word(标记字段)
477                         Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间
478                         包括:哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳
479                     Klass Pointer(类型指针)  
480             monitor
481                 java 中 Object和Class中都关联了一个Monitor,一个monitor只能被一个线程拥有
482                 Owner 活动线程
483                     初始时为NULL表示当前没有任何线程拥有该monitor record, 当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
484                 实现
485                     同步代码块采用 monitorenter、monitorexit指令显示的同步
486                     同步方法使用ACC_SYNCHRONIZED标记符隐式的实现
487         锁优化
488             自旋锁
489                 该线程等待一段时间,不会被立即挂起,循环看持有锁的线程是否会很快释放锁
490                 自旋数字难以控制(XX: preBlockSpin)
491                 存在理论:线程的频繁挂起、唤醒负担较重,可以认为每个线程占有锁的时间很短,线程挂起再唤醒得不偿失。
492                 缺点
493                     自旋次数无法确定
494             适应性自旋锁
495                 自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定
496                 自旋成功,则可以增加自旋次数,如果获取锁经常失败,那么自旋次数会减少
497             锁消除
498                 若不存在数据竞争的情况,JVM会消除锁机制
499                 判断依据
500                     变量逃逸
501             锁粗化
502                 将多个连续的加锁、解锁操作连接在一起,扩展成一个范围更大的锁。例如for循环内部获得锁
503             轻量级锁
504                 在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
505                 通过CAS(CompareandSwap),即比较并替换,来获取和释放锁
506                 缺点
507                     在多线程环境下,其运行效率比重量级锁还会慢
508                 性能依据
509                     对于绝大部分的锁,在整个生命周期内部都是不会存在竞争的
510             偏向锁
511                 为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径
512                 主要尽可能避免不必要的CAS操作,如果竞争锁失败,则升级为轻量级锁
513     CAS
514         Compare And Swap, 整个JUC体系最核心、最基础理论,Java中通过锁和CAS实现原子操作
515         内存地址V,旧的预期值A,要更新的值B,当且仅当内存地址V中的值等于旧的预期值A时才会将内存值V得值修改为B,否则什么也不干
516         native中存在四个参数
517         JVM中的CAS操作利用了处理器提供的CMPXCHG指令实现的。
518         缺陷
519             ABA问题
520                 检查不到值的变化,实际上却变化了
521                 解决方案
522                     变量前追加版本号版本号
523                     AtomicStampedReference
524                         这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子的方式将该引用和该标志的值设置为给定的更新值。
525             循环时间太长
526                 自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
527                 解决方法
528                     JVM如果能支持处理器提供的pause指令,那么效率一定会提升
529                     pause作用
530                         1. 可以延迟流水线执行指令(depipeline),使CPU不会消耗过多的执行资源
531                         2. 避免在退出循环的时候因内存顺序冲突而引起的CPU流水线被清空,从而提高CPU的执行效率
532             只能保证一个共享变量原则操作
533                 对多个共享变量操作时,CAS无法保证操作的原子性,需要用锁
534                 解决方案
535                     AtomicReference类保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作
536         CAS V.S. 锁
537             JVM中除了偏向锁,其他锁(轻量级锁、互斥锁)的实现方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。
538         原子操作类Atomic
539             java.util.concurrent.atomic里的原子操作类提供了线程安全地更新一个变量的方式
540             4大类型13个原子操作类
541                 基本类型类
542                     AtomicBoolean
543                     AtomicInteger
544                     AtomicLong
545                 数组
546                     AtomicIntegerArray
547                     AtomicLongArray
548                     AtomicReferenceArray
549                 引用
550                     AtomicReference
551                     AtomicReferenceFieldUpdater
552                     AtomicMarkableReference
553                 属性
554                     AtomicIntegerFieldUpdater
555                     AtomicLongFieldUpdater
556                     AtomicStampedReference
557             核心底层
558                 CAS
559                     Unsafe只提供了3中CAS方法
560                         final native boolean compareAndSwapObject()
561                         final native boolean compareAndSwapInt()
562                         final native boolean compareAndSwapLong()
View Code

 

11. 参考网址

  1. 参考来源:http://cmsblogs.com/wp-content/resources/img/sike-juc.png
  2. 《Java并发编程的艺术》_方腾飞PDF 提取码:o9vr
  3. http://ifeve.com/the-art-of-java-concurrency-program-1/
  4. Java并发学习系列-绪论
  5. Java并发编程实战
  6. 死磕 Java 并发精品合集

 

posted @ 2019-07-17 11:31  海米傻傻  阅读(402)  评论(0编辑  收藏  举报