Java语言内置了多线程支持:一个Java程序实际上是一个JVM进程,JVM进程用一个主线程来执行main()方法,在main()方法内部,我们又可以启动多个线程。此外,JVM还有负责垃圾回收的其他工作线程等。
-
进程和线程
进程和线程的关系就是:一个进程可以包含一个或多个线程,但至少会有一个线程。
和多线程相比,多进程的缺点在于:- 创建进程比创建线程开销大,尤其是在Windows系统上;
- 进程间通信比线程间通信要慢,因为线程间通信就是读写同一个变量,速度很快。
而多进程的优点在于:
多进程稳定性比多线程高,因为在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
- 线程基础
public class Main {
public static void main(String[] args) {
Thread t = new Thread(new MyRunnable());
t.start(); // 启动新线程
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
线程状态:
- New:新创建的线程,尚未执行;
- Runnable:运行中的线程,正在执行run()方法的Java代码;
- Blocked:运行中的线程,因为某些操作被阻塞而挂起;
- Waiting:运行中的线程,因为某些操作在等待中;
- TimedWaiting:运行中的线程,因为执行sleep()方法正在计时等待;
- Terminated:线程已终止,因为run()方法执行完毕。
thread.start();//启动新线程
(start()方法会在内部自动调用实例的run()方法)
Thread.sleep();//可以把当前线程暂停一段时间
Thread.setPriority(int n) // 1~10, 默认值5;
设置线程优先级,优先级越高被调度更频繁,但未必会先执行
thread.join(); // 等待线程结束
(通过对另一个线程对象调用join()方法可以等待其执行结束;可以指定等待时间,超过等待时间线程仍然没有结束就不再等待;对已经运行结束的线程调用join()方法会立刻返回。)
thread.interrupt();//中断线程
interrupt()方法仅仅向线程发出了“中断请求”,线程响应的方式:线程的while循环会检测isInterrupted()
class MyThread extends Thread {
public void run() {
int n = 0;
while (! isInterrupted()) {
n ++;
System.out.println(n + " hello!");
}
}
}
对目标线程调用interrupt()方法可以请求中断一个线程,目标线程通过检测isInterrupted()标志获取自身是否已中断。如果目标线程处于等待状态,该线程会捕获到InterruptedException;目标线程检测到isInterrupted()为true或者捕获了InterruptedException都应该立刻结束自身线程;
另一个常用的中断线程的方法是设置标志位。我们通常会用一个running标志位来标识线程是否应该继续运行,在外部线程中,通过把HelloThread.running置为false,就可以让线程结束
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
注意到HelloThread的标志位boolean running是一个线程间共享的变量。线程间共享变量需要使用volatile关键字标记,确保每个线程都能读取到更新后的变量值。
volatile关键字的目的是告诉虚拟机:
1.每次访问变量时,总是获取主内存的最新值;
2.每次修改变量后,立刻回写到主内存。
thread.setDaemon(true);
守护线程是为其他线程服务的线程;
所有非守护线程都执行完毕后,虚拟机退出;
守护线程不能持有需要关闭的资源(如打开文件等)。(这是因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失。)
Thread.currentThread()获取当前线程;
ThreadLocal
ThreadLocal表示线程的“局部变量”,它确保每个线程的ThreadLocal变量都是各自独立的;
ThreadLocal实例通常总是以静态字段初始化;
ThreadLocal适合在一个线程的处理流程中保持上下文(避免了同一参数在所有方法中传递);
使用ThreadLocal要用try … finally结构,并在finally中清除。
static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();
try {
threadLocalUser.set(user);
...
} finally {
threadLocalUser.remove();
}
(也可以通过AutoCloseable接口配合try (resource) {…}结构,让编译器自动为我们关闭)
public class UserContext implements AutoCloseable {
static final ThreadLocal<String> ctx = new ThreadLocal<>();
public UserContext(String user) {
ctx.set(user);
}
public static String currentUser() {
return ctx.get();
}
@Override
public void close() {
ctx.remove();
}
}
try (var ctx = new UserContext("Bob")) {
// 可任意调用UserContext.currentUser():
String currentUser = UserContext.currentUser();
} // 在此自动调用UserContext.close()方法释放ThreadLocal关联对象
- 线程同步
多线程同时读写共享变量时,会造成逻辑错误,因此需要通过synchronized同步;
同步的本质就是给指定对象加锁,加锁后才能继续执行后续代码;
注意加锁对象必须是同一个实例;
对JVM定义的单个原子操作不需要同步。
(原子操作是指不能被中断的一个或一系列操作。
保证一段代码的原子性就是通过加锁和解锁实现的。)
如何使用synchronized:
1.找出修改共享变量的线程代码块;
2.选择一个共享实例作为锁;
3.synchronized(lockObject) { // 获取锁
…
} // 释放锁
JVM规范定义了几种原子操作:
1.基本类型(long和double除外)赋值,例如:int n = m;不过在x64平台的JVM是把long和double的赋值作为原子操作实现的。
2.引用类型赋值,例如:List list = anotherList。
如果一个类被设计为允许多线程正确访问,我们就说这个类就是“线程安全”的(thread-safe);
没有特殊说明时,一个类默认是非线程安全的。
用synchronized修饰方法可以把整个方法变为同步代码块,synchronized方法加锁对象是this;
public synchronized void add(int n) { // 锁住this
count += n;
} // 解锁
对于static方法,是没有this实例的,因为static方法是针对类而不是实例。但是我们注意到任何一个类都有一个由JVM自动创建的Class实例,因此,对static方法添加synchronized,锁住的是该类的Class实例。
public synchronized static void test(int n) {
...
}
VM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
Java的synchronized锁是可重入锁;
死锁产生的条件是多线程各自持有不同的锁,并互相试图获取对方已持有的锁,导致无限等待;
避免死锁的方法是多线程获取锁的顺序要一致。
(死锁发生后,没有任何机制能解除死锁,只能强制结束JVM进程。)
- 锁LOCK
- synchronized(可重入锁)
在synchronized内部可以调用wait()使线程进入等待状态;
必须在已获得的锁对象上调用wait()方法;
在synchronized内部可以调用notify()或notifyAll()唤醒其他等待线程;
必须在已获得的锁对象上调用notify()或notifyAll()方法;(notify()随机唤醒一个,notifyAll()唤醒全部)
已唤醒的线程还需要重新获得锁后才能继续执行。
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
- ReentrantLock(可重入锁)
java.util.concurrent.locks包提供的ReentrantLock可以替代synchronized进行同步;
ReentrantLock获取锁更安全;
必须先获取到锁,再进入try {…}代码块,最后使用finally保证释放锁;
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();//加锁
try {
count += n;
} finally {
lock.unlock(); //释放
}
}
}
可以使用tryLock()尝试获取锁。
if (lock.tryLock(1, TimeUnit.SECONDS)) {
//尝试获取锁的时候,最多等待1秒。如果1秒后仍未获取到锁,tryLock()返回false
try {
...
} finally {
lock.unlock();
}
}
Condition对象必须从Lock对象获取;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
await()会释放当前锁,进入等待状态;
signal()会唤醒某个等待线程;
signalAll()会唤醒所有等待线程;
唤醒线程从await()返回后需要重新获得锁。
if (condition.await(1, TimeUnit.SECOND)) {
// 被其他线程唤醒
} else {
// 指定时间内没有被其他线程唤醒
}
- ReadWriteLock
使用ReadWriteLock可以提高读取效率:
- ReadWriteLock只允许一个线程写入;
- ReadWriteLock允许多个线程在没有写入时同时读取;
- ReadWriteLock适合读多写少的场景。
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
- StampedLock(不可重入锁)
Java 8引入StampedLock,它提供了乐观读锁,可取代ReadWriteLock以进一步提升并发性能;
private final StampedLock stampedLock = new StampedLock(); public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
double currentX = x;
double currentY = y;
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
- Concurrent(并发)集合(线程安全)
使用java.util.concurrent包提供的线程安全的并发集合可以大大简化多线程编程:
多线程同时读写并发集合是安全的;
尽量使用Java标准库提供的并发集合,避免自己编写同步代码
interface | non-thread-safe | thread-safe |
---|---|---|
List | ArrayList | CopyOnWriteArrayList |
Map | HashMap | ConcurrentHashMap |
Set | HashSet / TreeSet | CopyOnWriteArraySet |
Queue | ArrayDeque / LinkedList | ArrayBlockingQueue / LinkedBlockingQueue |
Deque | ArrayDeque / LinkedList | LinkedBlockingDeque |
使用这些并发集合与使用非线程安全的集合类完全相同。
Map<String, String> map = new ConcurrentHashMap<>();
java.util.Collections工具类还提供了一个旧的线程安全集合转换器,可以这么用:(这样获得的线程安全集合的性能比java.util.concurrent集合要低很多,所以不推荐使用)
Map unsafeMap = new HashMap();
Map threadSafeMap = Collections.synchronizedMap(unsafeMap);
- Atomic(原子操作的封装类)
使用java.util.concurrent.atomic提供的原子操作可以简化多线程编程:
原子操作实现了无锁的线程安全;
适用于计数器,累加器等。
Atomic类是通过无锁(lock-free)的方式实现的线程安全(thread-safe)访问。它的主要原理是利用了CAS:Compare and Set。
以AtomicInteger为例,它提供的主要操作有:
- 增加值并返回新值:int addAndGet(int delta)
- 加1后返回新值:int incrementAndGet()
- 获取当前值:int get()
- 用CAS方式设置:int compareAndSet(int expect, int update)
CAS编写incrementAndGet(),它大概长这样:
public int incrementAndGet(AtomicInteger var) {
int prev, next;
do {
prev = var.get();
next = prev + 1;
} while ( ! var.compareAndSet(prev, next));
return next;
}
CAS是指,在这个操作中,如果AtomicInteger的当前值是prev,那么就更新为next,返回true。如果AtomicInteger的当前值不是prev,就什么也不干,返回false。通过CAS操作并配合do ... while循环,即使其他线程修改了AtomicInteger的值,最终的结果也是正确的。
- 线程池
JDK提供了ExecutorService实现了线程池功能:
线程池内部维护一组线程,可以高效执行大量小任务;
Executors提供了静态方法创建不同类型的ExecutorService;
必须调用shutdown()关闭ExecutorService;
ExecutorService只是接口,Java标准库提供的几个常用实现类有
- FixedThreadPool:线程数固定的线程池;
ExecutorService executor = Executors.newFixedThreadPool(3);
- CachedThreadPool:线程数根据任务动态调整的线程池;
ExecutorService executor =Executors.newCachedThreadPool();
//线程池的大小限制在4~10个之间动态调整
ExecutorService es = new ThreadPoolExecutor(4, `10,
60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
- SingleThreadExecutor:仅单线程执行的线程池
- ScheduledThreadPool:定期反复执行
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
// 1秒后执行一次性任务:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,每3秒执行(不管上一个任务是否结束):
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
// 2秒后开始执行定时任务,以3秒为间隔执行(上一个任务执行完毕后隔3s):
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
Java标准库还提供了一个java.util.Timer类,这个类也可以定期执行任务,但是,一个Timer会对应一个Thread,所以,一个Timer只能定期执行一个任务,多个定时任务必须启动多个Timer,而一个ScheduledThreadPool就可以调度多个定时任务,所以,我们完全可以用ScheduledThreadPool取代旧的Timer。
CountDownLatch
public void Main(){
ExecutorService es = Executors.newFixedThreadPool(threadNum);
CountDownLatch latch = new CountDownLatch(threadNum);
for (int i = 0 ; i < threadNum ; i++) {
AuditConfirmationThread thread = new AuditConfirmationThread(latch,....);
es.execute(thread);
}
try{
// 等线程全部完成,再继续执行
latch.await();
// 关闭线程池
es.shutdown();
} catch (InterruptedException e){
throw new RuntimeException(e);
}
}
public class AuditConfirmationThread implements Runnable {
public AuditConfirmationThread(CountDownLatch threadCountDown) {
this.threadCountDown = threadCountDown;
}
@Override
public void run() {
// 先执行处理逻辑后,自减1
threadCountDown.countDown();
}
}
-
Future(任务返回结果对象)
Runnable接口无返回值,Callable接口有返回值。 executor.execute()执行线程无返回值;executor.submit()执行线程有返回值;
ExecutorService executor = Executors.newFixedThreadPool(4);
// 定义任务:
Callable<String> task = new Task();
// 提交任务并获得Future:
Future<String> future = executor.submit(task);
// 从Future获取异步执行返回的结果:
String result = future.get(); // 可能阻塞
一个Future接口表示一个未来可能会返回的结果,它定义的方法有:
- get():获取结果(可能会等待)
- get(long timeout, TimeUnit unit):获取结果,但只等待指定的时间;
- cancel(boolean mayInterruptIfRunning):取消当前任务;
- isDone():判断任务是否已完成。
CompletableFuture
java8引入。
CompletableFuture的命名规则:
xxx():表示该方法将继续在已有的线程中执行;
xxxAsync():表示将异步在线程池中执行。
public static void main(String[] args) throws Exception {
// 创建异步执行任务:
CompletableFuture<Double> cf = CompletableFuture.supplyAsync(Main::fetchPrice);
// 如果执行成功:
cf.thenAccept((result) -> {
System.out.println("price: " + result);
});
// 如果执行异常:
cf.exceptionally((e) -> {
e.printStackTrace();
return null;
});
// 主线程不要立刻结束,否则CompletableFuture默认使用的线程池会立刻关闭:
Thread.sleep(200);
}
CompletableFuture可以指定异步处理流程:
- thenAccept()处理正常结果;
- exceptional()处理异常结果;
- thenApplyAsync()用于串行化另一个CompletableFuture;
- anyOf()和allOf()用于并行化多个CompletableFuture。
(anyOf()可以实现“任意个CompletableFuture只要一个成功”,allOf()可以实现“所有CompletableFuture都必须成功)
public class Main {
public static void main(String[] args) throws Exception {
// 两个CompletableFuture执行异步查询:
CompletableFuture<String> cfQueryFromSina = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://finance.sina.com.cn/code/");
});
CompletableFuture<String> cfQueryFrom163 = CompletableFuture.supplyAsync(() -> {
return queryCode("中国石油", "https://money.163.com/code/");
});
// 用anyOf合并为一个新的CompletableFuture:
CompletableFuture<Object> cfQuery = CompletableFuture.anyOf(cfQueryFromSina, cfQueryFrom163);
}
}
- Fork/Join
java7引入。
Fork/Join是一种基于“分治”的算法:通过分解任务,并行执行,最后合并结果得到最终结果。
ForkJoinPool线程池可以把一个大任务分拆成小任务并行执行,任务类必须继承自RecursiveTask或RecursiveAction。
使用Fork/Join模式可以进行并行计算以提高效率。
class SumTask extends RecursiveTask<Long> {
protected Long compute() {
// “分裂”子任务:
SumTask subtask1 = new SumTask(...);
SumTask subtask2 = new SumTask(...);
// invokeAll会并行运行两个子任务:
invokeAll(subtask1, subtask2);
// 获得子任务的结果:
Long subresult1 = subtask1.join();
Long subresult2 = subtask2.join();
// 汇总结果:
return subresult1 + subresult2;
}
}
Java标准库提供的java.util.Arrays.parallelSort(array)可以进行并行排序,它的原理就是内部通过Fork/Join对大数组分拆进行并行排序,在多核CPU上就可以大大提高排序的速度。