1. 为什么要使用多线程?
1)从计算机底层来说
- 线程可比作轻量级进程,是程序执行的最小单位,线程间切换和调度的成本远远小于进程;
- 另外,多核CPU可支持多个线程同时执行,降低了线程上下文切换的开销。
单核计算机时代: 主要是为了提高CPU和IO设备等的综合利用率(如果只有一个线程,CPU执行时,IO空闲,IO执行时,CPU空闲,利用率只有50%,多线程可以一个CPU计算式,另一个进行IO操作)
多核计算机时代: 主要是为了提高计算机CPU利用率(如果只用一个CPU核心,其他核心空闲,多线程可以充分利用多个CPU核心)
2)从互联网发展趋势来说
- 当前系统动不动就会需要百万级甚至千万级的并发量;
- 多线程并发编程是支持高并发系统的基础,利用好多线程可提高系统整体并发能力及性能。
2. 使用多线程可能带来什么问题?(怎么解决这些问题?)
- 内存泄漏
- 上下文切换
- 死锁
3. 什么是上下文切换?
- 一个任务在CPU时间片用完时,会先保存自己的任务状态再切换到其他任务,这样下次加载时就从保存的位置开始执行。任务从保存到再加载的过程就叫做上下文切换。
详解:多线程编程时,一般线程数会大于CPU核心数。而一个CPU核心一次只能执行一个线程。为使所有线程都得到有效的执行,CPU采取的策略是每个线程分配一个时间片并轮转的方式。一个线程执行完自己的时间片就会重新进入就绪状态,CPU让给其他线程使用,等待下次时间片。这个过程就是一次上下文切换。
4. Java中有哪些锁(锁的分类)?
1)由ReentrantLock和Synchronized创建的一系列锁
- 公平锁与非公平锁(Synchronized只能实现非公平锁)
- 互斥锁与非互斥锁
- 可重入锁与非可重入锁
- 编译优化角度 -- 锁消除和锁粗化
- 不同的位置使用Synchronized -- 类锁和对象锁
2)从锁的设计理念来分
- 悲观锁(Java中Synchronized)
- 乐观锁(版本号机制、CAS算法)
CAS是一种更新的原子操作,比较当前值与传入值是否一样,一样则更新,否则失败。
5. 什么是线程死锁?
- 多个线程同时被阻塞 ,它们中的一个或多个在等待某个资源被释放。由于线程进入无限期阻塞等待,程序无法正常终止,即发生死锁。
- 例如:线程A持有资源1,线程B持有资源2,线程A和线程B都企图得到对方的资源,因此陷入互相等待状态,形成死锁。
- 这个例子满足形成死锁的四个必要条件:
a. 互斥条件
b. 请求保持条件
c. 不可剥夺条件
d. 循环等待条件
6. 如何避免线程死锁?
- 为了避免死锁,只需破坏四个必要条件中的一个即可。
a. 破坏互斥条件:这个条件无法破坏,因为锁的存在就是为了使线程互斥的(临界资源只能被互斥的访问)。
b. 破坏请求保持条件:一次性申请所有的资源。
c. 破坏不可剥夺条件:申请资源没有申请到时,释放自己持有的资源。
d. 破坏循环等待条件:靠按需申请资源来预防 。按某一顺序来申请资源,释放资源则反序释放。
7. 谈谈对Synchronized关键字的了解?
- Synchronized解决的是多线程访问资源的同步性。Synchronized可以保证同一时刻只有一个线程可以访问其修饰的方法或代码块。
- Java 6 之前(synchronized属于重量级锁):
a. 监视器锁(Monitor)依赖底层操作系统 Mutex Lock来实现;
b. Java的多线程会映射到底层操作系统的多线程之上;
c. 挂起或唤醒一个线程需要底层操作系统帮忙;
d. 操作系统实现线程切换需要从用户态转换到内核态,这个状态转换需要很长的时间。 - Java 6(JDK1.6) 之后:
a. Java官方从JVM层面对synchronized进行了较大优化,锁效率已经优化的很好了;
b. JDK1.6对锁引入了大量优化,如自旋锁、自适应自旋锁、锁消除、锁粗化、轻量级锁、偏向锁等技术来减少锁操作的开销。
8. 说说怎么使用Synchronized关键字?
- 修饰实例方法。作用于当前 对象实例加锁,进入同步代码块前,需要先获得当前 对象实例的锁。
- 修饰静态方法。给当前 类加锁,会作用于类的所有对象实例,进入同步代码块前,要获得当前class的锁。
a. 静态方法不属于任何一个实例对象 ,是类成员(static表示这是当前类的一个静态资源,不管new了多少个对象实例,只有一份)。
b. 如果线程A调用一个实例对象 的非静态synchronized方法,线程B可以调用这个实例对象所属类 的静态synchronized方法。
c. 因为访问静态synchronized方法占用的是当前类的锁,访问非静态synchronized方法占用的是当前实例对象锁 。 - 修饰同步代码块。指定对象加锁,对给定对象/类加锁。
a. synchronized(this|object) 表示进入指定同步代码块前要获得给定对象 的锁。
b. synchronized(类.class)表示进入指定同步代码块前要获得当前class类 的锁。
总结
- synchronized加到静态方法或synchronized(class)都是给当前类加锁。
- synchronized加到实例方法上是给对象实例上锁。
- 尽量不要使用synchronized(String a),因为在JVM中,字符串常量池具有缓存功能。
9. Synchronized关键字底层原理?
- synchronized修饰同步代码块的情况
a. 底层使用monitorenter和monitorexit指令 ,monitorenter指向同步代码块开始的地方,monitorexit指向同步代码块结束的地方。monitorenter指令执行时,线程试图获取锁,也就是获得对象监视器monitor的持有权。
b. monitorenter尝试获取同步代码块的锁 => 如果对象未锁定/当前线程已经持有这个锁 => 锁计数器+1。否则阻塞等待,直到持有锁的线程释放锁。
c. monitorexit是线程退出同步代码块时:锁计数器-1 => 锁计数器减为0,锁释放。 - synchronized修饰方法的情况
synchronized修饰方法时并没有使用monitorenter和monitorexit指令,而是使用的是ACC_SYNCHRONIZED 标识。JVM在调用时通过这个标识来区分这个方法是不是同步方法,从而执行相应的同步调用。
两者本质上都是对象监视器monitor的获取。
10. volatile关键字
首先了解一下Java内存模型JMM
-
JDK1.2之前
JMM总是从主存(共享内存)中读取变量,是不需要特别注意的。
-
当前
a. 线程可以把变量保存在本地内存中,而不是在主存中进行读写。
b. 这样就可能出现一个线程在主存中修改了一个值,而另一个线程还在使用它本地内存中这个变量值的拷贝,造成数据不一致 (存在的问题)。
要解决这个问题就需要用volatile来修饰变量,这就指示JVM,这个变量是共享且不稳定的,每次使用它,都要去主存(共享内存)中读取。
volatile关键字除了防止JVM指令重排,一个重要的作用就是保证变量的可见性。
11. 说说synchronized关键字和volatile关键字的区别
synchronized关键字和volatile关键字两者是互补的关系,不是对立的关系!
- volatile是线程同步的轻量级实现,性能比synchronized好。但是volatile只能用于变量,而synchronized可以修饰方法以及代码块。(性能和作用范围不同)
- volatile只能保证数据的可见性,不能保证数据的原子性,而synchronized都可以保证。(能保证的数据性质不同)
- volatile解决的是变量在多线程之间的可见性,synchronized解决的是多线程访问资源的同步性。(解决的问题不同)
12. 使用线程池的好处?
- 降低资源消耗:线程池可以重复使用已有的线程,减少了线程创建和销毁造成的资源消耗;
- 提高响应速度:任务到达时,不用等线程创建,可以直接执行;
- 提高线程的可管理性:线程是稀缺资源,如果无限的创建线程,不仅会增加资源消耗,还会降低系统稳定性,线程池可以对线程进行统一的分配、调优和监控。
13. 如何创建线程池?
Executor方法 (《阿里巴巴Java开发手册不允许使用》)
- newFixedThreadPool(int threads)
- newCachedThreadPool()
- newSingleThreadExecutor()
- newScheduledThreadPool(int curPoolSize)
不允许使用的原因是易造成内存泄漏(OOM)
造成OOM的原因(以newFixedThreadPool方法为例):public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
newFixedThreadPool与newSingleThreadExecutor导致OOM的原因都为以上原因;
newCachedThreadPool与newScheduledThreadPool创建的线程数都可能是Integer.MAX_VALUE,而创建这么多线程,必然有可能导致OOM。
参考链接:https://blog.csdn.net/yan88888888888888888/article/details/83927609
ThreadPoolExecutor
- 可以直接使用ThreadPoolExecutor,自己为它的队列设置容量。这样任务个数超过指定容量就会抛出异常。
14. ThreadPoolExecutor类
- ThreadPoolExecutor类提供了四个构造方法;
- 以下展示包含参数最多的一个构造方法:
/**
* ⽤给定的初始参数创建⼀个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
15. ThreadPoolExecutor类构造函数参数分析
最重要的三个参数
- corePoolSize
- maximumPoolSize
- workQueue
其他重要参数
- keepAliveTime:当线程池中的线程数大于corePoolSize时,核心线程池外的线程不会立即销毁,而是等待keepAliveTime时间后再被回收销毁。
- unit
- theadFactory:executor创建新线程时会用到。
- handler:饱和策略。
16. ThreadPoolExecutor饱和策略
- AbortPolicy
- CallerRunsPolicy
- DiscardOldestPolicy
- DiscardPolicy
参考:JavaGuide