juc,即java.util.concurrent
包的缩写,掌握了juc,就是拿到了Java并发编程的钥匙。
在《Java并发编程实战》等书中,已经详细介绍juc用法,如果你懒得看书,或者是忘了juc的用法,想快速回忆一下,可以看我这篇教程。
本教程很长,有很多的代码示例供食用~
基础
volatile关键字
volatile
关键字不属于juc的内容,但是为了铺垫后面的内容,这里先介绍下。
当多个线程之间共享一个数据时,该数据对彼此之间是不可见的。即使是同一个数据,每个线程还是会将其保存在自己独立的内存下面。
下面的代码显示了这一特性:
class Worker implements Runnable {
public boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.flag = true;
}
}
public class VolatileDemo {
public static void main(String[] args) {
Worker worker = new Worker();
new Thread(worker).start();
while (!worker.flag) ;
System.out.println("程序结束");
}
}
按理说,worker
线程将flag
改为true
,主线程在flag
变为true
之后会及时跳出循环,程序退出。
但是,实际运行下来程序并没有退出,这是因为worker
线程的flag
和主线程的flag
是不共享的,worker
对flag
的修改并不会影响到主线程。
要想改变这一点,需要将flag
声明为volatile
的。这个关键词的作用是,当变量被某个线程改变时,会及时刷新到主存中,读取时也会从主存中读取。可以保证变量是线程之间可见的。
要想让上面的程序及时退出,将上面的flag
声明改为:
public volatile boolean flag = false;
这样worker
对flag的改变对于主线程就是可见的了,程序可以及时退出了。
原子性
如果一个变量需要被多个线程同时访问,对其进行操作就要格外当心。除了可见性问题,可以使用volatile
修正,还有原子性问题。
如果一个变量的操作需要多步完成,操作可以细分,则该操作就不具备原子性,例如i++
操作就不具备原子性。在并发操作时,就可能因为线程执行非原子操作导致数据读写不一致的情况。
我们可以通过给操作加上synchronized
关键字,让操作只能允许一个线程进行,来实现操作的原子性。
另一种实现原子性的方法是使用CAS操作
(Compare And Swap)。CAS操作由CPU直接提供,CAS需要下面三个操作数:
- valueOffset:变量在内存中的位置
- expect:变量的预估值
- update:变量的更新值
CAS的操作过程:
- 从valueOffset取出value,若等于expect,则将valueOffset的值设为update
- 否则不进行任何操作
那么想要将i++
变为原子的,只需要将valueOffset
设为i
,expect
设为读取到的i
的值,update
设为i+1
。这样,只有当数据一致时,才会执行i+1
操作。
在java.util.concurrent.atomi
下,提供了很多原子变量,这些变量都具备:
- 使用
volatile
确保变量可见性 - 使用CAS操作确保操作是原子的
例如,下面的代码:
package cn.offer.juc;
import java.util.concurrent.atomic.AtomicInteger;
class AtomWorker implements Runnable {
private AtomicInteger i = new AtomicInteger();
@Override
public void run() {
while (true) {
System.out.println(i.addAndGet(1));
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Atomicity {
public static void main(String[] args) {
AtomWorker worker = new AtomWorker();
for (int i = 0; i < 10; ++i) {
new Thread(worker).start();
}
}
}
就可以保证各个线程不会读到重复的i
。
ThreadPool
线程池的概念不再介绍,这里只介绍juc提供的线程池操作。
要想说线程池,就不得不说一下juc的Executor
执行框架,在这个框架下,所有的并发执行单位都以“任务”的形式存在,将任务提交给ExecutorService
,即可实现任务的并发调度执行。
ExecutorService
可以有很多种,它负责接收任务,执行任务,使用Executors
可以创建各种ExecutorService
,有下面几种常用的:
- newSingleThreadExecutor:单一线程,任务会顺序执行
- newCachedThreadPool:大小不受限制的线程池
- newFixedThreadPool:大小固定的线程池,当线程不够时,任务需要等待
- newScheduledThreadPool:大小固定线程池,支持定时及周期性任务执行
ExecutorService
提供了下面的将Runnable
任务提交执行的方法:
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
关于Future
的使用,见Callable和Future,这里不关心。
下面的代码演示了将线程提交给线程池执行:
package cn.offer.juc;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
class Task implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getId() + "执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getId() + "执行完毕");
}
}
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 5; ++i) {
service.submit(new Task());
}
}
}
Callable和Future
使用传统的Runnable
,可以启动一个线程并发执行,但是run
方法是没有返回值的,如果我们想要线程能够返回一个值,就可以使用Callable
+Future
。
我们想要一个线程能返回值,这时候让其实现java.util.concurrent.Callable
接口,在泛型中指定返回类型,例如,我们让一个worker返回字符串:
class CallableWorker implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "运行完毕";
}
}
这个call
方法和传统的run
方法相比,有两个不同:
- 方法有返回值
- 方法可以抛出异常
那么,如何执行呢?一般使用ExecutorService
来执行,该接口中有如下这个方法:
<T> Future<T> submit(Callable<T> task);
Future
用于查询执行的Callable
(或Runnable
)的执行结果、是否完成等信息。该接口的定义如下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
这些函数的解释如下:
- cancel:尝试取消任务的执行。
- mayInterruptIfRunning:是否允许取消已经启动但是没有执行的任务。
- 返回:有如下几种情况:
- 任务已经完成,返回false
- 任务还没有启动,取消任务,返回取消结果。
- 任务已经启动,但是
mayInterruptIfRunning
为false
,返回false - 任务已经启动,且
mayInterruptIfRunning
为true
,取消任务,返回取消结果。
- isCancelled:返回任务是否在其正常结束之前被取消。
- isDone:任务是否结束。不论是任务正常结束、抛出异常、被cancel,该函数都会返回true。
- get():阻塞直到任务结束,随后获取其返回值。
- get(timeout, unit):在指定的
timeout
时间内等待任务结束并获取结果,如果超过这个时间没有结束,抛出TimeoutException
异常。
另外说明一下get
方法可能抛出的其它异常:
- CancellationException:在等待途中任务被cancel
- ExecutionException:任务抛出了异常
- InterruptedException:阻塞过程中被打断
通过Future
,我们就可以获取任务的返回值了:
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
CallableWorker worker = new CallableWorker();
// 单一线程执行器
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(worker);
// 获取线程的执行结果
System.out.println("执行结果:" + future.get());
}
}
你也可以用Future
做很多其它事情,就看你自己发挥了。
锁
Lock
Lock
接口的定义如下:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
下面简单介绍一下这些方法:
- lock:获取锁。如果锁已经被占用,会阻塞。
- lockInterruptibly:可中断地等待锁,如果等待被中断,抛出
InterruptedException
。 - tryLock:尝试获取锁,如果获取失败,返回false。
- tryLock(time, unit):在timeout时间内尝试获取锁,如果在这段时间内获取到锁,返回true,如果没有,返回false。
- unlock:释放锁
- newCondition:返回一个绑定到该锁的
Condition
示例,关于Condition,见Condition一节。
我们一般会使用到下面这个Lock
的实现类:
- ReentrantLock:可重入锁,也叫递归锁。指的是,当一个线程获取锁之后,再次获取时,不需要重复等待,可以直接获取锁。
- 构造时将
fair
设为true
,表示公平锁,公平锁指的是严格按照先来先得的顺序排队等待去获取锁。 - 构造时将
fair
设为false
,表示非公平锁,非公平锁每次获取锁时,是先直接尝试获取锁,获取不到,再按照先来先得的顺序排队等待。 - 默认是非公平锁。
- 构造时将
锁的操作不难,下面我们重点介绍下读写锁。
ReadWriteLock
读写锁指的是没有线程进行写操作时,多个线程可同时进行读操作,当有线程进行写操作时,其它读写操作只能等待。
即,对于读写锁来说,“读-读能共存,读-写不能共存,写-写不能共存”。
ReadWriteLock
接口定义如下:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
其中,readLock
用于获取读锁,writeLock
用于获取写锁。
我们一般使用实现类ReentrantReadWriteLock
,即可重入的读写锁。
下面我们来看一个具体的例子:
package cn.offer.juc