- 一、Java线程
- 1、说说线程有几种创建方式?
- 2、如何使用线程池执行定时任务?
- 3、为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?
- 4、线程有哪些常用的调度方法?
- 5、线程有几种状态?
- 6、守护线程了解吗?
- 7、线程间有哪些通信方式?
- 8、ThreadLocal是什么?
- 9、你在工作中用到过ThreadLocal吗?
- 10、ThreadLocal怎么实现的呢?ThreadLocal原理?
- 11、ThreadLocal 内存泄露是怎么回事?
- 12、ThreadLocalMap的结构了解吗?
- 13、ThreadLocalMap怎么解决Hash冲突的?
- 14、ThreadLocalMap扩容机制了解吗?
- 15、父子线程怎么共享数据?
- 16、synchronized怎么保证可见性?
- 17、synchronized怎么保证有序性?
- 18、synchronized怎么实现可重入性?
- 19、多线程有什么用?
- 20、Runnable接口和Callable接口的区别
- 21、CyclicBarrier和CountDownLatch的区别
- 22、什么是线程安全
- 23、Java中如何获取到线程dump文件
- 24、一个线程如果出现了运行时异常会怎么样
- 25、怎么检测一个线程是否持有对象监视器
- 23、synchronized用过吗?怎么使用?
- 24、synchronized的实现原理?
- 25、线程中为什么start方法不能重复调用?而run方法却可以?
- 26、说一下线程生命周期,以及转换过程?
- 27、为什么wait和notify必须放在synchronized中?
- 28、sleep和wait有什么区别?
- 29、如何正确停止线程?
- 30、为什么需要线程池?什么是池化技术?
- 31、线程池有几种创建方式?推荐使用哪种?
- 32、说一下线程池7个参数的含义?
- 33、线程池是如何执行的?拒绝策略有哪些?
- 34、什么是守护线程?它和用户线程有什么区别?
- 35、为什么创建线程池一定要用ThreadPoolExecutor?
- 36、线程池有哪些状态?状态是如何转换的?
- 37、Java中用到的线程调度算法是什么
- 38、Thread.sleep(0)的作用是什么
- 39、知道synchronized原理吗?
- 40、简述Java线程的状态
- 41、简述Executor框架
- 42、简述线程池的状态
- 43、什么是线程上下文切换?
- 二、锁
- 三、Java内存模型(JMM)
- 四、其它知识点
一、Java线程
1、说说线程有几种创建方式?
(1)继承Thread类
重写run()方法,调用start()方法启动线程(没有返回值)
public class MyThread extends Thread
{
@Override
public void run()
{
System.out.println("继承Thread类");
}
public static void main(String[] args)
{
MyThread t = new MyThread();
t.start();
}
}
(2)实现Runnable接口
重写run()方法(没有返回值)
public class MyRunnable implements Runnable
{
@Override
public void run()
{
System.out.println("实现Runnable接口");
}
public static void main(String[] args)
{
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
}
}
(3)实现Callable接口
重写call()方法,这种方式可以通过FutureTask获取任务执行的返回值
public class MyCallable implements Callable<String>
{
@Override
public String call() throws Exception
{
return "实现Callable接口的方式";
}
public static void main(String[] args)
{
//创建异步任务
FutureTask<String> task=new FutureTask<String>(new MyCallable());
//启动线程
new Thread(task).start();
try
{
//等待执行完成,并获取返回结果
String result=task.get();
System.out.println(result);
}
catch (Exception e)
{
e.printStackTrace();
}
}
}
2、如何使用线程池执行定时任务?
使用ScheduledThreadPool
线程池执行定时任务的方法如下:
(1)schedule
①特点:只能执行一次定时任务
②参数
- 第 1 个参数:传递一个任务,Runnable 或 Callable 对象;
- 第 2 个参数:添加定时任务后,再过多久开始执行定时任务;
- 第 3 个参数:时间单位,配合参数 2 一起使用。
③举例
public class ScheduledThreadPoolExample1
{
public static void main(String[] args)
{
// 创建ScheduledThreadPool线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
System.out.println("schedule方法添加任务:" + LocalDateTime.now());
threadPool.schedule(new Runnable()
{
@Override
public void run()
{
System.out.println("执行schedule方法:" + LocalDateTime.now());
}
},3, TimeUnit.SECONDS); // 3s后执行
}
}
(2)scheduleAtFixedRate
①特点:可以执行多次定时任务
②参数
- 第 1 个参数:传递一个任务,Runnable 或 Callable 对象;
- 第 2 个参数:添加定时任务后,再过多久开始执行定时任务;
- 第 3 个参数:定时任务执行的时间间隔;
- 第 4 个参数:时间单位,配合参数 2 和参数 3 一起使用。
③举例
public class ScheduledThreadPoolExample2
{
public static void main(String[] args)
{
// 创建ScheduledThreadPool线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
System.out.println("scheduleAtFixedRate方法添加任务:" + LocalDateTime.now());
threadPool.scheduleAtFixedRate(new Runnable()
{
@Override
public void run()
{
System.out.println("执行scheduleAtFixedRate方法:" + LocalDateTime.now());
}
},3,2, TimeUnit.SECONDS);
// 3秒后执行的定时任务,每个定时任务执行的时间间隔为2秒
}
}
(3)scheduleWithFixedDelay
①特点:在方法执行完成之后,再隔 N 秒执行下一个定时任务,方法的执行受定时任务执行的时长影响
②举例
public class ScheduledThreadPoolExample3
{
public static void main(String[] args)
{
// 创建ScheduledThreadPool线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(10);
System.out.println("scheduleAtFixedRate方法添加任务:" + LocalDateTime.now());
threadPool.scheduleWithFixedDelay(new Runnable()
{
@Override
public void run()
{
System.out.println("执行scheduleAtFixedRate方法:" + LocalDateTime.now());
// 休眠2s
try
{
TimeUnit.SECONDS.sleep(2);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
},3,2, TimeUnit.SECONDS);
// 3秒后执行的定时任务,每个定时任务执行的时间间隔为2秒
}
}
定时任务在 3s 之后开始执行,以后每隔 4s 执行一次,这 4s 包含了,定时任务执行花费的 2s,加上每隔 2s 执行一次的时间间隔。
3、为什么调用start()方法时会执行run()方法,那怎么不直接调用run()方法?
JVM执行start方法,会先创建一条线程,由创建出来的新线程去执行thread的run方法,这才起到多线程的效果。
如果直接调用Thread的run()方法,那么run方法还是运行在主线程中,相当于顺序执行,就起不到多线程的效果。
4、线程有哪些常用的调度方法?
(1)线程等待(Object类)
① wait():当一个线程A调用一个共享变量的 wait()方法时, 线程A会被阻塞挂起, 发生下面几种情况才会返回 :线程A调用了共享对象 notify()或者 notifyAll()方法;其他线程调用了线程A的 interrupt() 方法,线程A抛出InterruptedException异常返回。
② wait(long timeout) :线程A调用共享对象的wait(long timeout)方法后,没有在指定的timeout ms 时间内被其它线程唤醒,那么这个方法还是会因为超时而返回。
③ wait(long timeout, int nanos):内部调用的是 wait(long timout)方法
(2)通知/唤醒线程(Object类)
① notify() : 一个线程A调用共享对象的 notify() 方法后,会唤醒一个在这个共享变量上调用 wait 系列方法后被挂起的线程。一个共享变量上可能会有多个线程在等待,具体唤醒哪个等待的线程是随机的。
② notifyAll() :唤醒所有在该共享变量上由于调用 wait 系列方法而被挂起的线程。
(3)线程等待(Thread类)
join():当前线程A等待thread线程终止之后才从thread.join()返回。
(4)线程休眠
sleep(long millis):Thread类中的静态方法,当一个执行中的线程A调用了Thread的sleep方法后,线程A会暂时让出指定时间的执行权,但是线程A所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该方法会正常返回,接着参与 CPU 的调度,获取到 CPU 资源后就可以继续运行。
(5)让出优先权
yield() :Thread类中的静态方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的CPU ,但是线程调度器可以无条件忽略这个暗示。
(6)线程中断
通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
① void interrupt() :中断线程,例如,当线程A运行时,线程B可以调用线程 interrupt() 方法来设置线程的中断标志为true 并立即返回。设置标志仅仅是设置标志, 线程A实际并没有被中断, 会继续往下执行。
② boolean isInterrupted() :检测当前线程是否被中断。
③ boolean interrupted() :检测当前线程是否被中断,与 isInterrupted 不同的是,该方法如果发现当前线程被中断,则会清除中断标志。
5、线程有几种状态?
6、守护线程了解吗?
(1)概述
Java中的线程分为两类,分别为 daemon 线程(守护线程)和 user 线程(用户线程)。
在JVM 启动时会调用 main 函数,main函数所在的线程就是一个用户线程,其实在 JVM 内部同时还启动了很多守护线程, 比如垃圾回收线程。
(2)守护线程和用户线程的区别
当最后一个非守护线程束时, JVM会正常退出,而不管当前是否存在守护线程。
守护线程是否结束并不影响 JVM退出。
只要有一个用户线程还没结束,正常情况下JVM就不会退出。
7、线程间有哪些通信方式?
(1)volatile和synchronized关键字
① volatile可以用来修饰成员变量,告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
② synchronized可以修饰方法或者以同步块的形式来进行使用,确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,保证了线程对变量访问的可见性和排他性。
(2)等待/通知机制
通过Java内置的等待/通知机制(wait()/notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后进行相应的操作。
(3)管道输入/输出流
主要用于线程之间的数据传输,传输的媒介为内存。
管道输入/输出流主要包括了如下4种具体实现:PipedOutputStream、PipedInputStream、 PipedReader和PipedWriter,前两种面向字节,而后两种面向字符。
(4)使用Thread.join()
如果一个线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回。
(5)使用ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键、任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
8、ThreadLocal是什么?
ThreadLocal,即线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝。多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。
(1)创建
任何一个线程都能并发访问。
//创建一个ThreadLocal变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
(2)写入
线程可以在任何地方使用localVariable,写入变量。
localVariable.set("张三”);
(3)读取
线程在任何地方读取的都是它写入的变量。
localVariable.get();
9、你在工作中用到过ThreadLocal吗?
(1)用来做用户信息上下文的存储。
我们的系统应用是一个典型的MVC架构,登录后的用户每次访问接口,都会在请求头中携带一个token,在控制层可以根据这个token,解析出用户的基本信息。假如在服务层和持久层都要用到用户信息,那应该怎么办呢?
一种办法是显式定义用户相关的参数,可能需要大面积地修改代码,不太可取。
另一种方法是在控制层拦截请求把用户信息存入ThreadLocal,这样我们在任何一个地方,都可以取出ThreadLocal中存的用户数据。
(2)其它场景的cookie、session等等数据隔离可以通过ThreadLocal去实现。
(3)数据库连接池的连接交给ThreadLoca进行管理,保证当前线程的操作都是同一个Connnection。
10、ThreadLocal怎么实现的呢?ThreadLocal原理?
(1)Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
(2)ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal的弱引用,value是ThreadLocal的泛型值。
(3)每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
(4)ThreadLocal本身不存储值,它只是作为一个key来让线程往ThreadLocalMap里存取值。
11、ThreadLocal 内存泄露是怎么回事?
(1)ThreadLocal内存分配
在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。
所以,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。
(2)ThreadLocal 内存泄露
ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用(弱引用指只要垃圾回收机制一运行,不管JVM的内存空间是否充足,都会回收该对象占用的内存)
弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。
(3)解决内存泄露
使用完ThreadLocal后,及时调用remove()方法释放内存空间。
(4)为什么key设计成弱引用?
防止内存泄漏。
假如key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLoca的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。
12、ThreadLocalMap的结构了解吗?
没有实现Map接口的,结构和HashMap类似
(1)元素数组
一个table数组,存储Entry类型的元素,Entry是ThreadLocal弱引用作为key,Object作为value的结构。
private Entry[] table;
(2)散列方法
散列方法就是怎么把对应的key映射到table数组的相应下标,ThreadLocalMap用的是哈希取余法,取出key的threadLocalHashCode,然后和table数组长度减一&运算(相当于取余)。
int i = key.threadLocalHashCode & (table.length - 1);
每创建一个ThreadLocal对象,它就会新增0x61c88647,这个值很是斐波那契数 也叫黄金分割数。hash增量为这个数字,带来的好处就是 hash 分布非常均匀。
13、ThreadLocalMap怎么解决Hash冲突的?
开放定址法——简单来说,就是这个坑被人占了,那就接着去找空着的坑。
如果我们插入一个value=27的数据,通过 hash计算后应该落入第 4 个槽位中,而槽位 4 已经有了 Entry数据,而且Entry数据的key和当前不相等。此时就会线性向后查找,一直找到 Entry为 null的槽位才会停止查找,把元素放到空的槽中。
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该槽位Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。
14、ThreadLocalMap扩容机制了解吗?
(1)在ThreadLocalMap.set()方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry的数量已经达到了列表的扩容阈值(len*2/3),就开始执行rehash()
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
(2)rehash()具体实现:这里会先去清理过期的Entry,然后还要根据条件判断size >= threshold - threshold / 4 来决定是否需要扩容
private void rehash() {
//清理过期Entry
expungeStaleEntries();
//扩容
if (size >= threshold - threshold / 4)
resize();
}
//清理过期Entry
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
(3)具体的resize()方法:扩容后的newTab的大小为老数组的两倍,然后遍历老的table数组,散列方法重新计算位置,开放地址解决冲突,然后放到新的newTab,遍历完成之后,oldTab中所有的entry数据都已经放入到newTab中了,然后table引用指向newTab
private void resize() {
ThreadLocal.ThreadLocalMap.Entry[] oldTab = this.table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
ThreadLocal.ThreadLocalMap.Entry[] newTab = new ThreadLocal.ThreadLocalMap.Entry[newLen];
int count = 0;
ThreadLocal.ThreadLocalMap.Entry[] var6 = oldTab;
int var7 = oldTab.length;
for(int var8 = 0; var8 < var7; ++var8) {
ThreadLocal.ThreadLocalMap.Entry e = var6[var8];
if (e != null) {
ThreadLocal<?> k = (ThreadLocal)e.get();
if (k == null) {
e.value = null;
} else {
int h;
for(h = k.threadLocalHashCode & newLen - 1; newTab[h] != null; h = nextIndex(h, newLen)) {
}
newTab[h] = e;
++count;
}
}
}
this.setThreshold(newLen);
this.size = count;
this.table = newTab;
}
15、父子线程怎么共享数据?
(1)父线程不能用ThreadLocal
来给子线程传值
(2)父子线程共享数据需要用到InheritableThreadLocal
public class InheritableThreadLocalTest
{
public static void main(String[] args)
{
final ThreadLocal threadLocal = new InheritableThreadLocal();
//父线程
threadLocal.set("学生");
//子线程
Thread thread = new Thread(){
@Override
public void run()
{
super.run();
System.out.println("我是" + threadLocal.get()); //我是学生
}
};
thread.start();
}
}
(3)实现原理
在Thread类里有另外一个变量ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在Thread.init的时候,如果父线程的inheritableThreadLocals
不为空,就把它赋给当前线程(子线程)的inheritableThreadLocals
。
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
16、synchronized怎么保证可见性?
(1)线程加锁前,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值。
(2)线程加锁后,其它线程无法获取主内存中的共享变量。
(3)线程解锁前,必须把共享变量的最新值刷新到主内存中。
17、synchronized怎么保证有序性?
synchronized同步的代码块,具有排他性,一次只能被一个线程拥有,所以保证同一时刻,代码是单线程执行的。
synchronized保证的有序是执行结果的有序性,而不是防止指令重排的有序性。
18、synchronized怎么实现可重入性?
(1)synchronized 是可重入锁,也就是说,允许一个线程二次请求自己持有对象锁的临界资源
(2)synchronized 锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,就释放锁了。
(3)之所以是可重入的,是因为 synchronized 锁对象有个计数器,会随着线程获取锁后 +1 计数,当线程执行完毕后 -1,直到清零释放锁。
19、多线程有什么用?
(1)发挥多核CPU的优势
(2)防止线程阻塞:多条线程同时运行,一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行
(3)便于建模
20、Runnable接口和Callable接口的区别
(1)Runnable接口
其中的run()方法的返回值是void,只是纯粹地去执行run()方法中的代码而已;
(2)Callable接口
①其中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
②应用:可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务(因为多线程的运行往往是未知的)
21、CyclicBarrier和CountDownLatch的区别
在java.util.concurrent下,都可以用来表示代码运行到某个点上
(1)CyclicBarrier的某个线程运行到某个点上之后,该线程即停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch则不是,某线程运行到某个点上之后,只是给某个数值-1而已,该线程继续运行。
(2)CyclicBarrier只能唤起一个任务;CountDownLatch可以唤起多个任务。
(3)CyclicBarrier可重用;CountDownLatch不可重用,计数值为0该CountDownLatch就不可再用了。
22、什么是线程安全
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
23、Java中如何获取到线程dump文件
线程dump也就是线程堆栈,打线程dump是解决死循环、死锁、阻塞、页面打开慢等问题一个比较好的解决方式。
获取线程堆栈:
①获取到线程的pid,可以通过使用jps
命令,在Linux环境下还可以使用ps -ef | grep java
②打印线程堆栈,可以通过使用jstack pid
命令,在Linux环境下还可以使用kill -3 pid
24、一个线程如果出现了运行时异常会怎么样
如果这个异常没有被捕获的话,这个线程就停止执行了。如果这个线程持有某个对象的监视器,那么这个对象监视器会被立即释放。
25、怎么检测一个线程是否持有对象监视器
Thread类提供了一个holdsLock(Object obj)
方法,当且仅当对象obj的监视器被某条线程持有的时候才会返回true,这是一个static方法,意味着”某条线程”指的是当前线程
23、synchronized用过吗?怎么使用?
synchronized
用来保证代码的原子性
三种用法:
(1)修饰实例方法
作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
synchronized void method() {
//业务代码
}
(2)修饰静态方法
给当前类加锁,作用于类的所有对象实例 ,进入同步代码前要获得当前 class 的锁。
synchronized void staic method() {
//业务代码
}
注意:如果⼀个线程 A 调⽤⼀个实例对象的⾮静态 synchronized ⽅法,⽽线程 B 需要调⽤这个实例对象所属类的静态 synchronized ⽅法,是允许的,不会发⽣互斥现象,因为访问静态 synchronized ⽅法占⽤的锁是当前类的锁,⽽访问⾮静态 synchronized ⽅法占⽤的锁是当前实例对象锁。
(3)修饰代码块
指定加锁对象,对给定对象/类加锁。
synchronized(this) {
//业务代码
}
①synchronized(this|object)
表示进⼊同步代码前要获得给定对象的锁。
②synchronized(类.class)
表示进⼊同步代码前要获得当前 class 的锁。
24、synchronized的实现原理?
(1)synchronized是怎么加锁的
使用synchronized
不用自己去lock和unlock,这个操作由JVM完成
①synchronized修饰代码块
JVM采用monitorenter
、monitorexit
两个指令来实现同步,monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指向同步代码块的结束位置。
②synchronized修饰同步方法
JVM采用ACC_SYNCHRONIZED
标记符来实现同步,这个标识指明了该方法是一个同步方法。、
(2)synchronized锁住的是什么
monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor
实现的。
① monitorenter,在判断拥有同步标识 ACC_SYNCHRONIZED 抢先进入此方法的线程会优先拥有 Monitor 的 owner ,此时计数器 +1。
② monitorexit,当执行完退出后,计数器 -1,归 0 后被其他进入的线程获得。
25、线程中为什么start方法不能重复调用?而run方法却可以?
(1)start方法与run方法的区别
①方法性质不同
调用 start 方法是真正开启一个线程来执行任务,而调用 run 方法相当于执行普通方法 run,并不会开启新线程
②执行速度不同
run 方法也叫做线程体,它里面包含了具体要执行的业务代码,当调用 run 方法时,会立即执行 run 方法中的代码(如果当前线程时间片未用完);而调用 start 方法时,是启动一个线程并将线程的状态设置为就绪状态。也就是说调用 start 方法,并不会立即执行。
③调用次数不同
run 方法可以被调用多次,而 start 方法只能被调用一次
(2)为什么start不能被重复调用?
因为 start 方法在执行时,会先判断当前线程的状态是不是等于 0,也就是是否为新建状态 NEW,如果不等于新建状态,那么就会抛出“IllegalThreadStateException”非法线程状态异常(当线程调用了第一个 start 方法之后,线程的状态就会从新建状态 NEW,变为就绪状态 RUNNABLE,此时再次调用 start 方法,JVM 就会判断出当前的线程已经不等于新建状态,从而抛出 IllegalThreadStateException 非法线程状态异常)
26、说一下线程生命周期,以及转换过程?
(1)Java线程生命周期
NEW(初始化状态)
RUNNABLE(可运行/运行状态)
BLOCKED(阻塞状态)
WAITING(无时限等待状态)
TIMED_WAITING(有时限等待状态)
TERMINATED(终止状态)
(2)生命周期转换过程
① 从 NEW 到 RUNNABLE
当创建一个线程的时候,此时线程是 NEW 状态,调用了线程的 start 方法之后,线程的状态就从 NEW 变成了 RUNNABLE。
② 从 RUNNABLE 到 BLOCKED
当线程中的代码排队执行 synchronized 时,线程就会从 RUNNABLE 状态变为 BLOCKED 阻塞状态;当线程获取到 synchronized 锁之后,就会从 BLOCKED 状态转变为 RUNNABLE 状态。
③ 从 RUNNABLE 到 WAITTING
线程调用 wait() 方法之后,就会从 RUNNABLE 状态变为 WAITING 无时限等待状态;当调用了 notify/notifyAll 方法之后,线程会从 WAITING 状态变成 RUNNABLE 状态。
④ 从 RUNNABLE到TIMED_WATTING
当调用带超时时间的等待方法时,如 sleep(xxx),线程会从 RUNNABLE 状态变成 TIMED_WAITING 有时限状态;当超过了超时时间之后,线程就会从 TIMED_WAITING 状态变成 RUNNABLE 状态
⑤ RUNNABLE 到 TERMINATED
线程执行完之后,就会从 RUNNABLE 状态变成 TERMINATED 销毁状态
27、为什么wait和notify必须放在synchronized中?
JVM 在运行时会强制检查 wait 和 notify 有没有在 synchronized 代码中,如果没有的话就会报非法监视器状态异常(IllegalMonitorStateException)。
这样是为了防止多线程并发运行时,程序的执行混乱问题。
28、sleep和wait有什么区别?
sleep 方法和 wait 方法都是用来将线程进入休眠状态的,并且 sleep 和 wait 方法都可以响应 interrupt 中断,即线程在休眠的过程中,如果收到中断信号,都可以进行响应并中断,且都可以抛出 InterruptedException 异常
(1)语法使用不同
① wait 方法必须配合 synchronized 一起使用,不然在运行时就会抛出IllegalMonitorStateException 的异常
② sleep 可以单独使用
(2)所属类不同
wait 方法属于 Object 类的方法,而 sleep 属于 Thread 类的方法
(3)唤醒方式不同
sleep 方法必须要传递一个超时时间的参数,且过了超时时间之后,线程会自动唤醒。而 wait 方法可以不传递任何参数,不传递任何参数时表示永久休眠,直到另一个线程调用了 notify 或 notifyAll 之后,休眠的线程才能被唤醒。
(4)释放锁资源不同
wait 方法会主动的释放锁,而 sleep 方法则不会
(5)线程进入可能状态不同
调用 sleep 方法线程会进入 TIMED_WAITING 有时限等待状态,而调用无参数的 wait 方法,线程会进入 WAITING 无时限等待状态。
29、如何正确停止线程?
(1)interrupt中断线程(推荐使用)
使用 interrupt 方法可以给执行任务的线程,发送一个中断线程的指令,它并不直接中断线程,而是发送一个中断线程的信号,把是否正在中断线程的主动权交给代码编写者
public class InterruptTest
{
public static void main(String[] args) throws InterruptedException
{
// 创建线程可停止的实例
Thread thread = new Thread(() -> {
while(!Thread.currentThread().isInterrupted())
{
System.out.println("thread 执行步骤1:线程即将进入休眠状态");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
System.out.println("thread 线程接收到中断指令,执行中断操作");
// 中断当前线程的任务执行
break;
}
System.out.println("thread 执行步骤2:线程执行了任务");
}
});
thread.start();
// 休眠100ms,等待thread线程运行起来
Thread.sleep(100);
System.out.println("主线程:试图终止线程 thread");
// 发送中断线程的信号
thread.interrupt();
}
}
(2)自定义中断标识符
在程序中定义一个变量来决定线程是否要中断执行,缺点是线程中断的不够及时(因为线程在执行过程中,无法调用 while(!isInterrupt) 来判断线程是否为终止状态,它只能在下一轮运行时判断是否要终止当前线程,所以它中断线程不够及时)
public class InterruptTest1
{
// 自定义中断标识符
private static volatile boolean isInterrupt = false;
public static void main(String[] args) throws InterruptedException
{
// 创建线程可停止的实例
Thread thread = new Thread(() -> {
while(!isInterrupt)
{
System.out.println("thread 执行步骤1:线程即将进入休眠状态");
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
System.out.println("thread 执行步骤2:线程执行了任务");
}
});
thread.start();
// 休眠100ms,等待thread线程运行起来
Thread.sleep(100);
System.out.println("主线程:试图终止线程 thread");
// 修改中断标识符,中断线程
isInterrupt = true;
}
}
30、为什么需要线程池?什么是池化技术?
(1)池化技术
①定义
提前准备一些资源,在需要时可以重复使用这些预先准备的资源
②优点
提前准备、重复利用
(2)池化技术的应用
①线程池
类似于操作系统中的缓冲区的概念。
线程池中会先启动若干数量的线程,这些线程都处于睡眠状态;当客户端有一个新的请求时,就会唤醒线程池中的某一个睡眠的线程,让它来处理客户端的这个请求,当处理完这个请求之后,线程又处于睡眠的状态。
②内存池
更好地管理应用程序内存的使用,同时提高内存使用的频率。内存池在创建的过程中,会预先分配足够大的内存,形成一个初步的内存池。然后每次用户请求内存的时候,就会返回内存池中的一块空闲的内存,并将这块内存的标志置为已使用。当内存使用完毕释放内存的时候,也不是真正地调用 free 或 delete 的过程,而是把内存放回内存池的过程,且放回的过程要把标志置为空闲。
- 优点:减少内存碎片的产生;提高了内存的使用频率
- 缺点:会造成内存的浪费,因为要使用内存池需要在一开始分配一大块闲置的内存,而这些内存不一定全部被用到。
③数据库连接池
在系统初始化的时候将数据库连接作为对象存储在内存中,当用户需要访问数据库的时候,并非建立一个新的连接,而是从连接池中取出一个已建立的空闲连接对象。在使用完毕后,用户也不是将连接关闭,而是将连接放回到连接池中,以供下一个请求访问使用,而这些连接的建立、断开都是由连接池自身来管理
④HttpClient连接池
解决问题——HttpClient 的每次请求都会新建一个连接,当创建连接的频率比关闭连接的频率大的时候,就会导致系统中产生大量处于 TIME_CLOSED 状态的连接
(3)线程池
①什么是线程池
线程池是线程使用的一种模式,它将线程和任务的概念分离开,使用线程来执行任务,并提供统一的线程管理和任务管理的实现方法,避免了频繁创建和销毁线程所带来的性能开销。
②优点
- 复用线程,降低资源消耗
- 提高响应速度
- 管理线程数和任务数,可以控制最大并发数和最大任务数
-
31、线程池有几种创建方式?推荐使用哪种?
(1)通过 Executors 执行器自动创建线程池
① Executors.newFixedThreadPool
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。public static void fixedThreadPool1()
{
// 创建2个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 创建任务
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
}
};
// 线程池执行任务(一次添加4个任务),执行任务的方法有两种:submit 和 execute
threadPool.submit(runnable);
threadPool.submit(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
}
public static void fixedThreadPool2()
{
// 创建线程池
ExecutorService threadPool = Executors.newFixedThreadPool(2);
// 执行任务
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
});
}
② Executors.newCachedThreadPool
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。public static void cacheThreadPool()
{
// 创建线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
// 执行任务
for(int i = 0; i < 5; i++)
{
threadPool.execute(() -> {
System.out.println("任务被执行,线程:" + Thread.currentThread().getName());
try
{
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
});
}
}
使用场景:适合短时间内有突发大量任务的处理场景
③ Executors.newSingleThreadExecutor
创建单个线程数的线程池,它可以保证先进先出的执行顺序。public static void singleThreadExecutor()
{
// 创建线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();
// 执行任务
for(int i = 0; i < 5; i++)
{
final int index = i;
threadPool.execute(() -> {
System.out.println(index + ":任务被执行");
try
{
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
});
}
}
单个线程的线程池的优点:
可以复用线程
- 提供了任务管理功能:单个线程池也拥有任务队列,在任务队列可以存储多个任务,这是线程无法实现的,并且当任务队列满了之后,可以执行拒绝策略,这些都是线程不具备的。
④ Executors.newScheduledThreadPool
创建一个可以执行延迟任务的线程池。
public static void scheduledThreadPool()
{
// 创建线程池
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5);
// 添加定时执行任务(2s后执行)
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
},2,TimeUnit.SECONDS);
}
⑤ Executors.newSingleThreadScheduledExecutor
创建一个单线程的可以执行延迟任务的线程池。
public static void singleThreadScheduledExecutor()
{
// 创建线程池
ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor();
// 添加定时执行任务(3s后执行)
System.out.println("添加任务,时间:" + new Date());
threadPool.schedule(() -> {
System.out.println("任务被执行,时间:" + new Date());
try
{
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
},3,TimeUnit.SECONDS);
}
⑥ Executors.newWorkStealingPool
创建一个抢占式执行的线程池(任务执行顺序不确定,jdk1.8新增)
public static void workStealingPool()
{
// 创建线程池
ExecutorService threadPool = Executors.newWorkStealingPool();
// 执行任务
for(int i = 0; i < 8 ; i++)
{
final int index = i;
threadPool.execute(() -> {
System.out.println(index + "被执行,线程名:" + Thread.currentThread().getName());
});
}
// 确保任务执行完成
while(!threadPool.isTerminated()){}
}
(2)通过 ThreadPoolExecutor 手动创建线程池(推荐)
可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,规避资源耗尽的风险。
public static void myThreadPoolExecutor()
{
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));
// 执行任务
for (int i = 0; i < 10; i++)
{
final int index = i;
threadPool.execute(() -> {
System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName());
try
{
Thread.sleep(1000);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
});
}
}
32、说一下线程池7个参数的含义?
指在使用 ThreadPoolExecutor 创建线程池时所设置的 7 个参数
(1)corePoolSize:核心线程数
指线程池中长期存活的线程数
(2)maximumPoolSize:最大线程数
线程池允许创建的最大线程数量,当线程池的任务队列满了之后,可以创建的最大线程数(最大线程数 maximumPoolSize 的值不能小于核心线程数 corePoolSize)
(3)keepAliveTime:空闲线程存活时间
当线程池中没有任务时,会销毁一些线程,销毁的线程数 = maximumPoolSize(最大线程数)-corePoolSize(核心线程数)
(4)TimeUnit:时间单位
空闲线程存活时间的描述单位
TimeUnit.DAYS:天
TimeUnit.HOURS:小时
TimeUnit.MINUTES:分
TimeUnit.SECONDS:秒
TimeUnit.MILLISECONDS:毫秒
TimeUnit.MICROSECONDS:微妙
TimeUnit.NANOSECONDS:纳秒
(5)BlockingQueue:线程池任务队列
线程池存放任务的队列,用来存储线程池的所有待执行任务(阻塞队列)
可以设置以下几个值:
- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:一个由链表结构组成的有界阻塞队列
- SynchronousQueue:一个不存储元素的阻塞队列,即直接提交给线程不保持它们
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
- DelayQueue:一个使用优先级队列实现的无界阻塞队列,只有在延迟期满时才能从中提取元素
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列(常用)
(6)ThreadFactory:创建线程的工厂
线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等。
(7)RejectedExecutionHandler:拒绝策略
当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略
拒绝策略有以下几种:
- AbortPolicy:拒绝并抛出异常(线程池默认策略)
- CallerRunsPolicy:使用当前调用的线程来执行此任务
- DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务
- DiscardPolicy:忽略并抛弃当前任务
33、线程池是如何执行的?拒绝策略有哪些?
(1)线程池执行流程——从execute()源码分析
先判断当前线程数是否大于核心线程数?如果结果为 false,则新建线程并执行任务;如果结果为 true,则判断任务队列是否已满?如果结果为 false,则把任务添加到任务队列中等待线程执行,否则则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务,否则将执行线程池的拒绝策略
(2)线程池拒绝策略
当任务过多且线程池的任务队列已满时,此时就会执行线程池的拒绝策略,线程池默认的拒绝策略是AbortPolicy
①DiscardPolicy:忽略并抛弃当前任务
②CallerRunsPolicy:使用当前调用的线程来执行此任务
③DiscardOldestPolicy:抛弃队列头部(最旧)的一个任务,并执行当前任务
④AbortPolicy:拒绝并抛出异常(线程池默认策略)public class AbortPolicyTest
{
public static void main(String[] args)
{
// 任务的具体方法
Runnable runnable = new Runnable()
{
@Override
public void run()
{
System.out.println("当前任务被执行,执行时间:" + new Date() +
" 执行线程:" + Thread.currentThread().getName());
try
{
TimeUnit.SECONDS.sleep(1);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
};
// 创建线程,线程的任务队列的长度为1
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1,1,100, TimeUnit.SECONDS,new LinkedBlockingQueue<>(1),new ThreadPoolExecutor.AbortPolicy());
// 添加并执行4个任务
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
threadPool.execute(runnable);
// 关闭线程池
threadPool.shutdown();
}
}
34、什么是守护线程?它和用户线程有什么区别?
(1)概述
①线程分为两类:用户线程和守护线程,默认情况下我们创建的线程或线程池都是用户线程,所以用户线程也被称之为普通线程。
②可以通过Thread.isDaemon()
方法来判断,如果返回的结果是 true 则为守护线程,反之则为用户线程。
(2)守护线程(Daemon Thread)
①定义
为用户线程服务的,当程序中的用户线程全部执行结束之后,守护线程也会跟随结束。
②创建守护线程
③为线程池设置守护线程public class DaemonThreadTest
{
public static void main(String[] args)
{
Thread thread = new Thread(new Runnable()
{
@Override
public void run()
{
//...
}
});
// 设置线程为守护线程
thread.setDaemon(true);
System.out.println("Thread 线程类型:" +
(thread.isDaemon() == true ? "守护线程" : "用户线程"));
System.out.println("main 线程类型:" +
(Thread.currentThread().isDaemon() == true ? "守护线程" : "用户线程"));
}
}
④注意public class ThreadPoolDaemonTest
{
public static void main(String[] args) throws InterruptedException
{
// 线程工厂(设置守护线程)
ThreadFactory threadFactory = new ThreadFactory()
{
@Override
public Thread newThread(Runnable runnable)
{
Thread thread = new Thread(runnable);
// 设置为守护线程
thread.setDaemon(true);
return thread;
}
};
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100), threadFactory);
// 线程池执行任务
threadPool.submit(new Runnable()
{
@Override
public void run()
{
System.out.println("ThreadPool 线程类型:" +
(Thread.currentThread().isDaemon() == true ? "守护线程" : "用户线程"));
}
});
Thread.sleep(2000);
}
}
守护线程的设置 setDaemon(true) 必须要放在线程的 start() 之前,否则程序会报错。
(3)守护线程和用户线程的区别
程序会等用户线程执行完后才会结束进程;当线程设置为守护线程之后,整个程序不会等守护线程,而是当主线程结束之后就会结束进程。35、为什么创建线程池一定要用ThreadPoolExecutor?
因为这种方式可以通过参数来控制最大任务数和拒绝策略,让线程池的执行更加透明和可控,并且可以规避资源耗尽的风险。36、线程池有哪些状态?状态是如何转换的?
(1)线程池的状态
① RUNNING:运行状态。线程池创建好之后进入此状态,如果不手动调用关闭方法,那么线程池在整个程序运行期间都是此状态。
② SHUTDOWN:关闭状态。不再接受新任务提交,但是会将已保存在任务队列中的任务处理完。
③ STOP:停止状态。不再接受新任务提交,并且会中断当前正在执行的任务、放弃任务队列中已有的任务。
④ TIDYING:整理状态。所有的任务都执行完毕后(也包括任务队列中的任务执行完),当前线程池中的活动线程数降为 0 时的状态。
⑤ TERMINATED:销毁状态。当执行完线程池的 terminated() 方法之后就会变为此状态。
(2)状态转换
① 当调用 shutdown() 方法时,线程池的状态会从 RUNNING 到 SHUTDOWN,再到 TIDYING,最后到 TERMENATED 销毁状态。
② 当调用 shutdownNow() 方法时,线程池的状态会从 RUNNING 到 STOP,再到 TIDYING,最后到 TERMENATED 销毁状态。37、Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。38、Thread.sleep(0)的作用是什么
由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。39、知道synchronized原理吗?
synchronized是Java提供的原子性内置锁,也被称为监视器锁,使用synchronized之后,会在编译之后在同步的代码块前后加上monitorenter和monitorexit字节码指令,他依赖操作系统底层互斥锁实现。
作用:实现原子性操作和解决共享变量的内存可见性问题。
执行monitorenter指令时会尝试获取对象锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1。此时其他竞争锁的线程则会进入等待队列中。执行monitorexit指令时则会把计数器-1,当计数器值为0时,则锁释放,处于等待队列中的线程再继续竞争锁。
synchronized是排它锁,当一个线程获得锁之后,其他线程必须等待该线程释放锁后才能获得锁,而且由于Java中的线程和操作系统原生线程是一一对应的,线程被阻塞或者唤醒时时会从用户态切换到内核态,这种转换非常消耗性能。40、简述Java线程的状态
NEW:新建状态,线程被创建且未启动,此时还未调用 start 方法。
RUNNABLE:运行状态。其表示线程正在JVM中执行,但是这个执行,不一定真的在跑,也可能在排队等CPU。
BLOCKED:阻塞状态。线程等待获取锁,锁还没获得。
WAITING:等待状态。线程内run方法运行完语句Object.wait()/Thread.join()进入该状态。
TIMED_WAITING:限期等待。在一定时间之后跳出状态。调用Thread.sleep(long)、Object.wait(long)、 Thread.join(long)进入状态。其中这些参数代表等待的时间。
TERMINATED:结束状态。线程调用完run方法进入该状态。41、简述Executor框架
Executor框架目的是将任务提交和任务如何运行分离开来的机制。用户不再需要从代码层考虑设计任务的提交运行,只需要调用Executor框架实现类的Execute方法就可以提交任务。42、简述线程池的状态
Running:能接受新提交的任务,也可以处理阻塞队列的任务。
Shutdown:不再接受新提交的任务,但可以处理存量任务,线程池处于running时调用shutdown方法,会进入该状态。
Stop:不接受新任务,不处理存量任务,调用shutdownnow进入该状态。
Tidying:所有任务已经终止了,worker_count(有效线程数)为0。
Terminated:线程池彻底终止。在tidying模式下调用terminated方法会进入该状态。43、什么是线程上下文切换?
为了让用户感觉多个线程是在同时执行的, CPU 资源的分配采用了时间片轮转也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换。
二、锁
1、ReadWriteLock是什么
(1)定义
①ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离
②读锁是共享的,写锁是独占的
③读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
④需要显式的获取锁和释放锁
(2)局限性
如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
2、什么是乐观锁和悲观锁
(1)乐观锁
对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总是会发生,因此它不需要持有锁。
(2)悲观锁
对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
3、锁的优化机制了解吗?
锁的状态从低到高依次为无锁->偏向锁->轻量级锁->重量级锁,升级的过程就是从低到高,降级在一定条件也是有可能发生的。
自旋锁:让线程执行一个忙循环,可以理解为就是啥也不干,防止从用户态转入内核态,自旋锁可以通过设置-XX:+UseSpining来开启,自旋的默认次数是10次,可以使用-XX:PreBlockSpin设置。
自适应锁:自适应锁就是自适应的自旋锁,自旋的时间不是固定时间,而是由前一次在同一个锁上的自旋时间和锁的持有者状态来决定。
锁消除:指的是JVM检测到一些同步的代码块,完全不存在数据竞争的场景,也就是不需要加锁,就会进行锁消除。
锁粗化:指的是有很多操作都是对同一个对象进行加锁,就会把锁的同步范围扩展到整个操作序列之外。
偏向锁:当线程访问同步块获取锁时,会在对象头和栈帧中的锁记录里存储偏向锁的线程ID,之后这个线程再次进入同步块时都不需要CAS来加锁和解锁。
轻量级锁:JVM的对象的对象头中包含有一些锁的标志位,代码进入同步块的时候,JVM将会使用CAS方式来尝试获取锁,如果更新成功则会把对象头中的状态位标记为轻量级锁,如果更新失败,当前线程就尝试自旋来获得锁。
3、简述Java偏向锁
(1)概述
JDK 1.6 中提出了偏向锁的概念;
该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,一把锁往往是由同一个线程获得的;
偏向锁并不会主动释放,这样每次偏向锁进入的时候都会判断该资源是否是偏向自己的,如果是偏向自己的则不需要进行额外的操作,直接可以进入同步操作。
(2)申请流程
①首先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进入轻量级锁判断逻辑。否则继续下一步判断;
②判断目前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID 一致。如果一致,继续下一步的判断,如果不一致,跳转到步骤4;
③判断是否需要重偏向。如果不用的话,直接获得偏向锁;
④利用 CAS 算法将对象的 Mark Word 进行更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。
4、简述轻量级锁
(1)概述
轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗。
(2)申请流程
①如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建立一个锁记录空间,存储锁对象目前 Mark Word 的拷贝。
②虚拟机使用 CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针
③如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
④如果更新失败就意味着至少存在一条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧
⑤如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进入同步块继续执行
⑥如果不是则说明锁对象已经被其他线程抢占。
⑦如果出现两条以上线程争用同一个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为 10,此时Mark Word 存储的就是指向重量级锁的指针,后面等待锁的线程也必须阻塞。
5、简述AQS
AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。AQS是将每一条请求共享资源的线程封装成一个锁队列的一个结点(Node),来实现锁的分配。AQS是用来构建锁或其他同步组件的基础框架,它使用一个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进入同步队列等待;如果获取成功就执行临界区代码,释放资源时会通知同步队列中的等待线程。
6、AQS获取独占锁/释放独占锁原理
(1)获取
调用 tryAcquire 方法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter 方法加入到同步队列的尾部,在队列中自旋。
调用 acquireQueued 方法使得该节点以死循环的方式获取同步状态,如果获取不到则阻塞。
(2)释放
调用 tryRelease 方法释放同步状态
调用 unparkSuccessor 方法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。
7、AQS获取共享锁/释放共享锁原理
获取(acquireShared):调用 tryAcquireShared 方法尝试获取同步状态,返回值不小于 0 表示能获取同步状态。
释放(releaseShared):释放并唤醒后续处于等待状态的节点。
三、Java内存模型(JMM)
1、说一下你对Java内存模型(JMM)的理解?
Java内存模型(Java Memory Model,JMM),是一种抽象的模型,被定义出来屏蔽各种硬件和操作系统的内存访问差异。
Java内存模型定义了程序中各种变量的访问规则。其规定所有变量都存储在主内存,线程均有自己的工作内存。工作内存中保存被该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作空间进行,不能直接读写主内存数据。操作完成后,线程的工作内存通过缓存一致性协议将操作完的数据刷回主存。
2、说说你对原子性、可见性、有序性的理解?
(1)原子性:
指的是一个操作是不可分割、不可中断的,要么全部执行并且执行的过程不会被任何因素打断,要么就全不执行。
(2)可见性
指的是一个线程修改了某一个共享变量的值时,其它线程能够立即知道这个修改。
(3)有序性
指的是对于一个线程的执行代码,从前往后依次执行,单线程下可以认为程序是有序的,但是并发时有可能会发生指令重排。
3、原子性、可见性、有序性都应该怎么保证呢?
(1)原子性
JMM只能保证基本的原子性,如果要保证一个代码块的原子性,需要使用synchronized
。
(2)可见性
Java是利用volatile
关键字来保证可见性的,final
和synchronized
也能保证可见性。
(3)有序性synchronized
或者volatile
都可以保证多线程之间操作的有序性。
4、说说什么是指令重排?
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
(2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
5、指令重排有限制吗?happens-before了解吗?
指令重排有一些限制的,有两个规则happens-before
和as-if-serial
来约束。
(1)happens-before
的定义
①如果一个操作happens-before
另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
②两个操作之间存在happens-before
关系,并不意味着Java平台的具体实现必须要按照 happens-before
关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before
关系来执行的结果一致,那么这种重排序是合法的
(2)happens-before
的八大规则
①程序次序规则:一个线程内写在前面的操作先行发生于后面的。
②锁定规则:unlock 操作先行发生于后面对同一个锁的 lock 操作。
③volatile 规则:对 volatile 变量的写操作先行发生于后面的读操作。
④线程启动规则:线程的 start 方法先行发生于线程的每个动作。
⑤线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
⑥线程终止规则:线程中所有操作先行发生于对线程的终止检测。
⑦对象终结规则:对象的初始化先行发生于 finalize 方法。
⑧传递性规则:如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C
6、as-if-serial又是什么?单线程的程序一定是顺序的吗?
(1)定义
编译器等会对原始的程序进行指令重排序和优化。但不管怎么重排序,其结果和用户原始程序输出预定结果一致。
(2)说明
①编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。
②如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。
(3)举例
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C
A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。
(4)总结
①编译器、runtime和处理器都必须遵守as-if-serial
语义
②as- if-serial
语义使单线程情况下,我们不需要担心重排序的问题,可见性的问题。
7、volatile实现原理了解吗?
volatile
的作用是保证可见性和有序性。
(1)保证可见性
一个变量被声明为volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存 当其它线程读取该共享变量 ,会从主内存重新获取最新值,而不是使用当前线程的本地内存中的值。
①没有上下文切换的额外开销成本,确保对某个变量的更新对其他线程马上可见;
②例如,我们声明一个 volatile 变量 volatile int x = 0,线程A修改x=1,修改完之后就会把新的值刷新回主内存,线程B读取x的时候,就会清空本地内存变量,然后再从主内存获取最新值。
(2)保证有序性
valatile通过分别限制编译器重排序和处理器重排序这两种重排序实现有序性。
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序:
- 在每个volatile写操作的前面插入一个StoreStore屏障(禁止前面的普通写和后面的volatile写重排序)
- 在每个volatile写操作的后面插入一个StoreLoad屏障(禁止前面的volatile写与后面可能的volatile读/写重排序)
- 在每个volatile读操作的后面插入一个LoadLoad屏障(禁止前面的volatile读和后面的所有普通读操作重排序)
- 在每个volatile读操作的后面插入一个LoadStore屏障(禁止前面的volatile读和后面的普通写操作重排序)
8、as-if-serial 和 happens-before 的区别
as-if-serial 保证单线程程序的执行结果不变
happens-before 保证正确同步的多线程程序的执行结果不变
四、其它知识点
1、FutureTask是什么
(1)FutureTask表示一个异步运算的任务
(2)FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。
(3)由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
2、CAS的原理呢?
CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性。
包含三个操作数:变量内存地址,V表示;旧的预期值,A表示;准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
3、CAS有什么缺点吗?
(1)ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A。
(2)循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
(3)只能保证一个共享变量的原子操作
4、简述阻塞队列
阻塞队列是生产者消费者的实现具体组件之一。当阻塞队列为空时,从队列中获取元素的操作将会被阻塞,当阻塞队列满了,往队列添加元素的操作将会被阻塞。
实现:
- ArrayBlockingQueue:底层是由数组组成的有界阻塞队列。
- LinkedBlockingQueue:底层是由链表组成的有界阻塞队列。
- PriorityBlockingQueue:阻塞优先队列。
- DelayQueue:创建元素时可以指定多久才能从队列中获取当前元素
- SynchronousQueue:不存储元素的阻塞队列,每一个存储必须等待一个取出操作
- LinkedTransferQueue:与LinkedBlockingQueue相比多一个transfer方法,即如果当前有消费者正等待接收元素,可以把生产者传入的元素立刻传输给消费者。
- LinkedBlockingDeque:双向阻塞队列。
5、聊聊你对Java并发包下Unsafe类的理解
对于 Java 语言,没有直接的指针组件,一般也不能使用偏移量对某块内存进行操作。这些操作相对来讲是安全(safe)的。
Java 有个类叫 Unsafe 类,这个类使 Java 拥有了像 C 语言的指针一样操作内存空间的能力,同时也带来了指针的问题。6、Java中的乐观锁
对于乐观锁,开发者认为数据发送时发生并发冲突的概率不大,所以读操作前不上锁。
到了写操作时才会进行判断,数据在此期间是否被其他线程修改。如果发生修改,那就返回写入失败;如果没有被修改,那就执行修改操作,返回修改成功。乐观锁一般都采用 Compare And Swap(CAS)算法进行实现: 该算法认为不同线程对变量的操作时产生竞争的情况比较少。 该算法的核心是对当前读取变量值 E 和内存中的变量旧值 V 进行比较。如果相等,就代表其他线程没有对该变量进行修改,就将变量值更新为新值 N;如果不等,就认为在读取值 E 到比较阶段,有其他线程对变量进行过修改,不进行任何操作。
7、ABA问题及解决方法简述
(1)问题
CAS 算法是基于值来做比较的,如果当前有两个线程,一个线程将变量值从 A 改为 B ,再由 B 改回为 A ,当前线程开始执行 CAS 算法时,就很容易认为值没有变化,误认为读取数据到执行 CAS 算法的期间,没有线程修改过数据。
(2)解决
JUC包提供了一个AtomicStampedReference
,即在原始的版本下加入版本号戳,解决 ABA 问题。
8、简述常见的Atomic类
在很多时候,我们需要的仅仅是一个简单的、高效的、线程安全的++或者—方案,使用synchronized关键字和lock固然可以实现,但代价比较大,此时用原子类更加方便。
(1)基本数据类型的原子类
- AtomicInteger 原子更新整形
- AtomicLong 原子更新长整型
- AtomicBoolean 原子更新布尔类型
(2)Atomic数组类型
- AtomicIntegerArray 原子更新整形数组里的元素
- AtomicLongArray 原子更新长整型数组里的元素
- AtomicReferenceArray 原子更新引用类型数组里的元素
(3)Atomic引用类型
- AtomicReference 原子更新引用类型
- AtomicMarkableReference 原子更新带有标记位的引用类型,可以绑定一个 boolean 标记
- AtomicStampedReference 原子更新带有版本号的引用类型
(4)FieldUpdater类型
- AtomicIntegerFieldUpdater 原子更新整形字段的更新器
- AtomicLongFieldUpdater 原子更新长整形字段的更新器
AtomicReferenceFieldUpdater 原子更新引用类型字段的更新器
9、简述CountDownLatch
countDownLatch的作用是使一个线程等待其他线程各自执行完毕后再执行。
通过一个计数器来实现的,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法,计数器的值就减1,当计数器的值为0时,表示所有线程都执行完毕,然后在等待的线程就可以恢复工作了。
只能一次性使用,不能重复使用。10、简述CyclicBarrier
功能和countDownLatch类似,通过一个计数器,使一个线程等待其他线程各自执行完毕后再执行。
可以重复使用(reset)。11、简述Semaphore
Semaphore即信号量。
Semaphore 的构造方法参数接收一个 int 值,设置一个计数器,表示可用的许可数量即最大并发数。
使用 acquire 方法获得一个许可证,计数器减一;
使用 release 方法归还许可,计数器加一;
如果此时计数器值为0,线程进入休眠。12、简述Exchanger
Exchanger类可用于两个线程之间交换信息。
将Exchanger对象理解为一个包含两个格子的容器,通过exchanger方法可以向两个格子中填充信息。
线程通过exchange方法交换数据,第一个线程执行 exchange 方法后会阻塞等待第二个线程执行该方法,当两个线程都到达同步点时这两个线程就可以交换数据,当两个格子中的均被填充时,该对象会自动将两个格子的信息交换,然后返回给线程,从而实现两个线程的信息交换。13、简述ConcurrentHashMap
(1)JDK1.7
采用锁分段技术。
首先将数据分成 Segment 数据段,然后给每一个数据段配一把锁,当一个线程占用锁访问其中一个段的数据时,其他段的数据也能被其他线程访问。
get方法除了读到空值不需要加锁。该方法先经过一次再散列,再用这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。
put方法须加锁,首先定位到 Segment,然后进行插入操作,第一步判断是否需要对 Segment 里的 HashEntry 数组进行扩容,第二步定位添加元素的位置,然后将其放入数组。
(2)JDK1.8改进取消分段锁机制,采用CAS算法进行值的设置,如果CAS失败再使用 synchronized 加锁添加元素
- 引入红黑树结构,当某个槽内的元素个数超过8且Node数组容量大于64时,链表转为红黑树
- 使用了更加优化的方式统计集合内的元素数量