JAVA并发与多线程相关面试题总结

1、什么是进程、线程、协程,它们之间的关系是怎样的?

  • 进程:

    • 本质上是一个独立执行的程序,是计算机中的程序关于数据集合上的一次运行活动,进程是操作系统进行资源分配和调度的基本概念,操作系统进行资源分配和调度的一个独立单位。
  • 线程:

    • 操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一个进程中可以并发多个线程,每个线程执行不同的任务,切换受操作系统控制。
  • 协程:

    • 又称为微线程,是一种用户态的轻量级线程,协程不像线程和进程需要进行系统内核上的上下文切换,协程的上下文切换是由用户自己决定的,有自己的上下文,所以说是轻量级的线程,也称之为用户级别的线程就是协程,一个线程可以有多个协程,线程进程都是同步机制,而协程则是异步,JAVA原生语法中并没有实现协程,目前Python、Go和Lua等语言支持。
  • 关系:

    • 一个进程可以有多个线程,它允许计算机同时运行两个或者多个程序。线程是进程的最小执行单位,CPU的调度切换的是进程和线程,进程和线程多了之后调度会消耗大量的CPU,CPU上真正运行的是线程,线程可以对应多个协程:

2、说下并发和并行的区别,并举例说明

  • 并发 concurrency:

    • 一核CPU,模拟出来多条线程,快速交替执行
  • 并行 parallellism:
    • 多核CPU,多个线程可以同时执行
    • eg:线程池
  • 并发是指在在一段时间内宏观上去处理多个任务。并行是指同一个时刻,多个任务确实真的同时运行。

举例:

并发:是一心多用,听课和看电影,但是CPU大脑只有一个,所以轮着来。
并行:火影忍者中的影分身,有多个你出现,可以分别做不同的事情。

3、JAVA实现多线程有哪几种方式,有什么不同,比较常用哪种?

3.1、继承Thread

  • 继承Thread,重写里面的run()方法,创建实例,执行start
  • 优点:代码编写最简单直接操作
  • 缺点:没返回值,继承一个类后,没法继承其他的类,拓展性差
/*** @author wcc* @date 2021/9/29 19:26*/
public class ThreadDemo1 extends Thread {@Overridepublic void run() {System.out.println("继承Thread实现多线程,名称:"+Thread.currentThread().getName());}public static void main(String[] args) {ThreadDemo1 threadDemo1 = new ThreadDemo1();threadDemo1.setName("demo1");//执行startthreadDemo1.start();System.out.println("主线程名称:"+Thread.currentThread().getName());}
}

3.2、实现Runnable接口

  • 自定义类实现Runnable,实现里面run()方法,创建Thread类,使用Runnable接口的实现对象作为参数传递给Thread对象,调用start方法。
  • 优点:线程类可以实现多个接口,可以再继承一个类
  • 缺点:没返回值,不能直接启动,需要通过构造一个Thread实例传递进行启动
/*** @author wcc* @date 2021/9/29 19:26*/
public class ThreadDemo2 implements Runnable {@Overridepublic void run() {System.out.println("实现Runnable实现多线程,名称:"+Thread.currentThread().getName());}public static void main(String[] args) {ThreadDemo2 thread = new ThreadDemo2();Thread threadDemo2=new Thread(thread);threadDemo2.setName("demo2");//执行startthreadDemo2.start();System.out.println("主线程名称:"+Thread.currentThread().getName());test1();}//JDK8之后采用lamda表达式public static void test1(){Thread thread=new Thread(()->{System.out.println("通过Runnable实现多线程,名称:"+Thread.currentThread().getName());});thread.setName("demo02");//start线程执行thread.start();System.out.println("主线程名称:"+Thread.currentThread().getName());}
}

3.3、实现Callable接口

  • 创建callable接口的实现类,并实现call()方法,结合Future Task类包装Callable对象,实现多线程。
  • 优点:有返回值,拓展性也高
  • 缺点:jdk5以后才支持,需要重写call()方法,结合多个类比如FutureTask和Thread类
/*** @author wcc* @date 2021/9/29 19:38*/
public class MyTask implements Callable<Object> {@Overridepublic Object call() throws Exception {System.out.println("通过callable实现多线程,名称:"+Thread.currentThread().getName());return "这是返回值";}public static void main(String[] args) {//JDK1.8 lambda表达式FutureTask<Object> futureTask=new FutureTask<Object>(()->{System.out.println("通过Callable实现多线程,名称:"+Thread.currentThread().getName());return "这是返回值";});// MyTask myTask = new MyTask();// FutureTask<Object> futureTask = new FutureTask<>(myTask);// FutureTask继承了Runnable,可以放在Thread中启动执行Thread thread = new Thread(futureTask);thread.setName("demo3");// start线程执行thread.start();System.out.println("主线程名称:"+Thread.currentThread().getName());try {// 获取返回值System.out.println(futureTask.get());} catch (InterruptedException e) {// 阻塞等待中被中断,则抛出e.printStackTrace();} catch (ExecutionException e) {// 执行过程发送异常被抛出e.printStackTrace();}}
}

3.4、通过线程池创建线程

  • 自定义Runnable接口,实现run方法,创建线程池,调用执行方法并传入对象
  • 优点:安全高性能,复用线程
  • 缺点:jdk5后才支持,需要结合Runnable进行使用
/*** @author wcc* @date 2021/9/29 19:59*/
public class threadDemo4 implements Runnable {@Overridepublic void run() {System.out.println("通过线程池+Runnable实现多线程,名称:"+Thread.currentThread().getName());}public static void main(String[] args) {//创建线程池ExecutorService executorService= Executors.newFixedThreadPool(3);for (int i = 0; i < 10; i++) {//线程池执行任务executorService.execute(new threadDemo4());}System.out.println("主线程名称:"+Thread.currentThread().getName());//关闭线程池executorService.shutdown();}
}

一般常用的Runnable和第四种线程池+Runnable,简单方便扩展和高性能(池化的思想)

3.5、Runnable Callable Thread三者区别

  • Thread是一个抽象类,只能被继承,而Runnable Callable是接口,需要实现接口中的方法
  • 继承Thread重写run()方法,实现Runnable接口需要实现run()方法,而Callable是需要实现call()方法
  • Thread和Runnable没有返回值,Callable有返回值
  • 实现Runnable接口的类不能直接调用start()方法,需要new一个Thread并将该实现类放入Thread,再通过新建的Thread实例来调用start() 方法。
  • 实现Callable接口的类需要借助FutureTask(将该实现类放入其中),再将FutureTask实例放入Thread,再通过新建的Thread实例来调用start()方法。获取返回值只需要借助FutureTask实例调用get()方法即可!

4、线程的几个状态(生命周期)?

线程有几个状态(6个)

/*** @author wcc* @date 2021/9/29 20:10*/
public enum State {/*** 线程新生状态*/NEW,/*** 线程运行中*/RUNNABLE,/*** 线程阻塞状态* 一个线程因为临界区的锁被阻塞产生的状态* Lock 或者 synchronized关键字产生的状态*/BLOCKED,/*** 线程等待状态,死等* 一个线程进入了锁,但是需要等待其他线程执行某些操作,时间不确定* 当wait、join、park方法调用的时候,进入waiting状态,前提是这个线程已经拥有锁了*/WAITING,/*** 线程超时等待状态,超过一定时间就不在等待* 一个线程进入了锁,但是需要等待其他线程执行某些操作,时间根据参数而定* 通过sleep或者wait 参数timeout的方法进入的有限期等待的状态*/TIMED_WAITING,/*** 线程终止状态,代表线程执行完毕*/TERMINATED;
}

5、线程状态转换的相关方法:sleep/yield/join wait/notify/notifyAll

Thread下的方法

sleep():属于线程Thread的方法,让线程暂缓执行,等待预计时间之后再恢复交出CPU使用权,《不会释放锁》,抱着锁睡觉进入超时等待状态TIMED_WAITING,在同步状态下睡眠结束进入阻塞状态去争夺锁来获得CPU执行权yield():属于线程Thread的方法,暂停当前线程的对象,去执行其他线程交出CPU使用权,《不会释放锁》,和sleep类似作用:让相同优先级的线程轮流执行,但是不保证一定轮流注意:不会让线程进入阻塞状态BLOCKED,直接变为就绪状态RUNNABLE,只需要重新获取CPU使用权join():属于线程Thread的方法,再主线程上运行调用该方法,会让主线程休眠,《不会释放锁》,让调用join方法的线程先执行完毕,再执行其他线程类似救护车警车优先通过!!

Object类下的方法

wait():属于Object的方法,当前线程调用对象的wait方法,《会释放锁》,进入线程的等待队列需要依靠notify或者notifyAll唤醒,或者wait(timeout)时间自动唤醒notify():属于Object的方法唤醒再对象监视器上等待的单个线程,《随即唤醒某个线程》notifyAll:属于Object类的方法唤醒在对象监视器上等待的全部线程,《全部唤醒》

线程转换流程图

6、JAVA中可以有哪些方法来保证线程安全?

  • 加锁:比如synchronized/ReentrantLock
  • 使用volatile声明变量,轻量级同步,不能保证原子性
  • 使用线程安全类,例如原子类AtomicXXX
  • 使用线程安全集合容器,例如:CopyOnWriteArrayList/ConcurrentHasMap
  • ThreadLocal本地私有变量/信号量Semaphore

7、是否了解volatile关键字

线程安全性:

线程安全性包括两个方面:1、可见性,2、有序性

volatile特性

  • 参考文章:volatile关键字
  • volatile保证线程可见性案例:使用volatile关键字的案例分析
  • 源码分析文章参考:java同步系统之volatile解析

volatile关键字如何保证内存可见性?

在JAVA虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile修饰的白能力有什么不同?

  • 对于普通变量:读操作会优先读取工作内存中的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中,写操作只会修改工作内存的副本数据,这种情况下,其他线程就无法读取变量的最新值
  • 对于volatile关键字修饰的变量,读操作的时候JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作的时候,JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其他线程就可以读取变量的最新值

通俗的来说就是,线程A对一个volatile变量的修改,对于其他线程来说是可见的,即线程每次获取volatile变量的值都是最新的

二者对比

  • volatile是轻量级的synchronized,保证了共享变量的可见性,被volatile关键字修饰的变量,如果值发送了变化,其他线程立刻可见,避免出现脏读现象!
  • volatile轻量级,只能修饰变量。synchronized重量级,还可以修饰方法
  • volatile只能保证数据的可见性,不能用来同步,因为多个线程并发访问volatile修饰的变量不会阻塞synchronized不仅保证可见性,而且还保证原子性,因为,只有获得了锁的线程才可以进入临界区,从而保证临界区中的所有语句都全部执行。多个线程争抢synchronized锁对象的时候,会出现阻塞
  • volatile:保证可见性,但是不能保证原子性
  • synchronized:保证可见性,也保证原子性

使用场景

对变量的写操作不依赖当前值,如果多线程下执行a++,是无法通过volatile保证结果原子性的

例:volatile int i=0;并且大量线程调用i的自增操作,那么volatile可以保证变量的安全吗?

不可以保证!volatile不能保证变量操作的原子性

  • 自增操作包括三个步骤,分别是:读取,加一,写入,由于这三个操作的原子性不能被保证,那么n个线程总共调用ni++的操作后,最后i的值并不是大家想的n,而是一个比n小的数!

  • 解释

    • 比如A线程执行自增操作,刚读取到i的初始值0,然后就被阻塞了!
    • B线程现在开始执行,还是读取到i的初始值0,执行自增操作,此时i的值为1
    • 然后A线程阻塞结束,对刚才拿到的0执行加1与写入操作,执行成功后,i的值被写成1了!
    • 我们预期输出2,可是输出的是1,输出比预期小!
  • 代码示例:

    /*** @author wcc* @date 2021/9/30 15:10*/
    public class VolatileTest {public volatile int i=0;public void increase(){i++;}public static void main(String[] args) throws InterruptedException {List<Thread> list=new ArrayList<>();VolatileTest test=new VolatileTest();for (int i = 0; i < 10000; i++) {Thread thread=new Thread(new Runnable() {@Overridepublic void run() {test.increase();}});thread.start();list.add(thread);}//等待所有线程执行完毕for (Thread thread:list){thread.join();}System.out.println(test.i); //输出9998}
    }
    

总结:

volatile关键字不需要加锁,因此不会造成线程的阻塞,而且比synchronized更加轻量级,而synchronized可能导致线程的阻塞!volatile由于禁止了指令重排序,所以JVM相关的优化没了,效率会偏弱!

JAVA内存模型:JMM规定所有的变量存在主内存中,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,不能直接对主内存操作使用volatile修饰变量,每次读取前必须从主内存更新属性最新的值,每次写入需要立刻写入到主内存中,volatile关键字修饰的变量随时看到自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见的

8、volatile可以避免指令重排,能否解释下什么是指令重排?

  • 指令重排序分为两类:

    • 编译器重排序
    • 运行时重排序

JVM在编译JAVA代码或者CPU执行JVM字节码的时候,对现有的指令进行重排序,主要目的是为了优化运行效率(不改变程序结果的前提)

int a = 3;     // step:1
int b = 4;     // step:2
int c =5;      // step:3
int h = a*b*c; // step:4定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的

详细的指令重排序可以参考之前的博客:多线程中的指令重排序问题

上面讲了volatile可以保证实现可见性和禁止重排序,那么它是怎么实现的呢?

答案就是:内存屏障

内存屏障的两个作用:

  • 阻止屏障两侧的指令重排序
  • 强制把写缓冲区/高速缓冲中的数据回写到主内存中,让缓存中相应的数据失效

9、介绍一下并发编程三要素

  • 原子性
  • 有序性
  • 可见性

9.1、原子性

  • 原子性:

    • 一个不可再被分割的最小颗粒,原子性指的是一个或者多个操作要么全部执行成功要么全部执行失败,期间不能被中断,也不存在上下文切换,线程切换回带来原子性的问题。
int num = 1; // 原子操作
num++;       // 非原子操作,从主内存读取num到线程工作内存,进行+1,再把num写回到主内存, // 除非用原子类:即,java.util.concurrent.atomic里的原子变量类// 解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作
// 这里不能使用volatile,前面有说到:对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果原子性的
public class XdTest {// 方式1:使用原子类// AtomicInteger  num = 0;// 这种方式的话++操作就可以保证原子性了,而不需要再加锁了private int num = 0;// 方式2:使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作Lock lock = new ReentrantLock();public  void add1(){lock.lock();try {num++;}finally {lock.unlock();}}// 方式3:使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住public synchronized void add2(){num++;}
}

解决核心思想:把一个方法或者代码块看作一个整体,保证是一个不可分割的整体

9.2、有序性

  • 有序性:

    • 程序执行的顺序按照代码的先后顺序执行,因为处理器可能会对指令进行重排序,JVM再编译JAVA代码或者CPU执行JVM字节码的时候,对现有的指令进行重新排序,主要目的是优化运行效率(不改变程序结果的前提)
int a = 3;     // step:1
int b = 4;     // step:2
int c =5;      // step:3
int h = a*b*c; // step:4定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的(单线程情况下)
指令重排序可以提高执行效率,但是多线程上可能会影响结果!

假如下面的场景:

// 线程1
before(); //处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
// 线程2
while(flag){run(); //执行核心业务代码
}// -------------- 指令重排序后,导致顺序换了,程序出现问题,且难排查//线程1
flag = true; //标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
//线程2
while(flag){run(); //执行核心业务代码
}
before(); //处理初始化工作,处理完成后才可以正式运行下面的run方法

9.3、可见性

  • 可见性:

    • 一个线程A对共享变量的修改,另一个线程B能够立刻看到
// 线程A执行
int num = 0;
// 线程A执行
num++;
// 线程B执行
System.out.println(“num的值:”+num);线程A执行i++后再执行线程B,线程B可能有2个结果,可能是0和1

因为i++在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存中读取打印,此时打印的就是0;也可能线程A执行完成更新到主内存了,线程B的值是1

所以需要保证线程的可见性:

synchronized、lock和volatile能够保证线程的可见性

10、JAVA里面有哪些锁?分别解释下

乐观锁/悲观锁

  • 悲观锁:

    • 当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候总会上锁,别的线程去拿数据的时候就会阻塞,比如synchronized
  • 乐观锁:
    • 每次去拿数据的时候都认为别人不会修改数据,更新的时候会判断别人是否是回去更新数据,通过版本判断,如果数据被修改了就拒绝更新,比如CAS是乐观锁,但是严格来说并不是锁,通过原子性来保证数据的同步,比如数据库的乐观锁,通过版本控制来实现,CAS不会保证线程同步,乐观锁的认为在数据更新期间没有其他线程影响(关于数据库乐观锁的实现请看之前的博客:MySQL相关面试题总结)
  • 小结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐量会比悲观锁大!

公平锁/非公平锁

  • 公平锁:

    • 指多个线程按照申请锁的顺序来获取锁,简单的来说,如果一个线程组里面,能保证每个线程都能拿到锁,比如:ReentrantLock(底层同步队列FIFO:Firdt Input First Output来实现)
  • 非公平锁:
    • 获取锁的方式是随机获取的,保证不了每个线程都能拿到锁,也就是可能存在有线程饿死,一直拿不到锁,比如synchronized、ReentrantLock
  • 小结:非公平锁锁性能高于公平锁,更能重复利用CPU的时间。ReentrantLock中可以通过构造方法指定是否为公平锁,默认为非公平锁!synchronized无法指定为公平锁,一直都是非公平锁

重入锁/不可重入锁

  • 可重入锁:

    • 也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁。一个线程获取锁之后再次尝试获取该锁的时候也可以获取该锁,可重入锁的优点的是避免死锁。
  • 不可重入锁:
    • 若当前线程执行某个方法已经获取了该锁,那么再房中尝试再次获取锁的时候,就会获取不到被阻塞!
  • 小结:可重入锁能一定程度的避免死锁synchronized、ReentrantLock都是可重入锁

独占锁/共享锁

  • 独占锁:是指锁一次只能被一个线程持有

    • 也叫X锁/排他锁/写锁/独享锁:该锁每一个只能被一个线程锁持有,加锁后任何线程试图再次加锁的线程会被阻塞,直到当前线程解锁。例子:如果线程A对data1加上排他锁后,则其他线程不能再对data1加任何类型的锁,获得独占锁的线程既能读数据又能修改数据!
  • 共享锁:是指锁一次可以被多个线程持有。
    • 也叫S锁/读锁,能查看数据,但是无法修改和删除数据的一种锁,加锁后其他用户可以并发读取、查询数据,但不能修改,增加,删除数据,该锁可以被多个线程持有,用于资源数据共享!

ReentrantLock和synchronized都是独占锁,ReadWriteLock的读锁都是共享锁,写锁是独占锁

互斥锁/读写锁

与独享锁和共享锁对1概念差不多,是独占锁/共享锁的具体实现!

ReentrantLock和synchronized都是互斥锁,ReadWriteLock是读写锁

自旋锁

  • 自旋锁:

    • 一个线程再获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断地判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁
    • 不会发生线程状态地切换,一直处于用户态,减少了上下文切换地消耗,缺点是循环会消耗CPU
  • 常见地自旋锁:TicketLock、CLHLock、MSCLock

死锁

  • 死锁:

    • 两个或者两个以上地线程再执行过程中,由于竞争资源或者由于彼此通信而造成地一种阻塞现象,若无外力作用,它们都将无法让程序进行下去

下面3种是JVM为了提高锁地获取与释放效率而做地优化 针对synchronized的锁升级,锁的状态是通过对象监视器再对象头种的字段来表明的,是不可逆的过程

  • 偏向锁:

    • 一段同步代码一直被一个线程所访问,那么该线程就会自动获取锁,获取锁的代价更低
  • 轻量级锁:
    • 当锁是偏向锁的时候,被其他线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式尝试获取锁,但不会阻塞,且性能会高点。
  • 重量级锁:
    • 当锁为轻量级锁的时候,其他线程虽然是自旋,但自旋不会一直循环下去,当自旋一定次数的时候且还没有获取到锁,就会进入阻塞,该锁会升级为重量级锁,重量级锁会让其他申请的线程进入阻塞,性能也会降低!

11、写个多线程死锁的例子

线程在获得了A锁并且没有释放的情况下去申请锁B,这时候另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死循环:

public class DeadLockDemo {private static String locka = "locka";private static String lockb = "lockb";public void methodA(){synchronized (locka){System.out.println("我是A方法中获得了锁A "+Thread.currentThread().getName() );// 让出CPU执行权,不释放锁try {Thread.sleep(2000);// sleep不释放锁} catch (InterruptedException e) {e.printStackTrace();}synchronized(lockb){System.out.println("我是A方法中获得了锁B "+Thread.currentThread().getName() );}}}public void methodB(){synchronized (lockb){System.out.println("我是B方法中获得了锁B "+Thread.currentThread().getName() );// 让出CPU执行权,不释放锁try {Thread.sleep(2000);// sleep不释放锁} catch (InterruptedException e) {e.printStackTrace();}synchronized(locka){System.out.println("我是B方法中获得了锁A "+Thread.currentThread().getName() );}}}public static void main(String [] args){System.out.println("主线程运行开始运行:"+Thread.currentThread().getName());DeadLockDemo deadLockDemo = new DeadLockDemo();new Thread(()->{deadLockDemo.methodA();}).start();new Thread(()->{deadLockDemo.methodB();}).start();System.out.println("主线程运行结束:"+Thread.currentThread().getName());}
}

死锁的四个必要条件:

  • 互斥条件:资源不能共享,只能由一个线程使用!
  • 请求与保持条件:线程已经获得一些资源,但因为请求其他资源发生阻塞,对已经获得的资源保持不释放
  • 不可抢占:有些资源是不可强占的,当某个线程获得这个资源后,系统不能强行回收,只能由线程使用完自己释放!
  • 循环等待条件:多个线程形成环形链,每个都占用对方申请的下个资源!

只要发生死锁,上面的条件都满足,只要一个不满足,就不会发生死锁

12、设计一个简单的不可重入锁

不可重入锁:若当前线程执行某个方法已经获取了该锁,那么在其他方法中尝试再次获取锁的时候。就会获取不到被阻塞!

/*** @author wcc* @date 2021/9/30 21:08*/
public class UnreentrantLock {private boolean isLocked=false;//加锁方法public synchronized void lock() throws InterruptedException {System.out.println("进入lock加锁:"+Thread.currentThread().getName());//判断是否已经被锁,如果被锁则当前请求的线程进行等待while (isLocked){System.out.println("进入wait等待:"+Thread.currentThread().getName());wait();}//如果还没被加锁,则进行加锁isLocked=true;}//解锁方法public synchronized void unlock(){System.out.println("进入unlock解锁:"+Thread.currentThread().getName());isLocked=false;//随机唤醒对象锁池里面的一个线程notify();}
}class Main {private UnreentrantLock unreentrantLock=new UnreentrantLock();//加锁建议在try里面,解锁建议在finallypublic void methodA(){try {unreentrantLock.lock();System.out.println("methodA方法被调用");//methodA()中嵌套调用methodB(),测试methodB()是否能获取锁的执行权methodB();}catch (Exception e){e.printStackTrace();}finally {unreentrantLock.unlock();}}public void methodB(){try {unreentrantLock.lock();System.out.println("methodB方法被调用");}catch (Exception e){e.fillInStackTrace();}finally {unreentrantLock.unlock();}}public static void main(String[] args) {//演示同一个线程是否可重入!(如果单线程都是不可重入的话,多线程下就不用说了)new Main().methodA();}
}
//同一个线程,重复获取锁失败,形成死锁,这个就算不可重入锁

执行结果如下:

13、设计一个简单的可重入锁

可重入锁:也叫递归锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁

/*** @author wcc* @date 2021/9/30 21:29*/
public class ReentrantLock {private boolean isLocked=false;//用于记录是否是重入的线程private Thread lockOwener=null;//累计加锁次数,加锁一次累加1.解锁一次减少1private int lockedCount=0;//加锁方法public synchronized void lock() throws InterruptedException {System.out.println("进入lock加锁:"+Thread.currentThread().getName());//获取当前线程Thread thread=Thread.currentThread();//判断是否是同个线程获取锁,lockOwner!=thread引用的地址的比较//如果已经加锁,且当前线程不是之前加锁的线程则阻塞等待while (isLocked && lockOwener!=thread){System.out.println("进入wait等待:"+Thread.currentThread().getName());System.out.println("当前锁状态 islOcked="+isLocked);System.out.println("当前count数量 lockedCount="+lockedCount);wait();}//如果没有锁,或者当前线程是之前加锁的线程,则://进行加锁,两次线程地址相同,加锁次数++isLocked=true;lockOwener=thread;lockedCount++;}//解锁方法public synchronized void unlock() {System.out.println("进入unlock解锁:" + Thread.currentThread().getName());//获取当前线程Thread thread = Thread.currentThread();//线程A加的锁,只能由线程A来解锁,其他线程不能进行解锁if (thread == this.lockOwener) {lockedCount--;if (lockedCount == 0) {//解锁isLocked = false;lockOwener = null;//随机唤醒对象锁池中的一个线程notify();}}}
}
class Main1{private ReentrantLock reentrantLock=new ReentrantLock();//加锁建议加在try里面,解锁建议加载finally里面public void methodA(){try {reentrantLock.lock();System.out.println("methodA方法被调用");methodB();}catch (Exception e){e.fillInStackTrace();}finally {reentrantLock.unlock();}}public void methodB(){try {reentrantLock.lock();System.out.println("methodB方法被调用");}catch (Exception e){e.fillInStackTrace();}finally {reentrantLock.unlock();}}public static void main(String[] args) {for (int i = 0; i < 10; i++) {//演示的是单个线程new Main1().methodA();}}
}

执行结果如下:

进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main
进入lock加锁:main
methodA方法被调用
进入lock加锁:main
methodB方法被调用
进入unlock解锁:main
进入unlock解锁:main

4、介绍一下你对synchronized关键字的理解

4.1、简介

synchronized关键字是JAVA里面最基本的同步手段,它经过编译之后,会在同步代码块(临界区)的前后分别生成monitorentermonitorexit字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。

4.2、实现原理

在之前介绍JAVA内存模型(JMM)中的时候,我们介绍过两个指令:lockunlock

  • lock,锁定,作用于主内存的变量,它把工作内存中的变量标识为一条线程独占状态。
  • unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其他线程锁定。

但是这两个指令并没有直接提供给用户使用,而是提供了两个更高层次的指令monitorentermonitorexit来隐式的使用lock和unlock指令。

而synchronized就是使用monitorenter和monitorexit这两个指令来实现的

根据JVM规范的要求,在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象还没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit指令的时候会把计数器减1,当计数器减为0的时候,这个锁就释放了。

这里,我觉得以字节码指令去理解synchronized关键字会比较好理解,下面看一下:

public class SynchronizedTest {public static void sync(){synchronized(SynchronizedTest.class){synchronized(SynchronizedTest.class){}}}public static void main(String[] args){}
}

编译后的sync()的字节码指令如下,为了便于阅读,加上一些注释:

//加载常量池中的SynchronizedTest类对象到操作数栈中
0: ldc           #2                  // class com/boot/thread/SynchronizedTest
//复制栈顶元素
2: dup
//存储一个引用变量到本地变量表中的0槽位中
3: astore_0
//调用monitorenter,它的参数变量为0,也就是上面的SynchronizedTest类对象
4: monitorenter
再次加载常量池张的SynchronizedTest的类对象到操作数栈中
5: ldc           #2                  // class com/boot/thread/SynchronizedTest
//复制栈顶元素
7: dup
//存储一个引用到本地变量表的槽位1中
8: astore_1
//再次调用monitorenter,它的参数是变量1,也还是SynchronizedTest类对象
9: monitorenter
//从本地变量表的1号槽位中加载变量
10: aload_1
//调用monitorexit解锁,它的参数是上面加载的变量1
11: monitorexit
//跳到20行
12: goto          20
15: astore_2
16: aload_1
17: monitorexit
18: aload_2
19: athrow
//从本地变量表的0号槽位中加载变量
20: aload_0
//调用monitorexit解锁,它的参数是上面加载的变量0
21: monitorexit
22: goto          30
25: astore_3
26: aload_0
27: monitorexit
28: aload_3
29: athrow
30: return

根据注释我们可以理解到,我们的synchronized锁定给的是SynchronizedTest类对象,可以看到它从常量池中加载了两次SynchronizedTest类对象,分别存储在本地变量表的0号槽位和1号槽位中,解锁的时候正好是顺序相反的,先解锁1号槽位变量,在解锁0号槽位变量,实际上变量0和变量1指向的是同一个对象,所以synchronized关键字是可重入的。

4.3、Synchronized关键字是怎么样保证并发三大特性的呢?

  • 还是回到JAVA内存模型上来说,synchronized关键字底层是通过monitorenter和monitorexit指令实现的,而这两个指令又是通过lock和unlock指令来实现的。
  • 而lock和unlock在JAVA内存模型中是必须满足下面四条规则的:
    • 一个变量同一时刻只允许一条线程对其进行lock操作的,但是lock操作可以被同一个线程执行多次,多次执行lock操作后,只有执行相同次数的unlock操作,变量才能被解锁。
    • 如果对一个变量执行lock操作,将会清空此工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或者assign操作初始化变量的值
    • 如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其他线程锁定的变量
    • 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作
  • 通过规则(1),我们知道每次lock和unlock之间的代码,同一个时刻只允许一个线程访问,所以,synchronized关键字是具有原子性的
  • 通过规则(1)(2)(4),我们知道每次lock和unlock的时候都会从主内存加载或者把变量刷新回主内存,而lock和unlock之间的变量(这里指的是锁定的变量)是不会被其他线程修改的,所以,synchronized是具有可见性的
  • 通过规则(1)(3),我们知道所有对变量的加锁都要排队进行,且其他线程不允许解锁当前线程锁定的对象,所以,synchronized是具有有序性的

4.4、synchronized是否是非公平锁?

/*** @author wcc* @date 2021/10/1 15:29*/
public class SynchronizedTest1 {public static void sync(String tips){synchronized (SynchronizedTest1.class){System.out.println(tips);try {Thread.sleep(1000);}catch (Exception e){e.fillInStackTrace();}}}public static void main(String[] args) throws InterruptedException {new Thread(()->sync("线程1")).start();Thread.sleep(100);new Thread(()->sync("线程2")).start();Thread.sleep(100);new Thread(()->sync("线程3")).start();Thread.sleep(100);new Thread(()->sync("线程4")).start();Thread.sleep(100);}
}

在这段程序中,我们起了四线程,且分别间隔100ms启动(注意间隔时间不要过长,否则容易超出程序执行时间),每个线程里面打印一句话后等待1000ms,如果synchronized是公平锁,那么打印的结果应该依次是线程1、2、3、4.

但是实际结果是随机的结果,所以,synchronized是一个非公平锁

4.5、总结

  • synchronized在编译的时候会在同步代码块前后生成monitorenter和monitorexit字节码指令
  • monitorenter和monitorexit字节码指令需要一个引用的参数来进行加锁和解锁,基本类型不可以
  • monitorenter和monitorexit字节码指令更底层是使用JAVA内存模型的lock和unlock指令
  • synchronized是可重入锁
  • synchronized是非公平锁
  • synchronized可以同时保证原子性、可见性、有序性
  • synchronized有三种状态:偏向锁、轻量级锁、重量级锁。

15、解释下什么是CAS?以及ABA问题?

CAS全称:Compare and Swap 比较并交换

Unsafe实现原理,参考文章:死磕JAVA魔法类之Unsafe解析

  • CAS底层通过Unsafe类实现原子性操作,操作包括三个操作数:

    • 对象内存地址(V):
    • 预期原值(A):
    • 新值(B)
  • 理解方式1:比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行交换操作!如果不是就一直循环
  • 理解方式2:如果内存地址中的值于预期原值相匹配,那么处理器会自动将该地址的值更新为新值,如果在第一轮循环中,A线程获取地址里面的值被B线程修改了,那么A线程需要自旋,到下次循环才可能有机会执行。

CAS属于乐观锁,性能较悲观锁有很大的提高

AtomicXXX等原子类底层就算CAS实现的,一定程度比synchronized号,因为后者是悲观锁

这里通过之前对CAS的理解,以之前的一个案例入手:

案例

/*** @author wcc* @date 2021/10/1 15:53*/
public class CASDemo {//CAS compareAndSet : 比较并交换public static void main(String[] args) {AtomicInteger atomicInteger=new AtomicInteger(2020);//期望、更新//public final boolean compareAndASet(int expect, int update)//如果我期望值达到了,那么就更新,否则://就不更新,CAS是CPU的并发原语!//原语:一般是指由若干条指令逞的程序段,用来实现某个特定功能,在执行过程中不可被中断System.out.println(atomicInteger.compareAndSet(2020,2021)); //trueSystem.out.println(atomicInteger.get()); //2021//atomicInteger.getIncrement() 看底层如何实现++操作的System.out.println(atomicInteger.compareAndSet(2020,2021)); //falseSystem.out.println(atomicInteger.get()); //2021}
}

我们来看一下getAndIncrement()方法的底层实现:

public class AtomicInteger extends Number implements java.io.Serializable {private static final long serialVersionUID = 6214790243416807050L;// Unsafe类,底层是调用c++,java无法操作内存,所以这里借助c++来操作内存private static final Unsafe unsafe = Unsafe.getUnsafe();private static final long valueOffset;static {try {//获取内存偏移值 valueOffsetvalueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));} catch (Exception ex) { throw new Error(ex); }}//value被volatile关键字修饰,避免指令重排,且保证线程可见性和有序性private volatile int value;public final int getAndIncrement() {// 参数:// this: 当前对象// valueOffset:当前对象的内存偏移地址// 1:值return unsafe.getAndAddInt(this, valueOffset, 1);}....
}

大致了解Unsafe之后,我们继续点进getAndIncrement()方法中,unsafe调用的getAndAddInt()方法查看:

// 位于UnSafe类中
// 参数:var1 当前对象,var2 当前对象的内存偏移地址,var4 值(1)
public final int getAndAddInt(Object var1, long var2, int var4) {int var5;// 这里用到了自旋锁:一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有一个执行单元获得锁。do {// 获取内存地址中的原对象的值var5 = this.getIntVolatile(var1, var2);// 借助CAS比较并交换,来实现getAndIncrement()方法的自增+1功能!} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;
}...// 调用C++,执行比较并交换
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

CAS:比较当前工作内存于主内存中的值,如果这个值是期望的,那么则执行操作,如果不是,则一直循环

15.1、CAS的ABA问题?

狸猫换太子

/*** @author wcc* @date 2021/10/1 16:21*/
public class CASABATest {// CAS :compareAndSet 比较并交换public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(2002);/*** 类似于我们平时写的SQL:乐观锁* 如果某个线程在执行操作某个对象的时候,其他线程若是操作了该对象* 即使对象的内容未发生改变,也需要告诉我** 期望、更新* public final boolean compareAndSet(int except,int update)** 如果我期望的值达到了,那么久更新,否则,久不更新* CAS是CPU的并发原语*/// ============== 捣乱的线程 ==================System.out.println(atomicInteger.compareAndSet(2020, 2021));System.out.println(atomicInteger.get());System.out.println(atomicInteger.compareAndSet(2021, 2020));System.out.println(atomicInteger.get());// ============== 期望的线程 ==================System.out.println(atomicInteger.compareAndSet(2020, 6666));System.out.println(atomicInteger.get());}
}输出的结果:
true
2021
true
2020
true
6666

上述案例中:假设我们期望的线程本来是需要将2020更换成6666,然而有一个捣乱的线程抢在期望线程之前执行,先把2020更换为了2021,然后又将2021更换回了2020!

这样看上去当期望线程执行的时候,初始值仍为2020没有改变,但是实际上在捣乱线程中已经执行过2次更换操作了,而我们的期望线程并不知道,这就是ABA问题!

如何解决ABA问题?

本质上相当于采用乐观锁策略来解决ABA问题!

public class CASDemo {/*** AtomicStampedReference 注意,* 如果泛型是一个包装类,就需要注意对象的引用问题* 正常在业务操作,这里面比较的都是一个个对象*/// 参数1:初始值100// 参数2:初始对应的版本号 initialStamp=1static AtomicStampedReference<Integer> atomicStampedReference =new AtomicStampedReference<>(100,1);// CAS compareAndSet : 比较并交换!public static void main(String[] args) {// 线程A:new Thread(()->{// 线程执行时,先获得initialStamp版本号int stamp = atomicStampedReference.getStamp();System.out.println("A线程第1次拿到的版本号为:"+stamp);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// cas比较并交换:100--->101atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),// 获得最新版本号// 更新版本号atomicStampedReference.getStamp() + 1);System.out.println("A线程第2次拿到的版本号为:"+atomicStampedReference.getStamp());// cas比较并交换:101--->100System.out.println("A线程第2次是否执行了CAS:" +atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp() + 1));System.out.println("A线程第3次拿到的版本号为:"+atomicStampedReference.getStamp());},"A").start();// 乐观锁的原理相同!// 线程B:new Thread(()->{// 获得版本号int stamp = atomicStampedReference.getStamp();System.out.println("B线程第1次拿到的版本号为:"+stamp);try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}// cas比较并交换:100--->99System.out.println("B线程第1次是否执行了CAS:" +atomicStampedReference.compareAndSet(100,99,stamp,stamp + 1));System.out.println("B线程第2次拿到的版本号为:"+atomicStampedReference.getStamp());},"B").start();}
}

这样,在版本号initialStamp的限制下,每执行一次CAS,都会将版本号+1,这样即使出现了狸猫换太子的情况,期望线程也能及时知道!

输出结果如下:

A线程第1次拿到的版本号为:1
B线程第1次拿到的版本号为:1
A线程第2次拿到的版本号为:2
A线程第2次是否执行了CAS:true
A线程第3次拿到的版本号为:3
B线程第1次是否执行了CAS:false
B线程第2次拿到的版本号为:3

总的来说,与MySQL的乐观锁表中加一个version字段原理相同!

注意:

Integer使用了对象缓存机制,默认范围是-128~127,推荐使用静态工厂valueOf获取对象实例,而不是new,因为valueOf使用缓存,而new一定会创建新的对象分配新的内存空间

下面是查看阿里巴巴开发手册的规范点:

所以上面的案例,如果使用大于-128~127范围的数字就会出现两个false的情况!,这个问题困扰了我好一会。。

16、介绍一下你对AQS的理解?

参考文章:AQS面试详解

AQS的全称为(AbstraceQueuedSynchronizer抽象的队列式同步器。是除了JAVA自带的synchronized关键字之外的锁机制。这个类在java.util.concurrent.locks包下面

它是一个JAVA提高的底层同步工具类,比如:CountDownLatch、ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue、Future Task等等皆都是基于AQS的!

实现了AQS的有:自旋锁、互斥锁、读锁写锁、条件产量、信号量、栅栏都是AQS的衍生物!

这里,之后会把AQS的源码分析发布,最近太忙了,都在总结面试题。

17、ReentrantLock和Synchronized的差别?

  • ReentrantLock和sychronized都是独占锁,可重入锁,悲观锁

  • synchronized:

    • JAVA内置关键字
  • 无法判断是否获取锁的状态,只能是非公平锁

    • 加锁解锁的过程是隐式的,用户不需要手动进行操作,优点是操作简单但显得不够灵活
  • 一般并发场景使用足够,可以被放在递归执行的方法上,且不用担心线程最后能否正确释放锁

  • ReentrantLock:

    • 是个Lock接口的实现类
    • 可以判断是否获取到锁,可以为公平锁也可以是非公平锁(默认)
    • 需要手动加锁解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁
    • 创建的时候通过传进参数true创建公平锁,如果传入的是false或者没传参数则创建的是非公平锁
    • 底层是AQS的state和FIFO队列来控制加锁

18、ReentrantReadWriteLock和ReentrantLock有什么区别?

ReentrantReadWriteLock

  • 读写锁接口ReadWriteLock接口的一个具体实现,实现了读写锁分离
  • 支持公平和非公平锁,底层也是基于AQS实现
  • 允许从写锁降为读锁

流程:先获取写锁,然后获取读锁,最后释放写锁,但是不能从读锁升级到写锁

  • 重入:

    • 读锁后还可以获取读锁
    • 获取写锁之后既可以再次获取写锁又可以获取读锁
    • 读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,主要是为了提升读写的性能!

ReentrantLock是独占锁且可重入的,相比synchronized而言功能更加丰富也更适合复杂的并发场景,但是也有弊端,假如有两个线程A/B访问数据,加锁是为了防止A线程在写数据,B线程在读数据造成的数据不一致,但线程A在读数据,线程C也在读数据,读数据是不会改变数据没有必要加锁,但是ReentrantReadWriteLock读写锁接口!

19、是否了解阻塞队列BlockingQueue?

BlockingQueue阻塞队列

  • ArrayBlockingQueueLinkedBlockingQueue
  • put方法用来向队尾中存入元素,如果队列满,则阻塞
  • take方法用来从队首取元素,如果队列为空,则阻塞

BlcokingQueue:JUC包下的提供了线程安全的队列访问的接口,并发包下很多高级同步类的实现都是基于阻塞队列实现的!

常见的阻塞队列

  • ArrayBlockingQueue:

    • 基于数组实现的一个阻塞队列,需要指定容量大小,FIFO先进先出顺序;
  • LinkedBlockingQueue:
    • 基于链表实现的一个阻塞队列,如果不指定容量,默认Integer.MAX_VALUE,FIFO先进先出顺序;
  • PriorityBlockingQueue:
    • 一个支持优先级的无界阻塞队列,默认情况下元素采用自然顺序升序排序,也可以自定义排序实现java.lang.Comparable接口;
  • DelayQueue:
    • 延迟队列,在指定时间才能获取队列元素的功能,队列头元素是最接近过期的元素,里面的对象必须实现java.util.concurrent.Delayed接口并实现ComparableTo和getDelay方法;

20、JAVA里面有哪些是常用的线程池?

使用线程池的好处

重用存在的线程,减少对象创建销毁的开销,有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞,且可以定时定期执行、单线程、并发数控制,配置任务过多任务后的拒绝策略等功能

类别:

  • newFixedThreadPool

    • 一个定长线程池,可控制线程最大并发数
  • newCachedThreadPool
    • 一个可缓存线程池
  • newSingleThreadExecutor
    • 一个单线程化的线程池,用唯一的工作线程来执行任务
  • newScheduledThreadPool
    • 一个定长线程池,支持定时/周期性任务执行

阿里巴巴编码规范:线程池不允许使用Executors去创建,要通过ThreadPoolExecutor的方式原因?

Executors创建的线程池底层也是调用ThreadPoolExecutor,只不过使用不同的参数、队列、拒绝策略等
如果使用不当,会造成资源耗尽的问题
直接使用ThreadPoolExecutor让使用者更加清楚线程池允许规则,常见参数的使用,避免风险常见的线程池问题:
newFixedThreadPool和newSingleThreadExecutor:
队列使用LinkedBlockingQueue,队列长度Integer.MAX_VALUE,会造成堆积,导致OOMnewScheduledThreadPool和newCachedThreadPool:
线程池里面使用最大的线程数是Integer.MAX_VALUE,可能会创建过多线程,导致OOM

ThreadPoolExecutor构造函数里面的参数,能否解释下各个参数的作用?

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • corePoolSize核心线程数,线程池也会维护线程的最少数量,默认情况下核心线程会一直存活,即使没有任务也不会受keepAliveTime控制!

  • maximumPoolSize线程池维护线程的最大数量,超过将被阻塞!

    注意:当核心线程池满,且阻塞队列也满的时候,才会判断当前线程数是否小于最大线程数,才决定是否创建新的线程

  • keepAliveTime非核心线程的闲置超时时间,超过这个时间就会被回收,直到线程数量等于corePoolSize;

  • unit:指定keepAliveTime的单位,如TimeUnit.SECONDS、TimeUnit.milliseconds

  • workQueue线程池中的任务队列,常用的是ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue

  • threadFactory创建新线程的时候使用的工厂

  • handler:RejectedExecutorHandler是一个接口且只有一个方法,线程池中的数量大于maximumPoolSize,对拒绝任务的处理策略,默认有四种拒绝策略:

    • AbortPolicy策略:默认的饱和策略,会直接抛出异常,阻止系统工作
    • CallerRunsPolicy策略:只要线程池没有关闭,会在调用者线程中运行当前被丢弃的任务,调用者线程性能可能急速下降
    • DiscardPolicy策略:直接丢弃这个无法处理的任务
    • DiscardOldestPolicy策略:将任务队列中最老的任务丢弃,尝试再次提交新任务

20.1、自定义一个饱和策略(面试千寻的时候问到,吃了大亏)

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
/*** 循环处理,当队列有空位时,该任务进入队列,等待线程池处理*/
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {try {executor.getQueue().put(r);} catch (InterruptedException e) {e.printStackTrace();}}
}

20.2、ThreadPoolExecutor有哪些常用的方法:

ThreadPoolExecutor有如下常用的方法:

  • submit()/execute():执行线程池任务
  • shutdown()/shutdownNow():终止线程池
  • isShutDown():判断线程是否终止
  • getActiveCount():正在运行的线程数
  • getCorePoolSize():获取核心线程数
  • getMaximumPoolSize():获取最大线程数
  • getQueue():获取线程池中的任务队列
  • allowCoreThreadTimeOut(boolean):设置空闲的时候是否回收核心线程

20.3、说说shutdownNow()和shutdown()两个方法有什么区别?

  • shutdownNow()和shutdown()都是用来终止线程池的,它们的区别是:使用shutdown()程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完了在退出,执行了shutdown()之后就不能给线程池添加新任务了
  • shutdownNow()会试图马上停止任务,如果线程池中还有缓存任务正在执行,则会抛出java.lang.InterruptedException: sleep interrupted 异常。
    它们的区别是:使用shutdown()程序不会报错,也不会立即终止线程,它会等待线程池中的缓存任务执行完了在退出,执行了shutdown()之后就不能给线程池添加新任务了
  • shutdownNow()会试图马上停止任务,如果线程池中还有缓存任务正在执行,则会抛出java.lang.InterruptedException: sleep interrupted 异常。

20.4、说一下线程池的工作原理?

当线程池中有任务需要执行的时候,线程池会判断如果线程数量没有超过核心线程数量就会新创建线程进行任务执行,如果线程池中的线程数量已经超过了核心线程数,这时候任务就会被放入任务队列中排队等待执行,如果任务队列超过了最大队列数,并且线程池没有达到最大线程数,就会新创建线程来执行任务,如果超过了最大线程数,就会执行拒绝饱和策略。

20.5、线程池中核心线程数量大小怎么设置?(面试携程的时候问到,也吃了大亏)

  • CPU密集型任务

    • 比如像加解密,压缩,计算等一系列需要大量耗费CPU资源的任务,大部分场景下都是纯CPU计算,尽量使用较小的线程池,一般为CPU核心数+1。因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
  • IO密集型任务
    • 比如像MySQL数据库、文件的读写、网络通信等任务,这类任务特别不会消耗CPU资源,但是IO操作比较耗时,会占用比较多时间。可以使用稍大的线程池,一般为2*CPU核心数。IO密集型任务CPU使用率不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。

线程的平均工作时间所占比例越多,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程

20.6、线程池为什么要使用(阻塞)队列?

  • 因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成CPU过度切换
  • 创建线程池的消耗过高

20.7、线程池为什么使用阻塞队列而不是用非阻塞队列?

  • 阻塞队列可以保证任务队列中没有任务的时候阻塞获取任务的线程,使得线程进入wait状态
  • 当队列中有任务的时候才唤醒对应线程从队列中取出任务线程进行执行
  • 使得线程不至于一直占用CPU资源(线程执行完任务后通过循环再次从任务队列中取出任务执行,代码片段如下:)
    • while (task != null || (task = getTask()) != null) {})。

20.8、了解线程池状态吗?

通过获取线程池状态,可以判断线程池是否是运行状态、是否可以添加新的任务以及优雅的关闭线程池等

  • RUNNING:线程池的初始化状态,可以添加待执行的任务
  • SHUTDOWN:线程池处于待关闭状态,不接收新任务仅处理已经接受的任务。
  • STOP:线程池立即关闭,不接收新的任务,放弃缓存队列中的任务并且中断正在处理的任务
  • TIDEING:线程池自主整理状态,调用terminated()方法进行线程池整理。
  • TERMINATED:线程池终止状态。

JAVA并发与多线程相关面试题总结相关推荐

  1. Java并发编程71道面试题及答案

    Java并发编程71道面试题及答案 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方 ...

  2. Java并发编程75道面试题及答案

    1.在java中守护线程和本地线程区别?java中的线程分为两种:守护线程(Daemon)和用户线程(User).任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon(bo ...

  3. 并发与多线程相关知识点梳理

    文章目录 并发和并行的概念 如何保证线程安全 1. 数据单线程内可见 2. 只读对象 3. 线程安全类 4. 同步与锁机制 什么是锁 线程同步 引用类型 ThreadLocal LeetCode 相关 ...

  4. 100道Java并发和多线程面试题

    1.多线程有什么用? 一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡.所谓"知其然知其所以然","会用"只是 ...

  5. Java并发编程73道面试题及答案 —— 面试稳了 侵立删

    作者:乌枭 来自:https://blog.csdn.net/qq_34039315/article/details/78549311 最近后台和微信理有很多读者让我整理一些面试题,我就把这事放在心上 ...

  6. Java并发与多线程

    1.多线程优点 资源利用率更好:文件读写操作 程序设计在某些情况下更简单: 程序响应更快:端口监听操作 2.多线程的代价 设计更复杂:多线程共享数据时尤其需要注意 上下文切换的开销: CPU 会在一个 ...

  7. Java并发编程73道面试题及答案——稳了

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户 ...

  8. Java并发编程71道面试题及答案 1

    1.在java中守护线程和本地线程区别? java中的线程分为两种:守护线程(Daemon)和用户线程(User). 任何线程都可以设置为守护线程和用户线程,通过方法Thread.setDaemon( ...

  9. Java 并发编程73道面试题及答案 ——面试看这篇就够了!

    作者:乌枭 https://blog.csdn.net/qq_34039315/article/details/78549311 1.在java中守护线程和本地线程区别? java中的线程分为两种:守 ...

最新文章

  1. 中文分词jieba的简单使用
  2. Java异常处理总结
  3. 探坑mongoDB4.0事务回滚的辛酸历程
  4. jsp mysql utf-8 中文乱码_jsp插入mysql数据库显示中文乱码问题
  5. 庐山真面-Oxite的HelloWorld
  6. 如何使用TensorFlow玩转深度学习?
  7. 降雨插值_ArcGIS计算土壤侵蚀模数(二)降雨侵蚀力因子R计算
  8. 【2021杭电多校赛】2021“MINIEYE杯”中国大学生算法设计超级联赛(10)签到题2题
  9. centos6.5 vncserver安装与配置
  10. 数据库接口实验--php实现--
  11. 写一彩票程序,要求能随机产生并按照升序输出1-30之间的7个数,且其中任意两个数字不能重复
  12. Mac install ninja_玩游戏出现机器码或被封,用修改网卡mac物理地址的方法试一试...
  13. 软件测试和web前端该怎么选择
  14. ShadowGun 图形技术分析
  15. 什么是GC,GC是什么意思?为什么要有GC?
  16. 机械硬盘 与 固态硬盘SSD
  17. 安装java环境----血泪版
  18. Guava-Utilites学习测试类
  19. flask-restful 和 blueprint
  20. 程序员升职记-五种种说话套路

热门文章

  1. 【团队管理】这样开晨会,员工不累,效率加倍!
  2. word2007 无格式文本 选择性粘贴 快捷键 定制方法(转)
  3. NO.44-----QQ音乐全站爬虫
  4. 研大考研:2015考研英语之强调句
  5. 智立方通过注册:年营收5.48亿 超70%订单来自苹果
  6. excel元素批量添加前缀和后缀
  7. 从程序员到项目经理(9):程序员加油站 --要执着但不要固执
  8. 好心情:家有精神疾病患者,家属务必做好5件事
  9. java垃圾收集器zgc,ZGC简介:可扩展的低延时JVM垃圾收集器
  10. 星球乐园 | 入职大公司的第一天,我就……