Java并发编程

一、并发编程的挑战

并发编程的目的是为了让程序运行的更快。
但并发编程也会带来很多问题:例如上下文切换、死锁、硬件软件资源限制等。

上下文切换

1、什么是上下文切换?
cpu通过不断切换线程,让我们感觉多个线程同时执行。
这种切换前会保存上一个任务的状态,以便切换回此任务时,能加载这个任务的状态。
任务从保存到再加载就是一次上下文切换。

2、那么多线程一定快吗(并行一定比串行快吗?)
不一定,由于有线程创建和上下文切换的开销。

3、减少上下文切换
无锁并发编程:不同线程处理不同数据
CAS算法:使用CAS更新数据,不需要加锁
使用最少线程
协程:单线程内实现多任务调度

死锁

通常出现的原因是因为某种异常没有释放掉锁。
1、避免方法:

  1. 避免一个线程同时获取多个锁;
  2. 避免一个线程在锁内同时占用多个资源;
  3. 尝试定时锁
  4. 数据库锁的加锁解锁必须在一个数据库连接中,否则可能解锁失败
硬件软件资源限制

硬件用集群;软件用资源池;

二、并发机制底层实现原理

1、Volatile

定义: 如果一个字段被声明为volatile,Java内存模型确保所有线程看到这个变量的值是一致的。
作用: 保证共享变量的可见性
实现原理:

  1. 将当前处理器缓存行的数据写回内存
  2. 一个处理器的缓存回写到内存会导致其它处理器的缓存无效

使用优化: 64字节宽处理器通过追加字节能提高并发效率

2、Synchronized:同步

1.6后官方进行一系列优化,使它没有那么重量级
实现原理:

  1. 同步基础是,Java中每个对象都能作为锁
  2. 它在JVM中实现是基于Monitor对象的进入与退出,任何对象都有一个monitor关联,当一个monitor被持有他就处于锁定状态

锁的存储结构:
synchronized用的锁存储在Java对象头的Mark Word里,包含锁的标志位等。

优化引入锁升级机制:1.6锁一共四种状态(无锁、偏向锁、轻量锁、重量锁)

  • 偏向锁:
1、思想:
		锁不仅不存在多线程竞争,而且还由同一个线程多次获取。
2、实现:
		1)当线程获取偏向锁时,会在Java对象头存储线程ID,以后进入只要测试一下对象头即可;
		2)如果没有存储,则利用CAS存储,不成功则引发偏向锁撤销;
		3)撤销会暂停线程并且将对象头的线程ID置空,实现解锁。
  • 轻量级锁:
1、思想:
		通常锁竞争的时候很快就能拿到锁,并不需要阻塞,可通过自旋来等待
2、实现:
		1)分配空间将对象头Mark Word复制到栈中
		2)CAS尝试修改对象头的Mark Word指向当前栈,成功则获得锁;
		3)失败则采用自旋获取锁;
		4)自旋失败,锁膨胀为重量级锁,线程阻塞

各种锁的对比:

偏向锁
		优点:加锁解锁无消耗
		缺点:锁竞争时会带来锁撤销的消耗
		适用:单一线程访问同步
轻量级锁
		优点:竞争线程不会阻塞,提高响应速度
		缺点:自旋消耗CPU
		适用:同步代码执行速度非常快,追求响应时间
重量级锁
		优点:不使用自旋,不消耗CPU
		缺点:线程阻塞,响应时间慢
		适用:同步代码执行时间长,追求吞吐量
3、原子操作实现原理:

处理器保证原子操作:
1、总线锁:当前处理器独占共享内存,其它处理器请求阻塞,开销大
2、缓存锁定:

  1. 内存区域缓存到处理器内部;(原子操作在处理器内部执行)
  2. 回写数据到内存时修改内部的内存地址;
  3. 缓存一致性机制阻止其它回写操作。

3、针对以上两种机制,处理器通过指令来实现,这些指令操作的内存区域会加锁,导致其它处理器不能同时访问它。
Java实现原子操作:

  1. 循环CAS
  2. 并发包的原子类(AtomicInteger、AtomicLong等)

循环CAS:利用处理器的指令实现
自旋CAS就是基于循环进行CAS操作直到成功
存在的问题:

  1. ABA问题:版本号解决
  2. 循环时间过长,开销大:处理器的pause指令
  3. 只保证一个共享变量原子操作:锁或者多个变量合成一个

锁机制实现原子操作:除了偏向锁,其它都是基于 循环CAS

三、Java内存模型

1、引出:线程之间如何通信和同步共享内存和消息传递
2、Java采用共享内存模型,通信隐式进行,同步显式进行。

3、Java内存模型结构:主内存(共享变量)、本地内存(线程私有,共享变量的副本,抽象不存在)

Java线程间通信由Java内存模型(JMM)控制
通信流程:
		1)线程A把本地内存A的更新过的共享变量刷新到主内存去
		2)线程B到主内存读取线程A更新后的共享变量
JMM提供内存的可见性保证,但是重排序有时会破坏可见性,所以JMM针对采取措施

4、重排序:编译器和处理器对指令重排序,为了提高性能

JMM通过禁止一些重排序来提供内存可见性保证

5、happens-before:保证操作之间内存可见性

它对应的是编译器和处理器的重排序规则

6、as-if-serial:单线程内,编译器和处理器不会对存在数据依赖性的操作重排序

数据依赖性:两个操作访问同一变量,其中有写操作(读写、写读、写写)
这样可以保证单线程内不会因为重排序而影响执行结果的正确性。

7、在不改变执行结果的前提下,允许某些重排序来提高并行效率

happens-before的传递性

8、重排序对多线程的影响

对控制依赖的代码重排序,在单线程里不会改变执行结果;但在多线程会,所以要引入同步

9、JMM中在同步代码块内可以重排序,但不允许它跑到同步外面。可见在保证程序执行结果的前提下,尽可能满足编译器和处理器的重排序,来提高并行性能。

volatile内存语义:

一个volatile变量单个的读/写操作 == 和一个普通变量的读/写操作使用同一个锁来同步

volatile变量自身特性:

  1. 可见性(volatile的读,总是能看到对它最后的写)
  2. 原子性(对单个volatile变量读写有原子性,但 i++这种复合操作不具有原子性)

volatile对内存可见性影响:volatile变量的写-读可以实现线程通信
volatile变量的写-读 与 锁的释放-获取 具有相同内存语义

volatile变量的写-读内存语义:

写volatile:发送消息
		JMM会把线程对应的本地内存中的共享变量刷新到主内存
读volatile:接收消息
		JMM会把该线程对应的本地内存置为无效,线程接下来将到主内存读取共享变量

volatile内存语义的实现:通过限制编译器和处理器的重排序

方法:
	加入内存屏障(保守策略保证一定能实现语义)

volatile与 锁对比:

volatile:						 			锁:
	仅仅保证单个变量的读写具有原子性;				保证整个临界区代码执行具有原子性;
	执行性能好									功能更强大
锁 的内存语义:

从volatile的内存语义中可知:锁的 释放-获取 与 volatile 写-读 具有相同内存语义;

实现机制:

  1. 利用volatile变量写-读 所具有的内存语义
  2. 利用CAS附带的volatile读/写的内存语义

concurrent包的实现:
通用化的实现模式:

  1. 声明变量为volatile
  2. 使用CAS更新来实现线程同步
  3. 配合volatile写读和CAS附带的volatile实现线程之间的通信
final域 的内存语义:

写final域的重排序规则:

构造函数内final域的写入,与随后在构造函数外将对象的引用赋值给引用变量,这两个操作不能重排序

读final域的重排序规则:

初次读对象引用,与初次读该对象包含的final域,这两个操作不能重排序

final语义实现: 有些需要加内存屏障,但X86处理器不会插入任何内存屏障

happens-before:JMM的核心

JMM设计:

JMM设计主要考虑两个矛盾点:
	一是 要做到易于理解易于编程,提供足够强的内存可见性保证,就需要一个强内存模型,但这往往需要加强对编译器处理器重排序的束缚
	二是 编译器处理器希望是个弱内存模型,对他们的束缚越小越好,这样他们就能更大限度的优化性能

综上,JMM寻求他们之间的平衡点,遵循基本原则:只要不改变程序执行结果,怎样重排序优化都可以

happens-before的定义:
实际上就是JMM封装后的重排序规则,面向程序开发者。

happens-before的规则:

  1. 程序顺序
  2. 监视器
  3. volatile
  4. 传递性
  5. start
  6. join
双重检查锁定和延迟初始化:

双重检查锁定是延迟初始化的一种实现方式。
双重检查锁存在的问题:

new 对象的时候实际上分为三步:分配对象内存空间、初始化对象 、引用变量指向地址
这三步指令是可以被重排序的,在多线程访问下不能保证可见性

实现延迟初始化方法:

两种思路:不允许重排序;允许重排序但不允许其它线程看到
1)使用volatile声明这个变量
2)通过类初始化时JVM获取锁

四、并发编程基础

1、并发编程的优势
  1. 现代操作系统都是多核处理器,一个处理器只能执行一个线程,多线程执行能充分利用多核处理器,实现并行执行,提高效率;
  2. 提高响应时间,将一个复杂任务分发给多线程异步处理,提高响应速度
  3. Java提供高度封装的并发编程模型,让我们专注于业务问题,而不是纠结于底层实现原理
2、线程的状态:
  1. new
  2. runnable
  3. blocked:只有Synchronized能使线程进入
  4. waiting:其它方法进入(包括Lock接口)
  5. time_waiting
  6. terminated

中断:
interrupt():线程被中断
isInterrupted():判断中断标记,返回Boolean

Terminated和抛出InterruptException的线程会清空中断标记,所以返回 false
(过时)suspend:暂停线程,占用资源会导致死锁;
(过时) stop:终止线程,线程资源没有释放;
安全终止线程方法:interrupt和设置标记位

3、线程通信:

无论是volatile还是synchronized,都含有隐式的线程通信;
显式的通信是指object的wait、notify/notifyAll方法,它们依托同步synchronized实现;

等待-通知范式:

等待方:
	1)获取锁
	2)如果条件不满足,调用wait(),被通知后要检查条件
	3)条件满足才执行
接收方:
	1)获取锁
	2)改变条件
	3)通知等待线程

调用wait方法线程进入等待队列,并且释放锁;
调用notify,将一个等待线程从等待队列移到同步队列,释放锁
从wait中返回的前提是拿到锁

等待超时:

  1. 等待时间:remaining
  2. 超时时间:future = now + remaining
while(remaining >0){
	wait(remaining);
	remaining = future - now;
}
//如果remaining<=0,表示已超时,直接退出,否则继续执行wait
4、线程池:

好处:

  1. 减少系统创建消亡线程的开销
  2. 面对过量任务也能平稳劣化

调用execute(job)方法添加任务到线程池的jobs工作列表;
jobs调用其notify方法,确保有工作者线程被唤醒,执行任务

五、锁

Java中的锁指的是Lock接口。在没有Lock前,都是由Synchronized完成锁功能。
两者对比:

  1. syn隐式的获取释放锁;Lock显式
  2. Lock提供更丰富的特性如:锁可操作性、可中断可超时获取锁、非阻塞获取锁;
1、队列同步器(AQS):

实现锁依赖于同步器。或者说锁的底层功能由同步器来实现。
同步器包括:独占式同步状态、共享式同步状态、查询同步队列(FIFO双向队列);
同步器的实现:同步队列
独占式同步状态获取与释放:

  1. 线程获取同步状态;
  2. 失败则构造同步节点加入同步队列的末尾;
  3. 使该节点中的线程“死循环”获取同步状态(自旋)
  4. 获取不到则阻塞该线程
  5. 前驱节点(为头节点)释放唤醒后继节点,重新竞争同步状态
2、重入锁(ReentrantLock):

主要特性:支持显式重入、支持公平锁和非公平锁

1)显式重入:已经拿到锁的同时,再次获取同一把锁

实现方式:

  1. 再次获取时,增加同步状态。
  2. 最终释放时,逐步减少同步状态,自到同步状态==0;

syn是隐式重入:递归执行时不会阻塞

2)公平锁和非公平锁:
公平锁:按照时间顺序依次执行线程(FIFO队列)
非公平锁:默认选项,容易出现饥饿线程

对比:

  1. 公平锁减少了饥饿线程,但这是以增加上下文切换次数为代价的
  2. 非公平锁中,刚释放锁的线程再次获取锁机会大,减少上下文切换次数,增加吞吐量
3、读写锁(ReentrantReadWriteLock):

主要特性:非排他锁,允许多个线程同时访问同一变量,性能优势明显
内部维护了一个读锁、一个写锁

实现方式:读写状态
一个整形变量(int)高16位作为读状态;低16位作为写状态
判断:当这个变量不等于0时,写状态为0,则读锁被占用;

锁降级:写——>读
流程:拿写锁——拿读锁——放写锁;保证共享内存可见性操作

4、Condition:类比于Object监视器

Object的wait、notify方法与Syn配合可以实现,线程间的等待通知模式。
而Condition是与Lock配合实现线程通信的。

Condition与Object作为监视器对比:

  1. 支持多个等待队列,而Object就一个
  2. 支持等待状态下的中断、超时响应;Object不支持
  3. await、signal<——>wait、notify

实现方式:

  1. 等待队列:FIFO双向队列
  2. await()等待:同步队列的头节点,将当前线程构造成节点加入到等待队列末尾,释放锁,唤醒同步队列中的后继节点
  3. signal()通知:同步队列的头节点,将等待队列的头节点移到同步队列并唤醒它,只有当它成功获取锁时,才会从await中返回

六、Java中的并发容器和框架

1、ConcurrentHashMap

查看ConcurrentHashMap详细介绍
2、ConcurrentLinkedQueue
3、阻塞队列

4、Fork/Join框架

定义:

  • Java7提供的一个用于并行执行任务的框架,把大任务分割成若干个小任务,最终汇总每个小任务结果得到大任务结果的框架。

工作窃取算法:某个线程从其它队列里窃取任务来执行。

  1. 每个小任务分别放入不同的队列中
  2. 不同的线程都有一个自己的任务队列
  3. 线程并行执行自己的任务,当完成自己所有任务后,发现其它线程还没完成,就会从他的任务队列中窃取一个来帮助执行
  4. 为了避免竞争,被窃取线程从队列头部拿任务执行,窃取线程从队列尾部拿取执行。

优点:充分利用线程资源
缺点:某些情况还存在竞争,比如只剩一个任务在双端队列中

Fork/Join的工作流程:
1、分割任务
2、执行任务并合并结果

Fork/Join的实现原理:
1、ForkJoinTask类:我们需要创建ForkJoin任务,它提供在任务中执行的fork和join方法
通常使用它的子类:RecursiveAction、RecursiveTask
2、ForkJoinPool类:ForkJoinTask通过它来执行
3、fork方法:将当前任务存放到任务队列,然后唤醒一个工作线程来执行
4、join方法:阻塞当前线程等待获取结果

七、Java中的线程池

合理使用带来的好处:

  1. 降低资源消耗(线程创建与销毁)
  2. 提高响应速度(任务到达即执行,不需要等待线程创建后)
  3. 提高线程的可管理性(统一分配、调优和监控)
1、线程池处理任务流程:execute
  1. 提交一个新任务,如果当前运行线程少于corePoolSize则创建线程执行任务;
  2. 如果大于或等于corePoolSize,则将任务加入工作队列BlockingQueue
  3. 如果队列已满,则创建一个新的工作线程来执行任务。
  4. 如果创建新线程会超出maximumPoolSize,任务被拒绝,交给饱和策略处理。

注意,只要创建新线程都会获取全局锁;
应该尽量避免,让大多数execute()方法执行步骤2,加入工作队列

2、线程池的创建: ThreadPoolExecutor

传入参数:

  1. corePoolSize:线程池基本大小;(可以提前创建并启动所有基本线程)
  2. runnableTaskQueue:任务队列;根据需要选择不同的阻塞队列;
  3. maximumPoolSize:线程池最大数量(如果队列满了,线程数量又小于它则会创建新线程)
  4. TreadFactory:创建线程的工厂
  5. RejectedExecutionHandle:饱和策略(默认选择AbortPolicy直接抛出异常;也可以自定义实现它)
3、线程池提交任务: execute()和submit()

1)execute:用于提交不需要返回值的任务;所以无法判断任务是否只执行成功;任务是Runnable实例
2)submit:用于提交需要返回值的任务,并且通过future.get获取返回值;

4、关闭线程池: shutdown()和shutdownNow()

两者区别:

  • 它们的原理是遍历线程池,逐个调用interrupt方法中断线程;
  • shutdown停止没有执行任务的线程(等待线程执行完任务再停止它)
  • shutdownNow 立刻停止所有线程(任务不一定执行完)
  • 根据任务特性选择关闭方案
5、合理配置线程池

从任务的特性来分析:

  1. CPU密集型:尽可能少的线程
  2. IO密集型:尽可能多的线程
  3. 执行时间和优先级:使用优先队列完成需求
  4. 数据库连接池:线程需要等待返回结果,CPU空闲时间长,线程数量应该设置越多
  5. 建议使用有界队列,因为这可以增加系统稳定性和预警能力
6、线程池监控

1、使用一些参数进行监控:
taskCount、completedTaskCount、largestPoolSize(创建过的最大线程数)、getPoolSize、getActiveCount

2、 扩展线程池: 继承方式自定义线程池,重写beforeExecute、afterExecute和terminated方法。在任务执行前后和线程池关闭前进行监控。如监控任务的平均执行时间、最大最小执行时间等

八、Executor框架

Java的线程既是工作单位,也是执行机制,耦合在一起增加编程难度。
从JDK5开始,把工作单位与执行机制分离。工作单位指的是Runnable和Callable,而执行机制由Executor框架封装,只提供简单的编程接口。

1、Executor框架的两级调度模型

JVM将Java线程一对一映射到本地操作系统的线程上。
在上层,将程序分解成若干任务,Executor框架将这些任务映射为固定数量的线程;
在下层,操作系统内核将这些线程映射到硬件处理器

2、Executor框架的结构

主要由三部分组成:任务、任务的执行、异步计算的结果

  1. 任务:实现接口Runnable和Callable
  2. 任务的执行:Executor接口、继承Executor的ExecutorService接口,以及它的两个关键实现类(ThreadPoolExecutor和ScheduledThreadPoolExecutor
  3. 结果:包括接口Future和实现Future的接口FutureTask
3、Executor框架工作流程:
  1. 创建任务
    主线程首先创建实现了Runnable和Callable接口的任务对象。
    另外,Executors工具类可以把Runnable对象封装成Callable对象Executors.callable(Runnable task)Executors.callable(Runnable task , Object result)
  2. 执行任务
    ExecutorService执行,调用execute或submit方法
    execute只能传入Runnable对象ExecutorService.execute(Runnable task)
    submit能接收Runnable和Callable对象ExecutorService.submit(Runnable task)ExecutorService.submit(Callable task)

FutureTask实现了Runnable,因此也可以直接创建FutureTask对象交给ExecutorService执行

  1. 返回结果:submit
    submit返回的是实现了Future接口的实现类FutureTask对象
    主线程可以执行FutureTask.get方法来等待任务执行完成;或者执行FutureTask.cancel来取消任务执行

如果提交的是把Runnable封装成的Callable对象,并且传入result,则FutureTask.get返回的是result对象;如果没有传入result则返回 null

4、ThreadPoolExecutor

由Executors工厂类创建,有三种类型:SingleThreadExecutor、FixedThreadPool、CachedThreadPool
参数:corePool、maximumPool、BlockingQueue、RejectedExecutionHandle

1)FixedThreadPool:可重用固定线程数线程池,适用于负载比较重的服务器

//构造:Executors.newFixedThreadPool(nThreads)
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

构造方法:

  1. corePoolSize == maximumPoolSize 指定大小固定
  2. KeepAliveTime:多余空闲线程等待任务的最长时间,超过则终止线程; FixedThreadPool设置为0L,表示多余线程立即终止

execute方法:

  1. 如果当前运行的线程少于corePoolSize ,则创建线程执行任务;
  2. 线程池完成预热(运行线程数量等于corePoolSize ),将任务加入LinkedBlockingQueue无界队列
  3. 线程执行完一个任务后,循环从队列获取任务执行

注意:由于使用无界队列,则线程池数量永远不会超过corePoolSize ,也就是说不会在创建新的线程,不存在多余线程,也不会有机会执行饱和策略。

2)SingleThreadExecutor:使用单个线程,保证顺序执行每个任务

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

构造方法:
1、corePoolSize == maximumPoolSize =1
2、使用无界队列 LinkedBlockingQueue
3、与上面类似

3)CachedThreadPool:根据需要创建线程,大小无界。适用负载较轻,执行很多短期异步小程序

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

构造方法:

  1. corePoolSize = 0 ,maximumPoolSize =Integer.Max_Value(无界)
  2. KeepAliveTime设置为 60L ,多余线程超过60s被终止
  3. 使用SynchronousQueue(无容量队列)意味着每个插入必须等待一个移除操作

execute方法:

  1. 调用SynchronousQueue.offer(Runnable task)如果当前有空闲线程正在执行SynchronousQueue.poll,则刚好配对成功,该任务交给此线程执行。
  2. 如果没有空闲线程(初始或者多余线程被终止)则创建一个新线程执行任务;
  3. 线程执行完任务后,会一直调用SynchronousQueue.poll方法向SynchronousQueue队列中拿任务,60s内没拿到任务就终止(长时间空闲的CachedThreadPool不会使用任何资源)

但也意味着,如果提交速度过快,就会不断创建线程,极端情况下会耗尽CPU和内存资源

5、ScheduledThreadPoolExecutor

继承自ThreadPoolExecutor。作用是在给定延迟后运行任务,或者定期执行任务。

对ThreadPoolExecutor做了以下修改:

  1. 使用DelayQueue无界队列(封装的是一个PriorityQueue)
  2. 获取任务方式不同
  3. 执行周期任务后,增加额外处理

有两种类型:ScheduledThreadPoolExecutorSingleThreadScheduledExecutor

1)ScheduledThreadPoolExecutor:包含若干后台线程,执行周期任务

2)SingleThreadScheduledExecutor:只包含一个后台线程,执行周期任务

往DelayQueue添加实现RunnableScheduledFuture接口的ScheduledFutureTask
ScheduledFutureTask包含三个变量:
time(任务将要被执行的具体时间)、sequenceNumber(任务提交的顺序)、period(间隔周期)

工作流程:

  1. 线程从DelayQueue获取已到期的ScheduledFutureTask(DelayQueue.take()
  2. 线程执行拿到的任务
  3. 线程修改任务的变量time 为下次要被执行的时间
  4. 线程将修改后的任务放回DelayQueue(DelayQueue.add()

注意:
DelayQueue.take()操作,无限循环从PriorityQueue获取到一个任务,才会退出无限循环。

6、FutureTask

从前面我们可以知道FutureTask实现了Runnable接口和Future接口,所以它既可以交给Executor执行,也可以调用线程直接执行。返回的也是FutureTask,然后继续执行FutureTask.get()FutureTask.cancel()
1)根据FutureTask.run()被执行的时机,FutureTask处于三种状态。

  1. 未启动(new出来还没执行run方法)
  2. 已启动(正在执行run方法)
  3. 已完成(run方法执行完正常结束,或被取消FutureTask.cancel(),或抛出异常结束)

2)FutureTask.get()FutureTask.cancel()

  1. get方法:在前两种状态下阻塞,后一种状态立即返回结果或抛出异常
  2. cancel方法:在第一种状态不会执行,第二种状态传入true则中断,第三种状态返回false

3)使用FutureTask场景:当一个线程需要等待另一个线程把某个任务执行完后,他才能继续执行.
也就是说,一个线程的FutureTask.get方法会阻塞到另一个线程执行run方法结束后,才能返回。

4)FutureTask的实现
实现基于AbstractQueuedSynchronizer(AQS)
每一个基于AQS实现的同步器都会包含两种操作:acquire和release

acquire:阻塞线程,指导AQS的状态允许这个线程继续执行;FutureTask的get方法调用。
release:改变AQS的状态,改变后的状态可允许一个或多个阻塞线程解除阻塞;FutureTask的run和cancel方法调用。

get方法调用流程:

  1. 判断acquire操作是否成功,成功条件为state为执行完成状态RAN或取消状态CANCELLED
  2. 成功则从get中返回;如果失败则到等待队列中等待其它线程执行release操作。
  3. 当其他线程执行release操作,唤醒当前线程后,当前线程尝试acquire操作成功返回,当前线程离开等待队列并唤醒它的后继线程。(级联唤醒)
  4. 最后返回执行结果或抛出异常

run方法调用流程:

  1. 更新同步状态stateRAN,并设置返回值
  2. 执行release唤醒等待队列的第一个线程
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值