并发(concurrency):多条指令在多个处理器上同时执行并行( parallel):多个进程指令被快速轮换执行
进程(Process):处于运行过程中的程序(系统进行资源分配和调度的一个独立单位),每个进程有独立的内存空间线程( Thread):进程的执行单元(CPU 调度和分派的基本单位),线程之间共享堆空间,每个线程有独立的栈空间(共享父进程中的共享变量及部分环境)
进程通信方式:管道(pipe)、有名管道(named pipe)、信号量(semophore)、消息队列(message queue)、信号(sinal)、共享内存(shared memory)、套接字(socket)
操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程
- 线程调度:JVM 负责线程的调度,采用的是抢占式调度,而不是分时调度
- Java 程序运行时至少启动了 2 个线程:主线程 main、垃圾回收线程(后台线程)
- 多线程是为了同步完成多项任务,不是为了提高程序运行效率,而是通过提高资源使用效率来提高系统的效率
同步 异步 阻塞 非阻塞
- 同步/异步:数据如果尚未就绪,是否需要等待数据结果
阻塞/非阻塞:进程/线程需要操作的数据如果尚未就绪,是否妨碍了当前进程/线程的后续操作
同步与异步
- 同步和异步关注的是消息通信机制(synchronous communication/ asynchronous communication)
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了。换句话说,就是由调用者主动等待这个调用的结果
- 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用
线程阻塞与非阻塞
- 阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
- 阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程
进程的创建和启动
- Runtime 类中的 exec 方法,如
Runtime.getRuntime().exec("notepad");
- ProcessBuilder 类中的 start 方法,如
new ProcessBuilder("notepad").start();
Thread
- 实现了 Runnable 接口
- 所有的线程对象都必须是 Thread 类或其子类的实例
构造器
- Thread()、Thread(Runnable target)、Thread(String name)、Thread(Runnable target, String name)
- Thread(ThreadGroup group, Runnable target, String name):在指定的线程组中创建线程
类方法
Thread currentThread()
:返回当前正在执行的线程对象void sleep(long millis)
:让当前正在执行的线程暂停 millis 毫秒,并进入阻塞状态(线程睡眠)(该方法声明抛出了 InterruptedException 异常)void yield()
:暂停当前正在执行的线程对象,转入就绪状态(线程让步)
实例方法
void start()
:使该线程开始执行,Java 虚拟机调用该线程的 run 方法,只能被处于新建状态的线程调用,否则会引发 IllegalThreadStateException 异常void run()
:如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回void setName(String name)
:为线程设置名字,在默认情况下,主线程的名字为 main,用户启动的多个线程的名字依次为 Thread-0、Thread-1、Thread-2、…、Thread-n 等String getName()
:返回调用该方法的线程名字void join()
:等待调用该方法的线程执行完成,而当前正在执行的线程进入阻塞状态(联合线程)(该方法声明抛出了 InterruptedException 异常)void setDaemon(boolean on)
:on 为"true"时,将该线程设置成守护线程,该方法必须在 start() 之前调用,否则会引发 IllegalThreadStateException 异常boolean isDaemon()
:判断该线程是否为守护线程int getPriority()
:返回线程的优先级void setPriority(int newPriority)
:更改线程的优先级(范围是 1~10 之间)boolean isAlive()
:测试线程是否处于活动状态
线程的创建和启动
继承 Thread 类创建线程类
- 使用继承 Thread 类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量
// 定义 Thread 类的子类
public class MyThread extends Thread {
// 重写 Thread 类中的 run() 方法,线程执行体
public void run() {
}
}
public class Demo {
public static void main(String[] args) {
Thread t = new MyThread(); // 创建 Thread 子类的对象
t.start(); // 调用线程对象的 start() 方法来启动该线程
}
}
// 使用匿名内部类的方式创建
new Thread() {
public void run() {
}
}.start();
实现 Runnable 接口创建多线程
- 采用 Runnable 接口的方式创建的多个线程可以共享同一个 target 对象的实例变量
void run()
:使用实现接口 Runnable 的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的 run 方法
// 定义 Runnable 接口的实现类
public class MyRunnable implements Runnable {
// 重写 Runnable 接口中的 run() 方法,线程执行体
public void run() {
}
}
public class Demo {
public static void main(String[] args) {
Runnable target = new MyRunnable(); // 创建 Runnable 实现类的对象 target
Thread t = new Thread(target, "线程名"); // 将 target 作为运行目标来创建创建 Thread 类的对象
t.start();; // 调用线程对象的 start() 方法来启动该线程
}
}
// 使用匿名内部类的方式创建
new Thread(new Runnable() {
public void run() {
}
}).start();
使用 Callable 和 FutureTask 创建线程
Callable<V> 接口
- Callable
接口提供了一个 call() 方法(可以有返回值,可以声明抛出异常)可以作为线程执行体,Callable 接口里的泛型形参类型与 call() 方法返回值类型相同 V call()
:计算结果,如果无法计算结果,则抛出一个异常
Future<V> 接口
- Future
接口代表 Callable 接口里 call() 方法的返回值,表示异步计算的结果 - Future
接口的常用方法 V get()
:返回 Callable 任务里 call() 方法的返回值,如果计算抛出异常将会抛出 ExecutionException 异常,如果当前的线程在等待时被中断将会抛出 InterruptedException 异常(调用该方法将导致程序阻塞,必须等到子线程结束后才会得到返回值)V get(long timeout, TimeUnit unit)
:返回 Callable 任务里 call() 方法的返回值,该方法让程序最多阻塞 timeout 和 unit 指定的时间,如果经过指定时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常boolean cancel(boolean maylnterruptlfRunning)
:试图取消该 Future 里关联的 Callable 任务boolean isCancelled()
:如果在 Callable 任务正常完成前被取消,则返回 trueboolean isDone()
:如果 Callable 任务已完成,则返回 true
FutureTask<V> 类
- FutureTask
实现类实现了 RunnableFuture 接口(RunnableFuture 接口继承了 Runnable 接口和Future 接口) - 构造器:FutureTask(Callable
callable)、FutureTask(Runnable runnable, V result)(指定成功完成时 get 返回给定的结果为 result)
// 使用 Lambda 表达式创建 Callable<V> 接口的实现类,并实现 Call() 方法
// 使用 FutureTask 来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) () -> {
// call() 方法可以有返回值
return 100;
});
// 将 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
new Thread(task, "线程名").start();
try {
// 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值,在最多等待 1 秒之后退出
System.out.println("子线程的返回值:" + task.get(1, TimeUnit.SECONDS));
} catch (Exception e) {
e.printStackTrace();
}
创建线程的三种方式对比
继承 Thread 类
- 线程类已经继承了 Thread 类,不能再继承其它父类
- 如果需要访问当前线程,直接使用 this 即可获得当前线程
- 多个线程之间无法共享线程类中的实例变量
实现 Runnable、Callable 接口的方式创建多线程
- 线程类只是实现了 Runnable 接口,还可以继承其它类
- 如果需要访问当前线程,则必须使用 Thread. currentThread() 方法
- 所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个 target 对象的实例变量,所以适合多个相同线程来处理同一份资源的情况
线程安全
- 保证多线程环境下共享的、可修改的状态的正确性
- 线程安全需要保证几个基本特性:
- 原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现
- 可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的
- 有序性,是保证线程内串行语义,避免指令重排等
线程同步
- 原子操作(atomic operation):不可被中断的一个或一系列操作
- 只需要对那些会改变共享资源的、不可被中断的操作进行同步即可
- 保证在任一时刻只有一个线程可以进入修改共享资源的代码区,其它线程只能在该共享资源对象的锁池中等待获取锁
- 在 Java 中,每一个对象都拥有一个锁标记(monitor),也称为监视器
- 线程开始执行同步代码块或同步方法之前,必须先获得对同步监视器的锁定才能进入同步代码块或者同步方法进行操作
- 当前线程释放同步监视器:当前线程的同步代码块或同步方法执行结束,遇到 break 或 return 语句,出现了未处理的 Error 或 Exception,执行了同步监视器对象的 wait() 方法或 Thread.join() 方法
- 当前线程不会释放同步监视器:当前线程的同步代码块或同步方法中调用 Thread. sleep()、Thread.yield() 方法其它线程调用了该线程的 suspend() 方法
同步代码块
- 语法格式
synchronized(同步监视器对象) {
// 需要同步的代码
}
- 通常推荐使用可能被并发访问的共享资源作为同步监视器
同步方法
- 使用
synchronized
关键字来修饰某个方法,就相当于给调用该方法的对象加了锁 - 对于实例方法,同步方法的同步监视器是 this,即调用该方法的对象
- 对于类方法,同步方法的同步监视器是当前方法所在类的字节码对象(如 ArrayUtil.class)
- 不要使用
synchronized
修饰 run() 方法,而是把需要同步的操作定义在一个新的同步方法中,再在 run() 方法中调用该方法
public class Apple implements Runnable {
private int num = 50;
public void run() {
while (num > 0) {
eat();
}
}
// 同步方法
private synchronized void eat() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + num-- + " 的苹果");
}
}
}
同步锁(Lock)
- java.util.concurrent.locks 包中,Lock 替代了 synchronized 方法和语句的使用
- Lock 接口的实现允许锁在不同的作用范围内获取和释放,并允许以任何顺序获取和释放多个锁
- 常用的实现类ReentrantLock(可重入锁):java.util.concurrent.locks 包中,通常建议使用 finally 块来确保在必要时释放锁ReentrantLock 是可重入锁:当前持有该锁的线程能够多次获取该锁,无需等待(可以在递归算法中使用锁)
class Apple implements Runnable {
private int num = 50;
private final Lock lock = new ReentrantLock();
public void run() {
while (num > 0) {
lock.lock();
try {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + " 吃了编号为 " + num-- + " 的苹果");
}
} finally {
lock.unlock();
}
}
}
}
Lock 和 synchronized 的选择
- Lock 是一个接口,而 synchronized 是 Java 中的关键字,synchronized 是内置的语言实现
- synchronized 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而 Lock 在发生异常时,如果没有主动通过 unLock 去释放锁,则很可能造成死锁现象,因此使用 Lock 时需要在 finally 块中释放锁
- Lock 可以让等待锁的线程响应中断,而 synchronized 却不行,使用 synchronized 时,等待的线程会直等待下去,不能够响应中断
- 通过 Lock 可以知道有没有成功获取锁,而 synchronized 却无法办到
- Lock 可以提高多个线程进行读操作的效率
- 在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竟争),此时 Lock 的性能要远远优于 synchronized。所以说,在具体使用时要根据适当情况选择
线程通信
线程通信机制
并发模型 | 通信机制 | 同步机制 |
---|---|---|
共享内存 | 线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信 | 同步是显式进行的,即必须显式指定某个方法或某段代码需要在线程之间互斥执行 |
消息传递 | 线程之间通过显式的发送消息来达到交互目的,如 Actor 模型 | 由于消息的发送必须在消息的接收之前,因此同步是隐式进行的 |
- Java 的线程间通过共享内存的方式进行通信
使用 Object 类中的方法
- Object 类中用于操作线程通信的实例方法
wait()
:调用该方法的当前线程会释放对该同步监视器(调用者)的锁定,JVM 把该线程存放到等待池中,等待其他的线程唤醒该线程(该方法声明抛出了 InterruptedException 异常)(为了防止虚假唤醒,此方法应始终在循环中使用,即被唤醒后需要再次判断是否满足唤醒条件)notify()
:调用该方法的当前线程唤醒在等待池中的任意一个线程,并把该线程转到锁池中等待获取锁notifyAll()
:调用该方法的当前线程唤醒在等待池中的所有线程,并把该线程转到锁池中等待获取锁
- 这些方法必须在同步块中使用,且只能被同步监视器对象来调用,否则会引发 IllegalMonitorStateException 异常
public class ShareResource {
// 标识数据是否为空(初始状态为空)
private boolean empty = true;
// 需要同步的方法
public synchronized void doWork() {
try {
while (!empty) { // 不空,则等待
this.wait();
}
... // TODO
empty = false; // 修改标识
this.notifyAll(); // 通知其它线程
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
使用 Condition 接口中的方法
- java.util.concurrent.locks 包中,Condition 接口中的
await()
、signal()
、signalAll()
方法替代了 Object 监视器方法的使用(await() 方法也声明抛出了 InterruptedException 异常) - 通过 Lock 对象调用 newCondition() 方法,返回绑定到此 Lock 对象的 Condition 对象
public class ShareResource {
// 创建使用 private final 修饰的锁对象
private final Lock lock = new ReentrantLock();
// 获得指定 Lock 对象对应的 Condition
private final Condition cond = lock.newCondition();
// 标识数据是否为空(初始状态为空)
private boolean empty = true;
// 需要同步的方法
public void doWork() {
lock.lock(); // 进入方法后,立即获取锁
try {
while(!empty) { // 判断是否方法阻塞
cond.await();
}
... // TODO
empty = false; // 修改标识
cond.signalAll(); // 通知其它线程
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 使用 finally 块释放锁
}
}
}
死锁
- 当两个线程相互等待对方释放同步监视器时就会发生死锁,死锁无法解决,只能避免
- 一旦出现死锁,所有线程处于阻塞状态,程序无法继续向下执行
- 避免死锁
- 加锁顺序:所有的线程都以同样的顺序加锁和释放锁
- 加锁时限:线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁
- 定位死锁:利用 jstack 等工具获取线程栈,然后定位相互之间的依赖关系,进而找到死锁
线程的生命周期
- 线程对象的状态存放在 Thread 类的内部枚举类 State 中,枚举常量:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED
- 新建、可运行(就绪、运行)、阻塞、等待、计时等待、终止/死亡
控制线程
线程睡眠
- 让执行的线程暂停一段时间,进入阻塞状态
联合线程
- 让当前线程等待另一个线程完成,而当前线程进入阻塞状态
后台线程 / 守护线程(Daemon Thread)
- 后台线程 / 守护线程 / 精灵线程(Daemon Thread)
- 在后台运行,为其它线程提供服务的线程,如 垃圾回收线程
- 特征:如果所有的前台线程都死亡,后台线程会自动死亡
- 前台线程创建的子线程默认是前台线程,后台线程创建的子线程默认是后台线程
线程优先级
- 优先级的高低只和线程获得执行机会的次数多少有关
- 每个线程默认的优先级都与创建它的父线程的优先级相同
- int 类型的静态常量:MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY,值分别是10(最高优先级)、1(最低优先级)、5(默认优先级)
线程让步
- 让执行的线程暂停,进入就绪状态
- 当某个线程调用了 yield() 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会
定时器
- 在 java.util 包中提供了 Timer 类、TimerTask 类,可以定时执行特定的任务
线程组
- ThreadGroup 类,表示一个线程的集合,可以对一组线程进行集中管理(同时控制这批线程)
- 在默认情况下,子线程和创建它的父线程处于同一个线程组内
ThreadLocal<T>
- 代表一个线程局部变量
- 当运行于多线程环境的某个对象使用 ThreadLocal 维护变量时,ThreadLocal 为每一个使用该变量的线程分配一个独立的变量副本,从而解决多线程中对同一变量的访问冲突
- 其实现的思路:在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap,用于存储每个线程的变量副本,Map 中元素的 key 为线程对象,value 为对应线程的变量副本
- 构造器:
ThreadLocal<T>()
:创建一个线程局部变量,ThreadLocal 对象建议使用 static 修饰(这个变量是一个线程内所有操作共有的) - 实例方法
protected T initialValue()
:返回此线程局部变量的当前线程的“初始值”T get()
:返回此线程局部变量中当前线程副本中的值void remove()
:移除此线程局部变量中当前线程的值void set(T value)
:设置此线程局部变量中当前线程副本中的值
private static final ThreadLocal<DateFormat> sdfThreadLocal = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
ThreadLocal<DateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
同步机制与 ThreadLocal
- 如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制
- 如果仅仅需要隔离多个线程之间的共享冲突,则可以使用 ThreadLocal