小序:掌握基础概念及用法结合应用场景足以对付面试,哈哈哈
1.基础知识
1.为什么要使用并发编程
-
充分发挥计算机CPU的多核多线程的能力,性能提升
-
提升系统并发能力和性能
-
现在(移动)互联网已然是百万级甚至千万级并发量,多线程并发编程正是开发高并发系统的系统
-
面对复杂业务模式,使用并行的程序更适应业务需求,而并发编程更吻合这种业务拆分
2.多线程的应用场景
-
web中:接口上web容器帮开发人员做了多线程,如果是单线程的话只会一次处理一个请求,显然是不能满足需求的。但是web容器仅仅是做了请求层面上的处理,实际的业务逻辑是开发人员编写的。
-
后台任务,例如:定时向大量用户发送邮件
-
异步处理,例如:发微博,系统记录日志
-
分布式计算
多线程应用场景主要是围绕着并发、并行处理的思想。
3.并发编程的缺点
并发编程的三要素(线程的安全性问题体现在):
-
原子性
-
原子,即一个不可再被分割的颗粒。原子性是指一个或者多个操作要么全部执行成功要么全部执行失败。
-
可见性
-
一个线程对共享变量的修改,另外一个线程能够立刻看到。(synchronized,volatile)
-
有序性
-
程序执行的顺序按照代码的先后顺序执行。(处理器可能会对指令进行重排序)
出现线程安全问题的原因:
-
线程切换带来的原子性问题
-
缓存导致的可见性问题
-
编译优化带来的有序性问题
解决办法:
-
JDK Atomic开头的原子类、synchronized、LOCK、可以解决原子性问题
-
synchronized、volatile、Lock,可以解决可见性问题
-
Happens-Before规则可以解决有序性问题
原子性和可见性
原子性:
读写原子性,读写要么成功,要么失败。Java中使用Atimic原子包装类(内部value使用volatile关键字修饰)
可见性:
写成功后,通知其他线程更改缓存。使用关键字volatile修饰
保证线程安全还可以用synchronized关键字。
volatile < Atomic < synchronized
什么是线程死锁
死锁:
是指俩个或者俩个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁,这些永远在等待的进程(线程)称为死锁进程(线程)。
死锁产生的原因:
多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放。由于线程无限期地阻塞,因此程序不可能正常终止。
import java.util.Date;
public class LockTest {
public static String obj1 = "obj1";
public static String obj2 = "obj2";
public static void main(String[] args) {
LockA la = new LockA();
new Thread(la).start();
LockB lb = new LockB();
new Thread(lb).start();
}
}
class LockA implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockA 开始执行");
while(true){
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockA 锁住 obj1");
Thread.sleep(3000); // 此处等待是给B能锁住机会
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockA 锁住 obj2");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class LockB implements Runnable{
public void run() {
try {
System.out.println(new Date().toString() + " LockB 开始执行");
while(true){
synchronized (LockTest.obj2) {
System.out.println(new Date().toString() + " LockB 锁住 obj2");
Thread.sleep(3000); // 此处等待是给A能锁住机会
synchronized (LockTest.obj1) {
System.out.println(new Date().toString() + " LockB 锁住 obj1");
Thread.sleep(60 * 1000); // 为测试,占用了就不放
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
形成死锁的四个必要条件
-
互斥条件
-
在一段时间内某资源只由一个进程占用。如果此时还有其他进程请求资源,就只能等待,直至占有资源的进程用完释放。
-
占有且等待
-
本身锁一个对象,然后内部又有一个锁,双重锁。进程已经占用了至少一个资源,但又提出了新的资源请求,而新资源已被其他进程占有,此时请求进程阻塞,但又对自己已获得的其他资源保持不放。
-
不可抢占
-
别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来
-
循环等待
-
若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)
如何避免线程死锁
-
避免一个线程同时获得多个锁
-
避免一个线程在锁内同时占用多个资源,尽量保证每个所占用一个资源,占用锁隔离互不打扰
-
尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
创建线程的四种方式
-
集成Thread方式
-
实现Runnable接口
-
实现Callable接口(有返回值)
-
使用匿名内部类方式,jdk8之后的lambda表达式
什么是Callable和Future和FutureTask?
-
Callable
-
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常。而Callable功能更强大一些,被线程执行后,可以有返回值,这个返回值可以被Future拿到,也就是Future可以拿到异步执行任务的返回值。
-
Future
-
表示异步任务,是一个可能还没有完成的异步任务的结果。所以说Callable用于产生结果,Future用于获取结果
-
FutureTask
-
表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成,get方法将会阻塞。一个FutureTask对象可以对调用了Callable和Runnable的对象进行包装,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入到线程池中。
并发理论
Java内存模型JMM
共享内存模型就是Java内存模型,简称JMM。
JMM决定一个线程对共享变量的写入时,能对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系,线程之间的共享变量存储在主内存中(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读、写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓存区,寄存器以及其他的硬件和编译器优化。
什么是指令重排序
咱们从代码的角度去分析,每一行代码其实也是一个行的指令。
-
程序执行的顺序按照代码的先后顺序执行
-
一般来说处理器为了提高程序执行效率,可能会对输入代码进行优化,进行重新排序(重排序),他不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
int a = 5; 语句1
int r = 3; 语句2
a = a + 2; 语句3
r = a * a; 语句4
-
则因为重排序,那么他
可能
执行的顺序为:2-1-3-4,1-3-2-4,但绝不可能是2-1-4-3,因为这打破了依赖关系,4必须要在3之后。 -
显然重排序对单线程运行是不会有任何问题,但是多线程就不一定了,所以我们在多线程编程时就得考虑这个问题了。
并发关键字synchronized
-
在Java中,synchronized关键字是用来控制线程同步的,就是在对多线程的环境下,控制synchronized代码段不被多个线程同时执行。悲观锁的实现。synchronized可以修饰类、方法、变量。
-
JDK6之后Java官方从JVM层面对synchronized进行优化,所以现在的synchronized锁效率也优化的很不错了。JDK6对锁的实现引入了大量的优化,如:自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。
synchronized早期(JDK6之前)效率低的原因?
在Java早起版本中,synchronized属于重量级锁,效率低下。
因为监视器锁(monitor)是依赖于底层的操作系统的MutexLock来实现的,Java的线程是映射到操作系统的原生线程智商的。
如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换是需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。
说说自己是怎么使用synchronized关键字,在项目中用到了吗
synchronized锁对象【类class,对象,普通方法(方法的当前类对象实例|this),静态方法(方法的当前类class)】之后,进入代码块时要获得对象的锁。如果被占用,会等待。
synchronized关键字使用的三种方式:
-
修饰实例方法:作用于当前对象实例|this加锁(如果是俩个对象实例,this是跟实例对象一致)
-
修饰静态方法:作用于当前类class
-
修饰代码块:指定加锁对象,对指定对象进行加锁
总结:
synchronized关键字加到static静态方法和synchronized(class)代码块都是给class类加锁,class是同一个锁。
synchronized关键字加到实例方法上是给对象实例上锁。
尽量不要使用synchronized(String a)因为在JVM中,字符串常量池具有缓存功能!
说一下synchronized底层实现原理?
-
synchronized的语义底层是通过一个monitor(监视器锁)的对象来完成
-
每个对象有一个监视器锁(monitor),每个synchronized修饰过的代码当它的monitor被占用时就会处于锁定状态并且尝试获取monitor的所有权,过程:
-
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
-
2、如果线程已经占有该monitor,当前线程可重新进入,可重入加锁,则进入monitor的进入数加1。
-
3、如果其他线程已经占用了monitor,则该线程进入阻塞状态,知道monitor的进入数为0,再重新尝试获取monitor的所有权。
synchronized是可以通过 反汇编指令 javap
命令,查看响应的字节码文件。
synchronized可重入的原理
重入锁是指一个线程获取到该锁之后,该线程可以继续获得该锁
。
底层原理维护一个计数器,当线程获取到该锁时,计数器加一,在此获得锁继续加一,释放锁时,计数器减一,当计数器为0时,表明该锁未被任何线程所持有,其他线程可以竞争获取锁。
多线程synchronized锁升级的原理是什么?
锁升级的目的:锁升级是为了减低锁带来的性能消耗。在Java6之后优化synchronized的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而降低了锁带来的性能消耗。
-
synchronized 锁升级原理:在锁对象的对象头里面有一个threadid字段,在第一次访问的时候,threadid为空,jvm让其持有偏向锁,并将threadid设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了synchronized锁的升级。
偏向锁:
顾名思义,他会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁、解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
轻量级锁:
由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,自旋次数到了之后,如果没有获取到锁,则轻量级锁就会升级为重量级锁。
重量级锁:
重量级锁是synchronized,是Java虚拟机中最为基础的锁实现。在这种状态下,Java虚拟机会阻塞加锁失败的线程,并且在目标锁被释放的时候,唤醒这些线程。
synchronized、volatile、CAS 比较
-
(1)synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
-
(2)volatile 提供多线程共享变量可见性和禁止指令重排序优化。
-
(3)CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized 和 Lock 有什么区别?
-
首先synchronized是Java内置关键字,在JVM层面,Lock是个Java类;
-
synchronized 可以给类、方法、代码块加锁;而 lock 只能给代码块加锁。
-
synchronized 不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁;而 lock 需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁。
-
通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到。
Lock 是一个接口。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
synchronized 和 ReentrantLock 区别是什么?
ReentrantLock 是LOCK的实现类,大致和lock的区别一致。
volatile关键字的作用
-
可见性,当发生修改时,会通知其他线程。
-
不保证原子性
-
禁止指令重排
什么是自旋
-
很多synchronized里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁,可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然synchronized里面的代码执行非常快,不妨让等待锁的线程不要被阻塞,而是在synchronized的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样就可能是一种更好的策略。
-
忙循环:
-
就是程序员用循环让一个线程等待,不像传统方法wait(),sleep(),yield()它们都放弃了CPU的控制,而忙循环不会放弃CPU,它就是在运行一个
空循环
。这样做的目的是为了保留CPU缓存
,在多核系统中,一个等待线程醒来的时候可能会在另一个内核运行,这样会重建缓存。为了避免重建缓存
和减少等待重建的时间
就可以使用它了。
什么是乐观锁?
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候就会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
乐观锁适用于多读的场景,这样就可以提高吞吐量。实现方式:版本号,CAS。
什么是悲观锁?
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候,都会上锁,这样别人想拿这个数据的时候就会阻塞直到拿到锁。实现方式:传统数据库的锁机制,行锁、表锁、读锁、写锁等,都是在操作之前先上锁。再比如Java中的synchronized。
什么是CAS?
CAS是compare and swap的缩写,比较交换。
CAS是乐观锁。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。
CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环(重新获取值,下次同样的判断)才有可能机会执行。
volatile有三个特性:可见性,不保证原子性,禁止指令重排。
可见性:线程1从主内存中拿数据1到自己的线程工作空间进行操作(假设是加1)这个时候数据1已经改为数据2了,将数据2写回主内存时通知其他线程(线程2,线程3),主内存中的数据1已改为数据2了,让其他线程重新拿新的数据(数据2)。
不保证原子性:线程1从主内存中拿了一个值为1的数据到自己的工作空间里面进行加1的操作,值变为2,写回主内存,然后还没有来得及通知其他线程,线程1就被线程2抢占了,CPU分配,线程1被挂起,线程2还是拿着原来主内存中的数据值为1进行加1,值变成2,写回主内存,将主内存值为2的替换成2,这时线程1的通知到了,线程2重新去主内存拿值为2的数据。
禁止指令重排:首先指令重排是程序执行的时候不总是从上往下执行的,就像高考答题,可以先做容易的题目再做难的,这时做题的顺序就不是从上往下了。禁止指令重排就杜绝了这种情况
CAS会产生什么问题?
-
ABA问题
-
jdk5之后,atomic包下提供了一个类AtomicStampedReference来解决ABA问题。
-
循环时间长开销大
-
资源竞争(线程冲突)严重,自旋概率比较大
-
只能保证一个共享变量的原子操作
-
如果是多个变量,可以考虑AtomicReference类
什么是原子类?
JUC包:是原子类的小工具包,支持在单个变量上解除锁的线程安全编程原子变量类相当于一种泛华的volatile变量,能够支持原子和有条件的读-写-改操作。
原子类的常用类
-
AtomicBoolean
-
AtomicInteger
-
AtomicLong
-
AtomicReference
说一下 Atomic的原理?
Atomic包中的类基本的特性就是在多线程环境下,当有多个线程同时对单个(包括基本类型及引用类型)变量进行操作时,具有排他性,即当多个线程同时对该变量的值进行更新时,仅有一个线程能成功,而未成功的线程可以向自旋锁一样,继续尝试,一直等到执行成功。
线程池
什么是线程池
-
降低资源消耗
-
通过重复利用已创建的线程降低线程创建和销毁所造成的的消耗
-
提高响应速度
-
当任务到达时,不需要等到线程创建能直接执行。
-
可有效的控制最大并发线程数,提高资源的使用率,同时避免过多资源竞争,避免拥堵
-
提高线程的可管理性
-
线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性。
-
使用线程池可以统一分配,调优和监控,但是,要做到合理应用
-
线程池为了突然大量爆发的线程设计的
-
附加功能
-
提供定时执行,定期执行,单线程,并发数控制等功能
什么是ThreadPoolExecutor?
ThreadPoolExecutor就是Java中的线程池。
七大参数:
-
corePoolSize 核心线程数
-
maximumPoolSize 最大线程数量
-
keepAliveTime 线程存活时间,(超过核心线程数的其他线程,核心线程不会被销毁)
-
unit 时间单位
-
workQueue 阻塞队列
-
threadFactory 线程工厂
-
handler 线程池拒绝策略(丢弃抛异常,丢弃不抛异常,由调用线程(提交任务的线程)处理该任务,丢弃队列最前面的任务然后重新提交被拒绝的任务)
线程池工具类Executors
可参考Executor、Executors、ExecutorService
Executor框架便是Java 5中引入的,其内部使用了线程池机制
Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
1、public static ExecutorService newFiexedThreadPool(int Threads) 创建固定数目线程的线程池。
2、public static ExecutorService newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。
3、public static ExecutorService newSingleThreadExecutor():创建一个单线程化的Executor。
4、public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。
如何合理分配线程池的大小?
根据实际情况来定,简单的话就是根据CPU密集和IO密集来分配
什么是CPU密集CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程),而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那样。什么是IO密集IO密集型,即该任务需要大量的IO,即大量的阻塞。在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待。所以在IO密集型任务中使用多线程可以大大的加速程序运行,即时在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。分配CPU和IO密集:1.CPU密集型时,任务可以少配置线程数,大概和机器的cpu核数相当,这样可以使得每个线程都在执行任务2.IO密集型时,大部分线程都阻塞,故需要多配置线程数,2*cpu核数精确来说的话的话:·从以下几个角度分析任务的特性:。任务的性质:CPU密集型任务、IO密集型任务、混合型任务。o任务的优先级:高、中、低。。任务的执行时间:长、中、短。。任务的依赖性:是否依赖其他系统资源,如数据库连接等。
可以得出一个结论:线程等待时间比CPU执行时间比例越高,需要越多线程。线程CPU执行时间比等待时间比例越高,需要越少线程。
并发容器
什么是Vector
加synchronized的ArrayList
ArrayList和Vector有什么不同之处?
ArrayList不是线程安全的
为什么HashTable是线程安全的?
加synchronized的HashMap
用过ConcurrentHashMap,讲一下他和HashTable的不同之处?
ConcurrentHashMap采用分段锁
ConcurrentHashMap读时不加锁,value是加了关键字volatile保证可见性
Collections.synchronized * 是什么?
SynchronizedMap 和 ConcurrentHashMap 有什么区别?
CopyOnWriteArrayList 是什么?
读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,俩快空间,来解决并发冲突
并发队列
可参考(非)阻塞队列
Java 提供的线程安全的队列(也称为并发队列)分为阻塞队列和非阻塞队列两大类。
阻塞队列的典型例子就是 BlockingQueue 接口的实现类,BlockingQueue 下面有 6 种最主要的实现,分别是 ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界)、SynchronousQueue、DelayQueue、PriorityBlockingQueue 和 LinkedTransferQueue
非阻塞并发队列的典型例子是 ConcurrentLinkedQueue,这个类不会让线程阻塞,利用 CAS 保证了线程安全。
Deque 接口,它继承了 Queue。Deque 的意思是双端队列,音标是 [dek],是 double-ended-queue 的缩写,它从头和尾都能添加和删除元素
并发工具类
可参考Java并发包并发工具类
CountDownLatch
计数器
允许一个或者多个线程等待操作完成
-
CountDownLatch 是不可以重置的,无法重用,但是 CyclicBarrier则没有这个限制,可以重用。
-
CountDownLatch 的基本操作时 countDown/await。调用await 线程阻塞等待 countDown 足够的次数,不管是在一个线程还是多个线程里 CountDown,只要次数足够即可。
假设有10个人排队,我们将其分成5个人一批,使用CountDownLatc 来协调。
CyclicBarrier
回环栅栏
允许多个线程瞪大到达某个屏障
-
CyclicBarrier的基本操作组合,则就是 await,当所有的伙伴( parties)都调用了 await,才会继续进行任务,并自动进行重置。注意,正常情况下, CyclicBarrier的重置都是自动发生的,如果我们调用 reset 方法,但还有线程在等待,就会导致等待线程被打扰,抛出 BrokenBarrierException异常。CyclicBarrier侧重点是线程,而不是调用事件,它的典型应用场景是用来等待并发线程结束
Semaphore
Java 版本信号量的实现,通过允许一定数量的允许(Permit)的方式,来表达限制通用资源访问的目的。租车时,当很多空出租车就位时,为防止过度拥挤,调度员指挥排队等待坐车的队伍一次进来5个人上车,等这5个人坐车出发,再放进去下一批,这和 Semaphore的工作原理类似
线程尝试获得许可,获得许可则进入任务,任务执行完,然后释放许可。这时等待许可的其他线程,可以获得许可进入工作状态,知道全部处理结束。
AQS
AQS的全称是AbstractQueuedSynchronizer,也就是抽象队列同步器,它是在java.util.concurrent.locks包下的,也就是JUC并发包。
AQS内部实现了自旋锁、可重入锁、独占锁。
Java提供了synchronized关键字内置锁,还提供了显示锁,而大部分的显示锁的底层都用到了AQS,比如只有一个线程能执行ReentrantLock独占锁,又比如多个线程可以同时执行共享锁Semaphore、CountDownLatch、ReadWriteLock、CyclicBarrier。
AQS使用模板方法模式
,使用者继承AbstractQueuedSynchronizer
并重写指定的方法,重写的方法就是对于共享资源state的获取和释放
,将AQS在自定义同步组件的实现中,调用它的模板方法,这些模板方法会调用使用者重写的方法,这是模板方法很经典的一个应用。
另外AQS还维护了一个变量volatile in state
,代表了加锁的状态,初始状态下,state的值是0。另外还有一个虚拟的双向队列
,这个队列是不存在的,它是抽象的概念,存在节点之间的关联关系,它是将请求共享资源的线程,封装成一个Node结点来实现锁的分配。
除此之外AQS内部还有一个线程变量
,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null。
拿ReentrantLock加锁举例,线程调用ReentrantLock的lock()方法进行加锁,这个加锁的过程,用CAS讲state的值从0变为1。一旦线程加锁成功了之后,就可以设置当前加锁线程是自己。ReentrantLock通过多次执行lock()加锁和unlock()释放锁,对一个锁加多次,从而实现可重入锁。
每个线程可重入加锁一次,判断一次当前加锁线程是不是自己,如果是他自己就可以可重入多次加锁,每次加锁,就是把state的值给累加1。当state=1时,代表当前对象锁已经被占用,其他线程来加锁时则会失败,然后再去看加锁线程的变量里面是不是自己之前占用过这把锁,如果不是就说明有其他线程占用了这个锁,失败的线程呗放入到一个等待队列中,在等待唤醒的时候,经常会使用自旋(while(!cas()))的方式,不停地尝试获取锁,等待已经获得锁的线程,释放锁才能被唤醒。当它释放锁的时候,将AQS的state变量的值减一,如果state的值为0,就会释放锁,会将“加锁线程”变量设置为null。这个时候,会从等待队列的对头唤醒其他线程重新尝试加锁,获得锁成功之后,会把“加锁线程”设置为线程自己,同时线程自己就从等待队列出队。这个就是AQS实现自旋锁、可重入锁、独占锁的底层实现。
接着那CountDownLatch举例,任务分为5个子线程去执行,state也初始化为5。这五个子线程是并行执行的,每个子线程执行完成后countDown()一次,state会CAS减一,等到所有子线程都执行完成后,state=0,会unpark()主调用线程,然后主调用线程就会从awaiit()函数返回,继续后余动作。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/20567.html