文章目录
四 共享模型之内存
部分内容参考自JavaGuide 并发进阶
JMM
Java内存模型,定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
JMM 体现在以下几个方面
-
原子性
所有操作要么都执行,要么都不执行,
synchronized
可以保证代码片段的原子性 -
可见性
当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可见性 -
有序性/指令重排
指代码在执行的过程中的先后顺序,为了实现指令级的并行,JVM在编译器以及运行期间会进行优化,代码的执行顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁止指令进行重排序优化
volatile
修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,可以防止 JVM 的指令重排以及保证变量的可见性
public class Test11 {
volatile static boolean run=true;
public static void main(String[] args) throws InterruptedException {
new Thread(()-> {
while(run) {}
}).start();
Thread.sleep(1000);
run=false;
}
}
synchronized与volatile对比
volatile
关键字是线程同步的轻量级实现,所以volatile
性能肯定比synchronized
关键字要好volatile
关键字只能用于变量而synchronized
关键字可以修饰方法以及代码块volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证volatile
关键字主要用于解决变量在多个线程之间的可见性,而synchronized
关键字解决的是多个线程之间访问资源的同步性
五 ThreadLocal
部分内容参考自JavaGuide 并发进阶
简介
解决的是让每个线程拥有自己的专属本地变量
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
举例
public class ThreadLocalExample implements Runnable{
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj=new ThreadLocalExample();
for (int i=0;i<10;i++) {
Thread t=new Thread(obj,""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
原理
- Thread类中有一个threadLocals和 一个inheritableThreadLocals变量,它们都是ThreadLocalMap类型的变量。ThreadLocalMap是ThreadLocal的静态内部类
- 默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法
- 最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值
- 每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对
数据结构图示
内存泄漏问题
ThreadLocalMap(ThreadLocal<?>, Object)
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。
如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉,导致ThreadLocalMap
中就会出现 key 为 null 的 Entry。不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后 最好手动调用remove()
方法
六 线程池
部分内容参考自JavaGuide 线程池总结
线程池好处
- 降低资源消耗
- 提高响应速度
- 提高线程的可管理性
Executor
简介
Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题
补充:this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误
Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等
框架结构
1.任务
执行任务需要实现的 Runnable
接口 或 Callable
接口。Runnable
接口或 Callable
接口 实现类都可以被 ThreadPoolExecutor
或 ScheduledThreadPoolExecutor
执行
2.任务执行
包括任务执行机制的核心接口 Executor
,以及继承自 Executor
接口的 ExecutorService
接口。ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
这两个关键类实现了 ExecutorService 接口
3.异步计算结果
Future
接口以及 Future
接口的实现类 FutureTask
类都可以代表异步计算的结果
使用
- 主线程首先要创建实现
Runnable
或者Callable
接口的任务对象。 - 把创建完成的实现
Runnable
/Callable
接口的 对象直接交给ExecutorService
执行:ExecutorService.execute(Runnable command)
)或者也可以把Runnable
对象或Callable
对象提交给ExecutorService
执行(ExecutorService.submit(Runnable task)
或ExecutorService.submit(Callable <T> task)
)。 - 如果执行
ExecutorService.submit(…)
,ExecutorService
将返回一个实现Future
接口的对象(我们刚刚也提到过了执行execute()
方法和submit()
方法的区别,submit()
会返回一个FutureTask 对象)。由于 FutureTask
实现了Runnable
,我们也可以创建FutureTask
,然后直接交给ExecutorService
执行。 - 最后,主线程可以执行
FutureTask.get()
方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)
来取消此任务的执行。
ThreadPoolExecutor
重要参数
corePoolSize
: 核心线程数,定义了最小可以同时运行的线程数量maximumPoolSize
: 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数workQueue
: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中keepAliveTime
: 当线程数大于核心线程数时,多余的空闲线程存活的最长时间unit
:keepAliveTime
参数的时间单位threadFactory
:线程工厂,用来创建线程,一般默认即可handler
: 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务
更多参考https://blog.youkuaiyun.com/xiaojin21cen/article/details/87359148?ops_request_misc=&request_id=&biz_id=102&utm_term=ThreadPoolExecutor&utm_medium=distribute.pc_search_result.none-task-blog-2allsobaiduweb~default-3-87359148.nonecase&spm=1018.2226.3001.4187
拒绝策略
策略 | 备注 |
---|---|
ThreadPoolExecutor.AbortPolicy | 默认使用,抛出RejectedExecutionException异常 |
ThreadPoolExecutor.CallerRunsPolicy | 由向线程池提交任务的线程来执行该任务 |
ThreadPoolExecutor.DiscardPolicy | 抛弃当前的任务 |
ThreadPoolExecutor.DiscardOldestPolicy | 抛弃最旧的任务(最先提交而没有得到执行的任务) |
创建线程池
- 通过
ThreadPoolExecutor
构造函数实现(推荐) - 通过 Executor 框架的工具类 Executors 来实现
Runnable
+ThreadPoolExecutor
public class MyRunnable implements Runnable {
private String command;
public MyRunnable(String command) {
this.command = command;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "start time=" + new Date());
processCommand();
System.out.println(Thread.currentThread().getName() + "end time=" + new Date());
}
private void processCommand() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return command;
}
}
public class ThreadPoolExecutorDemo {
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 void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
Runnable runnable = new MyRunnable("" + i);
executor.execute(runnable);
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("finish");
}
}
Callable
+ThreadPoolExecutor
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return Thread.currentThread().getName();
}
}
public class CallableDemo {
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 void main(String[] args) {
ThreadPoolExecutor executor=new ThreadPoolExecutor(
CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_TIME,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy()
);
List<Future<String>> futureList=new ArrayList<>();
Callable<String> callable=new MyCallable();
for (int i=0;i<10;i++) {
Future<String> submit = executor.submit(callable);
futureList.add(submit);
}
for (Future<String> future: futureList) {
try {
System.out.println(new Date()+":"+future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
executor.shutdown();
}
}
线程池原理
ThreadPoolExecutor的execute方法
对比
Runnable
vs Callable
Runnable
接口不会返回结果或抛出检查异常,但是Callable
接口可以- 工具类
Executors
可以实现将Runnable
对象转换成Callable
对象。(Executors.callable(Runnable task)
或Executors.callable(Runnable task, Object result)
)
execute()
vs submit()
execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个Future
类型的对象,通过这个Future
对象可以判断任务是否执行成功 ,并且可以通过Future
的get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)
方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
shutdown()
VSshutdownNow()
shutdown()
:关闭线程池,线程池的状态变为SHUTDOWN
。线程池不再接受新任务了,但是队列里的任务得执行完毕shutdownNow()
:关闭线程池,线程的状态变为STOP
。线程池会终止当前正在运行的任务,并停止处理排队的任务并返回正在等待执行的 List
isTerminated()
VS isShutdown()
isShutDown
当调用shutdown()
方法后返回为 trueisTerminated
当调用shutdown()
方法后,并且所有提交的任务完成后返回为 true
常见线程池
FixedThreadPool
1.简介
可重用固定线程数的线程池
**corePoolSize
和 maximumPoolSize
**相同
2.缺点
FixedThreadPool
使用无界队列 LinkedBlockingQueue
(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响 :
- 当线程池中的线程数达到
corePoolSize
后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize; - 由于使用无界队列时
maximumPoolSize
将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建FixedThreadPool
的源码可以看出创建的FixedThreadPool
的corePoolSize
和maximumPoolSize
被设置为同一个值。 - 由于 1 和 2,使用无界队列时
keepAliveTime
将是一个无效参数; - 运行中的
FixedThreadPool
(未执行shutdown()
或shutdownNow()
)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。
SingleThreadExecutor
1.简介
只有一个线程的线程池
corePoolSize
和 maximumPoolSize
都被设置为 1
2.缺点
与FixedThreadPool相同
CachedThreadPool
1.简介
一个会根据需要创建新线程的线程池
corePoolSize
被设置为空(0),maximumPoolSize
被设置为 Integer.MAX.VALUE
2.缺点
CachedThreadPool
允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM
总结
Executors 返回线程池对象的弊端如下:
FixedThreadPool
和SingleThreadExecutor
: 允许请求的队列长度为Integer.MAX_VALUE
,可能堆积大量的请求,从而导致 OOM。- CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为
Integer.MAX_VALUE
,可能会创建大量线程,从而导致 OOM。
ScheduledThreadPoolExecutor
简介
- 主要用来在给定的延迟后运行任务,或者定期执行任务
- 使用的任务队列
DelayQueue
封装了一个PriorityQueue
,PriorityQueue
会对队列中的任务进行排序,执行所需时间短的放在前面先被执行(ScheduledFutureTask
的time
变量小的先执行),如果执行所需时间相同则先提交的任务将被先执行(ScheduledFutureTask
的squenceNumber
变量小的先执行)
ScheduledThreadPoolExecutor
为了实现周期性的执行任务,对 ThreadPoolExecutor
做了如下修改:
- 使用
DelayQueue
作为任务队列; - 获取任务的方不同
- 执行周期任务后,增加了额外的处理
ScheduledThreadPoolExecutor
和 Timer
的比较
Timer
对系统时钟的变化敏感,ScheduledThreadPoolExecutor
不是;Timer
只有一个执行线程,因此长时间运行的任务可以延迟其他任务。ScheduledThreadPoolExecutor
可以配置任意数量的线程。 此外,如果你想(通过提供 ThreadFactory),你可以完全控制创建的线程;- 在
TimerTask
中抛出的运行时异常会杀死一个线程,从而导致Timer
死机:-( …即计划任务将不再运行。ScheduledThreadExecutor
不仅捕获运行时异常,还允许您在需要时处理它们(通过重写afterExecute
方法ThreadPoolExecutor
)。抛出异常的任务将被取消,但其他任务将继续运行。
线程池大小确定
如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的! CPU 根本没有得到充分利用。
但是,如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。
有一个简单并且适用面比较广的公式:
- CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
- I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。单凡涉及到网络读取,文件读取这类都是 IO 密集型
七 Atomic原子类
部分内容参考自JavaGuide 原子类总结
简介
Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰
原子类都存放在java.util.concurrent.atomic
下
基本类型
分类
- AtomicInteger:整型原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
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 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
优势
多线程环境使用原子类保证线程安全(基本数据类型)
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
线程安全原理简单分析
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
数组类型
分类
- AtomicIntegerArray:整型数组原子类
- AtomicLongArray:长整型数组原子类
- AtomicReferenceArray :引用类型数组原子类
AtomicIntegerArray 类常用方法
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
引用类型
分类
- AtomicReference:引用类型原子类
- AtomicMarkableReference:原子更新带有标记的引用类型。该类将 boolean 标记与引用关联起来,不能解决ABA的问题,只是会降低ABA问题发生的几率
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题
对象属性修改类型
分类
- AtomicIntegerFieldUpdater:原子更新整型字段的更新器
- AtomicLongFieldUpdater:原子更新长整型字段的更新器
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段
要想原子地更新对象的属性需要两步。第一步,因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 newUpdater()创建一个更新器,并且需要设置想要更新的类和属性。第二步,更新的对象属性必须使用 public volatile 修饰符
CAS ABA 问题
描述: 第一个线程取到了变量 x 的值 A,然后巴拉巴拉干别的事,总之就是只拿到了变量 x 的值 A。这段时间内第二个线程也取到了变量 x 的值 A,然后把变量 x 的值改为 B,然后巴拉巴拉干别的事,最后又把变量 x 的值变为 A (相当于还原了)。在这之后第一个线程终于进行了变量 x 的操作,但是此时变量 x 的值还是 A,所以 compareAndSet 操作是成功
public class AtomicIntegerDefectDemo {
public static void main(String[] args) {
defectABA();
}
static void defectABA() {
final AtomicInteger atomicInteger = new AtomicInteger(1);
Thread coreThread = new Thread(() -> {
final int currentValue = atomicInteger.get();
System.out.println(Thread.currentThread().getName() + " currentValue=" + currentValue);
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean casResult = atomicInteger.compareAndSet(1, 2);
System.out.println(Thread.currentThread().getName() + " currentValue=" + currentValue +
"finalValue=" + atomicInteger.get() + " compareAndSet Result=" + casResult);
},"core");
coreThread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread amateurThread = new Thread(() -> {
int currentValue = atomicInteger.get();
boolean casResult = atomicInteger.compareAndSet(1, 2);
System.out.println(Thread.currentThread().getName() + " currentValue=" + currentValue +
"finalValue=" + atomicInteger.get() + " compareAndSet Result=" + casResult);
currentValue = atomicInteger.get();
casResult = atomicInteger.compareAndSet(2, 1);
System.out.println(Thread.currentThread().getName() + " currentValue=" + currentValue +
"finalValue=" + atomicInteger.get() + " compareAndSet Result=" + casResult);
},"amateur");
amateurThread.start();
}
}
八 AQS
部分内容参考自JavaGuide AQS总结
简介
-
AQS 的全称为
AbstractQueuedSynchronizer
,翻译过来的意思就是抽象队列同步器。这个类在java.util.concurrent.locks
包下面 -
AQS 就是一个抽象类,主要用来构建锁和同步器
-
AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的
ReentrantLock
,Semaphore
,其他的诸如ReentrantReadWriteLock
,SynchronousQueue
,FutureTask
(jdk1.7) 等等皆是基于 AQS 的
原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列来完成获取资源线程的排队工作。AQS 使用volatile修饰保证线程可见性,使用 CAS 对该同步状态进行原子操作实现对其值的修改
对资源的共享方式
Exclusive(独占)
只有一个线程能执行,如 ReentrantLock
。又可分为公平锁和非公平锁,ReentrantLock
同时支持两种锁,下面以 ReentrantLock
对这两种锁的定义做介绍:
- 公平锁 :按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁 :当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒
ReentrantLock
默认采用非公平锁,因为考虑获得更好的性能,通过 boolean
来决定是否用公平锁(传入 true 用公平锁)
公平锁和非公平锁只有两处不同:
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到
tryAcquire
方法,在tryAcquire
方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
Share(共享)
多个线程可同时执行,如 Semaphore/CountDownLatch
AQS 底层使用了模板方法模式
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样:
- 使用者继承
AbstractQueuedSynchronizer
并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) - 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
Semaphore(信号量)
简介
synchronized
和 ReentrantLock
都是一次只允许一个线程访问某个资源,Semaphore
(信号量)可以指定多个线程同时访问某个资源
Semaphore
有两种模式,公平模式和非公平模式。
- 公平模式: 调用
acquire()
方法的顺序就是获取许可证的顺序,遵循 FIFO; - 非公平模式: 抢占式的,默认
应用
public class SemaphoreDemo {
private static final int threadCount = 550;
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
threadPool.execute(() -> {
try {
semaphore.acquire();//执行 acquire() 方法阻塞,直到有一个许可证可以获得然后拿走一个许可证
test(threadNum);
semaphore.release();//每个 release 方法增加一个许可证,这可能会释放一个阻塞的 acquire() 方法
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadNum) throws InterruptedException {
Thread.sleep(1000);
System.out.println("threadNum=" + threadNum);
Thread.sleep(1000);
}
}
CountDownLatch (倒计时器)
简介
CountDownLatch
允许 count
个线程阻塞在一个地方,直至所有线程的任务都执行完毕
应用
CountDownLatch 的两种典型用法
1、某一线程在开始运行前等待 n 个线程执行完毕。
将 CountDownLatch
的计数器初始化为 n (new CountDownLatch(n)
),每当一个任务线程执行完毕,就将计数器减 1 (countdownlatch.countDown()
),当计数器的值变为 0 时,在 CountDownLatch 上 await()
的线程就会被唤醒。一个典型应用场景就是启动一个服务时,主线程需要等待多个组件加载完毕,之后再继续执行。
2、实现多个线程开始执行任务的最大并行性。
注意是并行性,不是并发,强调的是多个线程在某一时刻同时开始执行。类似于赛跑,将多个线程放到起点,等待发令枪响,然后同时开跑。做法是初始化一个共享的 CountDownLatch
对象,将其计数器初始化为 1 (new CountDownLatch(1)
),多个线程在开始执行任务前首先 coundownlatch.await()
,当主线程调用 countDown()
时,计数器变为 0,多个线程同时被唤醒。
public class CountDownLatchDemo {
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch=new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadNum) throws InterruptedException {
Thread.sleep(1000);
System.out.println("threadNum=" + threadNum);
Thread.sleep(1000);
}
}
不足
CountDownLatch
是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch
使用完毕后,它不能再次被使用
CyclicBarrier(循环栅栏)
简介
CyclicBarrier
的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。
应用
CyclicBarrier
默认的构造方法是 CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用 await()
方法告诉 CyclicBarrier
我已经到达了屏障,然后当前线程被阻塞
CyclicBarrier
可以用于多线程计算数据,最后合并计算结果的应用场景
public class CyclicBarrierDemo {
private static final int threadCount = 550;
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
test(threadNum);
});
}
}
public static void test(int threadNum) {
System.out.println("threadNum=" + threadNum + "is ready");
try {
cyclicBarrier.await(60, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
System.out.println("threadNum=" + threadNum + "is finish");
}
}
CyclicBarrier
还提供一个更高级的构造函数 CyclicBarrier(int parties, Runnable barrierAction)
,用于在线程到达屏障时,优先执行 barrierAction
,方便处理更复杂的业务场景
原理
当调用 CyclicBarrier
对象调用 await()
方法时,实际上调用的是 dowait(false, 0L)
方法。 await()
方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 parties
的值时,栅栏才会打开,线程才得以通过执行
CyclicBarrier
内部通过一个 count 变量作为计数器,count 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务
CyclicBarrier 和 CountDownLatch 的区别
对于 CountDownLatch
来说,重点是“一个线程(多个线程)等待”,而其他的 N 个线程在完成“某件事情”之后,可以终止,也可以等待。而对于 CyclicBarrier
,重点是多个线程,在任意一个线程没有完成,所有的线程都必须等待。
CountDownLatch
是计数器,线程完成一个记录一个,只不过计数不是递增而是递减,而 CyclicBarrier
更像是一个阀门,需要所有线程都到达,阀门才能打开,然后继续执行。