转载自
0、进程和线程的区别:
进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,上下文切换大约需要上千条指令,一个进程包含1--n个线程。(进程是资源分配的最小单位)
线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小,大约需要100条指令。(线程是cpu调度的最小单位)
多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。 线程安全指的是:无论各线程的相对执行次序如何,所有线程执行完的最终结果是确定的。
一、线程状态转换
新建(New)
线程创建后尚未启动。
可运行(Runnable)
线程可能正在运行,也可能等待获取 CPU。包含了操作系统线程状态中的 Running 和 Ready。
阻塞(Blocking)
当线程试图获取一个对象锁(Synchronized 锁,而不是 JUC 中的 Lock)未成功时,该线程进入阻塞状态。当其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞态。
无限期等待(Waiting)
当线程等待其他线程通知调度器满足条件时,它自己进入等待状态。如
进入方法 | 退出方法 |
---|---|
JUC 中的 Lock() | 其他线程执行了 unlock() |
FutureTask.get() | 线程执行结束 |
没有设置 Timeout 参数的 Thread.join() 方法 | 被调用的线程执行完毕 |
没有设置 Timeout 参数的 Object.wait() 方法 | Object.notify() / Object.notifyAll() |
Condition的 await() | 其他线程在本条件对象上执行了 signal()/signalAll() |
限期等待(Timed Waiting)
有几个方法有超时参数。调用它们导致线程进入限期等待状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有:
进入方法 | 退出方法 |
---|---|
Thread.sleep() 方法 | 超时 |
Lock.tryLock() 方法 | 超时 / 获得锁 |
FutureTask.get() | 超时 / 线程执行结束 |
设置了 Timeout 参数的 Thread.join() 方法 | 超时 / 被调用的线程执行完毕 |
设置了 Timeout 参数的 Object.wait() 方法 | 超时 / Object.notify() / Object.notifyAll() |
Condition.await() | 超时 / Condition.signal() / Condition.signalAll() |
线程终止
线程会以如下方式结束,结束后就处于死亡状态。
- run() 或 call() 方法执行完成,线程正常结束。
- 线程抛出一个未捕获的 Exception 或 Error。
可以调用线程对象的 isAlive() 方法测试线程是否已经死亡,当线程处于就绪、运行、阻塞三种状态时,该方法将返回 true;当线程处于新建、死亡两种状态时,该方法将返回 false。
二、创建线程
创建线程的方法有四种:
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 提交任务到线程池
实现 Runnable 和 Callable 接口的对象只能当做一个可以在线程中运行的任务,不是真正意义上的线程,最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。
继承 Thread 类
步骤如下:
启动线程后,虚拟机会将该线程放入就绪队列中等待调度,当一个线程被调度时会执行该线程的 run() 方法。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
实现 Runnable 接口
步骤如下:
- 需要创建一个 Runnable 接口的实现类对象(通常通过 Lambda 表达式创建)。
- 将 Runnable 接口对象作为参数,创建 Thread 对象。
- 调用 Thread 对象的 start() 方法来启动线程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
实现 Callable<E> 接口
步骤如下:
- 需要创建一个 Callable 接口的实现类对象(通常通过 Lambda 表达式创建)。
- 将 Callable 接口对象作为参数,创建 FutureTask<E> 对象。
- 将 FutureTask 对象作为参数,创建 Thread 对象。
- 调用 Thread 对象的 start() 方法来启动线程。
- 可以通过 FutureTask 的 get() 方法获取线程运行结果。
与 Runnable 相比,Callable 可以有返回值,类型参数表示返回值类型。返回值通过 FutureTask 进行封装。此外,call() 方法可以声明抛出异常。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
实现接口 VS 继承 Thread
实现接口会更好一些,因为:
- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
- 类可能只要求可执行就行,继承整个 Thread 类开销过大。
- 接口很容易的实现资源共享
三、Future<E> 接口
在介绍 Callable 时我们知道它可以有返回值,返回值通过 Future 进行封装。FutureTask 实现了 RunnableFuture 接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。
FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。
在 Future 接口里定义了如下几个方法来控制它关联的 Callable 任务以及获取任务执行结果:
- boolean cancel(boolean mayInterruptIfRunning):将 Future 里关联的 Callable 任务的中断标志置位。如果任务已经开始,且 mayInterruptIfRunning 为 true,它就会被中断。如果取消成功,则返回 true。
- V get() :获取 Callable 任务里的 call() 方法的返回值,若 call 方法未执行结束,调用该方法将导致程序阻塞,等到子线程结束后才会得到返回值。
- V get(long timeout, TimeUnit unit) :获取 Callable 任务里的 call() 方法的返回值。该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常。
- boolean isCancelled() :如果在 Callable 任务正常完成前被取消,则返回 true ;
- boolean isDone() :如果任务已结束,无论是正常结束、中途取消或发生异常,都返回 true 。
四、基础线程机制
Daemon
守护线程(后台线程 Daemon Thread)是程序运行时在后台提供服务的线程。
调用 Thread 对象的 setDaemon(true) 方法可以将指定线程设为后台线程。但必须在线程启动之前设定。
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.setDaemon(true);
thread.start();
}
isDaemon() 方法,用于判断指定线程是否为后台线程。
当前台线程未全部结束时,前台线程和后台线程交替运行,但是当所有前台线程都运行结束时,所有后台线程也结束。
main() 线程默认是前台线程,但并不是所有的线程都默认是前台线程——前台线程创建的子线程默认都是前台线程,后台线程创建的子线程默认是后台线程。后台线程应该永远不去访问固有的资源,如文件、数据库,因为它会在任何时候结束。
sleep()
Thread.sleep(millisec) 方法会休眠当前正在执行的线程,millisec 单位为毫秒。
当线程调用 sleep() 后,如果被其他线程中断,会抛出 InterruptedException,因为异常不能跨线程传播回 main() 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
yield()
是 Thread 类提供的一个静态方法,它可以让当前正在执行的线程进入就绪态,重新竞争CPU,不会阻塞该线程。重新竞争 CPU 时,优先级高的线程只是获得 CPU 的机会大,但并不一定会获得 CPU。
public void run() {
Thread.yield();
}
线程优先级
线程都具有一定的优先级,优先级高的线程获得较多的执行机会。每个线程默认的优先级都与创建它的父线程的优先级相同,在默认情况下,main 线程具有普通优先级(NORM_PRIORITY)。
Thread 类提供了 setPriority(int newPriority)、getPriority() 方法来设置和返回指定线程的优先级,其中 setPriority() 方法的参数为1~10之间的整数,也可以使用 Thread 类的如下三个静态常量。
- MAX_PRIORITY:其值是10。
- MIN_PRIORITY:其值是1。
- NORM_PRIORITY:其值是5。
虽然 java 提供了10个优先级级别,但是不同操作系统的优先级不能很好地和 java 的10个优先级对应,因此应该尽量避免直接用数字为线程指定优先级,而应该使用 java 提供的优先级常量。
五、线程池
创建和销毁线程时间以及系统资源的开销较大。如果程序中需要创建大量生命期短的线程,应该使用线程池。线程池中包含许多可运行的空闲线程。将一个 Runnable 或 Callable 对象提交给线程池,线程池就会启动一个线程来执行 run() 或 call() 方法。执行结束后,线程不会死亡,而是返回线程池中成为空闲状态,等待下一次执行。
注意:由于 Thread 类实现了 Runnable 接口,所以Thread类对象也可以提交给线程池。
此外,线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致虚拟机崩溃,而创建一个固定数量的线程池可以控制系统中并发线程数。
线程池创建
执行器( Executor )类有许多静态方法用来构建线程池。创建出的线程池分为两种:提交任务后立即执行 ( ExecutorService ) 类对象和提交任务后延迟指定时间再执行 ( ScheduledExecutorService ) 类对象。
线程池对象类型 | 创建线程池的静态方法 | 方法描述 |
---|---|---|
ExecutorService | newCachedThreadPool() | 创建一个具有缓存功能的线程池。如果线程池中有空闲线程,就利用空闲线程创建任务,否则,创建新线程,线程完成后放入线程池。 缓存型池子通常用于执行一些生存期很短的异步型任务。 空闲线程会被保留60秒。 超过60s,线程实例将被终止及移出池。 |
ExecutorService | newFixedThreadPool(int nThreads) | 创建一个可重用的、具有固定线程数的线程池。空闲线程会一直被保留。如果提交的任务数多于空闲线程,那么未服务的任务放入等待队列中。 |
ExecutorService | newSingleThreadExecutor() | 创建只有一个线程的线程池,顺序执行提交的每一个任务 |
ScheduledExecutorService | newScheduledThreadPool(int corePoolSize) | 用于预定执行而构建的固定线程池 |
ScheduledExecutorService | newSingleThreadScheduledExecutor() | 用于预定执行而构建的单线程池 |
任务提交
ExecutorService 线程池
对于 ExecutorService 类的线程池而言,有如下三个提交任务给 ExecutorService 线程池的方法:
- Future<?> submit(Runnable task):任务没有返回值
- <T> Future<T> submit(Runnable task, T result):task 任务的返回值为 result
- <T> Future<T> submit(Callable<T> task):可以通过调用返回的 Future 对象的 get() 方法获取任务的返回值。
ExecutorService 类的 submit() 方法返回一个 Future 泛型对象。由于 Runnable 对象没有返回值,所以返回值类型是泛型通配符。此外,可以通过调用 Future 对象的 cancel()、isCancelled()、isDone() 来查询和控制任务的状态。
用完线程池后,应该调用线程池的 shutdown() 方法关闭线程池,调用 shutdown() 方法后,线程池不再接收新任务,但会将以前所有已提交任务执行完成。当线程池中的所有任务都执行完成后,池中的所有线程都会死亡;另外也可以调用线程池的 shutdownNow() 方法来关闭线程池,相当于调用每个线程的 interrupt() 方法。
使用 ExecutorService 线程池来执行线程任务的步骤如下。
- 调用 Executors 类的静态工厂方法创建一个 ExecutorService 对象,该对象代表一个线程池。
- 创建 Runnable 或 Callable 对象,作为线程执行任务。
- 调用 ExecutorService 对象的 submit() 方法来提交 Runnable 或 Callable 对象。通过 submit() 方法返回的 Future 对象获取任务的执行结果。
- 当不想提交任何任务时,调用 ExecutorService 对象的 shutdown() 方法来关闭线程池。
public class Main implements Serializable {
public static void main(String[] args) throws Exception{
ExecutorService pool = Executors.newFixedThreadPool(5);
Callable<Integer> ca = ()->{System.out.println("hello world!");return 1;};
Future<Integer> future = pool.submit(ca);
System.out.println(future.get());
pool.shutdown();
}
}
ScheduledExecutorService 线程池
对于 ScheduledExecutorService 类的线程池而言,有如下三个提交任务给 ScheduledExecutorService 线程池的方法:
- ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit): Runnable 任务将在 delay 延迟后执行。
- ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit): Callable 任务将在 delay 延迟后执行。
- ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit):Runnable 任务将在 delay 延迟后执行,而且周期性的运行此任务。即在 initialDelay 后开始执行,依次在 initialDelay + period、initialDelay + 2*period··· 处重复执行。如果上次的线程还没有执行完成,那么会阻塞下一个线程的执行,即使有空闲线程。因此period可看作是线程重复执行的最小周期。
- ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit):创建并执行一个在 initialDelay 初始延迟后首次启用的定期操作,随后在每一次执行终止和下一次执行开始之间都暂停 delay 时间。如果任务在任一次执行时遇到异常,就会取消后续执行;否则只能通过程序来显式取消或终止该任务。
六、中断
一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。中断操作常用于让等待的线程结束运行。
interrupt()
当对一个线程调用 interrupt() 方法时,
- 对于可运行态的线程而言,只是将线程的中断状态被置为 true 。
- 当被中断线程处于可中断的等待态时(即此线程调用了 sleep() 、 wait() 、 join() 等而处于无限等待或限期等待状态时)
- 被中断线程将清除中断标志。
- 并抛出 InterruptionException 异常。
对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。
public class InterruptExample {
private static class MyThread1 extends Thread {
public void run() {
try {
Thread.sleep(2000);
System.out.println("Thread run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new MyThread1();
thread1.start();
thread1.interrupt();
System.out.println("Main run");
}
//输出
Main run
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at InterruptExample.lambda$main$0(InterruptExample.java:5)
at InterruptExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
interrupted()
interrupted() 查询当前线程是否被中断。此方法为 Thread 类的静态方法。interrupted() 方法会清除线程的中断状态。方法声明为:static boolean interrupted();
isInterrupted()
是 Thread 类的实例方法。查询线程的中断状态,此方法不会改变线程的中断状态。方法声明为:boolean isInterrupted()。
java 没有要求被中断的线程应该终止。中断线程仅仅是将中断标志置位,被中断的线程决定如何响应中断。但普遍的情况是,线程简单地将中断作为一个终止信号,此种线程的 run 方法模板如下:
Runnable r = () -> {
try{
...
while(!Thread.currentThread().isInterrupted() && more work to do ){
do more work
}
}
catch(InterruptedException e){
//thread was interrupted during sleep or wait
}
finally{
//cleanup,if required
}
}
如果在 do more work 中调用了 sleep() or wait() 方法,那么无须通过检查中断状态来结束线程,而需要捕获InterruptedException,例如:
Runnable r = () -> {
try{
...
while(more work to do ){
do more work
Thread.sleep(delay);
}
}
catch(InterruptedException e){
//thread was interrupted during sleep or wait
}
finally{
//cleanup,if required
}
}
线程池中的线程中断
调用线程池的 shutdownNow() 方法,相当于调用每个线程的 interrupt() 方法,可用于中断所有线程。
如果只想中断 Executor 中的一个线程,可以通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。
七、互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问。第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的类 Lock。
synchronized
synchronized 可作用于代码块、实例方法、静态方法。
1. 同步普通对象的代码块
同步代码块的语法格式如下:
public void func() {
synchronized (this) {
// ...
}
}
代码含义是:线程开始执行同步代码块之前,必须获得该对象锁。任何时候只有一个线程可以获得对相同对象的锁定。当同步代码块执行完成后,该线程会释放该对象锁。它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程。由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步。当一个线程进入同步语句块时,另一个线程就必须等待。
public class SynchronizedExample {
public void func1() {
synchronized (this) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e1.func1());
}
//输出结果
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
对于以下代码,两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func1());
executorService.execute(() -> e2.func1());
}
//输出结果
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
2. 同步类对象的代码块
public void func() {
synchronized (SynchronizedExample.class) {
// ...
}
}
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。
public class SynchronizedExample {
public void func2() {
synchronized (SynchronizedExample.class) {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
}
}
}
public static void main(String[] args) {
SynchronizedExample e1 = new SynchronizedExample();
SynchronizedExample e2 = new SynchronizedExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> e1.func2());
executorService.execute(() -> e2.func2());
}
//输出
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
3.同步实例方法
同步方法在方法定义的返回值类型前添加synchronized
关键字。效果相当于同步代码块中同步监视器为this。
Class Foo{
public synchronized static void methodA(){
///
}
public void methodB(){
synchronized(Foo.class)//两者效果相同
}
}
synchronized
关键字不能继承,基类的方法synchronized f(){}在继承类中并不自动是synchronized,而仍是f(){}
4. 同步一个静态方法
public synchronized static void fun() {
// ...
}
静态和非静态方法的锁互不干预。即类对象的锁和实例对象的锁互补干预。
Lock
锁提供了对共享资源的独占式访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。Lock只作用于使用相同 Lock 对象的各线程。
某些锁可能允许对共享资源并发访问,如 ReadWriteLock(读写锁)。 Lock、ReadWriteLock 是 Java 5 提供的两种根接口,并为 Lock 提供了 ReentrantLock 实现类(可重入锁)。
可重入锁
ReentrantLock 锁具有可重入性,即一个线程可以对已被加锁的 ReentrantLock 对象再次加锁, ReentrantLock 对象会维持一个计数器来追踪 lock() 方法的嵌套调用,线程在每次调用 lock() 加锁后,必须显式调用 unlock() 来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法,即在执行对象中所有同步方法不用再次获得锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
//do work
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
//效果如下
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
把 unlock() 放在 fimally 代码块至关重要,目的是:即使代码出现异常,也能保证线程释放锁,不会出现死锁的情况。
读写锁
ReentrantReadWriteLock 允许对共享资源并发访问,即允许多个读者线程同时访问,而每次只允许一个写者线程访问。
下面是使用读写锁的必要步骤:
构造一个 ReentrantReadWriteLock 对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
抽取读锁和写锁
private lock readLock = rwl.readLock(); private lock writeLock = rwl.writeLock();
对所有的获取方法加读锁:
public double getTotalBalance(){ readLock.lock(); try{...} finally{readLock.unlock();} }
对所有的修改方法加写锁
public void transfer(...){ writeLock.lock(); try{...} finaly{writeLock.unlock();} }
方法说明:
Lock readLock()
得到一个可以被多个读操作共用的读锁,但会排斥所有写操作。
Lock writeLock()
得到一个写锁,排斥所有其他的读操作和写操作。
tryLock()
tryLock 与 Lock 相比的优点时,当线程成功获得锁时,返回 true,否则,立即返回 false,线程不会阻塞。例如:
if(myLock.tryLock()){
try{...}
finally{myLock.unlock();}
}
else
//do something else
此外,调用 tryLock 时,可以使用超时参数,例如:
if(myLock.tryLock(100,TimeUnit.MILLISECONDS))
...
TimeUnit 是一个枚举类型,可以取得值包括:SECONDS、MILLISECONDS、MICROSECONDS、NANOSECONDS。
lock() 方法不能被中断,如果一个线程在等待一个锁时被中断,被中断的线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么 lock() 方法将无法终止。
而调用带有超时参数的 tryLock(),在等待期间被中断,将抛出中断异常,从而允许程序打破死锁。
比较
1. 锁的实现
synchronized 是一种悲观锁,是 JVM 实现的机制,JVM 会将 synchronized 解释为两条语句:monitorenter 和 monitorexit。由于synchronized 代码出现异常时,JVM 也会释放锁,所以有两条 monitorexit 指令。一条用于正常执行结束时释放锁,一条用于出现异常时释放锁。且每个对象内部都有一个对象锁。
而 ReentrantLock 是 JDK 实现的类。本质上属于乐观锁。它底层实现为 CAS 和 volatile。
2. 性能
新版本 Java 对 synchronized 进行了很多优化,例如线程自旋和适应性自旋,锁消除 , 锁粗化,轻量级锁和偏向所等。使得 synchronized 与 ReentrantLock 性能相差不大。
3. 等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,可设置超时,而 synchronized 不可中断,且不能设置超时。
4. 公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
5. 锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
八、线程之间的协作
当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。
join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程执行结束。
public class JoinExample {
private class A extends Thread {
public void run() {
System.out.println("A");
}
}
private class B extends Thread {
private A a;
B(A a) {
this.a = a;
}
public void run() throws Exception{
a.join();
System.out.println("B");
}
}
public void test() {
A a = new A();
B b = new B(a);
b.start();
a.start();
}
}
public static void main(String[] args) {
JoinExample example = new JoinExample();
example.test();
}
//输出
A
B
wait() notify() notifyAll()
Object 类提供了 wait()、notify() 和 notifyAll() 三个方法,三个方法必须由同步监视器来调用,即类对象或 this。
它们都属于 Object 的实例方法,不属于 Thread 的方法,且只能用在当前同步监视器的同步方法或者同步代码块中,否则会抛出 IllegalMonitorStateExeception。
方法说明:
wait():导致当前线程阻塞,直到其他线程调用该同步监视器的 notify() 方法或 notifyAll() 方法来唤醒该线程。该 wait() 方法有三种形式
无时间参数的 wait (一直等待,知道其他线程通知)
带毫秒参数的 wait() 。
带毫秒、毫微秒参数的 wait()(这两种方法都是等待指定时间后自动苏醒)。
调用 wait() 方法的当前线程会释放对同步监视器的锁定。调用wait()方法的线程在被唤醒且获得了锁之后,会从下一句继续执行。被唤醒线程应该再次测试条件,因为无法保证等待条件已经满足。
- notify():唤醒在此同步监视器上等待的单个线程。如果有多个线程都在此同步监视器上等待,则会随机唤醒一个线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
- notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
await() signal() signalAll()
当使用 Lock 进行线程同步时,java 使用 Condition 类进行线程通信。通过调用 Lock 对象的 newCondition() 可以创建该对 Lock 对象的 Condition 实例,一个 Lock 对象可以创建多个 Condition 实例。
Condition类提供了如下三个方法:
- await():类似于同步监视器上的 wait() 方法,导致当前线程等待,直到其他线程调用该 Condition 的 singnal 方法或 signalAll() 方法来唤醒该线程。该 await() 方法有更多变体,如 long awaitNanos(long nanosTimeout)、void awaitUninterruptibly()、awaitUntil(Date deadline) 等。
- signal():唤醒在此 Lock 对象上等待的单个线程。
- signalAll():唤醒在此 Lock 对象上等待的所有线程。
public class AwaitSignalExample {
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void before() {
lock.lock();
try {
System.out.println("before");
condition.signalAll();
} finally {
lock.unlock();
}
}
public void after() {
lock.lock();
try {
condition.await();
System.out.println("after");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
AwaitSignalExample example = new AwaitSignalExample();
executorService.execute(() -> example.after());
executorService.execute(() -> example.before());
}
//
before
after
九、线程局部变量
ThreadLocal 泛型类是线程局部变量的意思,主要功能是,为每一个使用该变量的线程都提供一个变量值的副本,使得每一个线程都可以独立地改变自己的副本,而不会与其他线程冲突,从线程角度看,就好象每一个线程都完全拥有该变量一样。
ThreadLocal 类的用法非常简单,它只提供了如下三个 public 方法。
- protected T initialValue():返回当前线程中线程局部变量的初始值。线程第一次调用 get() 方法时将调用此方法。但如果线程之前调用了 set() 方法,则不会对该线程再调用 initialValue() 方法,但如果局部变量有了初始值之后,又调用了 remove() 方法,则下次调用 get() 方法时,可能要再次调用此方法。
- T get():返回此线程局部变量中当前线程副本中的值。如果变量没有赋初始值,则先调用 initialValue() 进行初始化。
- void remove():删除此线程局部变量中当前线程的值。
- void set(T value):设置此线程局部变量中当前线程副本中的值。大部分子类不需要重写此方法,他们只依靠 initialValue() 方法来设置线程局部变量的值。
ThreadLocal 将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而无需对该变量进行同步。示例如下:
public class SafeTask implements Runnable {
private static ThreadLocal<Date> startDate = new ThreadLocal<Date>(){
protected Date initialValue() {
return new Date();
}
};
public void run() {
System.out.printf("Thread Finished: %s : %s\n", Thread.currentThread().getId(), startDate.get());
}
public static void main(String[] args){
SafeTask st = new SafeTask();
for(int i=0;i<10;i++){
Thread t = new Thread(st);
t.start();
}
}
}
十、J.U.C - AQS(AbstractQueueSynchronizer)
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
CountdownLatch
用来同步一个或多个任务,强制它们等待由其他任务执行的一组操作完成。
你可以向 CountdownLatch 对象设置一个初始计数值,任何在该对象上调用 wait() 的方法都阻塞,直到这个技术到达0.其他任务在结束其工作时,可以在该对象上调用 countDown() 来减小这个计数值。CountdownLatch 被设计为只出发依次,计数值不能重置。如果需要能够重置计数值的版本,则可以使用 CyclicBarrier。
调用 countDown() 的任务在产生这个调用时并没有被阻塞,只有对 await() 的调用会被阻塞,直至计数值到达0.
CountdownLatch 的典型用法是将一个程序分为 n 个互相独立的可解决任务,并创建值为0的 CountdownLatch。每当任务完成时,都会在这个锁存器上调用 countDown()。等待问题被解决的任务在这个锁存器上调用 await(),将它们自己拦住,直至锁存器技术结束。即,维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。
public class CountdownLatchExample {
public static void main(String[] args) throws InterruptedException {
final int totalThread = 10;
CountDownLatch countDownLatch = new CountDownLatch(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("run..");
countDownLatch.countDown();
});
}
countDownLatch.await();
System.out.println("end");
executorService.shutdown();
}
}
run..run..run..run..run..run..run..run..run..run..end
CyclicBarrier
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 awati() 方法而在等待的线程才能继续执行。
CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
用法如下:
public class CyclicBarrierExample {
public static void main(String[] args) {
final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
System.out.print("before..");
try {
cyclicBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.print("after..");
});
}
executorService.shutdown();
}
}
before..before..before..before..before..before..before..before..before..before..after..after..after..after..after..after...
Semaphore
Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。
以下代码模拟了对某个服务的并发请求,每次只能有 3 个客户端同时访问,请求总数为 10。
public class SemaphoreExample {
public static void main(String[] args) {
final int clientCount = 3;
final int totalRequestCount = 10;
Semaphore semaphore = new Semaphore(clientCount);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalRequestCount; i++) {
executorService.execute(()->{
try {
semaphore.acquire();
System.out.print(semaphore.availablePermits() + " ");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
});
}
executorService.shutdown();
}
}
2 1 2 2 2 2 2 1 2 2
BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue(固定长度)
- 优先级队列 :PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
使用 BlockingQueue 实现生产者消费者问题
public class ProducerConsumer {
private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
private static class Producer extends Thread {
public void run() {
try {
queue.put("product");
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("produce..");
}
}
private static class Consumer extends Thread {
public void run() {
try {
String product = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print("consume..");
}
}
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
Producer producer = new Producer();
producer.start();
}
for (int i = 0; i < 5; i++) {
Consumer consumer = new Consumer();
consumer.start();
}
for (int i = 0; i < 3; i++) {
Producer producer = new Producer();
producer.start();
}
}
produce..produce..consume..consume..produce..consume..produce..consume..produce..consume..
十一、多线程经典题目
建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。这个问题用Object的wait(),notify()就可以很方便的解决。代码如下:
/**
*@functon 多线程学习 yield
*@author 林炳文
*@time 2015.3.9
*/
public class Main {
public static void main(String[] args) throws Exception {
Object a = new Object(), b = new Object(), c = new Object();
MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);
new Thread(pa).start();
//确保按顺序A、B、C执行,sleep参数取值,取决于各线程执行时间。参数值过小,可能出现打印顺序混乱或者死锁
Thread.sleep(100);
new Thread(pb).start();
Thread.sleep(100);
new Thread(pc).start();
}
}
class MyThreadPrinter2 implements Runnable {
private String name;
private Object prev;
private Object self;
MyThreadPrinter2(String name, Object prev, Object self) {
this.name = name;
this.prev = prev;
this.self = self;
}
public void run() {
int count = 10;
while (count > 0) {
synchronized (prev) {
synchronized (self) {
System.out.print(name);
count--;
self.notify();
}
try{
//保证程序正常结束
if(count > 0){
prev.wait();
}
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
代码分析:
该问题为三线程间的同步唤醒操作,主要的目的就是 ThreadA->ThreadB->ThreadC->ThreadA 循环执行。为了控制线程执行的顺序,每个线程执行结束时,要先唤醒后继线程,然后 wait 前继线程。所以每个线程必须持有两个锁。
为了保证三个线程按照 ThreadA,ThreadB,ThreadC 的顺序启动。必须在线程启动代码之间插入睡眠时间,睡眠时间要保证 ThreadA 获取两把锁之后,ThreadB 才能启动。ThreadB 获取两把锁之后 ThreadC 才能启动。然后三线程就可以轮流输出。
此外,还需要注意,当 Thread 最后一次输出后,无须再等待前继线程,直接结束即可。