1. 进程和线程的区别
答:区别总结如下:
- 进程是程序的一次执行过程,是系统进行资源调度分配的基本单位;
- 线程是进程的更小的运行单位,一个进程拥有多个线程,线程间共享地址空间和资源;
- 线程上下文切换比进程上下文切换要快;
- 线程一般没有系统资源,但也有一些必不可少的资源,如ThreadLocal。
1.1 切换速度的差异原因
答:总结为:
- 进程切换时,涉及当前进程cpu环境的保存和新调度进程cpu环境的设置;
- 线程切换时,仅需要保存和设置少量寄存器内容。
1.2 进程间的通信方式
答:Socket套接字,共享内存,管道通信。
1.3 线程能拥有自己的资源吗?
答:能,通过ThreadLocal存储线程特有对象。
1.4 进程给了线程什么资源
答:线程共享进程的方法区、堆内存中进程所拥有的资源,线程独立资源有线程ID、寄存器组的值、线程栈和错误返回码。
2. 多线程和单线程的关系
答:概括为:
- 多线程是指一个进程,并发执行多个线程,每个线程有自己的功能;
- 多线程利用CPU轮询时间片的特点,提高资源利用率;
- 多线程会降低程序执行速度,因为存在线程上下文切换,但能减少用户的等待响应时间;
- 还会带来线程死锁等安全问题。
2.1 并发和并行的关系
答:并发指同一时间段,多个任务都在执行。并行指单位时间内,多个任务同时执行。
eg. 8-9点,我洗脸刷牙吃饭->并发,我左手洗脸右手刷牙->并行。
3. 线程的生命周期和状态
答:线程状态包括 新建、运行、阻塞等待和消亡。阻塞等待分为Blocked、Waiting和Time Waiting。
我们可以参照源码中Thread状态的枚举定义。
- New新建:创建后尚未调用start方法
- Runnable可运行:可能是正在运行或者正在等待CPU资源
- Blocked阻塞:线程进入同步块中,需要申请一个同步锁而进行的等待
- Waiting无限期等待:调用了Object.wait()或Object.notify()或LockSupport.park()方法,无限期等待其他线程来唤醒
- Time Waiting有限期等待:调用Thread.sleep()等方法,区别是等待时间是明确的
- Terminated消亡:线程执行结束或产生异常提前结束
4. 多线程编程中常用函数
答:常用函数的比较总结如下:
- sleep方法:Thread类的方法,让线程进入有限期等待休眠,之后自动苏醒。休眠不释放锁。常用于暂停执行。
- wait方法:Object类的方法,与synchronized一起使用,线程进入有限期或无限期等待,被notify方法调用才能解除阻塞,只有重新占用互斥锁才能进入Runnable。休眠释放互斥锁。常用于线程间通信交互。wait(long timeout)超时后也会自动苏醒。
- join方法:当前线程调用,其他线程全部停止,等待当前线程执行结束再执行。Stop the world
- yield方法:让线程放弃当前获得的CPU时间,使线程仍处于Runnable,随时可以再获得CPU时间。
5. 线程死锁
答:定义为多个线程之间相互等待对方而被无限期阻塞。
5.1 四个必要条件
答:OS的基础知识:
- 资源互斥:一个资源任意时刻只能被一个线程使用
- 请求和保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺:线程已获得的资源,再未使用完之前,不能强行剥夺
- 循环等待:若干线程间形成头尾相接的循环等待资源状态
5.2 避免死锁的方法
答:破坏死锁产生的四个条件中的任一:
- 破坏资源互斥:做不到
- 破坏请求和保持:一次性申请全部资源
- 破坏不剥夺:占用部分资源的线程再申请其他资源时,若申请不到就主动释放其占有的资源
- 破环循环等待:锁排序法,指定获取锁的顺序(也可认为指定获取资源的顺序),比如:只有获得A锁的线程才能获得B锁,只有AB锁都获得的才能操作资源C。
6. 线程锁死
答:定义为等待线程由于唤醒条件无法成立或其他线程无法唤醒这个线程,让此线程一直处于非运行状态。
6.1 线程锁死分类
- 信号丢失锁死:没有对应的线程来唤醒等待线程,导致一直等待。
- 嵌套监视器锁死:由于嵌套锁导致等待线程永远无法被唤醒的故障。比如,线程只释放了内层锁Y.wait(),没有释放外层锁X;但通知线程必须获得外层锁X,才能通过Y.notify()唤醒,这就出现嵌套等待现象。
7. 线程其他故障
7.1 线程活锁
答:定义为线程一直处于运行状态,但其执行的任务没有任何进展。比如,线程一直在请求其需要的资源,但无法申请成功。
7.2 线程饥饿
答:线程一直无法获得其所需的资源致使任务无法运行的情况。
7.3活性故障间转换
答:线程饥饿发生时,如果线程处于Runnable状态,就转变为活锁。线程死锁也是线程饥饿。
8. Java实现线程的方法
答:java中有三种方法实现线程。实现Runnable接口,实现Callable接口,继承Thread类。
建议采用实现接口的方式,因为继承整个Thread类开销过大且Java不支持多重继承,但支持多接口继承。
8.1 实现Runnable接口
继承Runnable接口,实现run方法,通过Thread调用start()启动线程。
public class MyRunnable implements Runnable {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable instance = new MyRunnable();
Thread thread = new Thread(instance);
thread.start();
}
8.2 实现Callable接口
Callable有返回值,通过FutureTask封装,通过Thread调用start()启动线程。
public class MyCallable implements Callable<Integer> {
public Integer call() {
return 123;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(mc);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
8.3 继承Thread类
通过start()启动,因为Thread类也是实现Runnable接口。
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
}
9. 原子性、可见性、有序性
答:线程安全体现在这三个方面。
9.1 原子性
定义:一组操作要么完全发生,要么没有发生,其余线程不会看到中间过程的存在。
实现方式:排他锁;CAS(Compare And Swap);volatile关键字修饰
注意问题:原子性针对多个线程的共享变量,对于局部变量来说无意义;volatile关键字只能保证写操作的原子性。
9.2 可见性
定义:一个线程对共享变量的更新对于另外一个线程是否可见的问题。
实现方式:刷新处理器缓存,让其他处理器的更关心同步到当前处理器缓存中;当前处理器更新共享变量后,冲刷处理器缓存,让更新写入缓存。
9.3 有序性
定义:一个线程对共享变量的更新在其余线程看起来是按照什么顺序执行的问题
10. synchronized关键字
10.1 说说对synchronized关键字的理解
答:synchronized是Java的一个关键字,是一个内部锁,保证其修饰的方法或代码块在任意时刻只有一个线程执行。解决的是多线程间访问资源的同步性。
10.2 底层原理
答:底层原理属于JVM层面。
修饰同步语句块:
- 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
- 当一个线程判断到计数器为0时,则当前锁空闲,可以占用;反之,当前线程进入等待状态
修饰方法:
使用ACC_SYNCHRONIZED标识指明方法是一个同步方法,JVM从而执行相应的同步调用。
10.3 JVM资源调度
答:分为公平调度和非公平调度。
- 公平调度:按照申请的先后顺序授予资源的独占权。缺点是吞吐率小。
- 非公平调度:新来的线程能先被授予资源的独占权。缺点是会产生饥饿现象。
10.4 synchronized和ReentrantLock的区别
答:总结为:
- 二者都是可重入锁。可重入锁就是自己可以再次获取自己的内部锁。同一线程每次获取锁,锁的计数器++,计数器为0时再释放锁。
- synchronized依赖于JVM,ReentrantLock依赖于API。
- ReentrantLock比synchronized增加了一些高级功能。
- 等待可中断。即正在等待的线程可以选择放弃等待,执行其他任务。
- ReentrantLock支持公平和非公平调度。synchronized只支持非公平锁。
- 支持选择性通知。synchronized相当于整个Lock只有一个Condition,所有线程都注册在一个上面,notifyAll()通知所有等待状态线程,效率不高。ReentrantLock的线程对象能注册在指定的Condition中,signalAll()只会唤醒该Condition实例中的等待线程。
11. 锁优化
答:锁优化主要是JVM对synchronized的优化。
11.1 自旋锁
主要思想是让一个线程在请求一个共享数据锁时忙循环(自旋)一段时间,若这段时间内能获得锁,则避免进入阻塞状态。缺点是自旋操作占用CPU时间。
总结:请求锁时先忙循环
11.2 锁粗化
若JVM探测到一串操作都对同一个对象加锁,就会把加锁范围扩展到整个操作的外部(粗化),以避免频繁加锁引起性能损耗。
11.3 偏向锁
主要思想是让第一个获取锁对象的线程在之后获得该锁就不再进行同步操作。当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。
总结:偏心眼,同步一次,就不再同步
12. Volatile关键字
答:Volatile关键字的主要作用就是保证变量的可见性和有序性,不能保证原子性。每次把变量写入/读取到主存中。
12.1 synchronized和volatile的区别
答:比较总结如下:
- volatile关键字是轻量级的锁,性能比synchronized好;
- volatile只能修饰变量,synchronized能修饰方法和代码块;
- volatile保证数据可见性和有序性,不保证原子性;synchronized都保证。
12.2 volatile在什么情况下能代替锁
答:volatile是轻量级的锁,适合多个线程共享一个或一组状态变量,来替代锁。
13. ThreadLocal
答:ThreadLocal变量,为每个使用该变量的线程提供独立的变量本地副本,通过get/set方法独立地改变自己的副本值,避免线程安全问题。
13.1 内部实现机制
答:在每个线程内部,都有一个类似HashMap的对象,称为ThreadLocalMap,里面存放若干个以ThreadLocal为key的键值对。其中key是弱引用,value是强引用,在gc时value不会被清理,长期不理会造成内存泄漏。所以在使用完ThreadLocal方法后,建议手动调用remove方法,因为官方在set/get/remove方法中内置了清理key为null的记录。
14. 线程池
14.1 为什么要用线程池
答:池化思想有利于降低资源销毁,节省创建资源的时间,提高线程的可管理性。
14.2 execute()和submit()区别
答:总结如下:
- execute()用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功;
- submit()用于提交需要返回值的任务,线程池返回Future类型对象,通过get()获得返回值。
14.3 线程池参数
答:使用ThreadPoolExecutor创建线程池,客户端调用submit(Runnable task)提交任务。
/**
* 用给定的初始参数创建一个新的ThreadPoolExecutor。
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> 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;
}
具体参数为:
- corePoolSize:核心线程数,即最小可同时运行的线程数量
- maximumPoolSize:最大线程数,即最大可同时运行的线程数量
- keepAliveTime :线程空闲但是保持不被回收的时间
- unit:时间单位
- workQueue:存储线程的队列
- threadFactory:创建线程的工厂
- handler:拒绝策略
推荐配置:
- corePoolSize: 核心线程数为 5。
- maximumPoolSize :最大线程数 10
- keepAliveTime : 等待时间为 1L。
- unit: 等待时间的单位为 TimeUnit.SECONDS。
- workQueue:任务队列为 ArrayBlockingQueue,并且容量为 100;
- handler:拒绝策略为 CallerRunsPolicy。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
//使用阿里巴巴推荐的创建线程池的方式
//通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 0; i < 10; i++) {
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
Runnable worker = new MyRunnable("" + i);
//执行Runnable
executor.execute(worker);
}
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
14.4 排队策略
答:向线程池提交任务时,需要遵循排队策略。
- 若运行的线程 < corePoolSize,Executor首选添加线程,不排队;
- 若运行的线程 >= corePoolSize,且队列未满,Executor首选将请求加入队列,不加新线程;
- 若队列已满,创建新线程,若超出maximumPoolSize ,拒绝此任务。
14.5 拒绝策略
答:线程达到max,队列也放满时,使用拒绝策略。
- ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
- ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。会降低对于新任务提交速度,影响程序的整体性能。
- ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
14.6 常见线程池类型
答:三种。
- newCachedThreadPool():核心线程池大小为0,最大线程池大小无限,来一个创建一个线程。
- newFixedThreadPool():固定大小的线程池。
- newSingleThreadExecutor():实现生产者-消费者模式。
14.7 常见阻塞队列
答:三种。
- ArrayBlockingQueue:
内部使用一个数组作为存储空间,数组的存储空间是预先分配的
优点是 put 和 take操作不会增加GC的负担(因为空间是预先分配的)
缺点是 put 和 take操作使用同一个锁,可能导致锁争用,导致较多的上下文切换。
适合在生产者线程和消费者线程之间的并发程序较低的情况下使用。 - LinkedBlockingQueue:
是一个无界队列(其实队列长度是Integer.MAX_VALUE)
内部存储空间是一个链表,并且链表节点所需的存储空间是动态分配的
优点是 put 和 take 操作使用两个显式锁(putLock和takeLock)
缺点是增加了GC的负担,因为空间是动态分配的。
LinkedBlockingQueue适合在生产者线程和消费者线程之间的并发程序较高的情况下使用。 - SynchronousQueue:
SynchronousQueue可以被看做一种特殊的有界队列。生产者线程生产一个产品之后,会等待消费者线程来取走这个产品,才会接着生产下一个产品,适合在生产者线程和消费者线程之间的处理能力相差不大的情况下使用。
15. Atomic原子类
答:简单来说,原子类就是具有原子操作特征的类,即这个类中的操作不可中断。原子类都方法JUC(java.util.concurrent)并发包.atomic下。
15.1 以AtomicInteger为例
常用方法有:
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
原理:原子类内部采用CAS(compare and swap)+volatile和native方法保证原子操作。CAS的原理是拿期望值和原本值比较,相同就更新为新值。将value值设为volatile变量,保证变量在内存中可见。
ABA
提一下ABA问题。如果一个遍历初次读取为A,而后被改成B,后来又被改回A,那CAS操作会误认为其从未改变过。解决方案是通过控制变量值的版本来保证CAS的正确性。
16. AQS
16.1 AQS介绍
答:AQS(AbstractQueuedSynchronizer)是用来构建锁和同步器的框架。
16.2 对原理的理解
答:AQS的核心思想就是,若被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,资源分配后将共享资源设为锁定状态。若被请求的共享资源被占用,则需要一套线程阻塞等待和唤醒锁分配的机制,这个机制AQS通过CLH队列实现,即将暂时获取不到锁的线程封装为一个结点加入到队列中。
CLH队列:是一个虚拟双向队列,仅存在结点间的关联联系。
内部使用int变量的state标识同步状态,FIFO的排队策略,CAS实现值的修改。
16.3 资源共享方式
答:两种。
- Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的 - Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock等等。
16.4 组件总结
- Semaphore(信号量):允许多个线程同时访问某个资源,synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。
- CountDownLatch:倒计时器协调器,用来实现线程等待其余线程完成某一特定操作后,再开始执行。内部维护一个计数器,为0则唤醒等待线程,不为0则暂停。
CyclicBarrier:循环栅栏,可以让一组线程到达一个同步点时被栅栏阻塞,直到最后一个线程到达栅栏时,所有被拦截的线程再继续执行。内部维护一个计数器 = 参与方个数,每个线程到达同步点调用await()方法使count-1,当判断到是最后一个参与方时,调用singalAll唤醒所有线程。到地一拦,人齐再走。