关于后端:面渣逆袭Java并发六十问快来看看你会多少道

42次阅读

共计 39600 个字符,预计需要花费 99 分钟才能阅读完成。

这篇文章有点长,四万字,图文详解六十道 Java 并发面试题。人曾经肝麻了,大家能够点赞、珍藏缓缓看!扶我起来,我还能肝!
根底
1. 并行跟并发有什么区别?
从操作系统的角度来看,线程是 CPU 调配的最小单位。

并行就是同一时刻,两个线程都在执行。这就要求有两个 CPU 去别离执行两个线程。
并发就是同一时刻,只有一个执行,然而一个时间段内,两个线程都执行了。并发的实现依赖于 CPU 切换线程,因为切换的工夫特地短,所以根本对于用户是无感知的。

就如同咱们去食堂打饭,并行就是咱们在多个窗口排队,几个阿姨同时打菜;并发就是咱们挤在一个窗口,阿姨给这个打一勺,又慌手慌脚地给那个打一勺。

2. 说说什么是过程和线程?
要说线程,必须得先说说过程。

过程:过程是代码在数据汇合上的一次运行流动,是零碎进行资源分配和调度的根本单位。
线程:线程是过程的一个执行门路,一个过程中至多有一个线程,过程中的多个线程共享过程的资源。

操作系统在分配资源时是把资源分配给过程的,然而 CPU 资源比拟非凡,它是被调配到线程的,因为真正要占用 CPU 运行的是线程,所以也说线程是 CPU 调配的根本单位。
比方在 Java 中,当咱们启动 main 函数其实就启动了一个 JVM 过程,而 main 函数在的线程就是这个过程中的一个线程,也称主线程。

一个过程中有多个线程,多个线程共用过程的堆和办法区资源,然而每个线程有本人的程序计数器和栈。
3. 说说线程有几种创立形式?
Java 中创立线程次要有三种形式,别离为继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。

继承 Thread 类,重写 run()办法,调用 start()办法启动线程

public class ThreadTest {

/**
 * 继承 Thread 类
 */
public static class MyThread extends Thread {
    @Override
    public void run() {System.out.println("This is child thread");
    }
}

public static void main(String[] args) {MyThread thread = new MyThread();
    thread.start();}

}

复制代码

实现 Runnable 接口,重写 run()办法

public class RunnableTask implements Runnable {

public void run() {System.out.println("Runnable!");
}

public static void main(String[] args) {RunnableTask task = new RunnableTask();
    new Thread(task).start();}

}

复制代码
下面两种都是没有返回值的,然而如果咱们须要获取线程的执行后果,该怎么办呢?

实现 Callable 接口,重写 call()办法,这种形式能够通过 FutureTask 获取工作执行的返回值

public class CallerTask implements Callable<String> {

public String call() throws Exception {return "Hello,i am running!";}

public static void main(String[] args) {
    // 创立异步工作
    FutureTask<String> task=new FutureTask<String>(new CallerTask());
    // 启动线程
    new Thread(task).start();
    try {
        // 期待执行实现,并获取返回后果
        String result=task.get();
        System.out.println(result);
    } catch (InterruptedException e) {e.printStackTrace();
    } catch (ExecutionException e) {e.printStackTrace();
    }
}

}

复制代码
4. 为什么调用 start() 办法时会执行 run()办法,那怎么不间接调用 run()办法?
JVM 执行 start 办法,会先创立一条线程,由创立进去的新线程去执行 thread 的 run 办法,这才起到多线程的成果。

为什么咱们不能间接调用 run()办法?也很分明,如果间接调用 Thread 的 run()办法,那么 run 办法还是运行在主线程中,相当于程序执行,就起不到多线程的成果。
5. 线程有哪些罕用的调度办法?

线程期待与告诉
在 Object 类中有一些函数能够用于线程的期待与告诉。

wait():当一个线程 A 调用一个共享变量的 wait()办法时,线程 A 会被阻塞挂起,产生上面几种状况才会返回:

(1)其它线程调用了共享对象的 notify()或者 notifyAll()办法;

(2)其余线程调用了线程 A 的 interrupt() 办法,线程 A 抛出 InterruptedException 异样返回。

wait(long timeout):这个办法相比 wait() 办法多了一个超时参数,它的不同之处在于,如果线程 A 调用共享对象的 wait(long timeout)办法后,没有在指定的 timeout ms 工夫内被其它线程唤醒,那么这个办法还是会因为超时而返回。

wait(long timeout, int nanos),其外部调用的是 wait(long timout)函数。

下面是线程期待的办法,而唤醒线程次要是上面两个办法:

notify() : 一个线程 A 调用共享对象的 notify() 办法后,会唤醒一个在这个共享变量上调用 wait 系列办法后被挂起的线程。一个共享变量上可能会有多个线程在期待,具体唤醒哪个期待的线程是随机的。
notifyAll():不同于在共享变量上调用 notify() 函数会唤醒被阻塞到该共享变量上的一个线程,notifyAll()办法则会唤醒所有在该共享变量上因为调用 wait 系列办法而被挂起的线程。

Thread 类也提供了一个办法用于期待的办法:

join():如果一个线程 A 执行了 thread.join()语句,其含意是:以后线程 A 期待 thread 线程终止之后才
从 thread.join()返回。

线程休眠

sleep(long millis) :Thread 类中的静态方法,当一个执行中的线程 A 调用了 Thread 的 sleep 办法后,线程 A 会临时让出指定工夫的执行权,然而线程 A 所领有的监视器资源,比方锁还是持有不让出的。指定的睡眠工夫到了后该函数会失常返回,接着参加 CPU 的调度,获取到 CPU 资源后就能够持续运行。

让出优先权

yield():Thread 类中的静态方法,当一个线程调用 yield 办法时,理论就是在暗示线程调度器以后线程申请让出本人的 CPU,然而线程调度器能够无条件疏忽这个暗示。

线程中断
Java 中的线程中断是一种线程间的合作模式,通过设置线程的中断标记并不能间接终止该线程的执行,而是被中断的线程依据中断状态自行处理。

void interrupt():中断线程,例如,当线程 A 运行时,线程 B 能够调用钱程 interrupt() 办法来设置线程的中断标记为 true 并立刻返回。设置标记仅仅是设置标记, 线程 A 理论并没有被中断,会持续往下执行。
boolean isInterrupted() 办法:检测以后线程是否被中断。
boolean interrupted() 办法:检测以后线程是否被中断,与 isInterrupted 不同的是,该办法如果发现以后线程被中断,则会革除中断标记。

6. 线程有几种状态?
在 Java 中,线程共有六种状态:

状态阐明 NEW 初始状态:线程被创立,但还没有调用 start()办法 RUNNABLE 运行状态:Java 线程将操作系统中的就绪和运行两种状态抽象的称作“运行”BLOCKED 阻塞状态:示意线程阻塞于锁 WAITING 期待状态:示意线程进入期待状态,进入该状态示意以后线程须要期待其余线程做出一些特定动作(告诉或中断)TIME_WAITING 超时期待状态:该状态不同于 WAITIND,它是能够在指定的工夫自行返回的 TERMINATED 终止状态:示意以后线程曾经执行结束
线程在本身的生命周期中,并不是固定地处于某个状态,而是随着代码的执行在不同的状态之间进行切换,Java 线程状态变动如图示:

7. 什么是线程上下文切换?
应用多线程的目标是为了充分利用 CPU,然而咱们晓得,并发其实是一个 CPU 来应酬多个线程。

为了让用户感觉多个线程是在同时执行的,CPU 资源的调配采纳了工夫片轮转也就是给每个线程调配一个工夫片,线程在工夫片内占用 CPU 执行工作。当线程应用完工夫片后,就会处于就绪状态并让出 CPU 让其余线程占用,这就是上下文切换。

8. 守护线程理解吗?
Java 中的线程分为两类,别离为 daemon 线程(守护线程)和 user 线程(用户线程)。
在 JVM 启动时会调用 main 函数,main 函数所在的钱程就是一个用户线程。其实在 JVM 外部同时还启动了很多守护线程,比方垃圾回收线程。
那么守护线程和用户线程有什么区别呢?区别之一是当最初一个非守护线程束时,JVM 会失常退出,而不论以后是否存在守护线程,也就是说守护线程是否完结并不影响 JVM 退出。换而言之,只有有一个用户线程还没完结,失常状况下 JVM 就不会退出。
9. 线程间有哪些通信形式?

volatile 和 synchronized 关键字

关键字 volatile 能够用来修饰字段(成员变量),就是告知程序任何对该变量的拜访均须要从共享内存中获取,而对它的扭转必须同步刷新回共享内存,它能保障所有线程对变量拜访的可见性。
关键字 synchronized 能够润饰办法或者以同步块的模式来进行应用,它次要确保多个线程在同一个时刻,只能有一个线程处于办法或者同步块中,它保障了线程对变量拜访的可见性和排他性。

期待 / 告诉机制

能够通过 Java 内置的期待 / 告诉机制(wait()/notify())实现一个线程批改一个对象的值,而另一个线程感知到了变动,而后进行相应的操作。

管道输出 / 输入流

管道输出 / 输入流和一般的文件输出 / 输入流或者网络输出 / 输入流不同之处在于,它次要用于线程之间的数据传输,而传输的媒介为内存。
管道输出 / 输入流次要包含了如下 4 种具体实现:PipedOutputStream、PipedInputStream、PipedReader 和 PipedWriter,前两种面向字节,而后两种面向字符。

应用 Thread.join()

如果一个线程 A 执行了 thread.join()语句,其含意是:以后线程 A 期待 thread 线程终止之后才从 thread.join()返回。。线程 Thread 除了提供 join()办法之外,还提供了 join(long millis)和 join(long millis,int nanos)两个具备超时个性的办法。

应用 ThreadLocal

ThreadLocal,即线程变量,是一个以 ThreadLocal 对象为键、任意对象为值的存储构造。这个构造被附带在线程上,也就是说一个线程能够依据一个 ThreadLocal 对象查问到绑定在这个线程上的一个值。
能够通过 set(T)办法来设置一个值,在以后线程下再通过 get()办法获取到原先设置的值。

对于多线程,其实很大概率还会出一些口试题,比方交替打印、银行转账、生产生产模型等等,前面老三会独自出一期来盘点一下常见的多线程口试题。

ThreadLocal
ThreadLocal 其实利用场景不是很多,但却是被炸了千百遍的面试老油条,波及到多线程、数据结构、JVM,可问的点比拟多,肯定要拿下。
10.ThreadLocal 是什么?
ThreadLocal,也就是线程本地变量。如果你创立了一个 ThreadLocal 变量,那么拜访这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,理论是操作本人本地内存外面的变量,从而起到线程隔离的作用,防止了线程平安问题。

创立

创立了一个 ThreadLoca 变量 localVariable,任何一个线程都能并发拜访 localVariable。
// 创立一个 ThreadLocal 变量
public static ThreadLocal<String> localVariable = new ThreadLocal<>();
复制代码

写入

线程能够在任何中央应用 localVariable,写入变量。
localVariable.set(“ 鄙人三某”);
复制代码

读取

线程在任何中央读取的都是它写入的变量。
localVariable.get();
复制代码
11. 你在工作中用到过 ThreadLocal 吗?
有用到过的,用来做用户信息上下文的存储。
咱们的零碎利用是一个典型的 MVC 架构,登录后的用户每次拜访接口,都会在申请头中携带一个 token,在管制层能够依据这个 token,解析出用户的根本信息。那么问题来了,如果在服务层和长久层都要用到用户信息,比方 rpc 调用、更新用户获取等等,那应该怎么办呢?
一种方法是显式定义用户相干的参数,比方账号、用户名……这样一来,咱们可能须要大面积地批改代码,多少有点瓜皮,那该怎么办呢?
这时候咱们就能够用到 ThreadLocal,在管制层拦挡申请把用户信息存入 ThreadLocal,这样咱们在任何一个中央,都能够取出 ThreadLocal 中存的用户数据。

很多其它场景的 cookie、session 等等数据隔离也都能够通过 ThreadLocal 去实现。
咱们罕用的数据库连接池也用到了 ThreadLocal:

数据库连接池的连贯交给 ThreadLoca 进行治理,保障以后线程的操作都是同一个 Connnection。

12.ThreadLocal 怎么实现的呢?
咱们看一下 ThreadLocal 的 set(T)办法,发现先获取到以后线程,再获取 ThreadLocalMap,而后把元素存到这个 map 中。

public void set(T value) {
    // 获取以后线程
    Thread t = Thread.currentThread();
    // 获取 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 讲以后元素存入 map
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

复制代码
ThreadLocal 实现的机密都在这个 ThreadLocalMap 了,能够 Thread 类中定义了一个类型为 ThreadLocal.ThreadLocalMap 的成员变量 threadLocals。
public class Thread implements Runnable {
//ThreadLocal.ThreadLocalMap 是 Thread 的属性
ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码
ThreadLocalMap 既然被称为 Map,那么毫无疑问它是 <key,value> 型的数据结构。咱们都晓得 map 的实质是一个个 <key,value> 模式的节点组成的数组,那 ThreadLocalMap 的节点是什么样的呢?

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        // 节点类
        Entry(ThreadLocal<?> k, Object v) {
            //key 赋值
            super(k);
            //value 赋值
            value = v;
        }
    }

复制代码
这里的节点,key 能够简略低视作 ThreadLocal,value 为代码中放入的值,当然实际上 key 并不是 ThreadLocal 自身,而是它的一个弱援用,能够看到 Entry 的 key 继承了 WeakReference(弱援用),再来看一下 key 怎么赋值的:

public WeakReference(T referent) {super(referent);
}

复制代码
key 的赋值,应用的是 WeakReference 的赋值。

所以,怎么答复 ThreadLocal 原理?要答出这几个点:

Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,每个线程都有一个属于本人的 ThreadLocalMap。
ThreadLocalMap 外部保护着 Entry 数组,每个 Entry 代表一个残缺的对象,key 是 ThreadLocal 的弱援用,value 是 ThreadLocal 的泛型值。
每个线程在往 ThreadLocal 里设置值的时候,都是往本人的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为援用,在本人的 map 里找对应的 key,从而实现了线程隔离。
ThreadLocal 自身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。

13.ThreadLocal 内存泄露是怎么回事?
咱们先来剖析一下应用 ThreadLocal 时的内存,咱们都晓得,在 JVM 中,栈内存线程公有,存储了对象的援用,堆内存线程共享,存储了对象实例。
所以呢,栈中存储了 ThreadLocal、Thread 的援用,堆中存储了它们的具体实例。

ThreadLocalMap 中应用的 key 为 ThreadLocal 的弱援用。

“弱援用:只有垃圾回收机制一运行,不论 JVM 的内存空间是否短缺,都会回收该对象占用的内存。”

那么当初问题就来了,弱援用很容易被回收,如果 ThreadLocal(ThreadLocalMap 的 Key)被垃圾回收器回收了,然而 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会呈现这种状况:ThreadLocalMap 的 key 没了,value 还在,这就会造成了内存透露问题。

那怎么解决内存透露问题呢?

很简略,应用完 ThreadLocal 后,及时调用 remove()办法开释内存空间。
ThreadLocal<String> localVariable = new ThreadLocal();
try {

localVariable.set(" 鄙人三某”);
……

} finally {

localVariable.remove();

}
复制代码

那为什么 key 还要设计成弱援用?

key 设计成弱援用同样是为了避免内存透露。
如果 key 被设计成强援用,如果 ThreadLocal Reference 被销毁,此时它指向 ThreadLoca 的强援用就没有了,然而此时 key 还强援用指向 ThreadLoca,就会导致 ThreadLocal 不能被回收,这时候就产生了内存透露的问题。
14.ThreadLocalMap 的构造理解吗?
ThreadLocalMap 尽管被叫做 Map,其实它是没有实现 Map 接口的,然而构造还是和 HashMap 比拟相似的,次要关注的是两个因素:元素数组和散列办法。

元素数组
一个 table 数组,存储 Entry 类型的元素,Entry 是 ThreaLocal 弱援用作为 key,Object 作为 value 的构造。

private Entry[] table;
复制代码

散列办法
散列办法就是怎么把对应的 key 映射到 table 数组的相应下标,ThreadLocalMap 用的是哈希取余法,取出 key 的 threadLocalHashCode,而后和 table 数组长度减一 & 运算(相当于取余)。

int i = key.threadLocalHashCode & (table.length – 1);
复制代码
这里的 threadLocalHashCode 计算有点货色,每创立一个 ThreadLocal 对象,它就会新增 0x61c88647,这个值很非凡,它是斐波那契数 也叫 黄金分割数。hash 增量为 这个数字,带来的益处就是 hash 散布十分平均。

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
}

复制代码
15.ThreadLocalMap 怎么解决 Hash 抵触的?
咱们可能都晓得 HashMap 应用了链表来解决抵触,也就是所谓的链地址法。
ThreadLocalMap 没有应用链表,天然也不是用链地址法来解决抵触了,它用的是另外一种形式——凋谢定址法。凋谢定址法是什么意思呢?简略来说,就是这个坑被人占了,那就接着去找空着的坑。

如上图所示,如果咱们插入一个 value=27 的数据,通过 hash 计算后应该落入第 4 个槽位中,而槽位 4 曾经有了 Entry 数据,而且 Entry 数据的 key 和以后不相等。此时就会线性向后查找,始终找到 Entry 为 null 的槽位才会进行查找,把元素放到空的槽中。
在 get 的时候,也会依据 ThreadLocal 对象的 hash 值,定位到 table 中的地位,而后判断该槽位 Entry 对象中的 key 是否和 get 的 key 统一,如果不统一,就判断下一个地位。
16.ThreadLocalMap 扩容机制理解吗?
在 ThreadLocalMap.set()办法的最初,如果执行完启发式清理工作后,未清理到任何数据,且以后散列数组中 Entry 的数量曾经达到了列表的扩容阈值 (len*2/3),就开始执行 rehash() 逻辑:
if (!cleanSomeSlots(i, sz) && sz >= threshold)

rehash();

复制代码
再着看 rehash()具体实现:这里会先去清理过期的 Entry,而后还要依据条件判断 size >= threshold – threshold / 4 也就是 size >= threshold* 3/ 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);
}

}

复制代码
接着看看具体的 resize()办法,扩容后的 newTab 的大小为老数组的两倍,而后遍历老的 table 数组,散列办法从新计算地位,凋谢地址解决抵触,而后放到新的 newTab,遍历实现之后,oldTab 中所有的 entry 数据都曾经放入到 newTab 中了,而后 table 援用指向 newTab

具体代码:

17. 父子线程怎么共享数据?
父线程能用 ThreadLocal 来给子线程传值吗?毫无疑问,不能。那该怎么办?
这时候能够用到另外一个类——InheritableThreadLocal。
应用起来很简略,在主线程的 InheritableThreadLocal 实例设置值,在子线程中就能够拿到了。
public class InheritableThreadLocalTest {


public static void main(String[] args) {final ThreadLocal threadLocal = new InheritableThreadLocal();
    // 主线程
    threadLocal.set("不擅技术");
    // 子线程
    Thread t = new Thread() {
        @Override
        public void run() {super.run();
            System.out.println("鄙人三某," + threadLocal.get());
        }
    };
    t.start();}

}
复制代码

那原理是什么呢?

原理很简略,在 Thread 类里还有另外一个变量:
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
复制代码
在 Thread.init 的时候,如果父线程的 inheritableThreadLocals 不为空,就把它赋给以后线程(子线程)的 inheritableThreadLocals。

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

复制代码
Java 内存模型
18. 说一下你对 Java 内存模型(JMM)的了解?
Java 内存模型(Java Memory Model,JMM),是一种形象的模型,被定义进去屏蔽各种硬件和操作系统的内存拜访差别。
JMM 定义了线程和主内存之间的形象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个公有的本地内存(Local Memory),本地内存中存储了该线程以读 / 写共享变量的正本。
Java 内存模型的形象图:

本地内存是 JMM 的 一个抽象概念,并不实在存在。它其实涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。

图外面的是一个双核 CPU 零碎架构,每个核有本人的控制器和运算器,其中控制器蕴含一组寄存器和操作控制器,运算器执行算术逻辅运算。每个核都有本人的一级缓存,在有些架构外面还有一个所有 CPU 共享的二级缓存。那么 Java 内存模型外面的工作内存,就对应这里的 Ll 缓存或者 L2 缓存或者 CPU 寄存器。
19. 说说你对原子性、可见性、有序性的了解?
原子性、有序性、可见性是并发编程中十分重要的根底概念,JMM 的很多技术都是围绕着这三大个性开展。

原子性:原子性指的是一个操作是不可分割、不可中断的,要么全副执行并且执行的过程不会被任何因素打断,要么就全不执行。
可见性:可见性指的是一个线程批改了某一个共享变量的值时,其它线程可能立刻晓得这个批改。
有序性:有序性指的是对于一个线程的执行代码,从前往后顺次执行,单线程下能够认为程序是有序的,然而并发时有可能会产生指令重排。

剖析上面几行代码的原子性?

int i = 2;
int j = i;
i++;
i = i + 1;
复制代码

第 1 句是根本类型赋值,是原子性操作。
第 2 句先读 i 的值,再赋值到 j,两步操作,不能保障原子性。
第 3 和第 4 句其实是等效的,先读取 i 的值,再 +1,最初赋值到 i,三步操作了,不能保障原子性。

原子性、可见性、有序性都应该怎么保障呢?

原子性:JMM 只能保障根本的原子性,如果要保障一个代码块的原子性,须要应用 synchronized。
可见性:Java 是利用 volatile 关键字来保障可见性的,除此之外,final 和 synchronized 也能保障可见性。
有序性:synchronized 或者 volatile 都能够保障多线程之间操作的有序性。

20. 那说说什么是指令重排?
在执行程序时,为了进步性能,编译器和处理器经常会对指令做重排序。重排序分 3 种类型。

编译器优化的重排序。编译器在不扭转单线程程序语义的前提下,能够重新安排语句的执行程序。
指令级并行的重排序。古代处理器采纳了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器能够扭转语句对应 机器指令的执行程序。
内存零碎的重排序。因为处理器应用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终理论执行的指令序列,会别离经验上面 3 种重排序,如图:

咱们比拟相熟的双重校验单例模式就是一个经典的指令重排的例子,Singleton instance=new Singleton();对应的 JVM 指令分为三步:分配内存空间 –> 初始化对象 —> 对象指向调配的内存空间,然而通过了编译器的指令重排序,第二步和第三步就可能会重排序。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供统一的内存可见性保障。
21. 指令重排有限度吗?happens-before 理解吗?
指令重排也是有一些限度的,有两个规定 happens-before 和 as-if-serial 来束缚。
happens-before 的定义:

如果一个操作 happens-before 另一个操作,那么第一个操作的执行后果将对第二个操作可见,而且第一个操作的执行程序排在第二个操作之前。
两个操作之间存在 happens-before 关系,并不意味着 Java 平台的具体实现必须要依照 happens-before 关系指定的程序来执行。如果重排序之后的执行后果,与按 happens-before 关系来执行的后果统一,那么这种重排序并不非法

happens-before 和咱们非亲非故的有六大规定:

程序程序规定:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
监视器锁规定:对一个锁的解锁,happens-before 于随后对这个锁的加锁。
volatile 变量规定:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。
start()规定:如果线程 A 执行操作 ThreadB.start()(启动线程 B),那么 A 线程的 ThreadB.start()操作 happens-before 于线程 B 中的任意操作。
join()规定:如果线程 A 执行操作 ThreadB.join()并胜利返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作胜利返回。

22.as-if-serial 又是什么?单线程的程序肯定是程序的吗?
as-if-serial 语义的意思是:不管怎么重排序(编译器和处理器为了进步并行度),单线程程序的执行后果不能被扭转。编译器、runtime 和处理器都必须恪守 as-if-serial 语义。
为了恪守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会扭转执行后果。然而,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。为了具体阐明,请看上面计算圆面积的代码示例。
double pi = 3.14; // A
double r = 1.0; // B
double area = pi r r; // C
复制代码
下面 3 个操作的数据依赖关系:

A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因而在最终执行的指令序列中,C 不能被重排序到 A 和 B 的后面(C 排到 A 和 B 的后面,程序的后果将会被扭转)。但 A 和 B 之间没有数据依赖关系,编译器和处理器能够重排序 A 和 B 之间的执行程序。
所以最终,程序可能会有两种执行程序:

as-if-serial 语义把单线程程序爱护了起来,恪守 as-if-serial 语义的编译器、runtime 和处理器独特编织了这么一个“楚门的世界”:单线程程序是按程序的“程序”来执行的。as- if-serial 语义使单线程状况下,咱们不须要放心重排序的问题,可见性的问题。
23.volatile 实现原理理解吗?
volatile 有两个作用,保障可见性和有序性。

volatile 怎么保障可见性的呢?

相比 synchronized 的加锁形式来解决共享变量的内存可见性问题,volatile 就是更轻量的抉择,它没有上下文切换的额定开销老本。
volatile 能够确保对某个变量的更新对其余线程马上可见,一个变量被申明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其余中央,而是会把值刷新回主内存 当其它线程读取该共享变量,会从主内存从新获取最新值,而不是应用以后线程的本地内存中的值。
例如,咱们申明一个 volatile 变量 volatile int x = 0,线程 A 批改 x =1,批改完之后就会把新的值刷新回主内存,线程 B 读取 x 的时候,就会清空本地内存变量,而后再从主内存获取最新值。

volatile 怎么保障有序性的呢?

重排序能够分为编译器重排序和处理器重排序,valatile 保障有序性,就是通过别离限度这两种类型的重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

在每个 volatile 写操作的后面插入一个 StoreStore 屏障
在每个 volatile 写操作的前面插入一个 StoreLoad 屏障
在每个 volatile 读操作的前面插入一个 LoadLoad 屏障
在每个 volatile 读操作的前面插入一个 LoadStore 屏障


24.synchronized 用过吗?怎么应用?
synchronized 常常用的,用来保障代码的原子性。
synchronized 次要有三种用法:

润饰实例办法: 作用于以后对象实例加锁,进入同步代码前要取得 以后对象实例的锁

synchronized void method() {
// 业务代码
}
复制代码

润饰静态方法:也就是给以后类加锁,会作⽤于类的所有对象实例,进⼊同步代码前要取得以后 class 的锁。因为动态成员不属于任何⼀个实例对象,是类成员(static 表明这是该类的⼀个动态资源,不论 new 了多少个对象,只有⼀份)。
如果⼀个线程 A 调⽤⼀个实例对象的⾮动态 synchronized ⽅法,⽽线程 B 须要调⽤这个实例对象所属类的动态 synchronized ⽅法,是容许的,不会发⽣互斥景象,因为拜访动态 synchronized ⽅法占⽤的锁是以后类的锁,⽽拜访⾮动态 synchronized ⽅法占⽤的锁是以后实例对象锁。

synchronized void staic method() {
// 业务代码
}
复制代码

润饰代码块:指定加锁对象,对给定对象 / 类加锁。synchronized(this|object) 示意进⼊同步代码库前要取得给定对象的锁。synchronized(类.class) 示意进⼊同步代码前要取得 以后 class 的锁

synchronized(this) {
// 业务代码
}
复制代码
25.synchronized 的实现原理?

synchronized 是怎么加锁的呢?

咱们应用 synchronized 的时候,发现不必本人去 lock 和 unlock,是因为 JVM 帮咱们把这个事件做了。

synchronized 润饰代码块时,JVM 采纳 monitorenter、monitorexit 两个指令来实现同步,monitorenter 指令指向同步代码块的开始地位,monitorexit 指令则指向同步代码块的完结地位。
反编译一段 synchronized 润饰代码块代码,javap -c -s -v -l SynchronizedDemo.class,能够看到相应的字节码指令。

synchronized 润饰同步办法时,JVM 采纳 ACC_SYNCHRONIZED 标记符来实现同步,这个标识指明了该办法是一个同步办法。
同样能够写段代码反编译看一下。

synchronized 锁住的是什么呢?

monitorenter、monitorexit 或者 ACC_SYNCHRONIZED 都是基于 Monitor 实现的。
实例对象构造里有对象头,对象头外面有一块构造叫 Mark Word,Mark Word 指针指向了 monitor。
所谓的 Monitor 其实是一种同步工具,也能够说是一种同步机制。在 Java 虚拟机(HotSpot)中,Monitor 是由 ObjectMonitor 实现的,能够叫做外部锁,或者 Monitor 锁。
ObjectMonitor 的工作原理:

ObjectMonitor 有两个队列:_WaitSet、_EntryList,用来保留 ObjectWaiter 对象列表。
_owner,获取 Monitor 对象的线程进入 _owner 区时,_count + 1。如果线程调用了 wait() 办法,此时会开释 Monitor 对象,_owner 复原为空,_count – 1。同时该期待线程进入 _WaitSet 中,期待被唤醒。

ObjectMonitor() {

_header       = NULL;
_count        = 0; // 记录线程获取锁的次数
_waiters      = 0,
_recursions   = 0;  // 锁的重入次数
_object       = NULL;
_owner        = NULL;  // 指向持有 ObjectMonitor 对象的线程
_WaitSet      = NULL;  // 处于 wait 状态的线程,会被退出到_WaitSet
_WaitSetLock  = 0 ;
_Responsible  = NULL ;
_succ         = NULL ;
_cxq          = NULL ;
FreeNext      = NULL ;
_EntryList    = NULL ;  // 处于期待锁 block 状态的线程,会被退出到该列表
_SpinFreq     = 0 ;
_SpinClock    = 0 ;
OwnerIsThread = 0 ;

}
复制代码
能够类比一个去医院就诊的例子[18]:

首先,患者在门诊大厅前台或自助挂号机进行挂号;

随后,挂号完结后患者找到对应的诊室就诊:

诊室每次只能有一个患者就诊;
如果此时诊室闲暇,间接进入就诊;
如果此时诊室内有其它患者就诊,那么以后患者进入候诊室,期待叫号;

就诊完结后,走出就诊室,候诊室的下一位候诊患者进入就诊室。

这个过程就和 Monitor 机制比拟类似:

门诊大厅:所有待进入的线程都必须先在入口 Entry Set 挂号才有资格;
就诊室:就诊室_Owner 里里只能有一个线程就诊,就诊完线程就自行来到
候诊室:就诊室忙碌时,进入期待区(Wait Set),就诊室闲暇的时候就从 期待区(Wait Set)叫新的线程

所以咱们就晓得了,同步是锁住的什么货色:

monitorenter,在判断领有同步标识 ACC_SYNCHRONIZED 领先进入此办法的线程会优先领有 Monitor 的 owner,此时计数器 +1。
monitorexit,当执行完退出后,计数器 -1,归 0 后被其余进入的线程取得。

26. 除了原子性,synchronized 可见性,有序性,可重入性怎么实现?

synchronized 怎么保障可见性?

线程加锁前,将清空工作内存中共享变量的值,从而应用共享变量时须要从主内存中从新读取最新的值。
线程加锁后,其它线程无奈获取主内存中的共享变量。
线程解锁前,必须把共享变量的最新值刷新到主内存中。

synchronized 怎么保障有序性?

synchronized 同步的代码块,具备排他性,一次只能被一个线程领有,所以 synchronized 保障同一时刻,代码是单线程执行的。
因为 as-if-serial 语义的存在,单线程的程序能保障最终后果是有序的,然而不保障不会指令重排。
所以 synchronized 保障的有序是执行后果的有序性,而不是避免指令重排的有序性。

synchronized 怎么实现可重入的呢?

synchronized 是可重入锁,也就是说,容许一个线程二次申请本人持有对象锁的临界资源,这种状况称为可重入锁。
synchronized 锁对象的时候有个计数器,他会记录下线程获取锁的次数,在执行完对应的代码块之后,计数器就会 -1,直到计数器清零,就开释锁了。
之所以,是可重入的。是因为 synchronized 锁对象有个计数器,会随着线程获取锁后 +1 计数,当线程执行结束后 -1,直到清零开释锁。
27. 锁降级?synchronized 优化理解吗?
理解锁降级,得先晓得,不同锁的状态是什么样的。这个状态指的是什么呢?
Java 对象头里,有一块构造,叫 Mark Word 标记字段,这块构造会随着锁的状态变动而变动。
64 位虚拟机 Mark Word 是 64bit,咱们来看看它的状态变动:

Mark Word 存储对象本身的运行数据,如哈希码、GC 分代年龄、锁状态标记、偏差工夫戳(Epoch)等。

synchronized 做了哪些优化?

在 JDK1.6 之前,synchronized 的实现间接调用 ObjectMonitor 的 enter 和 exit,这种锁被称之为重量级锁。从 JDK6 开始,HotSpot 虚拟机开发团队对 Java 中的锁进行优化,如减少了适应性自旋、锁打消、锁粗化、轻量级锁和偏差锁等优化策略,晋升了 synchronized 的性能。

偏差锁:在无竞争的状况下,只是在 Mark Word 里存储以后线程指针,CAS 操作都不做。

轻量级锁:在没有多线程竞争时,绝对重量级锁,缩小操作系统互斥量带来的性能耗费。然而,如果存在锁竞争,除了互斥量自身开销,还额定有 CAS 操作的开销。

自旋锁:缩小不必要的 CPU 上下文切换。在轻量级锁降级为重量级锁时,就应用了自旋加锁的形式

锁粗化:将多个间断的加锁、解锁操作连贯在一起,扩大成一个范畴更大的锁。

锁打消:虚拟机即时编译器在运行时,对一些代码上要求同步,然而被检测到不可能存在共享数据竞争的锁进行打消。

锁降级的过程是什么样的?

锁降级方向:无锁 –> 偏差锁 —> 轻量级锁 —-> 重量级锁,这个方向基本上是不可逆的。

咱们看一下降级的过程:
偏差锁:
偏差锁的获取:

判断是否为可偏差状态 –MarkWord 中锁标记是否为‘01’,是否偏差锁是否为‘1’
如果是可偏差状态,则查看线程 ID 是否为以后线程,如果是,则进入步骤 ’5’,否则进入步骤‘3’
通过 CAS 操作竞争锁,如果竞争胜利,则将 MarkWord 中线程 ID 设置为以后线程 ID,而后执行‘5’;竞争失败,则执行‘4’
CAS 获取偏差锁失败示意有竞争。当达到 safepoint 时取得偏差锁的线程被挂起,偏差锁降级为轻量级锁,而后被阻塞在平安点的线程持续往下执行同步代码块
执行同步代码

偏差锁的撤销:

偏差锁不会被动开释 (撤销),只有遇到其余线程竞争时才会执行撤销,因为撤销须要晓得以后持有该偏差锁的线程栈状态,因而要等到 safepoint 时执行,此时持有该偏差锁的线程(T)有‘2’,‘3’两种状况;
撤销 —- T 线程曾经退出同步代码块,或者曾经不再存活,则间接撤销偏差锁,变成无锁状态 —- 该状态达到阈值 20 则执行批量重偏差
降级 —- T 线程还在同步代码块中,则将 T 线程的偏差锁降级为轻量级锁,以后线程执行轻量级锁状态下的锁获取步骤 —- 该状态达到阈值 40 则执行批量撤销

轻量级锁:
轻量级锁的获取:

进行加锁操作时,jvm 会判断是否曾经时重量级锁,如果不是,则会在以后线程栈帧中划出一块空间,作为该锁的锁记录,并且将锁对象 MarkWord 复制到该锁记录中
复制胜利之后,jvm 应用 CAS 操作将对象头 MarkWord 更新为指向锁记录的指针,并将锁记录里的 owner 指针指向对象头的 MarkWord。如果胜利,则执行‘3’,否则执行‘4’
更新胜利,则以后线程持有该对象锁,并且对象 MarkWord 锁标记设置为‘00’,即示意此对象处于轻量级锁状态
更新失败,jvm 先查看对象 MarkWord 是否指向以后线程栈帧中的锁记录,如果是则执行‘5’,否则执行‘4’
示意锁重入;而后以后线程栈帧中减少一个锁记录第一局部(Displaced Mark Word)为 null,并指向 Mark Word 的锁对象,起到一个重入计数器的作用。
示意该锁对象曾经被其余线程抢占,则进行自旋期待(默认 10 次),期待次数达到阈值仍未获取到锁,则降级为重量级锁

大体上省简的降级过程:

残缺的降级过程:

28. 说说 synchronized 和 ReentrantLock 的区别?
能够从锁的实现、性能特点、性能等几个维度去答复这个问题:

锁的实现:synchronized 是 Java 语言的关键字,基于 JVM 实现。而 ReentrantLock 是基于 JDK 的 API 层面实现的(个别是 lock()和 unlock()办法配合 try/finally 语句块来实现。)
性能:在 JDK1.6 锁优化以前,synchronized 的性能比 ReenTrantLock 差很多。然而 JDK6 开始,减少了适应性自旋、锁打消等,两者性能就差不多了。
性能特点:ReentrantLock 比 synchronized 减少了一些高级性能,如期待可中断、可实现偏心锁、可实现选择性告诉。

ReentrantLock 提供了一种可能中断期待锁的线程的机制,通过 lock.lockInterruptibly()来实现这个机制
ReentrantLock 能够指定是偏心锁还是非偏心锁。而 synchronized 只能是非偏心锁。所谓的偏心锁就是先期待的线程先取得锁。
synchronized 与 wait() 和 notify()/notifyAll()办法联合实现期待 / 告诉机制,ReentrantLock 类借助 Condition 接口与 newCondition()办法实现。
ReentrantLock 须要手工申明来加锁和开释锁,个别跟 finally 配合开释锁。而 synchronized 不必手动开释锁。

上面的表格列出出了两种锁之间的区别:

29.AQS 理解多少?
AbstractQueuedSynchronizer 形象同步队列,简称 AQS,它是 Java 并发包的根基,并发包中的锁就是基于 AQS 实现的。

AQS 是基于一个 FIFO 的双向队列,其外部定义了一个节点类 Node,Node 节点外部的 SHARED 用来标记该线程是获取共享资源时被阻挂起后放入 AQS 队列的,EXCLUSIVE 用来标记线程是 取独占资源时被挂起后放入 AQS 队列
AQS 应用一个 volatile 润饰的 int 类型的成员变量 state 来示意同步状态,批改同步状态胜利即为取得锁,volatile 保障了变量在多线程之间的可见性,批改 State 值时通过 CAS 机制来保障批改的原子性
获取 state 的形式分为两种,独占形式和共享形式,一个线程应用独占形式获取了资源,其它线程就会在获取失败后被阻塞。一个线程应用共享形式获取了资源,另外一个线程还能够通过 CAS 的形式进行获取。
如果共享资源被占用,须要肯定的阻塞期待唤醒机制来保障锁的调配,AQS 中会将竞争共享资源失败的线程增加到一个变体的 CLH 队列中。

先简略理解一下 CLH:Craig、Landin and Hagersten 队列,是 单向链表实现的队列。申请线程只在本地变量上自旋,它一直轮询前驱的状态,如果发现 前驱节点开释了锁就完结自旋

AQS 中的队列是 CLH 变体的虚构双向队列,通过将每条申请共享资源的线程封装成一个节点来实现锁的调配:

AQS 中的 CLH 变体期待队列领有以下个性:

AQS 中队列是个双向链表,也是 FIFO 先进先出的个性
通过 Head、Tail 头尾两个节点来组成队列构造,通过 volatile 润饰保障可见性
Head 指向节点为已取得锁的节点,是一个虚构节点,节点自身不持有具体线程
获取不到同步状态,会将节点进行自旋获取锁,自旋肯定次数失败后会将线程阻塞,绝对于 CLH 队列性能较好

ps:AQS 源码外面有很多细节可问,倡议有工夫好好看看 AQS 源码。
30.ReentrantLock 实现原理?
ReentrantLock 是可重入的独占锁,只能有一个线程能够获取该锁,其它获取该锁的线程会被阻塞而被放入该锁的阻塞队列外面。
看看 ReentrantLock 的加锁操作:

// 创立非偏心锁
ReentrantLock lock = new ReentrantLock();
// 获取锁操作
lock.lock();
try {// 执行代码逻辑} catch (Exception ex) {// ...} finally {
    // 解锁操作
    lock.unlock();}

复制代码
new ReentrantLock() 构造函数默认创立的是非偏心锁 NonfairSync。
偏心锁 FairSync

偏心锁是指多个线程依照申请锁的程序来获取锁,线程间接进入队列中排队,队列中的第一个线程能力取得锁
偏心锁的长处是期待锁的线程不会饿死。毛病是整体吞吐效率绝对非偏心锁要低,期待队列中除第一个线程以外的所有线程都会阻塞,CPU 唤醒阻塞线程的开销比非偏心锁大

非偏心锁 NonfairSync

非偏心锁是多个线程加锁时间接尝试获取锁,获取不到才会到期待队列的队尾期待。但如果此时锁刚好可用,那么这个线程能够无需阻塞间接获取到锁
非偏心锁的长处是能够缩小唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞间接取得锁,CPU 不用唤醒所有线程。毛病是处于期待队列中的线程可能会饿死,或者等很久才会取得锁

默认创立的对象 lock()的时候:

如果锁以后没有被其它线程占用,并且以后线程之前没有获取过该锁,则以后线程会获取到该锁,而后设置以后锁的拥有者为以后线程,并设置 AQS 的状态值为 1,而后间接返回。如果以后线程之前己经获取过该锁,则这次只是简略地把 AQS 的状态值加 1 后返回。
如果该锁己经被其余线程持有,非偏心锁会尝试去获取锁,获取失败的话,则调用该办法线程会被放入 AQS 队列阻塞挂起。

31.ReentrantLock 怎么实现偏心锁的?
new ReentrantLock() 构造函数默认创立的是非偏心锁 NonfairSync
public ReentrantLock() {

sync = new NonfairSync();

}
复制代码
同时也能够在创立锁构造函数中传入具体参数创立偏心锁 FairSync
ReentrantLock lock = new ReentrantLock(true);
— ReentrantLock
// true 代表偏心锁,false 代表非偏心锁
public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}
复制代码
FairSync、NonfairSync 代表偏心锁和非偏心锁,两者都是 ReentrantLock 动态外部类,只不过实现不同锁语义。
非偏心锁和偏心锁的两处不同:

非偏心锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候凑巧锁没有被占用,那么间接就获取到锁返回了。
非偏心锁在 CAS 失败后,和偏心锁一样都会进入到 tryAcquire 办法,在 tryAcquire 办法中,如果发现锁这个时候被开释了(state == 0),非偏心锁会间接 CAS 抢锁,然而偏心锁会判断期待队列是否有线程处于期待状态,如果有则不去抢锁,乖乖排到前面。

相对来说,非偏心锁会有更好的性能,因为它的吞吐量比拟大。当然,非偏心锁让获取锁的工夫变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
32.CAS 呢?CAS 理解多少?
CAS 叫做 CompareAndSwap,⽐较并替换,次要是通过处理器的指令来保障操作的原⼦性的。
CAS 指令蕴含 3 个参数:共享变量的内存地址 A、预期的值 B 和共享变量的新值 C。
只有当内存中地址 A 处的值等于 B 时,能力将内存中地址 A 处的值更新为新值 C。作为一条 CPU 指令,CAS 指令自身是可能保障原子性的。
33.CAS 有什么问题?如何解决?
CAS 的经典三大问题:

ABA 问题
并发环境下,假如初始条件是 A,去批改数据时,发现是 A 就会执行批改。然而看到的尽管是 A,两头可能产生了 A 变 B,B 又变回 A 的状况。此时 A 曾经非彼 A,数据即便胜利批改,也可能有问题。

怎么解决 ABA 问题?

加版本号

每次批改变量,都在这个变量的版本号上加 1,这样,刚刚 A ->B->A,尽管 A 的值没变,然而它的版本号曾经变了,再判断版本号就会发现此时的 A 曾经被改过了。参考乐观锁的版本号,这种做法能够给数据带上了一种实效性的测验。
Java 提供了 AtomicStampReference 类,它的 compareAndSet 办法首先查看以后的对象援用值是否等于预期援用,并且以后印戳(Stamp)标记是否等于预期标记,如果全副相等,则以原子形式将援用值和印戳标记的值更新为给定的更新值。
循环性能开销
自旋 CAS,如果始终循环执行,始终不胜利,会给 CPU 带来十分大的执行开销。

怎么解决循环性能开销问题?

在 Java 中,很多应用自旋 CAS 的中央,会有一个自旋次数的限度,超过肯定次数,就进行自旋。
只能保障一个变量的原子操作
CAS 保障的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无奈间接保障操作的原子性的。

怎么解决只能保障一个变量的原子操作问题?

能够思考改用锁来保障操作的原子性
能够思考合并多个变量,将多个变量封装成一个对象,通过 AtomicReference 来保障原子性。

34.Java 有哪些保障原子性的办法?如何保障多线程下 i ++ 后果正确?

应用循环原子类,例如 AtomicInteger,实现 i ++ 原子操作
应用 juc 包下的锁,如 ReentrantLock,对 i ++ 操作加锁 lock.lock()来实现原子性
应用 synchronized,对 i ++ 操作加锁

35. 原子操作类理解多少?
当程序更新一个变量时,如果多线程同时更新这个变量,可能失去冀望之外的值,比方变量 i =1,A 线程更新 i +1,B 线程也更新 i +1,通过两个线程操作之后可能 i 不等于 3,而是等于 2。因为 A 和 B 线程在更新变量 i 的时候拿到的 i 都是 1,这就是线程不平安的更新操作,个别咱们会应用 synchronized 来解决这个问题,synchronized 会保障多线程不会同时更新变量 i。
其实除此之外,还有更轻量级的抉择,Java 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包,这个包中的原子操作类提供了一种用法简略、性能高效、线程平安地更新一个变量的形式。
因为变量的类型有很多种,所以在 Atomic 包里一共提供了 13 个类,属于 4 种类型的原子更新形式,别离是原子更新根本类型、原子更新数组、原子更新援用和原子更新属性(字段)。

Atomic 包里的类根本都是应用 Unsafe 实现的包装类。
应用原子的形式更新根本类型,Atomic 包提供了以下 3 个类:

AtomicBoolean:原子更新布尔类型。

AtomicInteger:原子更新整型。

AtomicLong:原子更新长整型。

通过原子的形式更新数组里的某个元素,Atomic 包提供了以下 4 个类:

AtomicIntegerArray:原子更新整型数组里的元素。

AtomicLongArray:原子更新长整型数组里的元素。

AtomicReferenceArray:原子更新援用类型数组里的元素。

AtomicIntegerArray 类次要是提供原子的形式更新数组里的整型

原子更新根本类型的 AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就须要应用这个原子更新援用类型提供的类。Atomic 包提供了以下 3 个类:

AtomicReference:原子更新援用类型。

AtomicReferenceFieldUpdater:原子更新援用类型里的字段。

AtomicMarkableReference:原子更新带有标记位的援用类型。能够原子更新一个布尔类型的标记位和援用类型。构造方法是 AtomicMarkableReference(V initialRef,boolean initialMark)。

如果需原子地更新某个类里的某个字段时,就须要应用原子更新字段类,Atomic 包提供了以下 3 个类进行原子字段更新:

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新长整型字段的更新器。
AtomicStampedReference:原子更新带有版本号的援用类型。该类将整数值与援用关联起来,可用于原子的更新数据和数据的版本号,能够解决应用 CAS 进行原子更新时可能呈现的 ABA 问题。

36.AtomicInteger 的原理?
一句话概括:应用 CAS 实现。
以 AtomicInteger 的增加办法为例:

public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
}

复制代码
通过 Unsafe 类的实例来进行增加操作,来看看具体的 CAS 操作:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

复制代码
compareAndSwapInt 是一个 native 办法,基于 CAS 来操作 int 类型变量。其它的原子操作类根本都是大同小异。
37. 线程死锁理解吗?该如何防止?
死锁是指两个或两个以上的线程在执行过程中,因抢夺资源而造成的相互期待的景象,在无外力作用的状况下,这些线程会始终互相期待而无奈持续运行上来。

那么为什么会产生死锁呢?死锁的产生必须具备以下四个条件:

互斥条件:指线程对己经获取到的资源进行它性应用,即该资源同时只由一个线程占用。如果此时还有其它线程申请获取获取该资源,则请求者只能期待,直至占有资源的线程开释该资源。
申请并持有条件:指一个 线程己经持有了至多一个资源,但又提出了新的资源申请,而新资源己被其它线程占有,所以以后线程会被阻塞,但阻塞 的同时并不开释本人曾经获取的资源。
不可剥夺条件:指线程获取到的资源在本人应用完之前不能被其它线程抢占,只有在本人应用结束后才由本人开释该资源。
环路期待条件:指在产生死锁时,必然存在一个线程——资源的环形链,即线程汇合 {T0,T1,T2,……,Tn} 中 T0 正在期待一 T1 占用的资源,Tl1 正在期待 T2 用的资源,…… Tn 在期待己被 T0 占用的资源。

该如何防止死锁呢?答案是至多毁坏死锁产生的一个条件。

其中,互斥这个条件咱们没有方法毁坏,因为用锁为的就是互斥。不过其余三个条件都是有方法毁坏掉的,到底如何做呢?

对于“申请并持有”这个条件,能够一次性申请所有的资源。

对于“不可剥夺”这个条件,占用局部资源的线程进一步申请其余资源时,如果申请不到,能够被动开释它占有的资源,这样不可抢占这个条件就毁坏掉了。

对于“环路期待”这个条件,能够靠按序申请资源来预防。所谓按序申请,是指资源是有线性程序的,申请的时候能够先申请资源序号小的,再申请资源序号大的,这样线性化后就不存在环路了。

38. 那死锁问题怎么排查呢?
能够应用 jdk 自带的命令行工具排查:

应用 jps 查找运行的 Java 过程:jps -l
应用 jstack 查看线程堆栈信息:jstack -l 过程 id

根本就能够看到死锁的信息。
还能够利用图形化工具,比方 JConsole。呈现线程死锁当前,点击 JConsole 线程面板的检测到死锁按钮,将会看到线程的死锁信息。

并发工具类
39.CountDownLatch(倒计数器)理解吗?
CountDownLatch,倒计数器,有两个常见的利用场景[18]:
场景 1:协调子线程完结动作:期待所有子线程运行完结
CountDownLatch 容许一个或多个线程期待其余线程实现操作。
例如,咱们很多人喜爱玩的王者光荣,开黑的时候,得等所有人都上线之后,能力开打。

CountDownLatch 模拟这个场景 (参考[18]):
创立大乔、兰陵王、安其拉、哪吒和铠等五个玩家,主线程必须在他们都实现确认后,才能够持续运行。
在这段代码中,new CountDownLatch(5)用户创立初始的 latch 数量,各玩家通过 countDownLatch.countDown()实现状态确认,主线程通过 countDownLatch.await()期待。

public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(5);

    Thread 大乔 = new Thread(countDownLatch::countDown);
    Thread 兰陵王 = new Thread(countDownLatch::countDown);
    Thread 安其拉 = new Thread(countDownLatch::countDown);
    Thread 哪吒 = new Thread(countDownLatch::countDown);
    Thread 铠 = new Thread(() -> {
        try {
            // 稍等,上个卫生间,马上到...
            Thread.sleep(1500);
            countDownLatch.countDown();} catch (InterruptedException ignored) {}});

    大乔.start();
    兰陵王.start();
    安其拉.start();
    哪吒.start();
    铠.start();
    countDownLatch.await();
    System.out.println("所有玩家曾经就位!");
}

复制代码
场景 2. 协调子线程开始动作:对立各线程动作开始的机会
王者游戏中也有相似的场景,游戏开始时,各玩家的初始状态必须统一。不能有的玩家都出完装了,有的才诞生。
所以大家得一块出世,在

在这个场景中,依然用五个线程代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。须要留神的是,各玩家尽管都调用了 start()线程,然而它们在运行时都在期待 countDownLatch 的信号,在信号未收到前,它们不会往下执行。

public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(1);

    Thread 大乔 = new Thread(() -> waitToFight(countDownLatch));
    Thread 兰陵王 = new Thread(() -> waitToFight(countDownLatch));
    Thread 安其拉 = new Thread(() -> waitToFight(countDownLatch));
    Thread 哪吒 = new Thread(() -> waitToFight(countDownLatch));
    Thread 铠 = new Thread(() -> waitToFight(countDownLatch));

    大乔.start();
    兰陵王.start();
    安其拉.start();
    哪吒.start();
    铠.start();
    Thread.sleep(1000);
    countDownLatch.countDown();
    System.out.println("敌方还有 5 秒达到战场,全军出击!");
}

private static void waitToFight(CountDownLatch countDownLatch) {
    try {countDownLatch.await(); // 在此期待信号再持续
        System.out.println("收到,发动防御!");
    } catch (InterruptedException e) {e.printStackTrace();
    }
}

复制代码
CountDownLatch 的外围办法也不多:

await():期待 latch 降为 0;
boolean await(long timeout, TimeUnit unit):期待 latch 降为 0,然而能够设置超时工夫。比方有玩家超时未确认,那就从新匹配,总不能为了某个玩家等到天荒地老。
countDown():latch 数量减 1;
getCount():获取以后的 latch 数量。

40.CyclicBarrier(同步屏障)理解吗?
CyclicBarrier 的字面意思是可循环应用(Cyclic)的屏障(Barrier)。它要做的事件是,让一 组线程达到一个屏障(也能够叫同步点)时被阻塞,直到最初一个线程达到屏障时,屏障才会开门,所有被屏障拦挡的线程才会持续运行。
它和 CountDownLatch 相似,都能够协调多线程的完结动作,在它们完结后都能够执行特定动作,然而为什么要有 CyclicBarrier,天然是它有和 CountDownLatch 不同的中央。
不晓得你听没听过一个新人 UP 主小约翰可汗,小约翰生平有两大恨——“想结衣结衣不依, 迷爱理爱理不理。”咱们来还原一下事件的通过:小约翰在亲政后意识了新垣结衣,于是决定第一次选妃,向结衣表白,期待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,赌咒生平不娶,忽然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,期待回应。

咱们拿代码模仿这一场景,发现 CountDownLatch 无能为力了,因为 CountDownLatch 的应用是一次性的,无奈反复利用,而这里期待了两次。此时,咱们用 CyclicBarrier 就能够实现,因为它能够反复利用。

运行后果:

CyclicBarrier 最最外围的办法,依然是 await():

如果以后线程不是第一个达到屏障的话,它将会进入期待,直到其余线程都达到,除非产生被中断、屏障被拆除、屏障被重设等状况;

下面的例子形象一下,实质上它的流程就是这样就是这样:

41.CyclicBarrier 和 CountDownLatch 有什么区别?
两者最外围的区别[18]:

CountDownLatch 是一次性的,而 CyclicBarrier 则能够屡次设置屏障,实现反复利用;
CountDownLatch 中的各个子线程不能够期待其余线程,只能实现本人的工作;而 CyclicBarrier 中的各个线程能够期待其余线程

它们区别用一个表格整顿:

CyclicBarrierCountDownLatchCyclicBarrier 是可重用的,其中的线程会期待所有的线程实现工作。届时,屏障将被拆除,并能够选择性地做一些特定的动作。CountDownLatch 是一次性的,不同的线程在同一个计数器上工作,直到计数器为 0.CyclicBarrier 面向的是线程数 CountDownLatch 面向的是工作数在应用 CyclicBarrier 时,你必须在结构中指定参加合作的线程数,这些线程必须调用 await()办法应用 CountDownLatch 时,则必须要指定工作数,至于这些工作由哪些线程实现无关紧要 CyclicBarrier 能够在所有的线程开释后从新应用 CountDownLatch 在计数器为 0 时不能再应用在 CyclicBarrier 中,如果某个线程遇到了中断、超时等问题时,则处于 await 的线程都会呈现问题在 CountDownLatch 中,如果某个线程呈现问题,其余线程不受影响
42.Semaphore(信号量)理解吗?
Semaphore(信号量)是用来管制同时拜访特定资源的线程数量,它通过协调各个线程,以保障正当的应用公共资源。
听起来仿佛很形象,当初汽车多了,开车出门在外的一个老大难问题就是停车。停车场的车位是无限的,只能容许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和残余的车位,车辆就能够驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字 0,车辆就得期待。如果满了的停车场有车来到,那么显示牌就又变绿,显示空车位数量,期待的车辆就能进停车场。

咱们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,来到停车场就是线程执行结束,看见红灯就示意线程被阻塞,不能执行,Semaphore 的实质就是协调多个线程对共享资源的获取。

咱们再来看一个 Semaphore 的用处:它能够用于做流量管制,特地是专用资源无限的利用场景,比方数据库连贯。
如果有一个需要,要读取几万个文件的数据,因为都是 IO 密集型工作,咱们能够启动几十个线程并发地读取,然而如果读到内存后,还须要存储到数据库中,而数据库的连接数只有 10 个,这时咱们必须管制只有 10 个线程同时获取数据库连贯保留数据,否则会报错无奈获取数据库连贯。这个时候,就能够应用 Semaphore 来做流量管制,如下:
public class SemaphoreTest {

private static final int THREAD_COUNT = 30;
private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);
private static Semaphore s = new Semaphore(10);

public static void main(String[] args) {for (int i = 0; i < THREAD_COUNT; i++) {threadPool.execute(new Runnable() {
            @Override
            public void run() {
                try {s.acquire();
                    System.out.println("save data");
                    s.release();} catch (InterruptedException e) {}}
        });
    }
    threadPool.shutdown();}

}
复制代码
在代码中,尽管有 30 个线程在执行,然而只容许 10 个并发执行。Semaphore 的构造方法 Semaphore(int permits)承受一个整型的数字,示意可用的许可证数量。Semaphore(10)示意容许 10 个线程获取许可证,也就是最大并发数是 10。Semaphore 的用法也很简略,首先线程应用 Semaphore 的 acquire()办法获取一个许可证,应用完之后调用 release()办法偿还许可证。还能够用 tryAcquire()办法尝试获取许可证。
43.Exchanger 理解吗?
Exchanger(替换者)是一个用于线程间合作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程能够替换彼此的数据。

这两个线程通过 exchange 办法替换数据,如果第一个线程先执行 exchange()办法,它会始终期待第二个线程也执行 exchange 办法,当两个线程都达到同步点时,这两个线程就能够替换数据,将本线程生产进去的数据传递给对方。
Exchanger 能够用于遗传算法,遗传算法里须要选出两个人作为交配对象,这时候会替换两人的数据,并应用穿插规定得出 2 个交配后果。Exchanger 也能够用于校对工作,比方咱们须要将纸制银行流水通过人工的形式录入成电子银行流水,为了防止谬误,采纳 AB 岗两人进行录入,录入到 Excel 之后,零碎须要加载这两个 Excel,并对两个 Excel 数据进行校对,看看是否录入统一。
public class ExchangerTest {

private static final Exchanger<String> exgr = new Exchanger<String>();
private static ExecutorService threadPool = Executors.newFixedThreadPool(2);

public static void main(String[] args) {threadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                String A = "银行流水 A"; // A 录入银行流水数据 
                exgr.exchange(A);
            } catch (InterruptedException e) {}}
    });
    threadPool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                String B = "银行流水 B"; // B 录入银行流水数据 
                String A = exgr.exchange("B");
                System.out.println("A 和 B 数据是否统一:" + A.equals(B) + ",A 录入的是:"
                        + A + ",B 录入是:" + B);
            } catch (InterruptedException e) {}}
    });
    threadPool.shutdown();}

}
复制代码
如果两个线程有一个没有执行 exchange()办法,则会始终期待,如果放心有非凡状况产生,防止始终期待,能够应用 exchange(V x, long timeOut, TimeUnit unit) 设置最大期待时长。
线程池
44. 什么是线程池?
线程池:简略了解,它就是一个治理线程的池子。

它帮咱们治理线程,防止减少创立线程和销毁线程的资源损耗。因为线程其实也是一个对象,创立一个对象,须要通过类加载过程,销毁一个对象,须要走 GC 垃圾回收流程,都是须要资源开销的。
进步响应速度。如果工作达到了,绝对于从线程池拿线程,从新去创立一条线程执行,速度必定慢很多。
反复利用。线程用完,再放回池子,能够达到反复利用的成果,节俭资源。

45. 能说说工作中线程池的利用吗?
之前咱们有一个和第三方对接的需要,须要向第三方推送数据,引入了多线程来晋升数据推送的效率,其中用到了线程池来治理线程。

次要代码如下:

残缺可运行代码地址:gitee.com/fighter3/th…
线程池的参数如下:

corePoolSize:线程外围参数抉择了 CPU 数×2

maximumPoolSize:最大线程数抉择了和外围线程数雷同

keepAliveTime:非核心闲置线程存活工夫间接置为 0

unit:非核心线程放弃存活的工夫抉择了 TimeUnit.SECONDS 秒

workQueue:线程池期待队列,应用 LinkedBlockingQueue 阻塞队列

同时还用了 synchronized 来加锁,保证数据不会被反复推送:
synchronized (PushProcessServiceImpl.class) {}
复制代码
ps: 这个例子只是简略地进行了数据推送,实际上还能够联合其余的业务,像什么数据荡涤啊、数据统计啊,都能够套用。
46. 能简略说一下线程池的工作流程吗?
用一个艰深的比喻:
有一个营业厅,总共有六个窗口,当初凋谢了三个窗口,当初有三个窗口坐着三个营业员小姐姐在营业。
老三去办业务,可能会遇到什么状况呢?

老三发现有空间的在营业的窗口,间接去找小姐姐办理业务。

老三发现没有闲暇的窗口,就在排队区排队等。

老三发现没有闲暇的窗口,期待区也满了,蚌埠住了,经理一看,就让劳动的小姐姐连忙回来下班,期待区号靠前的连忙去新窗口办,老三去排队区排队。小姐姐比拟辛苦,如果一段时间发现他们能够不必接着营业,经理就让她们接着劳动。

老三一看,六个窗口都满了,期待区也没地位了。老三急了,要闹,经理连忙进去了,经理该怎么办呢?

咱们银行零碎曾经瘫痪

谁叫你来办的你找谁去

看你比拟急,去队里加个塞

明天没方法,不行你看改一天

下面的这个流程简直就跟 JDK 线程池的大抵流程相似,

营业中的 3 个窗口对应外围线程池数:corePoolSize
总的营业窗口数 6 对应:maximumPoolSize
关上的长期窗口在多少工夫内无人办理则敞开对应:unit
排队区就是期待队列:workQueue
无奈办理的时候银行给出的解决办法对应:RejectedExecutionHandler
threadFactory 该参数在 JDK 中是 线程工厂,用来创立线程对象,个别不会动。

所以咱们线程池的工作流程也比拟好了解了:

线程池刚创立时,外面没有一个线程。工作队列是作为参数传进来的。不过,就算队列外面有工作,线程池也不会马上执行它们。
当调用 execute() 办法增加一个工作时,线程池会做如下判断:

如果正在运行的线程数量小于 corePoolSize,那么马上创立线程运行这个工作;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个工作放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创立非核心线程立即运行这个工作;
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会依据回绝策略来对应解决。

当一个线程实现工作时,它会从队列中取下一个工作来执行。

当一个线程无事可做,超过肯定的工夫(keepAliveTime)时,线程池会判断,如果以后运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有工作实现后,它最终会膨胀到 corePoolSize 的大小。

47. 线程池主要参数有哪些?

线程池有七大参数,须要重点关注 corePoolSize、maximumPoolSize、workQueue、handler 这四个。

corePoolSize

此值是用来初始化线程池中外围线程数,当线程池中线程池数 < corePoolSize 时,零碎默认是增加一个工作才创立一个线程池。当线程数 = corePoolSize 时,新工作会追加到 workQueue 中。

maximumPoolSize

maximumPoolSize 示意容许的最大线程数 = (非核心线程数 + 外围线程数),当 BlockingQueue 也满了,但线程池中总线程数 < maximumPoolSize 时候就会再次创立新的线程。

keepAliveTime

非核心线程 =(maximumPoolSize – corePoolSize) , 非核心线程闲置下来不干活最多存活工夫。

unit

线程池中非核心线程放弃存活的工夫的单位

TimeUnit.DAYS; 天
TimeUnit.HOURS; 小时
TimeUnit.MINUTES; 分钟
TimeUnit.SECONDS; 秒
TimeUnit.MILLISECONDS; 毫秒
TimeUnit.MICROSECONDS; 微秒
TimeUnit.NANOSECONDS; 纳秒

workQueue

线程池期待队列,保护着期待执行的 Runnable 对象。当运行当线程数 = corePoolSize 时,新的工作会被增加到 workQueue 中,如果 workQueue 也满了则尝试用非核心线程执行工作,期待队列应该尽量用有界的。

threadFactory

创立一个新线程时应用的工厂,能够用来设定线程名、是否为 daemon 线程等等。

handler

corePoolSize、workQueue、maximumPoolSize 都不可用的时候执行的饱和策略。
48. 线程池的回绝策略有哪些?
类比后面的例子,无奈办理业务时的解决形式,帮忙记忆:

AbortPolicy:间接抛出异样,默认应用此策略
CallerRunsPolicy:用调用者所在的线程来执行工作
DiscardOldestPolicy:抛弃阻塞队列里最老的工作,也就是队列里靠前的工作
DiscardPolicy:当前任务间接抛弃

想实现本人的回绝策略,实现 RejectedExecutionHandler 接口即可。
49. 线程池有哪几种工作队列?
罕用的阻塞队列次要有以下几种:

ArrayBlockingQueue:ArrayBlockingQueue(有界队列)是一个用数组实现的有界阻塞队列,按 FIFO 排序量。
LinkedBlockingQueue:LinkedBlockingQueue(可设置容量队列)是基于链表构造的阻塞队列,按 FIFO 排序工作,容量能够抉择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为 Integer.MAX_VALUE,吞吐量通常要高于 ArrayBlockingQuene;newFixedThreadPool 线程池应用了这个队列
DelayQueue:DelayQueue(提早队列)是一个工作定时周期的提早执行的队列。依据指定的执行工夫从小到大排序,否则依据插入到队列的先后排序。newScheduledThreadPool 线程池应用了这个队列。
PriorityBlockingQueue:PriorityBlockingQueue(优先级队列)是具备优先级的无界阻塞队列
SynchronousQueue:SynchronousQueue(同步队列)是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作始终处于阻塞状态,吞吐量通常要高于 LinkedBlockingQuene,newCachedThreadPool 线程池应用了这个队列。

50. 线程池提交 execute 和 submit 有什么区别?

execute 用于提交不须要返回值的工作

threadsPool.execute(new Runnable() {

@Override public void run() {// TODO Auto-generated method stub} 
});

复制代码

submit()办法用于提交须要返回值的工作。线程池会返回一个 future 类型的对象,通过这个 future 对象能够判断工作是否执行胜利,并且能够通过 future 的 get()办法来获取返回值

Future<Object> future = executor.submit(harReturnValuetask);
try {Object s = future.get(); } catch (InterruptedException e) {

// 解决中断异样 

} catch (ExecutionException e) {

// 解决无奈执行工作异样 

} finally {

// 敞开线程池 executor.shutdown();

}
复制代码
51. 线程池怎么敞开晓得吗?
能够通过调用线程池的 shutdown 或 shutdownNow 办法来敞开线程池。它们的原理是遍历线程池中的工作线程,而后一一调用线程的 interrupt 办法来中断线程,所以无奈响应中断的工作可能永远无奈终止。
shutdown() 将线程池状态置为 shutdown, 并不会立刻进行:

进行接管内部 submit 的工作
外部正在跑的工作和队列里期待的工作,会执行完
等到第二步实现后,才真正进行

shutdownNow() 将线程池状态置为 stop。个别会立刻进行,事实上不肯定:

和 shutdown()一样,先进行接管内部提交的工作
疏忽队列里期待的工作
尝试将正在跑的工作 interrupt 中断
返回未执行的工作列表

shutdown 和 shutdownnow 简略来说区别如下:

shutdownNow()能立刻进行线程池,正在跑的和正在期待的工作都停下了。这样做立刻失效,然而危险也比拟大。
shutdown()只是敞开了提交通道,用 submit()是有效的;而外部的工作该怎么跑还是怎么跑,跑完再彻底进行线程池。

52. 线程池的线程数应该怎么配置?
线程在 Java 中属于稀缺资源,线程池不是越大越好也不是越小越好。工作分为计算密集型、IO 密集型、混合型。

计算密集型:大部分都在用 CPU 跟内存,加密,逻辑操作业务解决等。
IO 密集型:数据库链接,网络通讯传输等。

个别的教训,不同类型线程池的参数配置:

计算密集型个别举荐线程池不要过大,个别是 CPU 数 + 1,+ 1 是因为可能存在页缺失(就是可能存在有些数据在硬盘中须要多来一个线程将数据读入内存)。如果线程池数太大,可能会频繁的 进行线程上下文切换跟任务调度。取得以后 CPU 外围数代码如下:

Runtime.getRuntime().availableProcessors();
复制代码

IO 密集型:线程数适当大一点,机器的 Cpu 外围数 *2。
混合型:能够思考杜绝状况将它拆分成 CPU 密集型和 IO 密集型工作,如果执行工夫相差不大,拆分能够晋升吞吐量,反之没有必要。

当然,理论利用中没有固定的公式,须要联合测试和监控来进行调整。
53. 有哪几种常见的线程池?
面试常问,次要有四种,都是通过工具类 Excutors 创立进去的,须要留神,阿里巴巴《Java 开发手册》里禁止应用这种形式来创立线程池。

newFixedThreadPool (固定数目线程的线程池)

newCachedThreadPool (可缓存线程的线程池)

newSingleThreadExecutor (单线程的线程池)

newScheduledThreadPool (定时及周期执行的线程池)

54. 能说一下四种常见线程池的原理吗?
前三种线程池的结构间接调用 ThreadPoolExecutor 的构造方法。
newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {

    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

复制代码
线程池特点

外围线程数为 1
最大线程数也为 1
阻塞队列是无界队列 LinkedBlockingQueue,可能会导致 OOM
keepAliveTime 为 0

工作流程:

提交工作
线程池是否有一条线程在,如果没有,新建线程执行工作
如果有,将工作加到阻塞队列
以后的惟一线程,从队列取工作,执行完一个,再持续取,一个线程执行工作。

实用场景
实用于串行执行工作的场景,一个工作一个工作地执行。
newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {

    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

复制代码
线程池特点:

外围线程数和最大线程数大小一样
没有所谓的非闲暇工夫,即 keepAliveTime 为 0
阻塞队列为无界队列 LinkedBlockingQueue,可能会导致 OOM

工作流程:

提交工作
如果线程数少于外围线程,创立外围线程执行工作
如果线程数等于外围线程,把工作增加到 LinkedBlockingQueue 阻塞队列
如果线程执行完工作,去阻塞队列取工作,继续执行。

应用场景
FixedThreadPool 实用于解决 CPU 密集型的工作,确保 CPU 在长期被工作线程应用的状况下,尽可能的少的调配线程,即实用执行长期的工作。
newCachedThreadPool
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {

    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

复制代码
线程池特点:

外围线程数为 0
最大线程数为 Integer.MAX_VALUE,即无限大,可能会因为有限创立线程,导致 OOM
阻塞队列是 SynchronousQueue
非核心线程闲暇存活工夫为 60 秒

当提交工作的速度大于解决工作的速度时,每次提交一个工作,就必然会创立一个线程。极其状况下会创立过多的线程,耗尽 CPU 和内存资源。因为闲暇 60 秒的线程会被终止,长时间放弃闲暇的 CachedThreadPool 不会占用任何资源。

工作流程:

提交工作
因为没有外围线程,所以工作间接加到 SynchronousQueue 队列。
判断是否有闲暇线程,如果有,就去取出工作执行。
如果没有闲暇线程,就新建一个线程执行。
执行完工作的线程,还能够存活 60 秒,如果在这期间,接到工作,能够持续活下去;否则,被销毁。

实用场景
用于并发执行大量短期的小工作。
newScheduledThreadPool

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

复制代码
线程池特点

最大线程数为 Integer.MAX_VALUE,也有 OOM 的危险
阻塞队列是 DelayedWorkQueue
keepAliveTime 为 0
scheduleAtFixedRate():按某种速率周期执行
scheduleWithFixedDelay():在某个提早后执行

工作机制

线程从 DelayQueue 中获取已到期的 ScheduledFutureTask(DelayQueue.take())。到期工作是指 ScheduledFutureTask 的 time 大于等于以后工夫。
线程执行这个 ScheduledFutureTask。
线程批改 ScheduledFutureTask 的 time 变量为下次将要被执行的工夫。
线程把这个批改 time 之后的 ScheduledFutureTask 放回 DelayQueue 中(DelayQueue.add())。

应用场景
周期性执行工作的场景,须要限度线程数量的场景

应用无界队列的线程池会导致什么问题吗?

例如 newFixedThreadPool 应用了无界的阻塞队列 LinkedBlockingQueue,如果线程获取一个工作后,工作的执行工夫比拟长,会导致队列的工作越积越多,导致机器内存应用不停飙升,最终导致 OOM。
55. 线程池异样怎么解决晓得吗?
在应用线程池解决工作的时候,工作代码可能抛出 RuntimeException,抛出异样后,线程池可能捕捉它,也可能创立一个新的线程来代替异样的线程,咱们可能无奈感知工作呈现了异样,因而咱们须要思考线程池异常情况。
常见的异样解决形式:

56. 能说一下线程池有几种状态吗?
线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。
// 线程池状态
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
复制代码
线程池各个状态切换图:

RUNNING

该状态的线程池会接管新工作,并解决阻塞队列中的工作;
调用线程池的 shutdown()办法,能够切换到 SHUTDOWN 状态;
调用线程池的 shutdownNow()办法,能够切换到 STOP 状态;

SHUTDOWN

该状态的线程池不会接管新工作,但会解决阻塞队列中的工作;
队列为空,并且线程池中执行的工作也为空, 进入 TIDYING 状态;

STOP

该状态的线程不会接管新工作,也不会解决阻塞队列中的工作,而且会中断正在运行的工作;
线程池中执行的工作为空, 进入 TIDYING 状态;

TIDYING

该状态表明所有的工作曾经运行终止,记录的工作数量为 0。
terminated()执行结束,进入 TERMINATED 状态

TERMINATED

该状态示意线程池彻底终止

57. 线程池如何实现参数的动静批改?
线程池提供了几个 setter 办法来设置线程池的参数。

这里次要有两个思路:

在咱们微服务的架构下,能够利用配置核心如 Nacos、Apollo 等等,也能够本人开发配置核心。业务服务读取线程池配置,获取相应的线程池实例来批改线程池的参数。

如果限度了配置核心的应用,也能够本人去扩大 ThreadPoolExecutor,重写办法,监听线程池参数变动,来动静批改线程池参数。

线程池调优理解吗?
线程池配置没有固定的公式,通常事先会对线程池进行肯定评估,常见的评估计划如下:

上线之前也要进行充沛的测试,上线之后要建设欠缺的线程池监控机制。
事中联合监控告警机制,剖析线程池的问题,或者可优化点,联合线程池动静参数配置机制来调整配置。
预先要留神仔细观察,随时调整。

具体的调优案例能够查看参考 [7] 美团技术博客。
58. 你能设计实现一个线程池吗?
⭐这道题在阿里的面试中呈现频率比拟高
线程池实现原理能够查看 要是以前有人这么讲线程池,我早就该明确了!,当然,咱们本人实现,只须要抓住线程池的外围流程 - 参考[6]:

咱们本人的实现就是实现这个外围流程:

线程池中有 N 个工作线程
把工作提交给线程池运行
如果线程池已满,把工作放入队列
最初当有闲暇时,获取队列中工作来执行

实现代码[6]:

这样,一个实现了线程池次要流程的类就实现了。
59. 单机线程池执行断电了应该怎么解决?
咱们能够对正在解决和阻塞队列的工作做事务管理或者对阻塞队列中的工作长久化解决,并且当断电或者零碎解体,操作无奈继续下去的时候,能够通过回溯日志的形式来撤销正在解决的曾经执行胜利的操作。而后从新执行整个阻塞队列。
也就是说,对阻塞队列长久化;正在解决工作事务管制;断电之后正在解决工作的回滚,通过日志复原该次操作;服务器重启后阻塞队列中的数据再加载。
并发容器和框架
对于一些并发容器,能够去看看 面渣逆袭:Java 汇合连环三十问,外面有 CopyOnWriteList 和 ConcurrentHashMap 这两种线程平安容器类的问答。。
60.Fork/Join 框架理解吗?
Fork/Join 框架是 Java7 提供的一个用于并行执行工作的框架,是一个把大工作宰割成若干个小工作,最终汇总每个小工作后果后失去大工作后果的框架。
要想把握 Fork/Join 框架,首先须要了解两个点,分而治之和工作窃取算法。
分而治之
Fork/Join 框架的定义,其实就体现了分治思维:将一个规模为 N 的问题合成为 K 个规模较小的子问题,这些子问题互相独立且与原问题性质雷同。求出子问题的解,就可失去原问题的解。

工作窃取算法
大工作拆成了若干个小工作,把这些小工作放到不同的队列里,各自创立独自线程来执行队列里的工作。
那么问题来了,有的线程干活块,有的线程干活慢。干完活的线程不能让它空下来,得让它去帮没干完活的线程干活。它去其它线程的队列里窃取一个工作来执行,这就是所谓的工作窃取。
工作窃取产生的时候,它们会拜访同一个队列,为了缩小窃取工作线程和被窃取工作线程之间的竞争,通常工作会应用双端队列,被窃取工作线程永远从双端队列的头部拿,而窃取工作的线程永远从双端队列的尾部拿工作执行。

看一个 Fork/Join 框架利用的例子,计算 1~n 之间的和:1+2+3+…+n

设置一个宰割阈值,工作大于阈值就拆分工作
工作有后果,所以须要继承 RecursiveTask

public class CountTask extends RecursiveTask<Integer> {

private static final int THRESHOLD = 16; // 阈值
private int start;
private int end;

public CountTask(int start, int end) {
    this.start = start;
    this.end = end;
}

@Override
protected Integer compute() {
    int sum = 0;
    // 如果工作足够小就计算工作
    boolean canCompute = (end - start) <= THRESHOLD;
    if (canCompute) {for (int i = start; i <= end; i++) {sum += i;}
    } else {
        // 如果工作大于阈值,就决裂成两个子工作计算
        int middle = (start + end) / 2;
        CountTask leftTask = new CountTask(start, middle);
        CountTask rightTask = new CountTask(middle + 1, end);
        // 执行子工作
        leftTask.fork();
        rightTask.fork(); // 期待子工作执行完,并失去其后果
        int leftResult = leftTask.join();
        int rightResult = rightTask.join(); // 合并子工作
        sum = leftResult + rightResult;
    }
    return sum;
}

public static void main(String[] args) {ForkJoinPool forkJoinPool = new ForkJoinPool(); // 生成一个计算工作,负责计算 1 +2+3+4
    CountTask task = new CountTask(1, 100); // 执行一个工作
    Future<Integer> result = forkJoinPool.submit(task);
    try {System.out.println(result.get());
    } catch (InterruptedException e) {} catch (ExecutionException e) {}}

}
复制代码
ForkJoinTask 与个别 Task 的次要区别在于它须要实现 compute 办法,在这个办法里,首先须要判断工作是否足够小,如果足够小就间接执行工作。如果比拟大,就必须宰割成两个子工作,每个子工作在调用 fork 办法时,又会进 compute 办法,看看以后子工作是否须要持续宰割成子工作,如果不须要持续宰割,则执行以后子工作并返回后果。应用 join 办法会期待子工作执行完并失去其后果。

正文完
 0