1. 进程、线程、协程
进程
进程是资源分配的基本单位。进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。
线程
线程是独立调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。
QQ 和浏览器是两个进程,浏览器进程里面有很多线程,如 HTTP 请求线程、事件响应线程等
协程
是一个特殊的函数,可以在某个地方挂起,并且可重新在挂起处外继续运行,一个线程拥有多个协程。完全由程序控制,在用户态执行,协程的切换就是函数的调用,一个线程的多个协程的运行是串行的。
区别与联系:
-
Ⅰ 拥有资源
进程是资源分配的基本单位,但是线程不拥有资源,线程共享进程的资源。 -
Ⅱ 调度
线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。 -
Ⅲ 系统开销
由于创建或撤销进程时,系统都要为之分配或回收资源,开销远大于线程。 -
Ⅳ 通信方面
线程间可以通过直接读写同一进程中的数据进行通信,但是进程通信需要借助 IPC。
补:操作系统一般有下面三种线程同步的方式:
- 共享内存:线程之间共享程序的公共状态,线程之间通过读-写内存中的公共状态来隐式通信。
volatile共享内存
- 消息传递:线程之间没有公共的状态,线程之间必须通过明确的发送信息来显示的进行通信。
wait/notify等待通知方式
join方式
通过 condition 的 await/signal 方法
- 互斥量
synchronized 关键词和各种 Lock 都是这种机制
2 为什么要多线程?为什么不能一直创建线程?
2.1 为什么要用多线程?
- 避免CPU空转,只用单线程去响应请求,经常涉及到RPC、数据库访问、磁盘IO等,CPU会存在大量的闲置时间 。
- 提升性能,如果一个任务具有并发性,可拆分为几个子任务,且子任务之间无依赖关系,明显提升性能。
- 避免阻塞(异步调用):单个线程中的程序,是顺序执行的。如果前面的操作发生了阻塞,就会影响后面的操作,若采用多线程,如ajax调用,浏览器会启一个新线程,不阻塞当前页面正常操作;
2.2 为什么不能一直创建线程?
- 线程越多,JVM 所占资源越多,最终可能会导致内存消耗过多,形成死机。
- 线程越多,越容易造成资源冲突,如果处理不当,造成死锁。
2.3 如何限制线程数目?
可通过线程池控制线程数量,一个任务执行完毕,再从队列的中取最前面的任务开始执行。
2.4 线程池的作用?
- 减少了创建和销毁线程的次数,每个工作线程都可重复利用,执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为线程过多而导致消耗完系统内存以及”过度上下文切换”。
- 能对线程进行简单管理,比如定时执行、间隔执行等。
2.5 线程池大小确定
合理线程数量=CPU数量CPU使用率(1+等待事件/计算时间)
- IO 密集型任务:线程很大部分消耗在等待上,所以可以启动更多的线程,CPU 个数 * 2
- CPU 密集型任务:线程绝大部分消耗都在CPU计算处理上,应当分配较少的线程, CPU 个数相当的大小 + 1。
2.6 高并发情况下,怎末使用线程池:
- (1)高并发、执行时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下文的切换
- (2)并发不高、任务执行时间长的业务要区分开看:
- a)假如业务集中在IO操作上,因为IO操作并不占用CPU,适当加大线程池中的线程数目,让CPU处理更多的业务
- b)假如业务集中在计算操作上,线程池中的线程数设置得少一些,减少线程上下文的切换。
- 3)并发高、业务执行时间长,解决的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2),最后业务执行时间长的问题,也可能需要分析一下,看看能不能使用中间件对任务进行拆分和解耦。
3. 线程的生命周期
注:当一个阻塞在 wait 的线程,被另一个线程 notify 后,重新进入 synchronized区域,此时需要重新获取锁,如果失败了,就变成 BLOCKED 状态。
因 wait 阻塞在 WAITING 状态的线程,被 notify后,会先转换为 RUNNABLE,等待 CPU时间片分配,当有机会真正运行时,才会去尝试抢锁,此时如果抢锁成功,直接就运行。如果抢锁失败,再从 RUNNABLE 变为 BLOCKED。
4. Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,可能出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
5. 说说并发与并行的区别?
- 并行:单位时间内,多个任务同时执行。在多处理器系统中存在。
- 并发:同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,在宏观上具有多个进程同时执行的效果。
6. 为什么调用 start() 方法时会执行 run() 方法,而不直接调用 run() 方法?
new一个Thread,线程进入新建阶段,调用start()方法,会启动一个线程并进入可运行阶段,当分配到时间片就可以运行了。此时会自动执行run()方法中的内容,这是真正的多线程工作。若直接执行run()方法,会把run方法当做一个main线程下的普通方法执行,并不会在某个线程下执行它,所以这并不是多线程工作。
调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
7. 说说 synchronized 关键字和 volatile 关键字的区别
- volatile关键字是线程同步的轻量级实现,所以性能相对要好。不过,synchronized关键字在1.6版本之后进行了性能优化,实际开发中使用 synchronized 的场景更多一些。
- volatile关键字只能用于变量,synchronized关键字可以修饰方法、类以及代码块。
- 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
- volatile关键字能保证数据的可见性,但不能保证原子性。后者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized解决的是多个线程间访问资源的同步性。
8. 谈谈 synchronized和ReentrantLock 的区别
1) 两者都是可重入锁
“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象锁时是可行的,如果不可锁重入,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
2)synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
synchronized 依赖于 JVM 。ReentrantLock 是 JDK 层面实现(需要 lock() 和 unlock() 方法配合 try/finally 来完成)。
3)异常是否释放锁
synchronized在发生异常时候会自动释放占有的锁;而lock发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生。
4) ReentrantLock 比 synchronized 增加了一些高级功能
主要来说主要有三点:等待可中断;可实现公平锁;可以绑定多个条件,实现选择性通知。
- ReentrantLock提供了一种能够中断等待锁机制,通过lock.lockInterruptibly()来实现。
- ReentrantLock可以通过构造方法指定是公平锁还是非公平锁。
- synchronized关键字与wait()和notify()/notifyAll()方法相结合实现等待/通知机制,ReentrantLock类借助于Condition接口与newCondition() 方法。在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而有选择性的进行线程通知。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程,会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。
可中断锁和不可中断锁有什么区别?
- 可中断锁:获取锁的过程中可以被中断,不需要一直等到获取锁后才处理其他逻辑。ReentrantLock 就属于是可中断锁。
- 不可中断锁:一旦线程申请了锁,就只能等拿到锁后才能进行其他的逻辑处理。 synchronized 就属于是不可中断锁。
// Thread 类中的实例方法,持有线程实例引用即可检测线程中断状态
public boolean isInterrupted() {}
// Thread 中的静态方法,检测调用这个方法的线程是否已经中断
// 注意:这个方法返回中断状态的同时,会将此线程的中断状态重置为 false
// 所以,如果我们连续调用两次这个方法的话,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}
// Thread 类中的实例方法,用于设置一个线程的中断状态为 true
public void interrupt() {}
9.JDK1.6之后,java对synchronized做了哪些优化
锁主要有四种状态:无锁、偏向锁、轻量级锁、重量级锁。随竞争状态而逐步升级,但只可升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
-
无锁状态:当一个线程访问一个没有被锁定的对象时,会将对象头中的标记位设置为无锁状态。
-
偏向锁状态:当一个线程访问一个没有竞争的锁时,会将对象头中的标记位设置为偏向锁状态,并将线程ID记录在对象头中。此时,其他线程访问该对象时,只需检查对象头中的线程ID是否与当前线程ID相同,如果相同,则表示可以获取锁,无需进行同步操作。
-
轻量级锁状态:当多个线程竞争同一个锁时,偏向锁会升级为轻量级锁。轻量级锁使用CAS操作来尝试获取锁,如果成功则表示获取锁成功,如果失败则表示存在竞争,需要进行锁膨胀。
-
重量级锁状态:当轻量级锁获取锁失败时,会进行锁膨胀,升级为重量级锁。重量级锁使用操作系统的互斥量来实现,保证同一时间只有一个线程可以获取锁。
自旋锁和自适应自旋
挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。为了让一个线程等待,只需要让线程执行一个忙循环(自旋),无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者。
锁消除
虚拟机编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。
锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,会带来很多不必要的性能消耗。
10. Synchronized可重入锁
public class Xttblog extends SuperXttblog {
public static void main(String[] args) {
Xttblog child = new Xttblog();
child.doSomething();
}
public synchronized void doSomething() {
System.out.println("child.doSomething()" + Thread.currentThread().getName());
doAnotherThing(); // 调用自己类中其他的synchronized方法
}
private synchronized void doAnotherThing() {
super.doSomething(); // 调用父类的synchronized方法
System.out.println("child.doAnotherThing()" + Thread.currentThread().getName());
}
}
class SuperXttblog {
public synchronized void doSomething() {
System.out.println("father.doSomething()" + Thread.currentThread().getName());
}
}
//运行结果:
//child.doSomething()Thread-5492
//father.doSomething()Thread-5492
//child.doAnotherThing()Thread-5492
- 子类通过可重入锁调用父类的同步方法。
- 同一线程对同一个对象锁是可重入的,线程的每个synchronized都获取的是同一个对象,就是 child 对象的锁。
11. monitor原理
monitor是由ObjectMonitor实现的
ObjectMonitor() {
_header = NULL;
_count = 0; //记录个数
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //_owner指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList
当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。