Java 对操作系统提供的功能进行了封装,包括进程和线程,运行一个程序会产生一个进程,进程包含至少一个线程,每个进程对应一个 JVM 实例,多个线程共享 JVM 里的堆、方法区,每个线程拥有自己的虚拟机栈、本地方法栈、程序计数器,这 3 个区域随线程而生,随线程而灭。
Java 线程采用的是单线程编程模型,程序会自动创建主线程,主线程可以创建子线程,主线程原则上要后于子线程完成执行。需要注意的是,JVM 线程是多线程的,JVM 实例在创建的时候会同时创建很多线程,例如垃圾收集器的线程等。
由于进程有独立的地址空间,而线程没有,所以多进程的程序比多线程的程序要健壮,但是进程的切换比线程的切换开销大,所以多线程比多进程拥有更高的性能。
Thread的使用
Thread的创建
- | 优点 | 缺点 |
---|---|---|
继承Thread类 | 编写简单,访问当前线程简单,获取当前线程直接使用this即可 | 不能继承其他父类 |
实现Runnable接口或Callable接口 | 共享一个target对象,适合多个线程来处理一个资源的情况 | 编程相对复杂,获取当前线程必须使用Thread.currentThread();方法 |
// new MyThread().start();
public class MyThread extends Thread {
@Override
public void run() { // 线程执行逻辑
}
}
// MyThread2 thread2 = new MyThread2();
// new Thread(thread2, "新线程1").start();
public class MyThread2 implements Runnable {
@Override
public void run() { // 线程执行逻辑
}
}
// 通过FutureTask获取子线程的返回值
// MyThread3 thread3 = new MyThread3();
// FutureTask<String> task = new FutureTask<String>(thread3);
// new Thread(task, "有返回值的线程").start();
// String value = task.get(); // get方法会阻塞,直到子线程执行结束才返回,带超时: task.get(500, TimeUnit.MILLISECONDS);
//
// 通过线程池获取子线程的返回值
// Future<String> future = threadPool.submit(thread3);
// String value = future.get();
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception { // 线程执行逻辑,可以有返回值
return "hello";
}
}
常见的问题:
Thread 中 start 和 run 方法的区别?
分析 JDK 源码后得知,主线程调用 start 方法,会调用 JVM 的 StartThread 方法去创建一个新的子线程,然后去执行这个子线程 run 方法里的内容。因此区别是调用 start 方法会创建一个新的子线程并启动,而 run 只是 Thread 的一个普通方法的调用。这两个方法不存在可比性。
如何实现处理线程的返回值?
- 主线程等待法,建立 while 循环,如果子线程还没执行完,那么 Thread.currentThread().sleep(100);,直至子线程处理完毕,缺点是需要自己实现循环等待的逻辑,代码臃肿,需要等待的时间也是不确定的,不能做到精准的控制。
- 使用 Thread 类的 join 阻塞当前线程以等待子线程处理完毕,缺点是粒度不够细,没法做到多个子线程之间的等待关系;
- 通过 Callable 接口实现,通过 FutureTask 或者线程池获取。
Thread的状态
- | 说明 |
---|---|
NEW(新建状态) | 创建后尚未启动的线程的状态。 |
RUNNABLE(运行状态) | 包含 Running 和 Ready,主线程调用 start 方法后处于 Running,处于 Running 状态的线程位于可运行线程池中,等待被调度选中,获取 CPU 的使用权;处于 Ready 状态的线程位于线程池中,等待被线程调度选中,获取 CPU 的使用权;而在 Ready 状态的线程获取 CPU 执行时间后就会变成 Running 状态的线程。 |
BLOCKED(阻塞状态) | 等待获取排他锁 |
WAITING(无限期等待) | 不会被分配 CPU 执行时间,需要显式被唤醒。 |
TIMED_WAITING(限期等待) | 在一定时间后会由系统自动唤醒。 |
TERMINATED(结束状态) | 已终止线程的状态,线程已经结束运行。 |
线程的状态转换图如下:
- 如果程序调用子线程的 start() 后子线程立即执行,程序可以使用 Thread.sleep(1) 来让当前运行的线程(主线程)睡眠1毫秒;
- 直接掉用线程的 stop() 来结束该线程,容易导致死锁,通常不推荐使用;
- 测试线程是否死亡可以用 isAlive(),当线程处于就绪、运行、阻塞三种状态时,该方法返回 true;当线程处于新建、死亡两种状态时返回 false;
- 不要对处于死亡的线程调用 start(),否则会引发 IllegalThreadStateException。
Thread的静态方法
yield方法
当调用 Thread.yield(); 方法时,会给线程调度器一个当前线程愿意让出 CPU 使用的暗示,但是线程调度器可能会忽略这个暗示。
public class YieldTask implements Runnable {
@Override
public void run() {
System.out.println("yieldTask is do");
Thread.yield();
System.out.println("yieldTask is done");
}
}
Thread的实例方法
join方法
方法 join 让一个线程等待另一个线程完成的方法。
SleepTask sleepTask = new SleepTask();
Thread t1 = new Thread(sleepTask, "sleepTask");
t1.start();
t1.join(); //暂停当前线程,执行t1线程
System.out.println("task is done");
interrupt方法
如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态并抛出一个 InterruptedException;如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能中断线程,中断需要被调用的线程配置中断才行。也就是说一个线程如果有被中断的需求,需要做到:
- 在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程;
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响。
public class InterruptTask implements Runnable {
@Override
public void run() {
try {
// 检查中断标志位, 如果被设置了中断标志就自行停止线程
while (!Thread.currentThread().isInterrupted()) {
// 业务逻辑
}
} catch (InterruptedException e) {
logger.error("{} ({}) catch InterruptedException", Thread.currentThread().getName(),Thread.currentThread().getState());
// 正确处理异常, 例如catch异常后就结束线程
}
}
}
调用 interrupt() 方法通知线程应该中断了:
@Test
public void test() throws Exception {
InterruptTask interruptTask = new InterruptTask(lock);
Thread t1 = new Thread(interruptTask, "waitTask");
t1.start();
Thread.sleep(1000);
t1.interrupt();
}
Object的wait/notify机制
对于 JVM 中运行程序的每个 Object 来说,都有两个池,锁池 EntryList 和等待池 WaitSet,而这两个池与 Object 类的 wait()、notify()、notifyAll() 三个方法以及 synchronized 相关,wait() 会让出 CPU,释放已经占有的同步锁,使线程进入无限期等待,除非调用 notify() 或 notifyAll() 唤醒,使等待的线程继续运行。
锁池和等待池都是针对对象而言的:
- | 说明 |
---|---|
锁池 EntryList | 假设线程 A 已经拥有了某个对象(不是类)的锁,而其他线程 B、C 想要调用这个对象的某个 synchronized 方法(或者块),由于 B、C 线程在进入对象的 synchronized 方法(或者块)之前必须先获得该对象锁的拥有权,而恰好该对象的锁目前正被线程 A 所占用,此时 B、C 线程就会被阻塞,进入一个地方去等待锁的释放,这个地方便是该对象的锁池。 |
等待池 WaitSet | 假设线程 A 调用了某个对象的 wait() 方法,线程 A 就会释放该对象的锁,同时线程 A 就进入到了该对象的等待池中,进入到等待池中的线程不会去竞争该对象的锁。当调用 notify/notifyAll 时,这时被环境的对象将会进入到该对象的锁池中,竞争该对象的锁。 |
Object 类的方法:
- | 说明 |
---|---|
wait() | 使当前执行代码的线程进行等待,将当前线程置入 “预执行队列” 中,并且在 wait() 所在的代码行处停止执行,直到接到通知或被中断为止。只能在同步方法或同步代码块中调用 wait() 方法,在执行 wait() 方法后,当前线程释放锁。在 wait() 返回前,线程与其他线程竞争重新获取锁。 |
wait(long) | 带一个参数的表示等待某一时间内是否有线程对锁进行唤醒,如果超过这个时间则自动唤醒。 |
notify() | 用来通知那些可能等待该对象的对象锁的其他线程。只能在同步方法或同步代码块中调用 notify() 方法,在执行 notify() 方法后,当前线程不会马上释放锁,呈 wait 状态的线程也不能马上获取该对象锁,要等到 notify() 方法的线程将程序执行完,也就是退出 synchronized 代码块后,当前线程才会释放锁,而呈 wait 状态所在线程才可以获取该对象锁。 |
public class WaitTask implements Runnable {
private final Object lock; // 这里的lock就是上面所说的某个对象(不是类)的锁
public WaitTask(Object lock){
super();
this.lock = lock;
}
@Override
public void run() {
System.out.println("notifyTask is do");
synchronized (lock) {
System.out.println("waitTask get lock");
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("waitTask is done");
}
}
}
public class NotifyTask implements Runnable {
private final Object lock;
public NotifyTask(Object lock){
super();
this.lock = lock;
}
@Override
public void run() {
System.out.println("notifyTask is do");
synchronized (lock) {
System.out.println("notifyTask get lock");
lock.notify();
System.out.println("notifyTask end");
}
}
}
测试类:
@Test
public void test() throws Exception {
Object lock = new Object();
WaitTask waitTask = new WaitTask(lock);
NotifyTask notifyTask = new NotifyTask(lock);
Thread t1 = new Thread(waitTask, "waitTask");
Thread t2 = new Thread(notifyTask, "notifyTask");
t1.start();
Thread.sleep(1000);
t2.start();
}
程序运行结果:
waitTask get lock
notifyTask get lock
notifyTask end
waitTask end
常见的问题:
sleep 和 wait 的区别?
sleep 是 Thread 类的方法,可以在任何地方使用;wait 是 Object 类中定义的方法,只能在 synchronized 方法或者 synchronized 块中使用。
最主要的本质区别是 Thread.sleep 只会让出 CPU,不会导致锁行为的改变;Object.wait 不仅会让出 CPU,还会释放已经占有的同步资源锁,这也是只能在 synchronized 方法或者 synchronized 块中使用的原因,只有获取锁了才能释放锁。
notify 和 notifyAll 的区别?
notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁的而已经待在锁池中的线程只能等待其他机会去获取锁,而不能再主动回到等待池中;
notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。
实现线程B等待线程A执行完成之后再执行?
- volatile修饰的标志位,线程B执行完成之后修改标志位,A线程判断标志位决定是否继续往下执行。
- A线程调用B.join()方法,等待B线程执行完成之后继续执行。
- 利用CountDownLatch,B线程执行完成之后调用countDown() 方法。
- 实现Callable接口future.get()取执行结果,此方法会导致同步阻塞;要不么使用isDone()轮询地判断Future是否完成,这样会耗费CPU的资源。
- 利用Java8新增的completablefuture框架。
三个线程交替顺序打印ABC
- 使用synchronized, wait和notifyAll
- 使用Lock->ReentrantLock 和 state标志
- 使用Lock->ReentrantLock 和Condition(await 、signal、signalAll)
- 使用Semaphore
- 使用AtomicInteger
参考:多线程实现顺序打印