join可以 实现 线程的 线性执行
线程通信 wait notfiy notifyAll
从JVM的角度看 :一个进程有多个线程,每个线程共享 栈和 方法区(1.8后为 元空间) ,
每个线程都要自己的 程序计数器 虚拟机栈 本地方法栈。
进程:
程序的一次执行过程, 或是 正在 运行的 一个程序
进程作为资源分配的单位
,系统运行时会为每个进程分配不同的内存区域。
线程:
进程可进一步细分为线程,是一个程序内部的一条执行路径
线程可作为调度和执行的单位,每个线程拥独立的运行栈和程序计数器
,线程切换的开销小。
总结: 线程 是进程 划分的 最小的 运行单位。 线程和 进程 最大的不同在于 基本上
各进程是独立的 而各线程不一定, 因为同一个进程中 的线程 极有可能 相互影响。
而 线程执行开销小 但不利于资源的管理 , 而 进程 则相反。
** 什么是线程安全? **
多个线程可能 会 同时 执行 一段代码 ,如果每次运行结果都和单线程运行的结果一样,而且其他变量的值与预期值也一样,则说明线程是安全的。
为什么程序计数器、虚拟机栈和本地⽅法栈是线程私有的呢?为什么堆和⽅
法区是线程共享的呢?
程序计数器 为什么是私有的?
主要有2个原因:
- 在多线程的情况下,程序计数器能够记录当前线程的执行位置,从而当线程被切换回来的时候 能够 知道线程 上次的执行位置。
- 字节解释器 通过改变 程序计数器的 来依次读取指令 ,从而实现 代码的流程控制。
例如 : 顺序执行 循环 异常的处理 选择。
需要 注意的是 当执行 的是 native 方法 ,那么程序计数器 记录的 是 undefined 地址, 只有执行代码时 程序计数器记录的才是 下一条 指令的地址。
所以, 程序计数器私有 是为了 线程 切换后能恢复到 正确的 执行位置。
虚拟机栈 和 本地方法栈 为什么 是 私有的?
虚拟机栈 : 每个 java 方法 在执行的同时 会 创建 一个 栈帧 用于 存储 局部变量表 操作数栈 常量池引用等信息, 从 方法调用 直至 执行 完成 的过程 ,就对应 一个 栈帧 在 java 虚拟机栈中 入栈 和 出栈 的过程。
本地方法栈 : 和虚拟机栈的作用非常相似。 区别就是 : 虚拟机栈 是为 虚拟机 执行 java代码(也就是字节码)服务, 而 本地方法栈 则为虚拟机 使用到的 navite 方法 服务 , 在 HotSpoot 虚拟机 中 和 java 虚拟机栈 合二为一。
所以: 为了保证线程中 的 局部变量 不被 别的线程 访问到 ,虚拟机栈 和 本地方法栈 是私有的。
** 一句话简单了解 堆 和 方法区 **
堆 和 方法区 都是 所有 线程 共享 的资源 , 其中 堆 是进程中最大的内存,主要用于存储 新创建的对象(所有对象都在这里分配内存) , 方法区 主要用于 存放 已被加载的 类信息 常量 静态变量 即时编译器 编译后的代码等数据。
** 说说 并发 和 并行 的区别? **
并发 : 同一时间段, 多个任务都执行。
并行: 单位时间内, 多个任务同时执行。
** 使用 多线程 可能 带来 什么问题? **
并发编程的目的 是为了 提高 程序的执行效率 和 提高 程序的 运行 速度,但是 并发编程 并不是 总能提高 程序 运行速度的 , 而且并发编程 可能会遇到很多问题。
比如: 内存泄露 上下文切换 死锁还有受限于硬件 和 软件 的资源闲置 问题。
** 线程 的生命周期**
Java 线程在运⾏的⽣命周期中的指定时刻只可能处于下⾯ 6 种不同状态的其中⼀个状态
线程在⽣命周期中并不是固定处于某⼀个状态⽽是随着代码的执⾏在不同状态之间切换。Java 线程状
态变迁如下图所示
线程创建之后进入 NEW(初始)状 态 ,调用 start() 方法 后 开始运行 线程就处于 READY(可运行) 状态 。 可运行状态 的线程 获得了 CPU 时间片 后就 处于 RUNNING(运行)状态 。
当线程执行 wait() 方法后 线程 就进入 WAITING(等待) 状态 。 进入等待状态的线程 需要其他 线程的通知 才能 返回到运行状态 , 而 TIME_WAITING (超时等待) 状态 相当于 在等待状态的 基础 上 增加了 超时限制 , 比如通过 sleep(long millis) 方法 或 wait(long millis) 方法 可以将 java 线程 置于 TIME_WAITING状态, 当超时时间到达后 java线程将返回 RUNNABLE 状态。
当线程 调用 同步方法时, 在没有 获取到 锁的情况下 线程进入 BLOCKED (阻塞) 状态。 线程在执行RUNNABLE 的 run() 方法之后就会 进入 到 TERMINATED (终止) 状态。
** 线程的优先级
什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),
比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
具体:
1.主动让出 CPU,比如调用了 sleep(), wait() 等。
2.时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
3.被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用 CPU,内存等系统资源进行处理,也就意味着效率会有一定损耗,如果频繁切换就会造成整体效率低下。
** 什么 是线程 死锁? 如何避免 死锁?
线程 死锁 : 两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
Output
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
/*
线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过
Thread.sleep(1000); 让线程 A 休眠 1s 为的是让线程 B 得到执⾏然后获取到 resource2 的监视 器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对⽅的资源,然后这两个线程就会陷⼊互相等
待的状态,这也就产⽣了死锁。上⾯的例⼦符合产⽣死锁的四个必要条件。
*/
产生死锁的原因
1、 资源分配不足
2、进程运行推进顺序不合适
3、资源分配不当
** 产生 死锁的 四个条件 **
- 互斥条件: 该资源任意一个时刻 只由 一个 线程 占用。
- 请求保持条件: 一个进程 因请求资源而堵塞时,对已获得的资源保持不放。
3.不剥夺条件: 线程已获得的资源在未使用完之前不能被其他线程 强行剥夺, 只有自己使用完毕后才释放资源。
4.循环等待条件: 若干线程之间形成了一个 头尾相接 循环 等待资源关系。
** 如何避免 死锁**
1.破坏 互斥条件 : 这个条件 无法破坏, 因为我们用 锁本来 就是想让他们 互斥 (临近资源 需要 互斥访问)
2. 破坏 请求与保存条件 : 一次性申请所有的资源
3.破坏 不剥夺条件 : 占用部分资源的线程 进一步访问其他资源 时, 如果申请不到,可以主动放弃它占用的资源。
4. 破坏 循环等待条件 : 靠 按序申请资源来预防。 按某一条件 顺序 申请资源, 释放资源时 则 反序 释放 ,破坏 循环等待条件。
我们对线程 2 的代码修改成下⾯这样就不会产⽣死锁了。
new Thread(() i> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get
resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get
resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get
resource2");
}
}
}, "线程 2").start();
Output
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
/*
线程 1 ⾸先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取
resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占
⽤,线程 2 获取到就可以执⾏了。这样就破坏了破坏循环等待条件,因此避免了死锁。
*/
线程通信
线程通信 : wait() notify() notifyAll() 这三种 定义在 Object中
wait(): 一旦执行,线程 就会 进入 堵塞等待状态, 并释放 同步监视器(锁)
notify():会 唤醒 被 wait() 的一个线程 ,如果 多个线程 被 wait() ,则按优先级唤醒
notifyAll(): 会 唤醒所有被 wait 的 线程
*注意点:
-
这三个 方法 必须 在 同步代码块 或者 同步方法 中 执行 (意味着 lock 接口类实现不了)
-
三个方法的 调用者 必须 同步代码块 或者 同步方法 中的 同步监视器(锁)
否则 会抛出异常 : IllegalMonitorStateException 异常 -
这三个 方法 都是 定义 在 Object 类中
**说说 sleep() 方法 和 wait () 方法 区别 和 共同点?
共同点: 两者 既可以 暂停 线程的执行, 使当前线程 进入 堵塞等待状态
区别:
-
sleep()方法 没有 释放锁 , wait() 方法 释放了锁
-
sleep 通常 用于 线程的暂停执行 , wait 通常 用于 线程间 的 通信 / 交互。
-
sleep() 方法 执行完成后 ,线程会 自动 苏醒;
wait() 方法 调用 后 , 线程 不会 自动 苏醒 ,
需要 别的 线程 调用 同一 对象 的 notify () 或者 notnotify() ,
也可以 使用 wait(long timeout) 超时后线程自动苏醒。 -
两个方法的 声明位置不同 , Thread 类中 声明 sleep(), Object 声明 wait()
-
调用场景不同:
sleep() 在任何场景都可以使用, wait() 只能 在 同步代码块或者同步方法
**经典例题: 生产者/ 消费者 问题
public class ProductTest {
public static void main(String[] args) {
Clerk clerk=new Clerk();
Product product=new Product(clerk);
Customer customer=new Customer(clerk);
Customer customer2=new Customer(clerk);
Thread prod = new Thread(product);
prod.setName("生成者1");
Thread cust = new Thread(customer);
cust.setName("消费者1");
Thread cust2= new Thread(customer);
cust2.setName("消费者2");
prod.start();
cust.start();
cust2.start();
}
}
class Clerk{
private Integer pro=0;
public synchronized void product() {
if ( pro < 20)
{
pro++;
System.out.println(Thread.currentThread().getName()+"生成产品第"+pro+"产品");
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void cust() {
if (pro > 0)
{
System.out.println(Thread.currentThread().getName()+"消费第"+pro+"产品");
pro--;
notify();
}else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class Product implements Runnable{
private Clerk clerk;
public Product(Clerk clerk) {
this.clerk = clerk;
}
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.product();
}
}
}
class Customer implements Runnable{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.cust();
}
}
}
生成者1生成产品第1产品
消费者1消费第1产品
生成者1生成产品第1产品
消费者2消费第1产品
生成者1生成产品第1产品
生成者1生成产品第2产品
消费者2消费第2产品
消费者1消费第1产品
生成者1生成产品第1产品
** ☆ 为什么我们在调用 start()方法 时 会 执行 run()方法 ,为什么 我们不直接调用 run() 方法?☆ ** (重点面试题)
new 一个 Thread, 线程就进入了 新建状态 ; 调用 start() 方法 ,会启动一个线程并使 线程 进入 就绪状态 ,当分配到 时间片 后就可以开始执行了。 start () 会执行 线程 的相应工作 ,然后 自动 执行 run()方法 的内容, 这就是真正的 多线程工作。 然而 直接 调用 run() 方法 ,会把 run()方法 当做 main 线程 下的 普通 方法去执行 并不会在 某个 线程中取执行它 , 所以 这不是 多线程 工作。
**同步锁 **
jdk5.0以后 出现 线程同步机制 ---- 通过显式 定义 同步锁对象来实现同步
同步锁使用 Lock对象充当
ReentrankLock 是 Lock 的 子类
private ReentrankLock lock =new ReentrankLock () //无参构造器默认实现 非公平锁
private ReentrankLock lock =new ReentrankLock (true)// 有参构造器 实现 公平锁
*步骤:
1.实例化ReentrankLock
private ReentrankLock lock =new ReentrankLock () ;
2.调用锁定方式 lock();
lock.lock();
3.调用 解锁方式 unlock();
lock.unlock();
优先顺序:
Lock --> 同步代码块 (已经进入方法体,分配了相应资源)–> 同步方法(在方法体之外)
** synchronized 关键字 **
synchronized 关键字 是 解决 多个线程 之间 访问 资源的同步性, synchronized 关键字 可以保证 被他修饰 的 方法 或者 代码块
在任意 时刻 只能有一个线程被执行。
多个线程 使用 同一把 锁
synchronized 关键字 最主要 的 三种 使用方式
修饰实例方法 : 作用于 当前对象实例 加锁,进入 同步代码 前要获得 当前对象实例的锁
(非静态的, 就是对象本身 this ,静态的,当前类,class
不能使用this,super(其实也用不了))
synchronized void method(){
}
修饰代码块 : 指定 加锁对象 ,对 给定 对象 加锁 ,进入同步代码块前 要获得 当前 对象实例 的锁。
(实现runnable接口的时候 , 可以考虑 this(当前类))
(继承 Thread类的时候, 不建议用 this(当前对象本身) 使用 (当前类.class))
synchronized(this){
}
修饰 静态方法 : 就是给 当前类加锁,会作用于 类的 所有对象 实例, 因为静态成员 不属于任何 一个 实例对象 ,是类成员(static 表明这是该类的⼀个静态资源,不管new了多少个对象,只有⼀份) 。
如果一个线程 A 调用 一个 实例 对象 的非静态 synchronized 方法, 而线程B 调用 这个 实例对象 所属类的 静态 synchronized 方法 不会出现 互斥现象。
因为 访问 静态 synchronized 方法占用的 锁 是当前类的 锁 ,而 访问 非静态 synchronized 方法 占用 的锁 是 当前实例 对象 的锁。
synchronized static void method(){
}
** 总结 :
synchronized 关键字 加到 static 静态 方法 和 synchronized(class) 代码块 都是 给 class 类(当前类) 上锁。 synchronized 关键字 加到 实例方法 上 是给 实例对象上锁。 尽量不要使用 synchronized(String a) 因为在jvm 中 ,字符串具有缓存功能!
package pers.zzk.jianzhi_offer;
public class DeadLockDemo {
private static String resource1="aaa";
private static String resource2="aaa";
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
//保持resource1不释放
while(true) {
System.out.println(Thread.currentThread() + "get resource1");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "线程 2").start();
}
}
以上代码中我们创建了两个资源resource1和resource2,String类型且指向同一个对象。
线程1中我们占用resource1,并一直占用不释放;线程2中我们占用resource2
1、使用Object类型创建数据,避免缓存
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
使用new 字符创建一个新的对象
private static String resource2=new String(“aaa”);
**
⾯试中⾯试官经常会说:“单例模式了解吗?来给我⼿写⼀下!给我解释⼀下双重检验锁⽅式实现单例模式的原理呗!”
双重校验锁实现对象单例(线程安全)
package test;
/**
* 双重校验锁实现对象单例(线程安全)
*/
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public synchronized static Singleton getUniqueInstance(){
//先判断对象是否已经实例过,没有实例化过才能进入加锁代码
if (uniqueInstance ==null){
//给类对象加锁
synchronized (Singleton.class){
if (uniqueInstance ==null){
uniqueInstance=new Singleton();
}
}
}
return uniqueInstance;
}
}
uniqueInstance 采⽤ volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton();
这段代码其实是分为三步执⾏:
- 为 uniqueInstance 分配内存空间
- 初始化 uniqueInstance
- 将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和3,此时 T2 调⽤ getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回uniqueInstance,但此时 uniqueInstance 还未被初始化。
使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏.
讲⼀下 synchronized 关键字的底层原理
synchronized同步语块
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
synchronized 同步语句块的实现使⽤的是 monitorenter 和 monitorexit 指令,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执⾏
monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象
头中,synchronized 锁便是通过这种⽅式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执⾏
monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞
等待,直到锁被另外⼀个线程释放为⽌
synchronized 修饰方法的情况
public class SynchronizedDemo2 {
public synchronized void method() {
System.out.println("synchronized ⽅法");
}
}
synchronized 修饰的⽅法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是
ACC_SYNCHRONIZED 标识,该标识指明了该⽅法是⼀个同步⽅法,JVM 通过该 ACC_SYNCHRONIZED 访问
标志来辨别⼀个⽅法是否声明为同步⽅法,从⽽执⾏相应的同步调⽤
说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍⼀下这些优化吗?
JDK1.6 对锁的实现引⼊了⼤量的优化,如偏向锁、轻量级锁、⾃旋锁、适应性⾃旋锁、锁消除、锁粗化等技术来减少锁操作的开销。
锁主要存在四种状态,依次是:⽆锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈⽽逐渐升级。注意锁可以升级不可降级,这种策略是为了提⾼获得锁和释放锁的效率。
** 谈谈 synchronized 和 Reentrantlock 的区别 **
相同点:
- 都是 可重入锁
可重入锁 : 自己可以 获取 自己的 内部锁。
比如 一个线程 获得了 某一个 对象的 锁 ,此时这个 对象锁 还没有 释放 ,当其 还想要再次 获取 这个对象 的 锁 还是可以的 ,如果 不可锁重入的 话, 就会造成 死锁。
同一个线程 每次 获取 锁的时候,锁的计数器 都会 自增 1 所以要等到 锁的计数器 下降为0 的时候 才能释放锁。
2.都是 解决线程安全问题
不同点:
3.Lock 是显示锁
(需要手动 lock()开启锁 , 手动关闭锁 unlock())
synchronizd 是隐式锁
, 出了作用域会自动释放锁
4.synchronize的 依赖于 虚拟机 , reentrantlock 依赖于 API
synchronize的 是依赖于jvm 实现的 ,虚拟机团队 在对 synchronized 关键字进行优化都是在 虚拟机层面的 并没有直接暴露给我们。
reentrantlock 是 JDK 层面(也就是 API 层面 需要 lock() 和 unlock() 方法配合 try/finally 语句块 来完成) 实现的 ,可以通过 查看 它的 源代码 查看它如何实现。
5.可实现公平锁 : ReentrantLock可以通过构造器 指定是公平锁还是⾮公平锁。
⽽synchronized只能是⾮公平锁
。所谓的公平锁就是先等待的线程先获得锁。
(非公平锁:假如3个线程, 谁先抢到锁,谁先执行,其他处于 线程堵塞等待)
6.等待可中断 :ReentrantLock提供了⼀种能够中断等待锁的线程的机制。使用 unlock()解锁
7.RenntrankLock 可实现选择性通知(锁可以绑定多个条件)
线程对象可以注册在指定的Condition中,从⽽可以有选择性的进⾏线程通知,在调度线程上更加灵活。
在使⽤notify()/notifyAll()⽅法进⾏通知时,被通知的线程是由 JVM 选择的,
⽤ReentrantLock类结合Condition实例可以实现“选择性通知”.
8.Lock锁 只有代码块锁 ,synchronized 有代码块锁和 方法锁
如何解决线程安全问题? 几种方式?
1.synchronized 同步代码块
①在实现 Runnable 接口的方式中,'可以考虑’使用this(当前类的对象),来充当同步监视器。
②在继承 Thread 类的方式中,'慎用’this充当同步监视器(这是因为,通常情况下,在继承 Thread 类的方式中,每一个线程对应一个Thread 类的子类的对象,所以这个时候当前对象本身(this),对于多个线程而言,不能保证‘多个线程共用用同一把锁’);'可以考虑’使用当前类(类名.class)来充当同步监视器 。
————————————————
2.synchronized 同步方法
默认地指定同步监视器,但仍需要直到此时的同步监视器是谁。
①在非static方法中,因为当前对象是唯一的,所以此时的同步监视器就是 当前对象本身----this。
②在static方法中,由于static方法是静态方法,随着类的加载而加载,不能使用this、super。也就没有某个类的对象,但是因为“类本身也是对象”,所以此时的 同步监视器是:当前类本身----当前类.class。
————————————————
- 使用 lock 接口
** CPU 缓存模型**
**为什么要弄一个 CPU 高速缓存呢?
CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。
CPU Cache 缓存的是内存数据用于解决 CPU 处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。
CPU Cache 示意图如下(实际上,现代的 CPU Cache 通常分为三层,分别叫 L1,L2,L3 Cache):
CPU Cache 的工作方式:
先复制一份数据到 CPU Cache 中,当 CPU 需要用到的时候就可以直接从 CPU Cache 中读取数据,当运算完成后,再将运算得到的数据写回 Main Memory 中。但是,这样存在 内存缓存不一致性的问题 !比如我执行一个 i++操作的话,如果两个线程同时执行的话,假设两个线程从 CPU Cache 中读取的 i=1,两个线程做了 1++运算完之后再写回 Main Memory 之后 i=2,而正确结果应该是 i=3。
CPU 为了解决内存缓存不一致性问题可以通过制定缓存一致协议或者其他手段来解决。
** volatile 关键字 **
volatile: 主要作用 是 保证 变量的 可见性 和 防止 JVM 指令重复排序。
**并发编程 的 三 个 特性 **
原子性 : 一个的操作 或者 多次操作 , 要么 所有操作都能得到执行并且不会收到任何 因素 的干扰 而 中断 ,要么 所有操作 都执行 要么 都不执行 synchronized 能够保持 代码片段 的原子性。
可见性 : 当一个变量 对 共享变量 进行了修改 ,那么 另外 的线程 也能够立即 看到 修改后的 最新值。 volatile 能够保持 共享 变量 的可见性。
有序性: 代码在执行过程中的 先后顺序 , java在编译器以及 运行期间的 优化, 代码的执行顺序 未必就是 编写 时候 代码的顺序, volatile 关键字 能够禁止 指令 进行 重排序优化。
** 说说 synchronized 和 volatile 关键字的区别
volatile 关键字 是 线程同步 的 轻量级实现 ,volatile 性能 肯定 比 synchronized 性能好。 但是 volatile 关键字 只能作用于 变量 而 synchronized 能够 修饰 方法 以及 代码块。
synchronized 关键字 的 使用场景多一些。
多线程访问 volatile 关键字 的时候 不会发生 堵塞 ,而 synchronized 可能会 发生 阻塞。
volatile 关键字 只能 保证 数据的 可见性 ,但不能 保证 数据的 原子性。而 synchronized 两者都能够 保证。
volatile 关键字 主要 用于 解决 变量 在 多个 线程 之间 的可见性 ; 而 synchronized 关键字 解决 的 是 多个线程 之间 访问 的 同步性。
** ThreadLocal **
ThreadLocal 类 主要是 让 每个 线程 绑定 自己的值。 可以将 ThreadLocal 形象 比喻成 一个 存放 数据的 盒子 ,盒子 中 存储了 每个线程 的 私有数据。
如果 你 创建 了 一个 ThreadLoacl 变量 ,那么 访问 这个变量 的 每个线程 都会有这个线程 的 本地副本, 这个也是ThreadLoacl 的由来。 可以使用 get() 和 set() 方法 去获取 默认值 或将其 值 更改 为 当前线程 所存 副本 的值,从而 避免了 线程 安全问题。
最终的变量是放在了当前线程的 ThreadLocalMap中,并不是存在 ThreadLocal 上, ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。
ThreadLocal 内部维护的是⼀个类似 Map 的 ThreadLocalMap 数据结构, key 为当前对象的Thread 对象,值为 Object 对象。
ThreadLocalMap 是 ThreadLocal 的静态内部类。
** ThreadLoacl 内存泄露问题 **
ThreadLocalMap 中使用的 key 为 ThreadLoacl 的弱引用,而 value 是强引用。
所以,在 ThreadLoacl 在没有被 外部 强引用 的 时候,在垃圾回收的时候, key会被清理掉 ,而 value 不会被 清理掉。 这样 一来 就会出现 ThreadLocalMap key为null 的Entry。 假如 我们不做任何措施 , value 永远 不会被 GC 回收,这 时候 就会出现 内存泄露问题。
ThreadLocalMap实现中已经考虑了这种情况,在调⽤ set() 、 get() 、 remove() ⽅法的时候,会清理掉 key 为 null 的记录。使⽤完ThreadLocal ⽅法后 ,最好⼿动调⽤ remove() ⽅法。
(弱引⽤介绍:
如果⼀个对象只具有弱引⽤,那就类似于可有可⽆的⽣活⽤品。
弱引⽤与软引⽤的区别在于:只具有弱引⽤的对象拥有更短暂的⽣命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,⼀旦发现了只具有弱引⽤的对象,不管当前内存空间⾜够与否,都会回收它的内存。
不过,由于垃圾回收器是⼀个优先级很低的线程, 因此不⼀定会很快发现那些只具有弱引⽤的对象。
弱引⽤可以和⼀个引⽤队列(ReferenceQueue)联合使⽤,如果弱引⽤所引⽤的对象被垃圾回收,
Java虚拟机就会把这个弱引⽤加⼊到与之关联的引⽤队列中。)
1.类去继承 thread ,然后 start
2.匿名类方式创建线程,然后.start
Thread.yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。(可能没有效果)
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。
因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。
结论:大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果
** 线程池 **
线程池 提供了 一种 限制 和 管理资源 (包括执行一个任务) 。每个 线程池 还 维护一些 基本 统计 信息 ,例如 已经完成任务 的数量。
使用线程池 的好处:
1.降低资源消耗。 通过重复利用已创建的线程降低 线程创建 和销毁 造成的消耗。
2.提高响应速度。 当 任务达到时, 任务可以不通过 线程的创建 就能立即执行。
3.提高线程的可管理性。 线程是稀缺资源 , 如果无限制的创建 ,不仅会消耗系统资源 , 还会降低 系统的稳定性 ,使用线程池 可以 进行 统一分配 调优和监控。
** 实现 Runnable 接口 和 Callable 接口 的区别 **
Runnable 不会返回结果 或 抛出异常 ,但 Callable 可以。
如果任务 不需要 返回值 或 抛出 异常 推荐 使用 Runnable ,可以 使 代码简洁。
工具类 Executos 可以 实现 Runnable 与 Callable 之间 的转换。
Runnable.java
@FunctionInterface
public interface Runnable(){
//线程被执行,没有返回值也不会抛出异常
public abstract run();
}
Callable.java
public interface Callable(){
/*
计算结果 或无法这样做的时候 抛出异常
@return 计算得出的结果
@throws 如果 无法 计算结果 则抛出异常
*/
V call throw Exception();
}
** 执行 execute() 和 sumbit() 的区别 是什么 ? **
-
execute() 方法 用于 提交 不需要 返回值 的任务 ,所以无法判断 线程池 执行成功与否。
-
sumbit () 方法 用于 提交 需要 返回值 的任务。 线程池 会 返回 一个 Future 类型对象 , 通过 Future 对象 可以 判断 这个 任务 是否 执行成功; 并且可以通过 get() 方法 来 获取 返回值。 get()方法 会 阻塞 当前 线程 直至任务 完成。 而 使用 get(long timeout,TimeUnit unit) 方法 会 阻塞 当前线程 一段时间后立即返回, 这可能 会使 任务 还没执行完成 就返回。
我们以 AbstractExecutorService 接⼝中的⼀个 submit ⽅法为例⼦来看看源代码:
上⾯⽅法调⽤的 newTaskFor ⽅法返回了⼀个 FutureTask 对象。
我们再来看看 execute() ⽅法:
public Future<?> submit(Runnable task) {
if (task WX null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T
value) {
return new FutureTask<T>(runnable, value);
}
public void execute(Runnable command) {
...
}
** 如何 创建 线程 池 **
《阿⾥巴巴Java开发⼿册》中强制线程池不允许使⽤ Executors 去创建,⽽是通过
ThreadPoolExecutor 的⽅式,这样的处理⽅式让写的同学更加明确线程池的运⾏规则,规避资源耗尽的⻛险。
Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列⻓度为Integer.MAX_VALUE,可能堆积⼤量的请求,从⽽导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建⼤量线程,从⽽导致OOM。
⽅式⼀:通过构造⽅法实现
⽅式⼆:通过Executor 框架的⼯具类Executors来实现 我们可以创建四种类型的ThreadPoolExecutor:
newCachedThreadPool: 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool : 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool : 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor :创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService service = Executors.newCachedThreadPool();
service.submit(线程);
service.execute(线程);
ThreadPoolExecutor 构造函数重要参数分析:
ThreadPoolExecutor 3 个最重要的参数:
corePoolSize : 核⼼线程数 定义了最⼩可以同时运⾏的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运⾏的线程数量变为最⼤线程数。
workQueue : 当新任务来的时候会先判断当前运⾏的线程数量是否达到核⼼线程数,如果达到的话,新任务就会被存放在队列中。
ThreadPoolExecutor 其他常⻅参数:
1. keepAliveTime :当线程池中的线程数量⼤于 corePoolSize 的时候,如果这时没有新的任务提交,核⼼线程外的线程不会⽴即销毁,⽽是会等待,直到等待的时间超过了keepAliveTime 才会被回收销毁;
2. unit : keepAliveTime 参数的时间单位。
3. threadFactory :executor 创建新线程的时候会⽤到。
4. handler :饱和策略。关于饱和策略下⾯单独介绍⼀下。
ThreadPoolExecutor 饱和策略定义:
如果当前同时运⾏的线程数量达到最⼤线程数量并且队列也已经被放满了任
时, ThreadPoolTaskExecutor 定义⼀些策略:
1.ThreadPoolExecutor.AbortPolicy :抛出 RejectedExecutionException 来拒绝新任务的处理。
2.ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
3.ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
2.ThreadPoolExecutor.CallerRunsPolicy :调用执行自己的线程运行任务,也就是直接在调用execute方法的线程中运行(run)被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
举个例⼦: Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 饱和策略的话来配置线程池的时候默认使⽤的是 ThreadPoolExecutor.AbortPolicy 。在默认情况下, ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代
表你将丢失对这个任务的处理。 对于可伸缩的应⽤程序,建议使⽤
ThreadPoolExecutor.CallerRunsPolicy 。当最⼤池被填满时,此策略为我们提供可伸缩队列。
** ⼀个简单的线程池Demo: Runnable + ThreadPoolExecutor **
package Pool;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 线程池
*/
public class DutyThreadPoolExecutor {
private static ThreadPoolExecutor pool=null;
private DutyThreadPoolExecutor(ThreadPoolExecutor pool) {
this.pool = pool;
}
private DutyThreadPoolExecutor() {
}
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static ThreadPoolExecutor getPool(){
if (pool ==null){
synchronized (DutyThreadPoolExecutor.class)
{
if (pool ==null){
pool=new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
}
return pool;
}
}
package Pool;
import java.util.Date;
import java.util.concurrent.Callable;
public class PoolCallable implements Callable<String> {
private String command;
public PoolCallable(String s) {
this.command = s;
}
@Override
public String toString() {
return this.command;
}
@Override
public String call() throws Exception {
System.out.println(Thread.currentThread().getName() + " Start.Time = " + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
return "线程操作成功";
}
private void processCommand() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package Pool;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
public class PoolTest {
public static void main(String[] args) {
ThreadPoolExecutor pool= DutyThreadPoolExecutor.getPool();
for (int i = 0; i < 10; i++) {
PoolCallable poolCallable = new PoolCallable("" + i);
FutureTask<String> future = new FutureTask<>(poolCallable);
try {
pool.submit(future);
} catch (RejectedExecutionException e) {
throw new RejectedExecutionException("系统任务繁忙,详细比对任务被拒绝执行");
}
try {
String s = future.get();
System.out.println(s);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
/*
可以看到我们上⾯的代码指定了:
1. corePoolSize : 核⼼线程数为 5。
2. maximumPoolSize :最⼤线程数 10
3. keepAliveTime : 等待时间为 1L。
4. unit : 等待时间的单位为 TimeUnit.SECONDS。
5. workQueue :任务队列为 ArrayBlockingQueue ,并且容量为 100;
6. handler :饱和策略为 CallerRunsPolicy 。
*/
** 线程池 原理 分析 **
我们在代码中模拟了 10 个任务,我们配置的核⼼线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执⾏,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之⾏完成后,才会之⾏剩下的 5 个任务。
** Atomic 原子类 **
含义: Atomic 指的是 一个操作是不可中断的 ,即使是 多个 线程 一起 执行 的时候 ,一个操作 一旦开始 ,就 不会 被 其他 线程 干扰。 所以 , 所谓 原子类 说 简单 点 就是 具有 原子/ 原子操作 特征 的类。
并发包 java.util.concurrent 的原⼦类都存放在 java.util.concurrent.atomic 下,如下图
所示。
** JUC 包下 的 原子类 是 哪 四类? **
基本类型 :
使用原子的方式更新基本类型
AtomicLong : 长整型 原子类
AtomicInteger : 整形 原子类
AtomicBoolean : 布尔类型 原子类
数组类型 :
使用原子的方式 更新 数组 里 的某个元素
AtomicIntegerArray : 整数数组 原子类
AtomicLongArray : 长整型 数组 原子类
AtomicReferenceArray : 引用 类型 数组 原子类
引用类型 :
AtomicReference : 引用类型 原子类
AtomicStampedReference : 原子更新带有版本号的引用类型。 该类 将 整数型值 与引用关联 起来 , 可以 用于 解决 原子 的更新 数据 和数据 的 版本号 ,可以 解决使用 CAS 进行 原子更新 时 可能 出现 的 ABA 问题。
AtomicMarkableReference: 原子更新 带有 标记位 的引用类型
对象的属性修改类型
AtomicIntegerFiledUpdater : 原子更新 整形 字段 的更新器
AtomicLongFiledUpdater : 原子更新 长整型 字段 的更新器
** 讲讲 AtomicInteger 的 使用 **
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并⾃增
public final int getAndDecrement() //获取当前的值,并⾃减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输⼊的数值等于预期
值,则以原⼦⽅式将该值设置为输⼊值(update)
public final void lazySet(int newValue)//最终设置为newValue,使⽤ lazySet
设置之后可能导致其他线程在之后的⼀⼩段时间内还是可以读到旧的值。
AtomicInteger 类的使⽤示例
使⽤ AtomicInteger 之后,不⽤对 increment() ⽅法加锁也可以保证线程安全。
class AtomicIntegerTest {
private AtomicInteger count = new AtomicInteger();
//使⽤AtomicInteger之后,不需要对该⽅法加锁,也可以实现线程安全。
public void increment() {
//返回新值(即加1后的值)
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
** 能不能给我 简单 介绍 一下 AtomicInteger 类 的原理
AtomicInteger 线程安全原理简单分析
AtomicInteger 类的部分源码:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提
供“⽐较并替换”的作⽤)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
AtomicInteger 类 主要 利用 CAS + volatile 和 native 方法 来 保证 原子 操作, 从而避免了 synchronized 的 高开销 执行效率 大为提高。
CAS的原理 是 拿 期望值 和 原本值 做一个比较 ,如果相同 则 更新为新的值。
(UnSafe 类的objectFieldOffset() ⽅法是⼀个本地⽅法,这个⽅法是⽤来拿到“原来的值”的内存地址,返回值是valueOffset。)
另外 , value 是一个 volatile 变量 ,在内存中可见 ,因此 JVM 可以 保证 在 任何时刻 任何线程 都能 拿到 该变量 的最新值。
** ☆ AQS ☆ (重点)**
AQS是一个用来构建锁 和同步器 的框架。 使用 AQS 能简单 且 高效 地 构建出 应用广泛的 大量 的同步器。
(比如 我们所提到的 ReentrantLock Semaphore ,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们⾃⼰也能利⽤AQS⾮常轻松容易地构造出符合我们⾃⼰需求的同步器。)
** 请你说一下 自己 对于 AQS 原理 的理解 **
AQS 核心思想是 : 如果 被请求 的 共享资源空闲 , 则将 当前 资源请求的线程 设置为 有效的 工作线程, 并且 将共享资源 设置为 锁定状态。 如果 被请求 的 共享资源 被占用 , 那么就需要 一套线程 阻塞 等待 以及被唤醒时 锁的 机制, 这个机制 AQS 是有 CLH队列 实现的 ,即将 暂时 获取不到 锁 的线程 加入到 队列中。
(CLH(Craig,Landin,and Hagersten)队列是⼀个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成⼀个CLH锁队列的⼀个结点(Node)来实现锁的分配。)
AQS使⽤⼀个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队⼯作。AQS使⽤CAS对该同步状态进⾏原⼦操作实现对其值的修改。
private volatile int state;//共享变量,使⽤volatile修饰保证线程可⻅性
状态信息通过protected类型的getState,setState,compareAndSetState进⾏操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原⼦地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等
于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect,
update);
}
** AQS 对 资源 的共享方式 **
Exclusive(独占) : 只有一个 线程 能执行 ,如 ReentrantLock。又分为 公平锁 和 非公平锁。
1.公平锁: 按照线程 在队列中 的排队顺序 ,先到者 先拿到 锁。
2.非公平锁:当线程 要获取 锁的时候 ,无视 队列顺序 直接 去枪锁 ,谁抢到 就是 谁的。
Share(共享) : 多个线程 可同时 执行 , 如
Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock。
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某⼀资源进⾏读。
不同的自定义同步器争用共享 资源 的方式 也不同。 自定义 同步器 在实现时 只需要 实现 共享资源
state 的获取 与 释放 方式 即可。 至于具体线程 等待队列 的维护 (如 获取资源失败入队 / 唤醒出队等) , AQS 已经在顶层实现好了。
** AQS 底层 使用了 模板 方法 模式 **
同步器的设计 是 基于 模板 方法 模式 的, 如果 需要 自定义 同步器 一般 的 方式 是这样 (模板⽅法模式很经典的⼀个应⽤):
1. 使用者 继承 AbstractQueuedSynchronizer 并重写指定方法。 (这些重写⽅法很简单,⽆⾮是对于共享资源state的获取和释放)
2. 将 AQS 组合 在 自定义 同步组件 的实现中,并调用其模板方法 ,而这些 模板 方法会 使用 调用使用者重写的 方法。
这和我们以往通过实现接⼝的⽅式有很⼤区别,这是模板⽅法模式很经典的⼀个运⽤。
AQS使⽤了模板⽅法模式,⾃定义同步器时需要重写下⾯⼏个AQS提供的模板⽅法:
isHeldExclusively()//该线程是否正在独占资源。只有⽤到condition才需要去实现它。
tryAcquire(int)//独占⽅式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int)//独占⽅式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int)//共享⽅式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可⽤资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享⽅式。尝试释放资源,成功则返回true,失败则返回false
默认情况下,每个⽅法都抛出 UnsupportedOperationException 。 这些⽅法的实现必须是内部
线程安全的,并且通常应该简短⽽不是阻塞。AQS类中的其他⽅法都是final ,所以⽆法被其他类使⽤,只有这⼏个⽅法可以被其他类使⽤。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调⽤tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为⽌,其它线程才有机会获取该锁。当然,释放锁之前,A线程⾃⼰是可以重复获取此锁的(state会累加),这就是可重⼊的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个⼦线程去执⾏,state也初始化为N(注意N要与线程个数⼀致)。这N个⼦线程是并⾏执⾏的,每个⼦线程执⾏完后countDown()⼀次,state会CAS(Compare and Swap)减1。等到所有⼦线程都执⾏完后(即state=0),会unpark()主调⽤线程,然后主调⽤线程就会从await()函数返回,继续后余动作。
⼀般来说,⾃定义同步器要么是独占⽅法,要么是共享⽅式,他们也只需实现 tryAcquiretryRelease 、 tryAcquireShared-tryReleaseShared 中的⼀种即可。但AQS也⽀持⾃定义同步器同时实现独占和共享两种⽅式,如 ReentrantReadWriteLock .
** AQS组件 总结 **
1.Semaphore(信号量) – 允许 多个 线程 同时访问。
synchronized 和 ReentrantLock 都是⼀次只允
许⼀个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。)
2.(倒计时器) CountDownLath 是一个 同步工具类 ,用来协调 多个 线程 之间 的同步。 这个⼯具通常⽤来控制线程等待,它可以让某⼀个线程等待直到倒计时结束,再开始执⾏。
3.(循环栅栏)
CyclicBarrier 和 CountDownLatch ⾮常类似,它也可以实现线程间的技术等待,但是它的功能⽐ CountDownLatch 更加复杂和强⼤。
主要应⽤场景和CountDownLatch 类似。CyclicBarrier 的字⾯意思是可循环使⽤(Cyclic)的屏障
(Barrier)。它要做的事情是,让⼀组线程到达⼀个屏障(也可以叫同步点)时被阻塞,直到最后⼀个线程到达屏障时,屏障才会开⻔,所有被屏障拦截的线程才会继续⼲活。 CyclicBarrier默认的构造⽅法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调⽤await()⽅法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。