目录
JUC指的是java.util中的并发编程工具包java.util.concurrent
主要包括以下四类:
- Lock框架和Tools类(AQS,实现如ReentrantLock,工具类)
- Collections并发集合(ConcurrentHashMap,CopyOnWriteList)
- Atomic原子类(CAS,Unsafe类,原子类)
- Executors线程池
一、AQS详解
AQS 的全称为 AbstractQueuedSynchronizer
,AQS 就是一个抽象类,主要用来构建锁和同步器
AQS原理
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中
AQS 对资源的共享方式
一共两种资源共享方式
- 独占:只有一个线程能执行,如 ReentrantLock
- 共享:多个线程可同时执行,如 Semaphore/CountDownLatch
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可
AQS 底层使用了模板方法模式
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。
AQS具体应用举例
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() 函数返回,继续后余动作
二、ConcurrentHashMap,CopyOnWriteList
详细参考【Java】容器 ,内有对ConcurrentHashMap,CopyOnWriteList的详细解读
三、Atomic原子类(CAS,Unsafe类,原子类)
原子类
原子类说简单点就是具有原子/原子操作特征的类,以AtomicInteger 类为例说明原子类的特点
AtomicInteger 类主要利用CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升
CAS 的原理:拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值
通过一个简单例子带大家看一下基本数据类型原子类的优势
- 多线程环境不使用原子类保证线程安全(基本数据类型)
class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}
public int getCount() {
return count;
}
}
- 多线程环境使用原子类保证线程安全(基本数据类型)
class Test2 {
private AtomicInteger count = new AtomicInteger();
public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}
Unsafe类
Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等。
这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再“安全”,因此对 Unsafe 的使用一定要慎重
CAS操作
CAS是Unsafe提供的方法
CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题
在 JUC 包的并发工具类中大量地使用了 CAS 操作,其作为乐观锁在并发工具类中广泛发挥了作用。在 Unsafe 类中,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的 CAS 操作
四、线程池Executor
Executor 框架结构(主要由三大部分组成)
-
任务(Runnable /Callable)
执行任务需要实现的 Runnable 接口 或 Callable接口
-
任务的执行(Executor)
ExecutorService
接口继承了Executor ,ThreadPoolExecutor
和ScheduledThreadPoolExecutor
这两个关键类实现了ExecutorService
接口
-
异步计算的结果(Future)
Future 接口以及 Future 接口的实现类 FutureTask 类都可以代表异步计算的结果。
当我们把 Runnable接口 或 Callable 接口 的实现类提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。(调用 submit() 方法时会返回一个 FutureTask 对象)
Executor 框架的使用示意图
- 主线程首先要创建实现 Runnable 或者 Callable 接口的任务对象。
- 把创建完成的实现 Runnable/Callable接口的 对象直接交给 ExecutorService 执行: ExecutorService.execute(Runnable command))或者也可以把 Runnable 对象或Callable 对象提交给 ExecutorService 执行(ExecutorService.submit(Runnable task)或 ExecutorService.submit(Callable task))。
- 如果执行 ExecutorService.submit(…),ExecutorService 将返回一个实现Future接口的对象(我们刚刚也提到过了执行 execute()方法和 submit()方法的区别,submit()会返回一个 FutureTask 对象)。由于 FutureTask 实现了 Runnable,我们也可以创建 FutureTask,然后直接交给 ExecutorService 执行。
- 最后,主线程可以执行 FutureTask.get()方法来等待任务执行完成。主线程也可以执行 FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
几种常见的线程池
- 创建线程池可以通过
new ThreadPoolExecutor(...)
自己指定参数获取线程池 - 也可以通过Executors 类的静态方法创建几种常见的线程池,这种方式本质上是用了固定参数的
new ThreadPoolExecutor(...)
返回的线程池- newFixedThreadPool:固定线程数的线程池
- SingleThreadExecutor:只有一个线程的线程池
- CachedThreadPool:根据需要创建新线程的线程池,最大创建Integer.MAX_VALUE个