线程
何为线程:线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈
,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
线程与进程的关系,区别以及优缺点
一个进程中可以有多个线程,多个线程共享进程的堆和方法区【共享的】(JDK1.8之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和本地方法栈【私有的】。
总结:线程是进程划分成的更小的运行单位,线程和进程的最大不同在于基本上各进程是相互独立的,而各线程则不一定,因为同一进程中的线程极有可能相互影响。线程执行开销小,但不利于资源的管理和保护;而进程则相反。
程序计数器为什么是私有的
程序计数器主要有两个作用:
- 字节码解释器通过程序计数器来依次读取指令,从而实现代码的流程控制,如顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道线程上次运行到哪了。
需要注意的是,如果执行的是native方法,那么程序计数器记录的是undefined地址,只有执行的是Java代码时程序计数器记录的才是下一条指令地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么是私有的
- 虚拟机栈:每个Java方法在执行之前会创建一个栈帧用于保存局部变量表、操作数栈、常量池引用等信息,从方法调用直至执行完成的过程,就对应着一个栈帧在Java虚拟机中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用相似,区别是:虚拟机栈为虚拟机执行Java方法而服务。本地方法栈为虚拟机使用Native方法服务。
所以,为了保证线程中局部变量不被其他线程访问到,虚拟机栈和本地方法栈都是线程私有的。
什么是堆和方法区
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象(几乎所有对象都在这里分配内存),方法区主要用于存放已经被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
线程的生命周期和状态
New:初始状态,线程被创建出来但没有调用start方法
RUNNABLE:运行状态,线程调用了start()等待运行的状态。
BLOCKED: 阻塞状态,需要等待释放锁。
WAITING:等待状态,表示线程需要等待其他线程做出一些特定动作
TIME_WAITING:等待超时状态,可以在指定的时间后自行返回而不是像WAITING那样一直等待。
TERMINATED:终止状态,表示该线程已经运行完毕。
线程创建之后处于New状态,调用start()方法后开始运行,线程此时处于Ready状态,可运行状态的线程获得了CPU时间片后就处于Running状态。
在操作系统层面,线程有Ready和Running状态;而在JVM层面,只能看到Runnable状态,所以一般将这两个状态称为Runnable状态。
为什么JVM没有区分这两种状态呢:现在的时分多任务操作系统架构通常都是用所谓“时间分片”方式进行抢占式轮转调度。这个时间分片通常是很小的,一个线程一次最多只能在CPU上运行10-20ms的时间(此时处于Running状态),时间片用完后就要被切换下来放入调度队列的队尾等待再次调度(也即回到Ready状态)。线程切换的如此之快,区分这两种状态就没什么意义了。
- 当线程执行wait()方法之后,线程进入WAITING状态,进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态。
- TIMED_WAITING(超时等待):状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long)或者wait(long)方法可以将线程置于TIMED_WAITING状态。当超时时间结束后,线程将会返回到Runnable状态。
- 当线程进入synchronized方法/块 或者调用wait后,被notify 重新进入sychronized方法/块,但是锁被其他线程占有,这个时候线程就会进入blocked状态。
- 线程在执行完了run()方法之后将会进入到TERMINATED(终止)状态。
wait和notify示例代码
package example;
class NotifyExample{
private static class WaitTask implements Runnable {
private Object lock;
public WaitTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "准备进入等待状态");
// 此线程在等待lock对象的notify方法唤醒
try {
lock.wait();
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "等待结束,本线程继续执行");
}
}
}
private static class NotifyTask implements Runnable {
private Object lock;
public NotifyTask(Object lock) {
this.lock = lock;
}
@Override
public void run() {
//!!!重要,获取到lock的线程才能去唤醒在lock上等待的线程
synchronized (lock) {
System.out.println("准备唤醒");
// 唤醒所有线程(随机)
lock.notifyAll();
System.out.println("唤醒结束");
}
}
}
public static void main(String[] args) throws InterruptedException {
//Object lock2 = new Object();
Object lock = new Object();
// 创建三个等待线程
Thread t1 = new Thread(new WaitTask(lock),"t1");
Thread t2 = new Thread(new WaitTask(lock),"t2");
Thread t3 = new Thread(new WaitTask(lock),"t3");
// 创建一个唤醒线程 Thread notify = new Thread(new NotifyTask(lock2),"notify线程");
Thread notify = new Thread(new NotifyTask(lock),"notify线程");
t1.start();
t2.start();
t3.start();
Thread.sleep(100);
notify.start();
// 当前正在执行的线程数
t1.join();
t2.join();
t3.join();
System.out.println(Thread.activeCount() - 1);
}
}
什么是线程上下文切换
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所提到的程序计数器、栈信息等,当出现如下情况的时候,线程会从占用CPU状态中退出。
- 主动让出CPU,比如调用了sleep(),wait()等
- 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如IO请求,线程被阻塞。
- 被终止或者运行结束。
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用CPU的时候恢复现场。并加载下一个将要占用CPU的线程上下文,这就是所谓的上下文切换。
上下文切换是现代操作系统的基本功能,因其每次需要保存信息恢复信息,这将会占用CPU,内存等系统资源进行处理,也就意味着效率会有一定的损耗,如果频繁切换就会造成整体效率低下。
Thread#sleep()方法和Object#wait()方法对比
共同点:两者都可以暂停线程的执行。
区别:
- sleep()方法没有释放锁,而wait()方法释放了锁
- wait()通常被用于线程间交互/通信,sleep通常用于暂停执行。
- wait方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify或者notifyAll方法。sleep方法执行完成后,线程会自动苏醒,或者也可以使用wait(long)超时后线程会自动苏醒。
- sleep()是Thread类的静态本地方法,wait()则是Object类的本地方法。(即Thread.sleep()&Object.wait())。
为什么wait()方法不定义在Thread中&为什么sleep方法定义在Thread中
wait是让获得对象锁的线程实现等待, 会自动释放当前线程占有的对象锁。每个对象都拥有对象锁,既然要释放当前线程占有的对象锁并让其进入WAITING状态,自然要操作对应的对象(Object)而费当前的线程(Thread)。
sleep()是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
可以直接调用Thread类的run方法吗?
new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入就绪状态,当分配到时间片后就可以开始运行了。start()会执行线程的相应准备工作,然后自动执行run()方法,这是真正的多线程工作。但是,直接执行run()方法,会把run方法当作一个main线程下的普通方法去执行,并不会在某个线程中执行他
。
总结:调用start()方法可启动线程并使线程进入就绪状态,直接执行run()方法就不会以多线程方式执行。
多线程
并发与并行的区别
- 并发:两个及两个以上的作业在同一时间段内执行。
- 并行:两个及以上的作业在
同一时刻
执行。
关键点在于并发是时间片的调度,宏观上的并行,并行是真正意义上的同时。
同步与异步的区别
- 同步:发出一个调用之后,在没有得到结果之前,该调用就不会返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,调用直接返回。
单核 CPU 上运行多个线程效率一定会高吗?
在单核CPU上,同一时刻只能有一个线程在运行,其他线程需要等待CPU的时间片分配,如果线程是CPU密集型,那么多个线程同时运行会导致频繁的线程切换,增加了系统开销,降低了效率。如果线程是IO密集型,那么多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。
因此,对于单核CPU来说,如果任务是CPU密集型,那么多线程反而会影响效率;如果任务是IO密集型,那么多线程会提高效率。
死锁
死锁产生的四个必要条件
- 互斥条件:该资源任意一个时刻只能由一个线程占用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待:若干个线程形成一种头尾相接的循环等待资源关系。
如何避免和预防死锁
如何预防:破坏死锁产生的必要条件
- 破坏请求和保持条件:一次性申请所有资源
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,释放他占用的所有资源。
- 破坏循环等待条件:按序申请资源,释放资源则反序释放。
如何避免:借助于银行家算法对资源分配进行计算评估,使其进入安全状态。
银行家算法核心内涵:在资源分配过程中,通过模拟分配后系统状态,确保系统始终处于安全状态,从而避免死锁。