文章目录
- 1.先说一下为什么要多线程?
- 2.进程?线程?进程和线程的通信方式?
- 3.进程和线程的区别?
- 4.创建线程的方式?
- 5.线程的几个状态以及状态之间转换的方法?
- 6.线程的几种状态,并指出在哪种状态下可以中断,中断原理?
- 7.sleep会触发哪个状态?什么操作会进入blocking? 什么操作进入waiting?
- 8.说一下 wait 和 sleep 的区别 ?(后面的await/signal与wait/notify类似)
- 9.Java 怎么去实现共享操作,多线程访问同一数据 ?
- 10.说一下 Future 和 FutureTask,以及他们之前的区别
- 11.java interrupt?
- 12.线程安全是什么?为什么会出现线程安全的问题?
- 13.Synchronized 及其实现原理?synchronize关键字底层原理是啥?
- 14.Synchronized修饰方法和代码块区别 ?
- 15.死锁?
- 16.锁用过哪些?synchronized和reentrantLock锁区别?sychronized 底层实现?ReentrantLock底层实现?
- 17.可不可以自己手动加锁,手动加锁你怎么实现,底层又是怎么实现的?
- 18.synchronize和Lock区别,synchronize使用场景,sleep方法和wait方法区别?
- 19.synchronized和lock都是用于什么场合?
- 20.ReentrantWriteReadLock的原理是什么?实现了什么样的锁?
- 21.synchronized可重入吗,怎么实现的 ?
- 22.synchronized怎么实现线程安全的?
- 23.volatile刨根问底 可见、有序性、禁止指令重排序原理、举一个你在使用过程中使用volatile 体现禁止重排序的例子?
- 24.synchronized刨根问底底层原理、1.7之后对重量级锁的优化、偏向锁、 轻量级锁、 锁升级过程?
- 25.除了synchronized还知道哪些锁?还有"ReentrantLock、还有CAS + volatile这种乐观锁机制、介绍一下?
- 26.AQS原理?
- 27.Synchronized 的锁优化,偏向锁 、轻量级锁、重量级锁 ?
- 28.说一下synchronized原理,使用方法。对象锁和类锁互斥吗?不互斥?
- 29.await和wait
- 30. JVM 中,对象在内存中分为哪三块区域
- 31.ReentrantLock加锁操作后,在catch中捕获的是什么异常,为什么会发生这用异常?
- 32.synchronized 的锁升级过程,还问了问锁消除以及锁膨胀,锁粗化,自旋优化,为什么要用锁?
- 33. synchronized 和 volatile ?
- 34. volatile为什么不保证原子性 ?
- 35.用volatile+synchronized写一个单例模式,用双重校验锁方法,说出两个if判断语句的作用 ?
- 36. 手写一个生产者消费者模式,用的ReentrantLock,为什么判断当前count是否满足生产或者消费时用while ?
- 37. i++是不是一个原子操作,说几种方法java如何保证对整型变量写操作线程安全的方法 ?
- 38.内存屏障原理(屏障前屏障后啥区别)
- 39. 什么是指令重排序?既然重排序有这么好处,为什么还要禁止指令重排序?重排序有什么后果?
- 40. ThreadLocal
- 41.介绍一下线程池,使用线程池的好处,参数有哪些?
- 42.线程池有哪几种,优劣是啥?
- 43.线程池的实现原理?
- 44.线程池的拒绝策略有哪些?
- 45.线程池execute 和 submit的区别 ?
- 46.说一下JUC包下的同步工具 ?
- 47.JUC 包中的原子类是哪4类 ?
- 48.AtomicInteger 类的原理
- 49. 单线程线程池的应用场景?
- 50.怎么实现多个计算线程全部到达之后再进行下一步的操作(我说了 CountDownLatch 和 join) ?
- 51.说说线程池的工作流程,4种拒绝策略,4种队列,其中一个线程挂掉了会怎么样?
- 52. 如果提交一个cpu密集型的任务怎么选取线程池 ?
1.先说一下为什么要多线程?
总的来说有3个原因:
(1)避免阻塞(异步调用):单个线程中的程序,是顺序执行的。如果前面的操作发生了阻塞,那么就会影响后面的操作。这时就可以采用多线程,我感觉就是异步调用。比如异步请求时,当前的请求并不会阻塞整个页面的正常操作。
(2)避免CPU空转:如果使用单线程响应HTTP请求,即处理完一条请求,再处理下一条请求,CPU会存在大量的空闲时间(不能处理新的请求),使用多线程的请求,可以避免CPU的空转。
(3)提升性能:在多核CPU的情况下,这是多线程我们最常说的优势了吧。因为多线程可以处理不同的任务,这样减少了按步操作的过程,这样就提高了性能。单核CPU处理多线程,存在上下文频繁切换,影响性能。
2.进程?线程?进程和线程的通信方式?
**进程:**通常一个程序运行就是一个进程。程序是由指令和数据组成,指令要运行,数据要读写,就必须将指令加载到cpu,数据加载到内存,指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的,当磁盘加载这个程序代码到内存,这时就开启了进程。一个程序通常可以运行多个实例进程。
**线程:**一个进程包含多个线程,一个线程就是一个指令流,将指令流中的一条条指令按照一定的顺序交给CPU执行,java中线程作为最小的调度单位,进程作为资源分配的最小单位,在Windows中进程是不活动的,作为线程的容器。
**通信方式:**进程基本上是相互独立的,而线程存在于进程内,是进程的一个子集;进程拥有共享的资源,如内存空间,使其内部的线程共享;
进程间的通信较为复杂:同一台计算机的进程通信为IPC,不同计算机之间的进程通信,需要通过网络,遵守共同的协议,如HTTP。
线程通信较为简单:(共享内存)因为进程内有共享的资源,多个线程可以访问同一个共享变量;(线程上下文切换)线程的切换比进程低,可以通过CPU的调度来进行切换通信。
3.进程和线程的区别?
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
4.创建线程的方式?
创建线程有三种方式:(1)继承Thread类创建线程:这种将创建线程和执行任务结合在一起,通过重写run方法(run方法就代表线程要完成的任务,因此run方法称为执行体),创建Thread子类的实例,即创建了线程对象。
(2)实现Runnable接口:在底层中Thread类是实现Runnable接口的,这种方式是把创建线程和执行任务分开,通过把Runnable对象实例作为创建线程对象的参数(Thread t = new Thread( runnable ); ).这种方式是创建Runnable对象的同时,定义一个内部类去重写Runnable接口的run方法,把对象作为参数传给Thread。
(3)FutureTask 配合Callable:FutureTask实现了RunnableFuture接口,RunnableFuture接口继承了Runnable接口,等于FutureTask间接实现了Runnable接口。将new Callable()类型作为参数传递给new FutureTask().
上述三种方法都是间接实现或继承Runnable接口,重写它的run方法,更推荐用第二种方式,方法2把创建线程和执行任务分开了,用Runnable更容易与线程池等高级API配合;Runnable让任务类脱离了Thread继承体系,更灵活。
5.线程的几个状态以及状态之间转换的方法?
(1)从操作系统层面,分为5种状态:初始状态、可运行状态、运行状态、阻塞状态、终止状态。初始状态就是创建线程对象后,当new线程对象过后,线程就进入了初始状态;可运行状态就是线程调用了start()方法后,线程启动就进入可运行状态;运行状态就是线程正在执行,CPU把时间片分给了该线程,当时间片用完,会进入可运行状态(上下文切换),或者执行完逻辑任务,就进入终止状态;阻塞状态就是同步线程加锁情况,或者调用sleep()、join()方法,线程进入阻塞状态等待其他线程唤醒,唤醒过后进入可运行状态等待分配资源,阻塞状态的线程不会获得cpu时间片。终止状态就是当线程的run方法完成时或者main方法结束时,就认为它终止了,线程一旦终止,就不能复生。
(2)从Java API层面来说,分为6种状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。
NEW就是初始状态,线程创建还未调用start()方法;
RUNNABLE把操作系统层面运行状态、阻塞状态(由于BIO的导致线程阻塞在java里不易区分,认为是可运行的)、可运行状态放在一起,当调用start()方法后,线程进入等待分配cpu时间片,变为就绪状态,当获得CPU时间片的使用权后,就由就绪状态变为运行状态;
BLOCKED是阻塞状态:没有获得锁时的状态,等待其他线程唤醒获取锁;不占用cpu时间片
WAITING:没有时效的等待状态,如调用 obj.wait() 方法、当前线程调用 t.join() 方法时、当前线程调用 LockSupport.park()都会进入WAITING状态 ,t 线程从 RUNNABLE --> WAITING ;不占用cpu时间片
TIMED_WAITING:有时效性的等待,如obj.wait(long n) 、Thread.sleep(long n)、t.join(long n) 、LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) ,RUNNABLE <–> TIMED_WAITING ;不占用cpu时间片
TERMINATED就是所有线程的任务都执行完毕
6.线程的几种状态,并指出在哪种状态下可以中断,中断原理?
线程只有在RUNNABLE、BLOCKED、WAITING、TIMED_WAITING这几个状态可以中断;有几个中断的方法:成员方法isInterrupted()判断是否被打断,不清除打断标记;静态方法Interrupted()判断是否被打断,清除打断标记;Interrupt()就是打断线程的方法:如果被打断线程正在 sleep,wait,join 会导致被打断的线程抛出 InterruptedException异常打断 ,并清除打断标记;如果打断的正在运行的线程,则会设置打断标记 ;park 的线程被打断,也会设置打断标记。
中断原理:中断机制就是设置一个boolean类型,当线程t1想中断线程t2,只需要在线程t1中将线程t2对象的中断标识置为true,然后线程2可以选择在合适的时候处理该中断请求,甚至可以不理会该请求,就像这个线程没有被中断一样。
7.sleep会触发哪个状态?什么操作会进入blocking? 什么操作进入waiting?
sleep方法会进入TIMED_WAITING状态;当线程没有获得锁时,等待其他线程唤醒获取锁,进入blocking;当调用了wait()、t.join()、LockSupport.park()都会进入WAITING状态
8.说一下 wait 和 sleep 的区别 ?(后面的await/signal与wait/notify类似)
(1)wait是Object的方法,需要在线程加锁(Synchronized)的情况下去使用,当调用该方法时,在加锁的线程就会进入waitSet区等待,然后释放锁给其他线程使用,无限制等待,直到notify唤醒或notifyAll唤醒,进入EntryList重新竞争锁;当有时效性等待时,可以等够n毫秒继续向下执行,或者被notify唤醒,没有等够n毫秒也继续向下执行。
(2)sleep是Thread的方法,sleep不需要强制和synchronized配合使用,与wait相反,它在睡眠的过程中不会释放对象锁,这与wait相反;
(3)当wait(long n)时效性时,它们的状态都是TIMED_WAITING。
9.Java 怎么去实现共享操作,多线程访问同一数据 ?
一般共享操作有两种方式:
(1)当多线程访问共享数据的代码相同时,可以使用同一个Runnable对象,将共享数据作为Runnable对象的参数传给创建的各个线程作为参数,就是new Thread(new Runnable(共享数据));
(2)当多线程访问共享数据时,它们的代码不相同,就使用多个Runnable对象:分为2种情况:
1.把共享数据放在另一个创建的对象里,然后把这个共享数据传给各个Runnable对象作为参数,将各个带有共享数据的Runnable对象也作为参数传给new Thread();
public static void main(String[] args) {
ShareData data1=new ShareData();
new Thread(new MyRunnable1(data1)).start();
new Thread(new MyRunnable2(data1)).start();
System.out.println("mian: "+data1.getCount());
}
2.将各个Runnable对象作为new Thread的参数,然后声明成匿名内部类,把共享数据data1声明成成员变量,通过内部类共享外部成员变量的特性来访问共享数据。
static ShareData1 data1=new ShareData1();
public static void main(String[] args) {
new Thread(new Runnable(){
@Override
public void run() {
data1.increment();
}
}).start();
new Thread(new Runnable(){
@Override
public void run() {
data1.decrement();
}
}).start();
10.说一下 Future 和 FutureTask,以及他们之前的区别
1.简介:
线程的创建方式中有两种,一种是实现Runnable接口,另一种是继承Thread,但是这两种方式都有个缺点,那就是在任务执行完成之后无法获取返回结果,于是就有了Callable接口,Future接口与FutureTask类的配和取得返回的结果.
先回顾一下java.lang.Runnable接口,就声明了run(),其返回值为void,当然就无法获取结果。
public interface Runnable {
public abstract void run();
}
Callable的接口定义如下:该接口声明了一个名称为call()的方法,同时这个方法可以有返回值V,也可以抛出异常
public interface Callable<V> {
V call() throws Exception;
}
线程池解决:
无论是Runnable接口的实现类还是Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行,ThreadPoolExecutor或ScheduledThreadPoolExecutor都实现了ExcutorService接口,而因此Callable需要和Executor框架中的ExcutorService结合使用,我们先看看ExecutorService提供的方法:
//ExcutorService接口的方法
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
第一个方法:submit提交一个实现Callable接口的任务,并且返回封装了异步计算结果的Future。
第二个方法:submit提交一个实现Runnable接口的任务,并且指定了在调用Future的get方法时返回的result对象。(不常用)
第三个方法:submit提交一个实现Runnable接口的任务,并且返回封装了异步计算结果的Future。
因此我们只要创建好我们的线程对象(实现Callable接口或者Runnable接口),然后通过上面3个方法提交给线程池去执行即可。
工厂类Executors解决:
还有点要注意的是,除了我们自己实现Callable对象外,我们还可以使用工厂类Executors来把一个Runnable对象包装成Callable对象。Executors工厂类提供的方法如下:
//工厂类Executors中的 方法
public static Callable<Object> callable(Runnable task)
public static <T> Callable<T> callable(Runnable task, T result)
2.Future接口:
Future接口是用来获取异步计算结果的,说白了就是对具体的Runnable或者Callable对象任务执行的结果进行获取(get()),取消(cancel()),判断是否完成等操作。我们看看Future接口的源码:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
方法解析:
V get()
:获取异步执行的结果,如果没有结果可用,此方法会阻塞直到异步计算完成。一般我们就是调用get方法返回线程池结果。
V get(Long timeout , TimeUnit unit)
:获取异步执行结果,带时延的结果,如果没有结果可用,此方法会阻塞,但是会有时间限制,如果阻塞时间超过设定的timeout时间,该方法将抛出异常。
boolean isDone()
:如果任务执行结束,无论是正常结束或是中途取消还是发生异常,都返回true。
boolean isCanceller()
:如果任务完成前被取消,则返回true。
boolean cancel(boolean mayInterruptRunning)
:如果任务还没开始,执行cancel(…)方法将返回false;如果任务已经启动,执行cancel(true)方法将以中断执行此任务线程的方式来试图停止任务,如果停止成功,返回true;当任务已经启动,执行cancel(false)方法将不会对正在执行的任务线程产生影响(让线程正常执行到完成),此时返回false;当任务已经完成,执行cancel(…)方法将返回false。mayInterruptRunning参数表示是否中断执行中的线程。
通过方法分析我们也知道实际上Future提供了3种功能:
(1)能够中断执行中的任务
(2)判断任务是否执行完成
(3)获取任务执行完成后额结果。
但是我们必须明白Future只是一个接口,我们无法直接创建对象,因此就需要其实现类FutureTask登场啦。
3.FutureTask类:
我们先来看看FutureTask的实现
public class FutureTask<V> implements RunnableFuture<V> {
FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:
public interface RunnableFuture<V> extends Runnable, Future<V> {
void run();
}
分析:FutureTask除了实现了Future接口外还实现了Runnable接口(即可以通过Runnable接口实现线程,也可以通过Future取得线程执行完后的结果),因此FutureTask也可以直接提交给Executor执行。
最后我们给出FutureTask的两种构造函数:
public FutureTask(Callable<V> callable) {
}
public FutureTask(Runnable runnable, V result) {
}
4.Callable/Future/FutureTask的使用(封装了异步获取结果的Future!!!)
通过上面的介绍,我们对Callable,Future,FutureTask都有了比较清晰的了解了,那么它们到底有什么用呢?我们前面说过通过这样的方式去创建线程的话,最大的好处就是能够返回结果,加入有这样的场景,我们现在需要计算一个数据,而这个数据的计算比较耗时,而我们后面的程序也要用到这个数据结果,那么这个时Callable岂不是最好的选择?我们可以开设一个线程去执行计算,而主线程继续做其他事,而后面需要使用到这个数据时,我们再使用Future获取不就可以了吗?下面我们就来编写一个这样的实例
4.1 使用Callable+Future获取执行结果
Callable实现类如下:
package com.zejian.Executor;
import java.util.concurrent.Callable;
/**
* @author zejian
* @time 2016年3月15日 下午2:02:42
* @decrition Callable接口实例
*/
public class CallableDemo implements Callable<Integer> {
private int sum;
@Override
public Integer call() throws Exception {
System.out.println("Callable子线程开始计算啦!");
Thread.sleep(2000);
for(int i=0 ;i<5000;i++){
sum=sum+i;
}
System.out.println("Callable子线程计算结束!");
return sum;
}
}
Callable执行测试类如下:
public class CallableTest {
public static void main(String[] args) {
//创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//创建Callable对象任务
CallableDemo calTask=new CallableDemo();
//提交任务并获取执行结果
Future<Integer> future =es.submit(calTask);
//关闭线程池
es.shutdown();
try {
Thread.sleep(2000);
System.out.println("主线程在执行其他任务");
if(future.get()!=null){
//输出获取到的结果
System.out.println("future.get()-->"+future.get());
}else{
//输出获取到的结果
System.out.println("future.get()未获取到结果");
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程在执行完成");
}
}
4.2 使用Callable+FutureTask获取执行结果
public class CallableTest {
public static void main(String[] args) {
// //创建线程池
// ExecutorService es = Executors.newSingleThreadExecutor();
// //创建Callable对象任务
// CallableDemo calTask=new CallableDemo();
// //提交任务并获取执行结果
// Future<Integer> future =es.submit(calTask);
// //关闭线程池
// es.shutdown();
//创建线程池
ExecutorService es = Executors.newSingleThreadExecutor();
//创建Callable对象任务
CallableDemo calTask=new CallableDemo();
//创建FutureTask
FutureTask<Integer> futureTask=new FutureTask<>(calTask);
//执行任务
es.submit(futureTask);
//关闭线程池
es.shutdown();
try {
Thread.sleep(2000);
System.out.println("主线程在执行其他任务");
if(futureTask.get()!=null){
//输出获取到的结果
System.out.println("futureTask.get()-->"+futureTask.get());
}else{
//输出获取到的结果
System.out.println("futureTask.get()未获取到结果");
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("主线程在执行完成");
}
}
总结一下Future 和 FutureTask两者的区别:
(1)当我们用第三种方式Callable分别配合Future 和 FutureTask创建线程时,需要这个接口和类。之前的继承Thread类和实现Runnable接口创建线程,在任务执行完后都是没有返回值的,但是Callable分别配合Future 和 FutureTask创建线程时,在任务执行完成后,会有返回值(对于线程计算大的数据,将其结果返回后以便于下一次使用该数据,不然下一次用到该数据还得重新计算一遍)。这是因为Future接口是用来获取异步计算结果的,用过get()方法去返回Callable实现类或者Runnable实现类的结果。
(2) FutureTask类是继承RunnableFuture类,而RunnableFuture类实现了Future和Runnable接口。这个创建线程可以使用Callable结合Executor框架中的ExcutorService结合使用,或者直接使用Callable结合Executors工厂类。比如用线程池ExcutorService中的方法<T> Future<T> submit(Callable<T> task);
把Callable实现类的结果提交给线程池去执行任务,任务结束后通过Future接口中的get()方法获取Callable结果的返回值。或者也可以将Callable实现类的结果传给FutureTask类,再将包装过Callable实现类的FutureTask类提交给线程池去执行任务,最后通过FutureTask实现类的get()方法(重写Future接口中的get()方法)获取返回值结果。
记住ExcutorService接口中的方法是需要其实现类ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。所以一般先创建线程池ExecutorService es = Executors.newSingleThreadExecutor();
11.java interrupt?
java中的中断操作主要是通过一个中断标示位,为true表示中断。通常一个线程main去打断另一个线程t,直接在main线程去调用t.interrupt()方法。当打断的线程正在sleep、wait、join,那么会导致被打断的线程抛出 InterruptedException异常,并清除打断标记true;当打断正在运行的线程,会设置标示位true;打断park的线程,也会设置标示位true。
我们可以通过isinterrupted方法去判断是否被打断,因为不会清除打断标记;也可以通过interrupted方法去判断是否被打断,直接清除标记,等于复位。t1线程去打断t2线程,t2线程收到了打断的标示位信号,通过线程去调用isinterrupted或静态方法interrupted来判断自身是否被中断,在合适的时间可以响应中断,也可以不理睬。如果线程已经处于终止状态,那么调用isinterrupted方法,标示位仍然是false.
12.线程安全是什么?为什么会出现线程安全的问题?
(1)当多线程去执行业务逻辑时,我们希望逻辑是正确的,是按照我们预期的结果出现的,这就是线程安全的。
(2)线程安全的问题:一个程序去运行多个线程本身并没有问题。问题出现在访问共享数据上:当多个线程对共享数据进行读操作时也没有问题;但是当多个线程对共享数据的读写指令发生交错时,就会出现线程安全问题。
13.Synchronized 及其实现原理?synchronize关键字底层原理是啥?
Synchronized 底层实现原理是属于JVM层面。
(1)当Synchronized加在同步代码块上时,执行原理是用两个指令monitorenter和monitorexit。monitorenter指令指向同步代码块开始的位置,而monitorexit指令指向同步代码块结束的位置。当执行monitorenter指令时,线程尝试去获取锁,也就是获取Synchronized锁对象关联的monitor(monitor对象是操作系统层面的,每次java对象加锁时就会自动关联monitor对象,这就是Synchronized获取锁的方式),当获取锁后,monitor对象中的Owner就会被置为该线程,锁计数器就会加1,当该线程已经拥有monitor,又重新进入monitor,每次获取锁都会计数加1。当执行monitorexit指令,锁计数器就会减一,直到锁计数器为0,该线程就不再拥有monitor对象,锁就释放了,然后去Entrylist队列去唤醒未获得锁的阻塞线程竞争。如果获取对象锁失败,就会进入阻塞队列等待下一次获取锁。
(2)当Synchronized加在方法上时,JVM可以通过ACC_SYNCHRONIZED标识符来辨别一个方法是否声明为同步方法,方法级别的同步是隐式的,ACC_SYNCHRONIZED会隐式的调用monitorenter和monitorexit两个指令,作为方法调用的一部分。同步方法的常量池中会有一个ACC_SYNCHRONIZED标志。当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁。在这期间,如果其他线程来请求执行方法,会因为无法获得监视器monitor锁而被阻断住。如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
二者实现区别:
synchronized同步代码块的时候通过加入字节码monitorenter和monitorexit指令来实现monitor的获取和释放,也就是需要JVM通过字节码显式的去获取和释放monitor实现同步,而synchronized同步方法的时候,只是通过ACC_SYNCHRONIZED隐式的调用monitorenter和monitorexit两个指令,是检查方法的ACC_SYNCHRONIZED标志是否被设置,如果设置了则线程需要先去获取monitor,执行完毕了线程再释放monitor,也就是不需要JVM去显式的实现。这两个同步方式实际都是通过获取monitor和释放monitor来实现同步的,而monitor的实现依赖于底层操作系统的mutex互斥原语,而操作系统实现线程之间的切换的时候需要从用户态转到内核态,这个转成过程开销比较大。
线程获取、释放monitor的过程如下:
线程尝试获取monitor的所有权,如果获取失败说明monitor被其他线程占用,则将线程加入到的同步队列中,等待其他线程释放monitor,当其他线程释放monitor后,有可能刚好有线程来获取monitor的所有权,那么系统会将monitor的所有权给这个线程,而不会去唤醒同步队列的第一个节点去获取,所以synchronized是非公平锁。如果线程获取monitor成功则进入到monitor中,并且将其进入数+1。
两个指令:monitorenter和monitorexit
monitorenter:
如果monitor的进入数为0,则线程进入到monitor,然后将进入数设置为1,该线程称为monitor的所有者。
如果是线程已经拥有此monitor(即monitor进入数不为0),然后该线程又重新进入monitor,则将monitor的进入数+1,这个即为锁的重入。
如果其他线程已经占用了monitor,则该线程进入到阻塞状态,知道monitor的进入数为0,该线程再去重新尝试获取monitor的所有权。
monitorexit:
执行该指令的线程必须是monitor的所有者,指令执行时,monitor进入数-1,如果-1后进入数为0,那么线程退出monitor,不再是这个monitor的所有者。这个时候其它阻塞的线程可以尝试获取monitor的所有权。
14.Synchronized修饰方法和代码块区别 ?
(1)修饰普通方法:锁就是对象实例,可以用this作为代码块锁。一个对象一把锁,多个对象多把锁。
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
(2)修饰静态方法:锁是类对象,不管有多少实例对象,类只有一个,所以类锁是惟一的。可以用.class作为代码块锁。
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {
}
}
}
(3)修饰代码块:锁就是Synchronized括号里面的对象,如果Synchronized()里没锁对象,则进入不了同步代码块。
同步:必须是同一对象锁才叫同步。如果A是类锁,B是对象锁,则不同步;两个线程:线程一取A对象锁,线程二取B对象锁,两个对象锁不是同一个,则不同步;
15.死锁?
死锁就是多个线程想要申请对方的资源,但是被阻塞住了,进入无限期的等待,不能终止。比如:t1线程拥有锁资源A,想要获取t2线程拥有的锁资源B,而t2线程拥有锁资源B,想要获取t1线程的锁资源A,两个线程都对各自拥有的资源加锁了,这样两个线程就会互相等待而不能终止。
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
解决方法:(1)可以将两个线程的加锁顺序一致,就是A线程:Synchronized(资源1)、Synchronized(资源2),B线程:Synchronized(资源1)、Synchronized(资源2)。当A线程首先获得锁后,B线程进入阻塞队列等待获取锁,所以A线程可以访问资源2,当A线程结束释放锁后;B线程获得锁,再去执行B线程的逻辑。
(2)锁可打断:还有一种就是可打断的锁:new ReentrantLock().lockInterruptibly(); 通过调用interrupt方法打断正在阻塞队列的线程(还没获得锁),阻塞中的线程就会直接结束,避免一直等待导致死锁发生。记住synchronized锁和Reentrant锁的new ReentrantLock().lock都不能被打断,只能等锁释放。
(3)或者锁超时:.tryLock(),超时等待**.tryLock(long n)**,都可以return结束锁线程。
16.锁用过哪些?synchronized和reentrantLock锁区别?sychronized 底层实现?ReentrantLock底层实现?
(1)synchronized和reentranLock锁、reentranLockreadwrite锁(读写锁),还有一种CAS+volatile无锁机制。
(2)synchronized和reentrantLock锁区别:
—synchronized是java关键字,是依赖于JVM层面实现的,而reentrantLock类是java API层面的。
—synchronized在1.6之前是重量级锁,1.6之后进行优化,引入偏向锁和轻量级锁,其性能和reentrantLock锁差不多。
—synchronized和reentrantLock锁都是一种阻塞式同步锁,当线程持有锁的使用权,其他线程是获取不到锁的。synchronized的使用比较方便简洁,并且由编译器去保证锁的加锁和释放,但是reentrantLock锁需要手动的去加锁和释放锁,通过lock()和unlock()方法配合try/finally语句块来完成。
—synchronized锁是不能够中断的,是非公平锁,但是reentrantLock锁是可以中断的,通过lock.lockInterruptibly() 方法去实现打断机制,它默认是非公平锁,但是可以通过new ReentrantLock(true)变成公平锁。两个都是可重入锁。
—synchronized锁的条件变量:当调用wait()方法时,线程会进入waitSet区域等待队列,并释放锁,只有一个waitSet区域,通过notify()或notifyAll()方法唤醒,否则一直等待,或者有时效性的notify()等待自行唤醒;ReentrantLock通过借助Condition 接口和newCondition()方法,可以创建多个Condition实例,当调用Condition实例的await()方法时,这样就有多个ConditionObject等待队列区域,可以放置不同的条件变量,这是与synchronized不同的,需要Condition实例的signal()或signalAll()方法去唤醒。
(3) ReentrantLock底层实现:
ReentrantLock类有3个内部类:Sync同步器类、NonfairSync非公平锁、fairSync公平锁,其中非公平锁和公平锁继承Sync,Sync继承AQS同步队列器类。这个AQS是所有JUC并发工具的核心类,里面有加锁状态state,volatile修饰(0是未加锁,1加锁,n可重入),ExclusiveOwnerThread表示加锁线程,还有一些基本方法,基本上所有并发工具类底层都是基于AQS实现的。**非公平加锁流程:**ReentrantLock类默认是非公平锁的机制,当t1线程加锁时,通过lock方法加锁,第一次CAS操作加锁成功(state=0会更新为1),第二次线程t2通过一次CAS操作加锁(state已经为1)会失败,当加锁失败又会调用accure方法,然后去调用tryaccure方法通过CAS再尝试加锁,只有这次又失败了,会调用addWaiter方法把获取锁失败的线程加入AQS同步双向队列,这时会再次调用accureQueen方法将阻塞队列中的线程再次尝试加锁,这里会使用for()死循环,将队列的老二线程尝试加锁,当加锁失败后,会调用shouldParkAfterFailedAcquire方法判断该线程是否挂起阻塞,当该线程的前驱结点的waitStatus=-1(这个-1用来唤醒后继节点的线程),就说明可以阻塞休眠了,这时调用parkAndCheckInterrupt方法将该线程阻塞。这里会经历2次for循环挂起该线程。如果park被唤醒或打断,就会再次for循环尝试获取锁,如果失败会再次阻塞,成功则加锁成功退出循环。公平锁加锁与非公平的主要区别就是在tryaccure方法中,公平锁需要判断AQS队列是否有前驱结点,即队列中有线程存在,如果存在就不会加锁,而是进入AQS同步队列队尾排队竞争。但是非公平锁不会检查AQS队列是否有前驱结点,当一个线程刚好释放锁时,正好一个线程来了,那么它就会竞争加上锁,而老二线程被唤醒后发现没有获得锁资源又重新挂起(阻塞)。非公平和公平释放锁原理一样,都是通过unlock方法调用release,再调用tryRelease,然后调用unparkSuccessor()唤醒后继节点线程。这个释放锁成功的过程中会将加锁状态state置为0,ExclusiveOwnerThread锁持有者置为null。
17.可不可以自己手动加锁,手动加锁你怎么实现,底层又是怎么实现的?
应该是ReentrantLock
18.synchronize和Lock区别,synchronize使用场景,sleep方法和wait方法区别?
这题问的是16题
19.synchronized和lock都是用于什么场合?
这题应该就是简述区别。
20.ReentrantWriteReadLock的原理是什么?实现了什么样的锁?
Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,读-写和写-写互斥,提高性能。
整个加锁底层原理就是刚开始t1写锁线程加写锁时,因为state为0,加写锁成功,写锁状态占了 state 的低 16 位 。当加的第二个线程是t2读锁时,肯定会失败,因为读写互斥,原因是进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程,如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败 。此时进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点到AQS同步队列,不同之处在于节点被设置为Node.SHARED 共享模式(读锁共享)而非 Node.EXCLUSIVE 模式(写锁独占模式),注意此时 t2 仍处于活跃状态 。t2读线程会看自己是否是老二线程,如果是,会在for循环里尝试加读锁,当加读锁失败,会进入shouldParkAfterFailedAcquire方法判断是否被挂起,里面会进行从尾向头找到前驱结点的waitStatus=0,再由CAS更新为-1,第二次for循环,如果加读锁还是失败,最后进入parkAndCheckInterrupt() 处 park 。(这个过程和 ReentrantLock加锁一样)。如果此时又有t3加读锁,t4加写锁,均会进入AQS队列。
释放锁原理:这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功 ,接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行 ,这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一 ,这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点 ,事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared(加读锁的线程),如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行 ,这回再来一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一 ,这时 t3 已经恢复运行,接下来 t3 调用setHeadAndPropagate(node, 1),它原本所在节点被置为头节点 ,下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点 。
接下来继续释放t2,t3读锁线程:t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零 ,t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即 t4写锁线程,之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束 。
21.synchronized可重入吗,怎么实现的 ?
synchronized可重入。此时应该是偏向锁的状态。一个线程对同一个对象反复加锁的状态。在执行同步代码块时,会在栈帧里创建锁记录,第一次使用CAS操作时将线程ID存储到栈帧锁记录里和Mark Word对象头里。当再次访问锁对象时,不需要再次使用CAS操作,只要检查Mark Word里是否有线程ID,如果有这个线程ID,说明该线程已经获得了锁,此时在栈帧里再次创建一个锁状态为null的锁记录,可重入计数加1。如果没有这个线程ID,就会再次检查Mark Word里偏向锁的状态标识是否是1,如果没有设置,则会CAS重新竞争锁;如果已经设置为1,则将Mark Word对象头里的偏向锁重新指向当前线程。退出同步代码块方法时,锁记录里的值为null,可重入计数减1,直到为0,锁释放。
22.synchronized怎么实现线程安全的?
synchronized俗称对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
synchronized实际上是用对象锁保证了临界区代码块的原子性,临界区内的代码是不可再分割的,不会被线程切换所打断
虽然java中互斥和同步都可以采用synchronized关键字来完成,但是他们还是有区别的:互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码;同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
23.volatile刨根问底 可见、有序性、禁止指令重排序原理、举一个你在使用过程中使用volatile 体现禁止重排序的例子?
**1.缓存一致性原则:**java代码会被编译成.class字节码文件,在JVM中,当运行时,会把这些字节码文件转化成具体的CPU执行指令,CPU去加载这些指令逐条执行。但是CPU的执行速度与计算机主存之间的速度相差很大,当读写操作时,CPU执行很快,如果每次都和计算机主存打交道太耗费时间了,所以在主存和CPU处理器之间加上一层或多层的高速缓存,也就是之后的工作内存,主存会将运算数据同步到高速缓存中。这样CPU处理器就无需等待主存的数据了,直接从缓存中拿数据,当运行结束后,再把最新的数据从缓存中同步到主存中。
缓存一致性问题:但是加入高速缓存后,如果存在多核CPU处理器同时对主存的共享数据进行操作,那么每一个处理器都有自己的高速缓存区,刚开始里面是主存中同步的数据,当CPU指令执行结束后,将缓存中的最新数据同步到主存中,到底以哪一个高速缓存为主?所以需要各个高速缓存遵守缓存一致性协议(MESI协议),它确保每一个缓存中使用的共享变量的副本是一致的。
2.java内存模型(JMM):JMM规定java中所有的共享变量(实例变量和类变量)都存储于主存中,不包含局部变量,因为局部变量是线程私有的,不存在竞争问题。每一个线程还存在自己的工作内存(相当于高速缓存),线程对变量的读写操作都必须在工作内存中完成,而不能直接读写主存中的共享变量,同时不同线程之间也不能访问对方工作内存的变量,线程间变量的值的传递需要通过主内存中转完成(线程通信:共享变量)。
java内存模型图
java并发编程的3个特性:可见性、有序性、原子性
3.可见性
因为各个线程共享一块主内存区域,为了让各个线程之间能对变量操作可见,需要可见性。可见性可以通过加锁或者Volatile关键字实现。
加锁可以解决可见性是因为某一个线程进入synchronized同步代码块前后,线程会获得锁,清空工作内存,从主内存拷贝共享变量最新的值到工作内存成为副本。执行代码,将修改后的副本的值刷新回主内存中,线程释放锁。而获取不到锁的线程会阻塞等待,所以变量的值肯定一直都是最新的。
3.1 Volatile关键字可见性原理(两个角度)
(1)lock前缀指令角度:当加入volatile关键字后的变量,在生成代码发现,会比不加volatile关键字修饰的变量多出一行lock前缀指令,这个指令实际上相当于一个内存屏障。
缓存行(cache line):CPU高速缓存中可以分配的最小存储单位。处理器填写缓存行时会加载整个缓存行。
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障。
lock指令在多核处理器下会引发下面的事件:
将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。(下面讲解)
**不加volatile的普通变量:**为了提高处理速度,处理器一般不直接和内存通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完成后并不知道处理器何时将缓存数据写回到内存。
但如果对**加了volatile修饰的变量:**进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量在缓存行的数据写回到主存。这时只是写回到主存,但其他cpu处理器的缓存行中的数据还是旧的(通知其他CPU将该变量的缓存行置为无效),要使其他处理器缓存行的数据也是新写回到主存的数据,就需要实现缓存一致性协议。
至于是怎么发现数据是否失效呢?(嗅探)
当写数据的CPU将缓存行的数据同步到主内存后,其他的cpu处理器就会通过嗅探在总线上传播的数据来检查自己缓存的值是否过期了,当其他CPU发现自己缓存行的数据对应的内存地址已经被修改,就会将当前cpu处理器的缓存行置为无效状态,当cpu处理器对这个数据进行修改操作时,就会重新从主存中把数据读到缓存中。
总线风暴:由于Volatile的MESI缓存一致性协议,需要不断的从主存中嗅探和CAS不断循环,无效交互会导致总线带宽达到峰值。所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。
(2)内存屏障角度:
读操作在读指令之前插入读屏障,重新从主存加载最新值进来,让工作内存中的数据无效,强制从主存中加载数据到工作内存(读屏障保证在该屏障之后,对共享变量的读取,加载的是主存中的最新数据)。
写操作在写指令之后插入写屏障,能让写入工作内存中的最新数据更新写入到主内存,让其他线程可见。(写屏障保证在该屏障之前的,对共享变量的改动,都同步到主存当中,其他线程就可以读到最新的结果了)。
如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个变量可见。
4.有序性
4.1指令重排序
重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。(好处)
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
4.2重排序的类型有哪些呢?源码到最终执行会经过哪些重排序呢?
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2)指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。这些重排序可能会导致多线程程序出现内存可见性问题。不管怎么重排序,单线程下的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
4.3有序性原理:那Volatile是怎么保证不会被执行重排序的呢?
内存屏障角度:volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。这里与可见性说的不太一样?其实都是一样的内存屏障,可见性的写屏障指的是有序性重排序中的StoreLoad屏障,而可见性的读屏障也是重排序中的StoreLoad屏障,只不过图上没标出。
这里见38.
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
写
读
5.禁止重排序的例子
单线程下需要保证执行结果不能改变,在多线程下,当指令重排序时,可能会影响正确结果。比如int a=2,b=a;如果重排序,调换顺序,那么结果就会变。
24.synchronized刨根问底底层原理、1.7之后对重量级锁的优化、偏向锁、 轻量级锁、 锁升级过程?
synchronized锁优化问题:JavaSE6之前,synchronized被称为重量级锁,它的线程获取锁就是java对象关联一个Monitor对象,这种状态下,线程获得Monitor锁后,就是获得了该锁的持有权,其他线程再想获得moitor锁就会被阻塞到Entrylist队列中等待(未获锁)。在javaSE6中引入了偏向锁、轻量级锁,这时锁的优先级状态:无锁–>偏向锁–>轻量级锁–>重量级锁。锁只能升级不能降级。
**偏向锁(101):**当一个程序里不存在多线程竞争,有同一个线程对同一对象反复加锁时,这是我们是默认开启偏向锁的,但是一般开启会延迟,可以设置JVM参数取消延迟。偏向锁的机制就是在线程访问同步代码方法时,第一次使用CAS操作将栈帧锁记录里的线程ID存储到对象头的Mark Word里,以后该线程再次访问同一把锁对象时,不需要CAS操作(6之前每次可重入,都需要进行CAS操作,效率低,现在只需要一次了),只需要查看Mark Word里是否还有这个线程ID,如果有这个线程ID,就表示已获得了偏向锁。如果没有这个线程ID,就会再次检查Mark Word的偏向锁标识是否是1(1 01),如果没有设置的话,就用CAS重新竞锁;如果设置的话,就尝试用CAS操作将Mark Word的对象头的偏向锁重新指向当前线程。当偏向锁出现竞争时就会释放锁。
**轻量级锁(00):**当多个线程对对象加锁时,但是他们的加锁时间是错开的,也就是没有竞争,一般使用的是轻量级锁。轻量级锁的机制就是在栈帧里创建一个锁记录,当线程访问同步代码块时,每次使用CAS操作将锁记录的轻量级锁状态(00)替换到对象头的Mark Word的状态里。如果cas替换成功,说明当前线程获得轻量级锁,Mark Word对象头锁状态由01–>00。如果cas操作失败,有2种情况:一种是自己执行了 synchronized 锁重入 ,此时再创建一个锁状态为null的锁记录,锁计数加1(重入只需一次CAS,所以失败);另一种是有其他线程正在竞争锁,并且其他线程已经持有了该轻量级锁,此时进入锁膨胀过程。解锁时:如果退出synchronized 代码块,此时锁记录有null值,说明有锁重入,更新锁记录,重入计数减1;当退出synchronized 代码块,锁记录没有null值,就用CAS操作mark word的锁状态为01,成功就解锁成功,失败说明当前锁存在竞争则进入锁膨胀重量级解锁流程。
**重量级锁(10):**在通过锁膨胀进入重量级锁,此时需要通过关联的monitor对象找到monitor地址,如果当前线程尝试获得moitor锁成功,此时的moitor里的唯一所有者Owner就会置为当前获得锁的线程,其他未获得锁的线程进入Entrylist队列进入阻塞状态,如果有其他已经获得了锁,但是条件没有满足又释放了锁的线程就会进入waitSet条件区域等待notify(或超时)唤醒。当下一次锁释放时,owner就会为空,锁计数器也为0,其他线程就开始竞争锁。
25.除了synchronized还知道哪些锁?还有"ReentrantLock、还有CAS + volatile这种乐观锁机制、介绍一下?
CAS + volatile这种乐观锁机制:
1.CAS:CAS能够保证线程的原子性。它是一种乐观锁的实现方式,只是一种思想,它是很多JUC并发工具类的核心。比较并交换。
2.CAS是如何实现线程安全的?
线程在读取数据时不进行加锁,在准备写回数据时,先去查询原值,操作的时候比较原值是否被其他线程所修改,若未被修改,直接更新到替换值,若已经被修改,则更新失败,再次CAS操作(前提是在循环中)。这就是种乐观思想,在提交数据时才会进行检测,一般假设数据不会被其他线程修改。
3.CAS操作存在的问题?
- ABA问题
ABA就是某个线程前后读取的数据一致,但是其实数据在中途已经被修改了。比如线程1去读取了数据A;线程2通过CAS操作发现数据A与原值一致,把数据A修改成数据B;线程3去读取数据B,通过CAS操作发现原值与最新的值一致,把数据B修改为A;线程1通过CAS操作,发现数据A没有变,就会更新成自己想要的值。
虽然不会影响线程1的CAS操作,但是值已经被修改了。
解决方法:通过添加版本号或者时间戳。
比如通过原子引用AtomicStampedReference对象。这个是在JDK 1.5之后提供的一个原子类来解决ABA问题。通过创建该对象并调用getReference()方法来获取原值prev,再调用getStamp()方法获取当前版本号,在CAS操作时就会比较原值和版本号是否与最新值一致,如果一致,则更新成功,原值被更新和版本号加1。每次变量更新的时候版本号都加1,即使最后的值可能相等,但是版本号不相等,CAS操作也会失败(因为比较的变量是两个变量都要与原值一致),因为版本号是递增,不会减少,那就可以知道数据被修改多少次了。
也可以加时间戳,因为时间也是递增的,不会减少。
14:04:28.851 c.TestCAS1 [main] - main start...
14:04:28.856 c.TestCAS1 [main] - 版本 0
14:04:28.977 c.TestCAS1 [t1] - change A->B true
14:04:28.977 c.TestCAS1 [t1] - 更新版本为 1
14:04:29.482 c.TestCAS1 [t2] - change B->A true
14:04:29.483 c.TestCAS1 [t2] - 更新版本为 2
14:04:30.477 c.TestCAS1 [main] - change A->C false
- 循环时间长开销大的问题:如果CAS操作在一个循环里,那么CAS操作失败,它就会一直自旋操作,直至CAS操作更新成功,就相当于死循环,CPU压力会很大,可能会造成栈或堆内存溢出。
- 只能保证一个共享变量的原子操作:在JDK 1.5之前,CAS操作只是针对单个共享变量,这时可以保证原子操作,但是多个共享变量就不能保证了。JDK 5之后,通过引入JUC并发包的原子类来保证多个变量的原子性,就可以把多个共享变量放入CAS中操作。比如AtomicReference原子引用,拿AtomicInteger举例
while(!ref.compareAndSwapInt(var1,var2,var5,var5+var4));
CAS + volatile:这是一种无锁机制,是基于乐观锁的思想进行加锁。CAS可以保证共享变量的原子性,但是可见性和有序性只能靠volatile来保证,volatile是不能解决指令交错问题,即原子性问题,所以配合CAS。
**无锁情况下,即使CAS操作失败,线程还是在高速运行(**while循环),没有停歇,而synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞队列。结合CAS+volatile可以实现无锁并发,适用于线程少,多核CPU的场景。这种机制基于乐观的思想,不怕其他线程去修改共享变量的值,就算修改了也没事,直接CAS自旋操作即可,总会更新成功,最多耗费CPU。
26.AQS原理?
AQS是一个队列式同步器,它是JUC并发包工具类的核心,比如ReentrantLock
、ReentrantReadWriteLock
、CountDownLatch
、Semaphore
等都是基于AQS
来实现,AQS是这些类的内部类,它们类里面都有内部类sync
同步器类,这个类继承AQS,这样所有的JUC都以AQS来实现。
AQS中有volatile int state
(代表共享资源,加锁数,0没加锁,1加锁,n重加锁)和一个FIFO
线程等待队列(多线程争用资源被阻塞时会进入此队列),还有。关键变量ExclusiveOwnerThread
,用来记录当前加锁的是哪个线程,初始化状态下,这个变量是null
AQS内部有很多方法,加锁方法、阻塞方法、释放锁方法等等,原理参考ReentrantLock
锁。
27.Synchronized 的锁优化,偏向锁 、轻量级锁、重量级锁 ?
同24题
28.说一下synchronized原理,使用方法。对象锁和类锁互斥吗?不互斥?
synchronized原理:synchronized的底层原理属于JVM层面。
**synchronized加在同步代码块上时:**会用到字节码指令:monitorenter和monitorexit。它们都是monitor对象的指令。当一个对象加了synchronized重量级锁后,java对象的markWord对象头就会关联操作系统层面的monitor对象(这就是为什么每次线程尝试去获取锁后就会关联一个monitor对象,这就是synchronized加锁的方式)。此时monitor的所有者Owner就会被置为该加锁线程,Owner是唯一的,所以每次只能保证一个线程获得锁,其他线程进入阻塞队列等待(这是互斥)。从JVM指令层面,当执行monitorenter指令时,线程尝试去获取monitor锁,当线程成为Owner时,锁计数器就加1,表示获得了monitor的持有权(当该线程拥有monitor锁后又再次获得monitor锁,计数器还是加1),当执行monitorexit指令后,线程就会释放锁,Owner就会置为null,锁计数器变为0,不再拥有monitor的持有权,然后唤醒其他线程来竞争锁。
**synchronized加在方法上:**JVM通过加一个标识符ACC_SYNCHRONIZED来辨别该方法是否是同步方法,如果该同步方法设置了ACC_SYNCHRONIZED,则去调用该同步方法前,需要获取monitor锁,此时owner被置为该线程,当方法执行结束就会释放锁,将monitor持有权交由其他线程竞争。该方法执行时,无论是正常return结束还是异常结束,都会释放monitor锁,当方法出现异常,方法执行体内部没有解决,向外抛出异常前就会释放monitor锁。
对象锁和类锁不互斥:对象锁的是实例对象,而类锁锁的是整个类对象,它们之间加锁是不互斥的,因为不是同一个锁对象。
29.await和wait
(1)await是基于ReentrantLock锁,它和wait类似,wait是基于synchronized锁。它们都需要在加锁的情况下才能使用其方法。
(2)await:当调用await()方法后,线程就会进入conditionObject 等待 (这里的等待区域比waitSet区域多,因为ReentrantLock 支持多间休息室 ,即支持多个条件变量在不同conditionObject区域,而synchronized却将多个条件变量都放在一个waitSet区域),之后释放锁给其他线程使用;
当调用signal() /signall()去唤醒conditionObject区域线程(或打断、超时),会进入阻塞队列去重新竞争锁(与wait一样)
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
waitCigaretteQueue.await()
30. JVM 中,对象在内存中分为哪三块区域
(1)对象头:
- Mark Word(标记字段):默认存储对象的HashCode,分代年龄和锁标志位信息。它会根据对象的状态复用自己的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
- Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
(2)实例数据:
- 这部分主要是存放类的数据信息,父类的信息
(3)对其填充:
-
由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了字节对齐。
Tip:不知道大家有没有被问过一个空对象占多少个字节?就是8个字节,是因为对齐填充的关系哈,不到8个字节对其填充会帮我们自动补齐。
31.ReentrantLock加锁操作后,在catch中捕获的是什么异常,为什么会发生这用异常?
捕获打断异常,当我们去执行打断t1.interrupt();
,它就会捕获到异常,这类异常是锁中断原理,进行调用 lockInterruptibly()方法进行加锁。其他lock()方法加锁不可中断。
try {
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断");
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
32.synchronized 的锁升级过程,还问了问锁消除以及锁膨胀,锁粗化,自旋优化,为什么要用锁?
升级看前面
1.锁消除
锁消除是发生在编译器级别的一种锁优化方式。有时候我们写的代码完全不需要加锁,却执行了加锁操作。
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 比如线程的局部变量并没有逃离作用范围,所以不需要加锁,没有线程安全问题,加锁就是浪费资源。
2.锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
-
这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值(00)恢复给对象头(01),解锁失败,因为此时已经是重量级锁关联的monitor地址了,属于操作系统层面,没有java层面的Mark Word。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程
3.锁粗化
这是一种锁优化技术,为了保证多线程之间的有效并发,会使每个线程持有锁的时间尽可能短。如果一个线程不断的对某一对象重复加锁,那么会消耗系统的资源,也不利于其他线程获取锁,就没有有效并发了。所以我们尽可能将多个锁重入的请求合并成一个请求,把这些锁重入内部的业务逻辑都合并到一个锁去执行。这样可以降低短时间内大量的锁请求、同步、释放带来的性能损耗。
public void doSomethingMethod(){
synchronized(lock){
//do some thing
}
//这是还有一些代码,做其它不需要同步的工作,但能很快执行完毕
synchronized(lock){
//do other thing
}
}
修改:
上面的代码是有两块需要同步操作的,但在这两块需要同步操作的代码之间,需要做一些其它的工作,而这些工作只会花费很少的时间,那么我们就可以把这些工作代码放入锁内,将两个同步代码块合并成一个,以降低多次锁请求、同步、释放带来的系统性能消耗,合并后
//注意:这样做是有前提的,就是中间不需要同步的代码能够很快速地完成,如果不需要同步的代码需要花很长时间,就会导致同步块的执行需要花费很长的时间,这样做也就不合理了。
public void doSomethingMethod(){
//进行锁粗化:整合成一次锁请求、同步、释放
synchronized(lock){
//do some thing
//做其它不需要同步但能很快执行完的工作
//do other thing
}
}
还有一种是将for循环里的锁放到外面。
4 自旋优化(自旋锁)
重量级锁竞争的时候,还可以使用自旋来优化。就是在**持有锁的线程还未释放完锁的过程中,竞争锁的线程会自旋一段时间,就是不会立即进入阻塞队列,**因为线程频繁的挂起和恢复会影响性能。如果竞争锁的线程自旋成功(即原持有锁的线程释放了锁),这时竞争锁的线程就可以避免被挂起进入阻塞队列,而获得了锁。
注意:自旋的时间是有限的或者说自旋次数是有限的,如果达到自旋限制还未自旋成功获得锁,那么线程就需要进入阻塞队列等待。
**自适应自旋锁:**自适应自旋锁的自适应反映在自旋的时间不在固定了。在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能 。
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- Java 7 之后不能控制是否开启自旋功能
5.加锁的原因
为了保证线程安全性。使得持有锁的线程能够安全的在临界区内执行代码。保证了原子性。同时还有有序性、可见性。
33. synchronized 和 volatile ?
- volatile只能修饰共享变量(实例变量和类变量),而synchronized可以修饰方法、代码块。
- volatile只能保证可见性、有序性,但是不保证原子性(多线程读没什么影响,但是写操作不保证线程安全);而synchronized都可以保证,是一种互斥机制。
- 多线程访问volatile修饰的变量不会发生阻塞,而synchronized锁机制会发生阻塞
- volatile 是一种无锁机制,通常配合CAS操作实现锁机制原理,CAS+volatile相当于synchronized锁。
34. volatile为什么不保证原子性 ?
java中只对基本类型变量的赋值和读取是原子操作,如i=1的赋值操作,但是像j=i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再把i的值赋值给j,两个原子操作加起来就不是原子操作了。
虽然volatile修饰的变量,可以保证每次读取这个变量都是主存中同步过来的最新的值,但是一旦对该变量进行自增这样的非原子操作,就不会保证该变量的原子性了。
比如当i=5的时候A,B线程都同时读取到了i的值,然后A线程进行了i+1操作,此时i的值还没有变化,还没有将缓存行的数据同步到主存中,然后还没有通知B线程把缓存行数据置为无效,B线程就已经对i+1,此时A,B线程都读取的值为6,并没有按预期结果两次i+1应该为7,所以比预期结果少了1,没有保证原子性。
35.用volatile+synchronized写一个单例模式,用双重校验锁方法,说出两个if判断语句的作用 ?
1.以单线程synchronized未加volatile为例
在单线程下,不加volatile修饰变量没什么关系,因为没有线程安全问题。
- 懒惰实例化
- 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
- 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
public final class Singleton {
//1.构造方法私有化
private Singleton() { }
//2.提供私有的静态的该类型变量
private static Singleton INSTANCE = null;
//3.对外提供一个公有的静态该类型方法
public static Singleton getInstance() {
if(INSTANCE == null) { // t2
// 首次访问会同步,而之后的使用没有 synchronized,因为下次判断INSTANCE!=null,就不会进入if语句里竞争锁了
synchronized(Singleton.class) {
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;//返回对象
}
}
2.多线程volatile+synchronized(双重校验锁)
上述的未加volatile在多线程下会出现问题:比如t1线程先获得锁,然后创建实例对象,还未赋值给INSTANCE引用,这时t2线程还是会判断INSTANCE==null,进入加锁代码块,并执行创建实例对象,在创建对象时,t1线程赋值完毕,这就是指令重排序造成了安全问题。
public final class Singleton {
private Singleton() { }
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) { // t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) { // t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
36. 手写一个生产者消费者模式,用的ReentrantLock,为什么判断当前count是否满足生产或者消费时用while ?
package com.aop8.proAndcum;
import java.util.Random;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyContainer2<T> {
//Vector作为任务队列,存放任务的
private final Vector<T> countNum = new Vector<>();
//任务队列的容量
private final int MAX = 10;
//定义Lock锁
private Lock lock = new ReentrantLock();
//定义条件变量
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
// 生产者
public void put(T t) {
try {
lock.lock();
//判断此时任务队列是否满
while (countNum.size() == MAX) {
System.err.println("产品已满");
//如果队列任务满了,直接去ConditionObject区域等待唤醒
producer.await();
}
System.out.println("产品 add");
//如果上面while不满足,就继续添加任务
countNum.add(t);
consumer.signalAll(); // 通知消费者线程进行消费
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
// 消费者
public T get() {
T t = null;
try {
lock.lock();
//判断队列是否为空
while (countNum.size() == 0) {
System.err.println("缺货");
//队列为空,直接去等待队列被唤醒
consumer.await();
}
//消费者开始消费
t = countNum.remove(0);
producer.signalAll(); // 通知生产者进行生产
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
MyContainer2<String> myContainer2 = new MyContainer2<String>();
// 启消费者线程
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 2; j++) {
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者--"+myContainer2.get());
}
}, "consumer_" + i).start();
}
// 启动生产者线程
for (int i = 0; i < 2; i++) {
new Thread(() -> {
for (int j = 0; j < 10; j++) {
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}
myContainer2.put(Thread.currentThread().getName() + "" + j);
}
}, "产品-" + i).start();
}
}
}
//-----------------------------使用阻塞队列来实现---------------------------------------
//生产者
import java.util.concurrent.BlockingQueue;
public class Producer implements Runnable{
private final BlockingQueue sharedQueue;
public Producer(BlockingQueue sharedQueue){
this.sharedQueue = sharedQueue;
}
public void run(){
for(int i=0;i<100;i++){
try{
System.out.println("Producer:"+i);
sharedQueue.put(i);
}catch(InterruptedException ex){
System.out.println("exception:"+ex);
}
}
}
}
//消费者
import java.util.concurrent.BlockingQueue;
public class Consumer_self implements Runnable {
private final BlockingQueue shareQueue;
public Consumer_self(BlockingQueue shareQueue){
this.shareQueue=shareQueue;
}
@Override
public void run() {
while(true){
try{
System.out.println("comsumer:"+shareQueue.take());
}catch (Exception e){
System.out.println(e.getMessage());
}
}
}
}
//测试main
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class ProducerConsumerPattern {
public static void main(String[] args) {
//阻塞队列
BlockingQueue sharedQueue = new LinkedBlockingDeque();
//创建生产者和消费者,共享队列
Thread prodThread = new Thread(new Producer(sharedQueue));
Thread consThread = new Thread(new Consumer(sharedQueue));
//开启生产者和消费者进程
prodThread.start();
consThread.start();
}
}
37. i++是不是一个原子操作,说几种方法java如何保证对整型变量写操作线程安全的方法 ?
i++不是一个原子操作,它是多次原子操作结合在一起,就不是原子操作了。i先赋值一次原子操作,再自增又一次原子操作,两次原子操作结合在一起就不是原子操作了。通过synchronized对数据加锁,通过原子类AtomicInteger,通过CAS+volatile。
38.内存屏障原理(屏障前屏障后啥区别)
1.volatile写-读的内存语义
**volatile写的内存语义:**当对volatile变量进行写操作(赋值修改)时,JMM会把工作内存中的最新变量值同步到主内存中。此时工作内存的变量值和主内存的共享变量值一致。
**volatile读的内存语义:**当对volatile变量进行读操作时(读取变量),JMM会把该线程对应的工作内存中的缓存行数据置为无效,重新从主存中读取最新的共享变量值。
2.内存屏障原理
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。内存屏障插入准则:
1)volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障。
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
2)volatile写插入内存屏障后生成的指令序列示意图:
StoreStore屏障可以保证在volatile写之前,其前面的所有普通写操作(共享变量副本)已经对任意处理器(其他线程)可见了。这是因为StoreStore屏障将保障上面所有的普通写在volatile写之前刷新到主内存。
volatile写后面的StoreLoad屏障。此屏障的作用除了将volatile写变量让其他线程可见外,还可以避免volatile写与后面可能有的volatile读/写操作重排序。(这里就是之前说的可见性中在volatile写后面加StoreLoad屏障,在volatile读前面加StoreLoad屏障)
3)volatile读插入内存屏障后生成的指令序列示意图
LoadLoad屏障用来禁止处理器把上面的volatile读与下面的普通读重排序。LoadStore屏障用来禁止处理器把上面的volatile读与下面的普通写重排序。
可能volatile读前面还有StoreLoad屏障,这是可见性中将StoreLoad屏障后面对共享变量的读取都从主内存读取最新值。
39. 什么是指令重排序?既然重排序有这么好处,为什么还要禁止指令重排序?重排序有什么后果?
重排序是编译器和处理器为了优化程序性能而对特定的指令序列进行重新排序的手段,但是不管怎么重排序,都不能违背单线程的执行逻辑顺序。因为在多线程的操作下,改变了执行顺序,可能会使程序错误。
40. ThreadLocal
1.ThreadLocal简介
通常情况下,我们创建的变量可以被任何一个线程访问并修改(线程之间可以通过主内存中转来访问并修改对方工作内存的变量),但是如果我们要实现每一个线程都有自己的专属本地变量该如何解决?。JDK提供ThreadLocal 类主要解决就是让每一个线程绑定自己的值(其他线程通过主内存中转也修改不了对方变量),可以将ThreadLocal 类对象比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据,相当于缓存。
自定义ThreadLocal案例:
public class ThreadlocalDemo {
//创建ThreadLocal对象
ThreadLocal<String> t1= new ThreadLocal<String>();
private String content;
public String getContent() {
String s = t1.get();
return s;
}
public void setContent(String content) {
t1.set(content);
}
public static void main(String[] args) {
ThreadlocalDemo demo=new ThreadlocalDemo();
//5个线程0-->4
for (int i = 0; i < 5; i++) {
//实现Runnable接口创建线程:先创建线程,再通过new Runnable()执行run任务
//创建线程和执行任务分开
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//每个线程存一个变量,过一会儿,再取出这个变量
demo.setContent(Thread.currentThread().getName()+"的数据");
System.out.println(Thread.currentThread().getName()+"-->"+demo.getContent());
System.out.println("---------------------------");
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
/**
* 线程1-->线程1的数据
* ---------------------------
* 线程3-->线程3的数据
* ---------------------------
* 线程2-->线程2的数据
* ---------------------------
* 线程4-->线程4的数据
* ---------------------------
* 线程0-->线程0的数据
* ---------------------------
* */
**2.原理:**见博客
3.ThreadLocal 内存泄露问题 见博客
41.介绍一下线程池,使用线程池的好处,参数有哪些?
(1)介绍一下线程池:池化技术这种思想,包括线程池、数据库连接池、Http连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。线程池提供了一种限制和管理资源(包括执行一个任务),是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。
(2)使用线程池的好处:
- 第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。但是,要做到合理利用线程池,必须对其实现原理了如指掌。
(3)参数有哪些:核心线程数(corepoolsize)、最大线程数(maxninumpoolsize)、线程存活时间(keepaliveTime,针对救急线程)、存活时间单位(unit)、任务队列(workerqueen)、线程工厂(threadexctor)、拒绝策略(hander)。
42.线程池有哪几种,优劣是啥?
Executors工厂内部四种线程池:newFixedThreadPool(固定线程池)、newCachedThreadPool(缓存线程池)、newSingleThreadExecutor(单个线程池)、newScheduledThreadPool(定时任务线程池)
43.线程池的实现原理?
见博客:线程池刚开始没有线程,当提交一个任务时,线程池会创建一个新线程去执行该任务。当线程数达到核心线程数,此时也没有线程空闲,这时将新加入的任务放入阻塞队列。如果该任务队列是有界队列,则核心线程数和有界队列都满了,这时会创建最大线程数-核心线程数的救急线程数来执行任务。只有当最大线程数也满了,此时再加入任务,线程池会执行拒绝策略。注意,救急线程数到达存活时间后,会被回收。
44.线程池的拒绝策略有哪些?
(1)让任务调用者执行任务;(2)丢弃当前任务;(3)丢弃队列中最早的任务;(4)让调用者抛出异常
45.线程池execute 和 submit的区别 ?
可以使用两个方法向线程池提交任务,分别为execute()
和submit()
方法。execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。(分为不超时get()和有时限等待get(long timeout,TimeUnit unit))。
当一个线程池里面的线程异常后,执行方式是execute
时,可以看到堆栈异常的输出。当执行方式是submit
时,堆栈异常没有输出。但是调用Future.get()方法时,可以捕获到异常。
46.说一下JUC包下的同步工具 ?
(1)CountDownLatch—闭锁
;其作用是在完成一组正在其他线程中执行的操作之前,允许一个或多个线程一直阻塞
(2)CyclicBarrier—循环栅栏
;允许一组线程互相等待,直到到达某个公共的屏障点 (common barrier point)。在涉及一组固定大小的线程的程序中,这些线程必须不时地互相等待,此时 CyclicBarrier 很有用。因为该 barrier在释放等待线程后可以重用,所以称它为循环的barrier。
(3)Semaphore——信号量
;信号量通常用来限制线程可以同时访问的(物理或逻辑)资源数量
(4)Exchanger—交换器
两个线程到达同步点后交换数据;Exchanger提供了一个同步点,在这个同步点,一对线程可以交换数据。每个线程通过exchange()方法的入口提供数据给他的伙伴线程,并接收他的伙伴线程提供的数据并返回。当两个线程通过Exchanger交换了对象,这个交换对于两个线程来说都是安全的。Exchanger可以认为是 SynchronousQueue 的双向形式,在运用到遗传算法和管道设计的应用中比较有用。
(5)Phaser一种可重用的同步屏障
,功能上类似于CyclicBarrier和CountDownLatch,但使用上更为灵活。非常适用于在多线程环境下同步协调分阶段计算任务(Fork/Join框架中的子任务之间需同步时,优先使用Phaser)
47.JUC 包中的原子类是哪4类 ?
1.基本类型
AtomicInteger 整形原子类
AtomicLong 长整型原子类
AtomicBoolean 布尔型原子类
2.数组类型
AtomicIntegerArray 整型数组
AtomicLongArray 长整型数组
AtomicReferenceArray 引用类型数组原子类
3.原子引用类型
AtomicReference 引用类型原子类
AtomicMarkableReference 原子更新带有标记位的引用类型,修改的次数
AtomicStampedReference 原子更新带有版本号的引用类型,解决ABA
4.对象的属性修改类型(字段更新器 )
AtomicReferenceFieldUpdater // 域 字段
AtomicIntegerFieldUpdater 原子更新整型字段的更新器
AtomicLongFieldUpdater 原子更新长整型字段的更新器
48.AtomicInteger 类的原理
部分源码:
// setup to use Unsafe.compareAndSwapInt for updates(更新操作时提供比较并交换)
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
copyonwriteArrayList,咋实现线程安全的。
copyonwriteArrayList的加锁时机
copyonwriteArrayList写的时候读会读到空数据吗 ?
49. 单线程线程池的应用场景?
newSingleThreadExecutor(单个线程池):希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放,还会继续执行下一次任务。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
50.怎么实现多个计算线程全部到达之后再进行下一步的操作(我说了 CountDownLatch 和 join) ?
CyclicBarrier,CountDownLatch,Phaser三者功能都类似
51.说说线程池的工作流程,4种拒绝策略,4种队列,其中一个线程挂掉了会怎么样?
//三种阻塞队列:
BlockingQueue<Runnable> workQueue = null;
workQueue = new ArrayBlockingQueue<>(5);//基于数组的先进先出队列,有界
workQueue = new LinkedBlockingQueue<>();//基于链表的先进先出队列,无界
workQueue = new SynchronousQueue<>();//无缓冲的等待队列,无界
//四种拒绝策略:
RejectedExecutionHandler rejected = null;
rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常
rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常
rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列
rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
//五种线程池:
ExecutorService threadPool = null;
threadPool = Executors.newCachedThreadPool();//有缓冲的线程池,线程数 JVM 控制
threadPool = Executors.newFixedThreadPool(3);//固定大小的线程池
threadPool = Executors.newScheduledThreadPool(2);
threadPool = Executors.newSingleThreadExecutor();//单线程的线程池,只有一个线程在工作
threadPool = new ThreadPoolExecutor();//默认线程池,可控制参数比较多
线程异常解决:
- 当一个线程池里面的线程异常后,执行方式是
execute
时,可以看到堆栈异常的输出。当执行方式是submit
时,堆栈异常没有输出。但是调用Future.get()方法时,可以捕获到异常。 - 不会影响线程池里面其他线程的正常执行。线程池会把这个线程移除掉,并创建一个新的线程放到线程池中。
52. 如果提交一个cpu密集型的任务怎么选取线程池 ?
newCachedThreadPool(缓存线程池):整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1分钟后释放线程。 适合任务数比较密集,但每个任务执行时间较短的情况。