Java 对操作系统提供的功能进行了封装,包括进程和线程,运行一个程序会产生一个进程,进程包含至少一个线程,每个进程对应一个 JVM 实例,多个线程共享 JVM 里的堆、方法区,每个线程拥有自己的虚拟机栈、本地方法栈、程序计数器,这 3 个区域随线程而生,随线程而灭。看下 HotSpot VM 运行时数据区:
进程是资源分配的最小单位,线程是 CPU 调度的最小单位,线程的执行是抢占式的。Java 线程采用的是单线程编程模型,程序会自动创建主线程,主线程可以创建子线程,主线程原则上要后于子线程完成执行。需要注意的是,JVM 线程是多线程的,JVM 实例在创建的时候会同时创建很多线程,例如垃圾收集器的线程等。
由于进程有独立的地址空间,而线程没有,所以多进程的程序比多线程的程序要健壮,但是进程的切换比线程的切换开销大,所以多线程比多进程拥有更高的性能。
并发性和并行性的区别:
并发性:同一时刻只有一条指令执行,多个进程指令被快速轮换执行。
并行性:在同一时刻有多条指令在多个处理器上同时执行。
使用多线程编程的好处:
进程之间不能共享内存,但线程之间可以;使用多线程来实现多任务并发比多进程的效率高;Java内置了多线程功能支持。
多线程编程中三大特性:原子性,可见性,有序性。
1、原子性
原子性即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个最简单的例子,假如为一个 32 位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
假若一个线程执行到这个语句时,我暂且假设为一个 32 位的变量赋值包括两个过程:为低 16 位赋值,为高 16 位赋值。
那么就可能发生一种情况:当将低 16 位数值写入之后,突然被中断,而此时又有一个线程去读取 i 的值,那么读取到的就是错误的数据。
2、可见性
可见性即当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。举个简单的例子,看下面这段代码:
// 线程1执行的代码
int i = 0;
i = 10;
// 线程2执行的代码
j = i;
假若执行线程 1 的是 CPU1,执行线程 2 的是 CPU2。由上面的分析可知,当线程 1 执行 i =10 这句时,会先把 i 的初始值加载到 CPU1 的高速缓存中,然后赋值为 10,那么在 CPU1 的高速缓存当中 i 的值变为 10 了,却没有立即写入到主存当中。
此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到 CPU2 的缓存当中,注意此时内存当中 i 的值还是0,那么就会使得 j 的值为0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立即看到线程 1 修改的值。
当 CPU 执行 int i = 0; i = 10; 这段代码时,会从主存获取 i 的初始值,然后把 i 的初始值加载到 CPU1 的高速缓存中,然后赋值为 10,最后重新写入到主存当中。
3、有序性
有序性即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; // 语句1
flag = true; // 语句2
上面代码定义了一个 int 型变量,定义了一个 boolean 类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句 2 前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句 1 和语句 2 谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句 2 先执行而语句 1 后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10; // 语句1
int r = 2; // 语句2
a = a + 3; // 语句3
r = a*a; // 语句4
这段代码有4个语句,那么可能的一个执行顺序是:语句 2 -> 语句 1 -> 语句 3 -> 语句 4
那么可不可能是这个执行顺序呢: 语句 2 -> 语句 1 -> 语句 4 -> 语句 3
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令 Instruction 2 必须用到 Instruction 1 的结果,那么处理器会保证 Instruction 1 会在 Instruction 2 之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
// 线程1:
context = loadContext(); // 语句1
inited = true; // 语句2
// 线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句 1 和语句 2 没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程 1 执行过程中先执行语句 2,而此是线程 2 会以为初始化工作已经完成,那么就会跳出 while 循环,去执行 doSomethingwithconfig(context) 方法,而此时 context 并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
优点 | 缺点 | |
---|---|---|
继承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 task = new FutureTask(thread3);
// new Thread(task, "有返回值的线程").start();
// String value = task.get(); // get方法会阻塞,直到子线程执行结束才返回,带超时: task.get(500, TimeUnit.MILLISECONDS);
//
// 通过线程池获取子线程的返回值
// Future future = threadPool.submit(thread3);
// String value = future.get();
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception { // 线程执行逻辑,可以有返回值
return "hello";
}
}
常见的问题:
1、Thread 中 start 和 run 方法的区别?
分析 JDK 源码后得知,主线程调用 start 方法,会调用 JVM 的 StartThread 方法去创建一个新的子线程,然后去执行这个子线程 run 方法里的内容。因此区别是调用 start 方法会创建一个新的子线程并启动,而 run 只是 Thread 的一个普通方法的调用。这两个方法不存在可比性。
2、Thread 和 Runnable 是什么关系?
Thread 是实现了 Runnable 接口的类,通过 start 方法给 Runnable 接口的 run 方法赋予了多线程的特性,由于 Java 类的单一继承原则,提升系统的可扩展性,推荐多使用 Runnable 接口。
3、如何给 run 方法传参?
实现方式主要有构造函数传参、成员变量传参、回调函数传参。
4、如何实现处理线程的返回值?
阅读源码 java.lang.Thread.State 可知,线程的状态分为:
说明 | |
---|---|
NEW(新建状态) | 创建后尚未启动的线程的状态。 |
RUNNABLE(运行状态) | 包含 Running 和 Ready,主线程调用 start 方法后处于 Running,处于 Running 状态的线程位于可运行线程池中,等待被调度选中,获取 CPU 的使用权;处于 Ready 状态的线程位于线程池中,等待被线程调度选中,获取 CPU 的使用权;而在 Ready 状态的线程获取 CPU 执行时间后就会变成 Running 状态的线程。 |
BLOCKED(阻塞状态) | 等待获取排他锁 |
WAITING(无限期等待) | 不会被分配 CPU 执行时间,需要显式被唤醒。 |
TIMED_WAITING(限期等待) | 在一定时间后会由系统自动唤醒。 |
TERMINATED(结束状态) | 已终止线程的状态,线程已经结束运行。 |
线程的状态转换图如下:
常见的问题:
1、什么情况会让线程陷入 WAITING(无限期等待)状态?
没有设置 Timeout 参数的 Object.wait() 方法、没有设置 Timeout 参数的 Thread.join() 方法和 LockSupport.park() 方法会让线程陷入 WAITING 状态。
2、什么情况会让线程陷入 TIMED_WAITING (限期等待)状态?
Thread.sleep() 方法、设置了 Timeout 参数的 Object.wait() 方法、设置了 Timeout 参数的 Thread.join() 方法、LockSupport.parkNanos() 方法和 LockSupport.parkUntil() 方法会让线程陷入 TIMED_WAITING 状态。
3、阻塞状态与等待状态的区别?
阻塞状态等待获取排他锁,这个事件将在另外一个线程放弃锁的时候发生。而等待状态是在等待一段时间或者有唤醒动作的时候发生。
静态方法 Thread.sleep(long millis); 用来暂停程序的执行,暂停 millis 毫秒,暂停期间进入阻塞状态。
public class SleepTask implements Runnable {
@Override
public void run() {
System.out.println("sleepTask is do");
Thread.sleep(2000);
System.out.println("sleepTask is done");
}
}
当调用 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");
}
}
方法 join 让一个线程等待另一个线程完成的方法。
SleepTask sleepTask = new SleepTask();
Thread t1 = new Thread(sleepTask, "sleepTask");
t1.start();
t1.join();
System.out.println("task is done");
停止线程之前我们可以调用 Thread 实例对象的 stop() 方法,stop() 方法是由一个线程去停止另一个线程,这种方式太过暴力且是不安全的,所以这个方法在新的 JDK 版本中已经被抛弃了。类似还有 Thread 实例对象的 suspend() 和 resume() 方法,也同样都被废弃了。
目前停止线程使用的是 interrupt() 方法,通知线程应该中断了。
如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态并抛出一个 InterruptedException;如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,被设置中断标志的线程将继续正常运行,不受影响。
interrupt() 并不能中断线程,中断需要被调用的线程配置中断才行。也就是说一个线程如果有被中断的需求,需要做到:
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();
}
1、setDaemon 方法
调用Thread对象的setDaemon(true)方法可将指定线程设置成后台线程。如果前台线程都死亡,后台线程都死亡。isDaemon()方法用于判断指定线程是否为后台线程,主线程默认为前台线程。
2、setPriority 方法
调用 setPriority 方法 改变线程优先级
设置成最高优先级:.setPriority(Thread.MAX_PRIORITY);
设置成最低优先级:.setPriority(Thread.MIN_PRIORITY);
改变主线程的优先级:Thread.currentThread().setPriority(6);
对于 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
常见的问题:
1、sleep 和 wait 的区别?
sleep 是 Thread 类的方法,可以在任何地方使用;wait 是 Object 类中定义的方法,只能在 synchronized 方法或者 synchronized 块中使用。
最主要的本质区别是 Thread.sleep 只会让出 CPU,不会导致锁行为的改变;Object.wait 不仅会让出 CPU,还会释放已经占有的同步资源锁,这也是只能在 synchronized 方法或者 synchronized 块中使用的原因,只有获取锁了才能释放锁。
2、notify 和 notifyAll 的区别?
notifyAll 会让所有处于等待池的线程全部进入锁池去竞争获取锁的机会,没有获取到锁的而已经待在锁池中的线程只能等待其他机会去获取锁,而不能再主动回到等待池中;
notify 只会随机选取一个处于等待池中的线程进入锁池去竞争获取锁的机会。