本文整理自RedSpider社区开源文章:深入浅出Java多线程
一、进程与线程
1.1 基本概念
进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位
进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每一个时刻运行的状态
此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配一个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另一个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完
当进程暂停时,它会保存当前进程的状态(进程标识,进程使用的资源等),在下一次切换回来时根据之前保存的状态进行恢复,接着继续执行
一个进程就包含了多个线程,每个线程负责一个单独的子任务
进程让操作系统的并发性成为了可能,而线程让进程内部的并发成为了可能
进程与线程的区别:
- 进程单独占有各自的内存空间,有内存隔离,数据共享过程复杂但同步简单;线程在其对应的进程中共享内存空间,数据共享简单但同步复杂
- 因为进程之间互不干扰,故不容易影响主程序的稳定性;而某个线程崩溃可能影响到整个进程崩溃,可靠性低
- 因进程独占内存空间,故创建和销毁开销大(重量级);线程开销小(轻量级)
1.2 上下文切换
CPU从一个进程/线程切换到另一个进程/线程
上下文:某一时间点CPU寄存器和程序计数器中的内容
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。
但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。所以减少系统中上下文切换次数,可以提升多线程性能
二、多线程实现
2.1 继承Thread类
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("This is myThread");
}
public static void main(String[] args) {
// 调用start()方法后,等待获取到时间片后就会执行run方法
new Thread(new MyThread()).start();
}
}
2.2 实现Runnable接口
public class MyThread implements Runnable{
@Override
public void run() {
System.out.println("This is myThread");
}
public static void main(String[] args) {
new Thread(new MyThread()).start();
}
}
2.3 实现Callable接口
Callable
一般是配合线程池工具ExecutorService
来使用的
与前两种多线程实现方式不同的是,实现Callable
接口的方式具有返回值
public class MyOtherThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 2;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new MyOtherThread());
// 注意get()方法会阻塞当前线程
System.out.println(result.get());
}
}
2.4 Future接口
public abstract interface Future<V> {
// 尝试取消,但不一定成功,参数表示是否采用中断的方式取消线程执行
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}
2.5 FutureTask类
FutureTask可以在高并发环境下确保任务只执行一次
FutureTask
实现了RunnableFuture
接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
示例:
与上面1.3不同的是,此处调用的是submit(Runnable task)
方法,同时使用的是futureTask
调用get()
;而1.3处调用的是submit(Callable<T> task)
方法,同时使用的是其返回值去调用get()
public class MyOtherThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 2;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
FutureTask<Integer> futureTask = new FutureTask<>(new MyOtherThread());
executor.submit(futureTask);
// Future<Integer> result = executor.submit(new MyOtherThread());
System.out.println(futureTask.get());
}
}
FutureTask的几个状态:
/**
*
* state可能的状态转变路径如下:
* NEW -> COMPLETING -> NORMAL
* NEW -> COMPLETING -> EXCEPTIONAL
* NEW -> CANCELLED
* NEW -> INTERRUPTING -> INTERRUPTED
*/
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
2.6 Thread类和Runnable接口的区别
- Runnable更符合面向对象,使用更灵活(Java多实现,单继承特性)
- Runnable降低了线程对象和线程任务的耦合性
- 建议优先使用Runnable接口方式自定义线程类
三、线程组和线程优先级
3.1 线程组(ThreadGroup)
ThreadGroup包含并管理着Thread,执行main()方法线程的名字是main,如果在new Thread时没有显式指定线程组,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组
线程组的作用就是统一控制线程的优先级和检查线程的权限
public class Demo {
public static void main(String[] args) {
Thread testThread = new Thread(() -> {
System.out.println("testThread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
System.out.println("testThread线程名字:" +
Thread.currentThread().getName());
});
testThread.start();
System.out.println("执行main所在线程的线程组名字: " + Thread.currentThread().getThreadGroup().getName());
System.out.println("执行main方法线程名字:" + Thread.currentThread().getName());
}
}
// 执行main所在线程的线程组名字:main
// 执行main方法线程名字:main
// testThread当前线程组名字:main
// testThread线程名字:Thread-0
3.2 线程优先级
Java中默认线程优先级为5
线程优先级范围是1~10(由低到高),但该值只是提供给操作系统做参考(有更高的概率获取到时间片去执行),不过最终决定权还是在操作系统的手上(线程调度算法)
public class PriorityThread implements Runnable {
@Override
public void run() {
System.out.println("当前线程名:" + Thread.currentThread().getName() + ", 优先级为:" + Thread.currentThread().getPriority());
}
public static void main(String[] args) {
PriorityThread priorityThread = new PriorityThread();
IntStream.range(1, 10).forEach(
i -> {
Thread t = new Thread(priorityThread);
t.setPriority(i);
t.start();
}
);
}
}
运行结果:
当前线程名:Thread-0, 优先级为:1
当前线程名:Thread-5, 优先级为:6
当前线程名:Thread-7, 优先级为:8
当前线程名:Thread-6, 优先级为:7
当前线程名:Thread-4, 优先级为:5
当前线程名:Thread-1, 优先级为:2
当前线程名:Thread-3, 优先级为:4
当前线程名:Thread-2, 优先级为:3
当前线程名:Thread-8, 优先级为:9
若某个线程优先级大于线程所在线程组的最大优先级,以线程组的最大优先级为准
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("t1");
threadGroup.setMaxPriority(6);
Thread thread = new Thread(threadGroup,"thread");
thread.setPriority(9);
System.out.println("我是线程组的优先级"+threadGroup.getMaxPriority()); // 6
System.out.println("我是线程的优先级"+thread.getPriority()); // 6
}
四、Java线程状态
4.1 操作系统中的线程状态转换
在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的
- 就绪状态(Ready)
- 执行状态(Running)
- 等待状态(Waiting)
4.2 Java线程的6个状态
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
NEW
:新建一个线程,处于等待状态Thread thread = new Thread(() -> { }); System.out.println(thread.getState());
Runnable
:包含Ready
和Running
两个状态。当线程调用start()
方法后就进入就绪Ready
态,等待操作系统分配CPU时间片,分配后进入Running
运行态。当调用yield()
方法后,允许当前线程让出CPU,但具体让不让由操作系统决定。如果让了,那么当前线程则会处于Ready
态继续竞争CPU,直至执行thread.start();
Timed_waiting
:指定时间让出CPU,此时线程不会执行,也不会被系统调度,直到等待时间到期后才会执行Object obj = new Object(); Thread thread = new Thread(() -> { synchronized (obj) { try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true) { Thread.sleep(1000); System.out.println(thread.getState()); }
Waiting
:可以被唤醒的等待状态,此时线程不会被执行也不会被系统调度。此状态可以通过synchronized
获得锁,调用wait
方法进入等待状态。最后通过notify、notifyall
唤醒Object obj = new Object(); Thread thread = new Thread(() -> { synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true) { Thread.sleep(1000); System.out.println(thread.getState()); }
Blocked
:当发生锁竞争状态下,没有获得锁的线程会处于挂起状态。例如synchronized
锁,先获得的先执行,没有获得的进入阻塞状态// 两个线程,产生锁竞争 Object obj = new Object(); // 该线程获取锁后休眠一段时间,不释放锁 new Thread(() -> { synchronized (obj) { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 该线程无法获取锁,被挂起 Blocked Thread thread = new Thread(() -> { synchronized (obj) { try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); while (true) { Thread.sleep(1000); System.out.println(thread.getState()); }
Terminated
:这个是终止状态,从 New 到 Terminated 是不可逆的。一般是程序流程正常结束或者发生了异常// 正常运行结束即可 Thread thread = new Thread(() -> { }); thread.start(); System.out.println(thread.getState()); System.out.println(thread.getState()); System.out.println(thread.getState());
4.3 线程状态转换
4.3.1 BLOCKED与RUNNABLE状态切换
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
// 目的是为了让main线程休息下,从而让两个线程充分争夺锁
Thread.sleep(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
4.3.2 WAITING状态与RUNNABLE状态切换
public void blockedTest() {
······
a.start();
// join()会让main线程等待a线程执行完,才继续
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
System.out.println(b.getName() + ":" + b.getState()); // RUNNABLE/TERMINATED
}
4.3.3 TIMED_WAITING与RUNNABLE状态切换
public void blockedTest() {
······
a.start();
// join(long)使当前线程执行指定时间,并且使线程进入TIMED_WAITING状态
a.join(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
System.out.println(b.getName() + ":" + b.getState());
}
4.3.4 线程中断
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以在合适的时间处理中断请求,也可以完全不处理继续执行下去
Thread.interrupt()
:中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是flase)Thread.interrupted()
: 测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为true,连续调用两次会使得这个线程的中断状态重新转为falseThread.isInterrupted()
: 测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态
五、Java线程间的通信
5.1 锁与同步
在Java中,锁的概念是基于对象的,所以又叫对象锁
那么线程与锁的关系就像是婚姻
线程持有锁(结婚)
线程释放锁(离婚),其它线程才能获取锁
线程同步就是线程间按照一定的顺序执行
无锁示例:
public class NoneLock {
static class ThreadA implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
new Thread(new ThreadB()).start();
}
}
上述代码执行的结果是无序的,每一次的结果都不一致
使用对象锁示例:
public class ObjectLock {
// 对象锁
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A " + i);
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B " + i);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
// 同一时间只能有一个线程持有锁,睡眠是让A先执行
Thread.sleep(10);
new Thread(new ThreadB()).start();
}
}
5.2 等待/通知机制
Object.wait()
: 进入等待,并释放锁notify()
: 随机叫醒一个正在等待的线程notifyAll()
: 叫醒所有正在等待的线程
public class WaitAndNotify {
// 使用同一个对象锁
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadA: " + i);
// 随机叫醒等待中的线程
lock.notify();
// 进入等待,等待唤醒,释放锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
try {
System.out.println("ThreadB: " + i);
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lock.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
// 输出:
ThreadA: 0
ThreadB: 0
ThreadA: 1
ThreadB: 1
ThreadA: 2
ThreadB: 2
ThreadA: 3
ThreadB: 3
ThreadA: 4
ThreadB: 4
5.3 使用volatile实现信号量
volatile关键字能够保证内存的可见性,如果用volatile关键字声明了一个变量,在一个线程里面改变了这个变量的值,那其它线程是立马可见更改后的值的
示例(非线程安全的):
public class Signal {
// 保证可见性
private static volatile int signal = 0;
static class ThreadA implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 0) {
System.out.println("threadA: " + signal);
signal++;
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
while (signal < 5) {
if (signal % 2 == 1) {
System.out.println("threadB: " + signal);
signal = signal + 1;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
new Thread(new ThreadA()).start();
Thread.sleep(1000);
new Thread(new ThreadB()).start();
}
}
// 输出:
threadA: 0
threadB: 1
threadA: 2
threadB: 3
threadA: 4
5.4 管道
基于字符的:
PipedWriter
PipedReader
基于字节流的:
PipedOutputStream
PipedInputStream
public class Pipe {
static class ReaderThread implements Runnable {
private PipedReader reader;
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
System.out.println("this is reader");
int receive = 0;
try {
while ((receive = reader.read()) != -1) {
System.out.print((char)receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable {
private PipedWriter writer;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("this is writer");
int receive = 0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
PipedWriter writer = new PipedWriter();
PipedReader reader = new PipedReader();
writer.connect(reader); // 这里注意一定要连接,才能通信
new Thread(new ReaderThread(reader)).start();
Thread.sleep(1000);
new Thread(new WriterThread(writer)).start();
}
}
// 输出:
this is reader
this is writer
test
5.5 一些方法
5.5.1 join方法
让当前线程陷入“等待”状态,等join的这个线程执行完成后,再继续执行当前线程
public class Join {
static class ThreadA implements Runnable {
@Override
public void run() {
try {
System.out.println("我是子线程,我先睡一秒");
Thread.sleep(1000);
System.out.println("我是子线程,我睡完了一秒");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new ThreadA());
thread.start();
thread.join();
System.out.println("如果不加join方法,我会先被打出来,加了就不一样了");
}
}
5.5.2 sleep方法
sleep方法并不会释放锁,而wait方法会释放锁
区别:
- wait可以指定时间,也可以不指定;而sleep必须指定时间
- wait释放cpu资源,同时释放锁;sleep释放cpu资源,但是不释放锁,所以易死锁
- wait必须放在同步块或同步方法中,而sleep可以在任意位置