第3章 Java多线程
本部分主要介绍Java多线程面试考点!最近博主在学习多线程,会持续更新多线程内容!敬请关注!(最近暂停更新,博主正在寻求更好的整理策略,有可能在本地把多线程知识点整理完之后,再统一写博客)
3.1 什么是线程和进程?(有待补充)
- 进程:进程是程序的一次执行过程,是系统运行程序的基本单位。进程是动态的,系统执行一个程序是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中执行运算的最小单位。
- 举例子:假设用户启动了一个窗口中的数据库应用程序,操作系统就将对数据库的调用表示为一个进程。假设用户要从数据库中产生一份工资单报表,并传到一个文件中,这是一个子任务;在产生工资单报表的过程中,用户又可以输人数据库查询请求,这又是一个子任务。这样,操作系统则把每一个请求――工资单报表和新输人的数据查询表示为数据库进程中的独立的线程。
3.2 并发和并行的区别?
- 并行:单位时间内,多个任务一块执行。
- 并发:一段时间内,多个任务一块执行。(单位时间内,不一定一块执行)
3.3 为什么使用多线程?
- 线程是进程的子单位,是程序执行的最小单位。线程之间的切换和调用的成本远远小于进程。
- 在多核CPU时代,多个线程可以同时运行,能够提高CPU的利用率。
- 在单核CPU时代,多线程主要是为了提高CPU和IO的综合利用率。(如果单线程的话,CPU被利用时,IO就空闲。IO被利用的时候,CPU空闲。)
3.4 使用多线程会带来什么问题?
并发编程的目的是为了提高程序的执行效率,进而提高程序的运行速度。但是,并发编程会带来很多问题:
内存泄漏、上下文切换、死锁、软件和硬件的闲置等问题。
3.5 说一下线程的生命周期和状态?(有待整理)
3.6 什么是上下文切换?
- 当前任务执行完CPU时间片后切换到另一任务之前,将当前状态保存起来。以便下次再切回这个任务时,能够加载这个任务的状态。任务从保存到加载的过程就是一次上下文切换。
- 上下文切换需要消耗系统大量的时间。可能是操作系统里面消耗时间最多的操作。
3.7 什么是线程死锁?(用代码解释)
线程死锁:多个线程被堵塞,一个或多个线程都在等待某个资源的释放。线程无限期地堵塞,因此程序不可能被正常终止。
举例:
线程A持有资源2,线程B持有资源1,两者都想得到对方的资源,所以这两个线程就会相互等待而进入死锁状态。
产生死锁的四个必要条件:
- 请求和保持条件:一个进程因请求某个资源而堵塞时,对自己持有的资源不释放。
- 互斥条件:一个资源在任意时刻只能被一个线程占用。
- 不剥夺条件:一个线程对已申请得到的资源,只有使用完毕后才释放资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
3.8 怎样避免死锁?
上面介绍了四个产生死锁的必要条件,我们只需要破坏其中的一个条件就可以避免死锁。
- 破坏请求和保持条件:申请所有资源。
- 无法破坏互斥条件:用锁就是让资源互斥的。
- 破坏不剥夺条件:当一个线程申请其他资源的时候,如果申请不到,先释放自己所占用的资源。
- 破坏循环等待条件:按照某一顺序来申请资源,释放资源的时候反序释放。
3.9 wait() 和 sleep() 的区别?
- 两者都可以暂停线程的执行。
- sleep()方法没有释放锁,wait()方法释放了锁。
- 线程使用sleep()后,使线程进入睡眠状态,让出执行机会给其他线程使用,当睡眠结束后,线程进入就绪状态和其他线程一起竞争CPU的执行时间。
- 调用sleep()方法的线程不会释放对象锁,而调用wait() 方***释放对象锁
代码示例:
public class Service {
public void mSleep(){
synchronized(this){
try{
System.out.println(" Sleep 。当前时间:"+System.currentTimeMillis());
Thread.sleep(3*1000);
}
catch(Exception e){
System.out.println(e);
}
}
}
public void mWait(){
synchronized(this){
System.out.println(" Wait 。结束时间:"+System.currentTimeMillis());
}
}
}
public class SleepThread implements Runnable{
private Service service;
public SleepThread(Service service){
this.service = service;
}
public void run(){
service.mSleep();
}
}
public class WaitThread implements Runnable{
private Service service;
public WaitThread(Service service){
this.service = service;
}
public void run(){
service.mWait();
}
}
public class Test{
public static void main(String[] args){
Service mService = new Service();
Thread sleepThread = new Thread(new SleepThread(mService));
Thread waitThread = new Thread(new WaitThread(mService));
sleepThread.start();
waitThread.start();
}
}
代码解读:
首先创建一个Service对象,然后sleepThread进程启动起来,在synchronized同步块里面获取service的同步锁,该进程进入休眠状态(休眠3000s),但是此时该进程没释放Service对象的锁。这个时候waitThread 进程也开始启动,但是由于Service对象没有被释放,waitThread 进程只能等着sleepThread进程休眠结束后,才能拿到同步锁,继续执行。(可以看出来sleep->wait时间是3s)
参考博客
- wait()通常用于线程之间的通信/交互,sleep()用于暂缓执行。
- wait()被调用后不会自动苏醒,需要使用notify()或notifyAll()来唤醒进程。sleep()对调用后,执行完休眠时间,就会自动苏醒。
3.10 为什么调用start()会执行run(),为什么不能直接调用run()?
当new一个线程的时候,线程进入新建状态。start()用来启动一个线程,当调用start()方法时,系统才会开启一个线程并进入就绪状态,CPU分配时间片(准备工作)后就可以执行run()方法。这是真正的多线程工作。如果直接调用run(),会当做普通类中的方法执行,不是多线程工作。
3.11 关于synchronized关键字?
3.11.1 说一下对synchronized关键字的了解?
- synchronized解决多个线程之间访问资源的同步性。synchronized可以保证被他修饰的方法或代码块在同一时间只能一个线程访问。
- synchronized只能锁对象,不能锁基本数据类型。
- 在Java早期版本中,synchronized是重量级锁、效率低下。因为监视器锁是依赖于底层的操作系统来实现的,java的线程映射到操作系统的原生线程上面。如果说用户需要挂起或唤醒一个线程,那么需要操作系统实现从用户态到内核态的进程切换,那么这个进程切换时间开销较大,这就是synchronized效率下的原因。
3.11.2 说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍⼀下这些优化吗?
- Java6以后官方从JVM层面对synchronized进行优化,引入自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁和轻量级锁等技术来减少锁开销。
- 锁存在的四种状态:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。会随着竞争的激烈而升级,不可降级。(策略的目的:为了提高获得锁和释放锁的效率)
3.11.3 说一下synchronized关键字三种使用方式?
- 修饰方法:
public synchronized void show (){ //TODO }
注意:调用该方法需要获取当前实例对象的对象锁。 - 修饰静态方法:
public class SynClass { public static synchronized void show (){} }
注意:锁的是SynClass这个类的所有对象。 - 修饰代码块:
可以将锁的范围缩小,可实现类级锁和对象锁。 - 实现类级锁(相当于修饰静态方法)
public class SynClass {
public static void main(String[] args) {
synchronized(SynClass.class){
//TODO
}
}
}
- 实现当前实例对象的对象锁(相当于修饰方法)
public class SynClass {
public void show(){
synchronized(this){
//TODO
}
}
}
- 实现某个对象的对象锁
public class SynClass {
Object lock = new Object();
public void show(){
synchronized(lock){
//TODO
}
}
}
这里定义了一个Object对象lock,专门锁该对象。
3.11.4 讲⼀下 synchronized 关键字的底层原理?
- synchronized同步语句块使用mointerenter和mointerexit两个指令,mointerenter指令指向同步语句块的开始位置,mointerexit指令指向同步语句块的结束位置。
- 当执行mointerenter指令时,线程试图获取锁(mointer的持有权),只有当锁计数器为0时,才可获取到。获取到之后,锁计数器变为1。
- 当执行mointerexit指令时,将锁计数器变为0,锁被释放。
- 如果获取锁失败,那么线程就到堵塞等待,直到锁被释放才能获取。
3.11.5 讲⼀下 synchronized 和ReentrantLock的区别?
- 两者都是可重入锁。(自己可以再次获取自己的内部锁)
- synchronized的实现依赖于JVM,但是ReentrantLock的实现依赖于API。前面讲到过,synchronized在早期Java版本中性能不好,官方在JDK1.6在JVM层面上对synchronized进行了锁优化。ReentrantLock是依赖于API的(需要lock()和unlock()来配合使用),可以通过查看源码来看它是如何实现的。
- ReentrantLock可以再定义的时候通过参数来指定是否是公平锁(默认是非公平的,参数为true就设定为公平锁),但是synchronized只能是非公平锁。公平锁:等待时间长的线程获得锁。非公平锁:为等待的线程随机分配锁。
- ReentrantLock提供一种可中断进程等待的机制,进程可以选择放弃等待来处理其他事情,这样可以避免死锁。但是synchronized实现锁的时候,一个堵塞的进程如果获取不到锁,会一直无限期地等待下去。这样就会解决死锁问题。
3.12 关于volatile关键字?
3.12.1 说一下你对volatile关键字的了解?
- volatile变量的作用:volatile保证该变量对所有线程的可见性(变量修改后,立即同步到主存,及时更新到所有线程上面)、防止指令重排。
- 在访问volatile变量时不会执行锁操作,所以不会存在线程堵塞的情况,它是一种比synchronized更加轻量级的同步机制。
- 普通变量进行读写的时候,线程先从内存拷贝到CPU缓存里面,如果有多个CPU,就会出现一个线程拷贝多个不同的CPU缓存的情况,出现脏数据。但是volatile修饰的变量,线程从内存直接读,少了CPU缓存的步骤。
3.12.2 说一下volatile、synschonized 的区别和运用场景?(有待整理)
- volatile关键字比sychronized关键字更加轻量级。volatile只能作用于变量上面,synchronized可以作用于代码块或方法上面。
- synchronized关键字比volatile关键字的应用场景多。
- 多线程访问volatile关键字不会发生堵塞,但是访问synchronized有可能会发生堵塞。
- volatile关键字主要解决变量在多线程之间的可见性(一个变量更新后,能够及时更新到多个线程上面),但是synchronized关键字解决多线程之间访问资源的同步性。
- volatile关键字能够保证数据的可见性,但是不能保证原子性。synchronized关键字两者都可以保证。
3.12.3 说一下并发编程的三个重要特性?
- 可见性:一个线程状态的修改,其他线程能够立即看到。volatile、synchronized和final能够保证可见性。
- 原子性:一个操作是不可再细分的,那么就称它具有原子性。一个非原子性的操作都有可能存在线程安全问题,我们可以利用同步技术synchronized将这个非原子性操作变为原子性操作。在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
- 有序性:程序的执行顺序和代码顺序相同。volatile关键字禁止指令重排,保证有序性。(虽然指令重排会优化执行时间,在单线程下面也不会影响程序的执行顺序。但是在多线程下面,就会影响并发执行的正确性。)
3.13 Java怎么保证多线程的安全?
3.14 Java锁机制,lock实现?
3.15 Java里面如何去关闭一个线程?
3.16 Java线程池的原理和实现,一些机制?
3.17 jdk 中线程池的类型
3.18 线程池的 BlockQueue 的作用
3.19 AQS 的实现原理
更新于 2020-10-31 22:30