【2021Java后端面试题整理】Java多线程面试题+面经答案

目录

链接一下目录方便查找

菜鸡的2021春招实习之旅(面经+自己总结的笔记)

多线程

Java多线程

1.什么是线程和进程

什么是进程?

进程就是系统执行程序的一次过程,是系统运行程序的基本单位,因此进程是动态的。一次程序的运行是一个进程从创建、运行到消亡的过程。

什么是线程?

线程是比进程更小的执行单位,通常在一个进程执行的过程中会产生许多的进程,他们可能会紧密相关。从JVM的角度来看,不同的线程共享堆和元空间,拥有自己独立的程序计数器、虚拟机栈和本地方法栈。

2.请简要描述线程与进程的关系,区别及优缺点?

在一个进程运行的过程中,会产生一系列的线程,它是比进程更小的执行单位。进程和进程之间是相互独立的,线程却不一定,同一进程中的线程可能会相互影响,同一进程中的所有线程都共享堆和元空间(方法区),都有自己私有的程序计数器,虚拟机栈,本地方法栈。线程相对于进程而言,开销更小,但是缺点就是不易于管理和保护

3.并发与并行的区别

  • 并发: 同一时间段,多个任务都在执行 (单位时间内不一定同时执行);
  • 并行: 单位时间内,多个任务同时执行。

并发:

  • 多个事件在同一时间段内发生
  • 同一个CPU执行多个任务,按细分的时间片交替执行
  • 并发的多个任务会相互抢占资源

并行:

  • 多个事件在同一时间点上发生
  • 在多个CPU上同时处理多个任务
  • 并行的多个任务不会相互抢占资源

4.为什么要使用多线程呢?

先从总体上来说:

  • 从计算机底层来说: 线程可以比作是轻量级的进程,是程序执行的最小单位,线程间的切换和调度的成本远远小于进程。另外,多核 CPU 时代意味着多个线程可以同时运行,这减少了线程上下文切换的开销。
  • 从当代互联网发展趋势来说: 现在的系统动不动就要求百万级甚至千万级的并发量,而多线程并发编程正是开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力以及性能。

再深入到计算机底层来探讨:

  • 单核时代: 在单核时代多线程主要是为了提高 CPU 和 IO 设备的综合利用率。举个例子:当只有一个线程的时候会导致 CPU 计算时,IO 设备空闲;进行 IO 操作时,CPU 空闲。我们可以简单地说这两者的利用率目前都是 50%左右。但是当有两个线程的时候就不一样了,当一个线程执行 CPU 计算时,另外一个线程可以进行 IO 操作,这样两个的利用率就可以在理想情况下达到 100%了。
  • 多核时代: 多核时代多线程主要是为了提高 CPU 利用率。举个例子:假如我们要计算一个复杂的任务,我们只用一个线程的话,CPU 只会一个 CPU 核心被利用到,而创建多个线程就可以让多个 CPU 核心被利用到,这样就提高了 CPU 的利用率。

5.使用多线程可能带来什么问题?

并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、上下文切换、死锁还有受限于硬件和软件的资源闲置问题。

5.5线程的生命周期

在这里插入图片描述

new,runnable,blocked,waiting,timed waiting,terminated

**六大状态:**NEW、RUNNABLE、BLOCKED、WAITIING、TIME_WAITING、TERMINAED

1,当进入synchronized同步代码块或同步方法时,且没有获取到锁,线程就进入了blocked状态,直到锁被释放,重新进入runnable状态

2,当线程调用wait()或者join时,线程都会进入到waiting状态,当调用notify或notifyAll时,或者join的线程执行结束后,会进入runnable状态

3,当线程调用sleep(time),或者wait(time)时,进入timed waiting状态,

当休眠时间结束后,或者调用notify或notifyAll时会重新runnable状态。

4,程序执行结束,线程进入terminated状态

blocked,waiting,timed waiting 我们都称为阻塞状态

上述的就绪状态和运行状态,都表现为runnable状态

6.什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

7.什么是线程死锁?如何避免死锁?

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

如何避免线程死锁?

为了避免死锁,我们只要破坏产生死锁的四个条件中的其中一个就可以了。现在我们来挨个分析一下:

  1. 破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
  2. 破坏请求与保持条件 :一次性申请所有的资源。
  3. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  4. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

8.说说 sleep() 方法和 wait() 方法区别和共同点?

  • sleep()方法是让线程进入休眠,wait()让线程进入等待,两者都可以暂停线程的执行,但这之间最重要的却别就是前者没有释放锁,而后者释放了锁
  • wait()通常被用于线程间交互通信,sleep()通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout)超时后线程会自动苏醒。

9.为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可开启一个新线程并使线程进入就绪状态,而 run() 方法只是 thread 的一个普通方法调用,如果在主线程中调用,还是在主线程里执行。

10.synchronized 关键字

1.说一说自己对于 synchronized 关键字的了解

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。

另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因。庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

2.说说自己是怎么使用 synchronized 关键字,在项目中用到了吗
3.synchronized关键字最主要的三种使用方式
  • 修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
  • 修饰静态方法: 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static 表明这是该类的一个静态资源,不管ne
  • w了多少个对象,只有一份)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁
  • 修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

总结: synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。synchronized 关键字加到实例方法上是给对象实例上锁。尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓存功能!

下面我以一个常见的面试题为例讲解一下 synchronized 关键字的具体使用。

面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”

双重校验锁实现对象单例(线程安全)

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public synchronized static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

4.讲一下 synchronized 关键字的底层原理

synchronized 关键字底层原理属于 JVM 层面。

① synchronized 同步语句块的情况

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("synchronized 代码块");
        }
    }
}

通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行javap -c -s -v -l SynchronizedDemo.class

从上面我们可以看出:

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

synchronized保证原子性

ObjectMonitor

jdk6以后,加锁方式变成了CAS加锁,如果锁计数器变成1,则加锁成功,owner将赋值为线程1

在这里插入图片描述

synchronized内存屏障保证可见性和有序性

按可见性划分有Load屏障和Store屏障

Load屏障作用是执行refresh处理器缓存的操作,也就是加载别的处理器更新过的变量,保证自己看的是最新的数据.

Store屏障的作用是执行flush处理器缓存的操作,把自己的当前处理器更新的值都刷到高速缓存或者主内存中

在monitorexit指令之后,会有一个Store屏障,让线程把自己在同步代码块里修改的变量的值都执行 fush处理器缓存的操作,刷到高速缓存(或者主内存〉里去,然后在monitorenter指令之后会加一个 Load屏障,执行refresh处理器缓存的操作,把别的处理器修改过的最新值加戟到自己高速缓存里来

按照有序性保障来划分的话,还可分为Acquire屏障和Release屏障。
在monitorenter指令之后,Load屏障之后,会加一个 Acquire屏障,这个屏障的作用是禁止读操作和读写操作之间发生指令重排序。在monitorexit指令之前,会加一个Release屏障,这个屏障的作用是禁止写操作和读写操作之间发生重排序。

synchronized(A) {->monitorenter

Load内存屏障

Acquire内存屏障

读写操作 ->内部还是可以指令重排,但是和外面的代码不会指令重排

Release内存屏障

}

->monitorexit

Store内存屏障

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TamxuQqH-1617160289483)(D:\学习笔记\面试\pic\synchronized底层2.png)]

② synchronized 修饰方法的的情况

public class SynchronizedDemo2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}
5.说说 JDK1.6 之后的synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

(1)锁消除

JIT编译器对synchronized加锁的优化,JIT编译器会通过逃逸分析技术,分析是不是只可能被一个线程加锁,有没有其他线程来竞争加锁,这时候编译器不会加入monitorenter和monitorexit的指令

就是如果只有一个线程竞争锁,就可以消除这个锁

(2)锁粗化

JIT编译器如果发现代码有多次加锁释放锁是连续的,会合并成一个锁,避免多次加锁释放锁

(3)偏向锁

这个意思就是说, monitorenter和monitorexit是要使用CAS操作加锁和释放锁的,开销大,因此如果发现大概率只有一个线程会主要竞争一个锁,那么会给这个锁维护一个偏好(Bias),后面他加锁和释放锁,基于Bias来执行,不需要通过CAS

但是如果有偏好之外的线程来竞争锁,要收回之前分配好的Bias偏好

(4)轻量级锁

如果偏向锁没有实现成功实现,就是因为不同线程竞争太过频繁,会尝试使用轻量级锁,将对象头的MarkWord里面有一个轻量锁指针,尝试将指针指向自己看看是不是自己加的锁

如果是自己家的锁,就执行代码

如果不是自己加的锁,那就加锁失败,说明别人家了锁,这时候会膨胀为为重量级锁,CAS

(5)自适应性锁

如果各个线程持有锁的时间很短,那么就会产生频繁的上下文切换,开销过大.这时候就需要自旋锁,不断获取锁

6.谈谈 synchronized和ReentrantLock 的区别

① 两者都是可重入锁

两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。

② synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API

synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。

③ ReentrantLock 比 synchronized 增加了一些高级功能

相比synchronized,ReentrantLock增加了一些高级功能。主要来说主要有三点:①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)

  • ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。
  • ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
  • synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。

如果你想使用上述功能,那么选择ReentrantLock是一个不错的选择。

④ 性能已不是选择标准

12.volatile关键字

1.什么是volatile

volatile是JVM提供的轻量级的同步机制,有以下三个重要特性:保证可见性,不保证原子性,禁止指令重排

可见性是Load和Store的内存屏障

有序性

对于volatile修改变量的读写操作,都会加入内存屏障
每个volatile 写操作前面,加storeStore屏障,禁止上面的普通写和他重排,每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的 volatile读/写重排
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屑障,禁止下面的普通写和volatile读重排

2.请你谈谈JMM(java内存模型)

JMM是指Java内存模型,不是Java内存布局,不是所谓的栈、堆、方法区。是一组规则或者规范

每个Java线程都有自己的工作内存。操作数据,首先从主内存中读,得到一份拷贝,操作完毕后再写回到主内存。

JMM可能带来可见性原子性有序性问题。所谓可见性,就是某个线程对主内存内容的更改,应该立刻通知到其它线程。原子性是指一个操作是不可分割的,不能执行到一半,就不执行了。所谓有序性,就是指令是有序的,不会被重排。

可见性

线程将主内存中的数据拷贝到自己的内存中,然后将数据进行修改,需要通知其他所有线程,这就叫可见性

class MyData{
    int number=0;
    //volatile int number=0;

    AtomicInteger atomicInteger=new AtomicInteger();
    public void setTo60(){
        this.number=60;
    }

    //此时number前面已经加了volatile,但是不保证原子性
    public void addPlusPlus(){
        number++;
    }

    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }
}

//volatile可以保证可见性,及时通知其它线程主物理内存的值已被修改
private static void volatileVisibilityDemo() {
    System.out.println("可见性测试");
    MyData myData=new MyData();//资源类
    //启动一个线程操作共享数据
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+"\t come in");
        try {TimeUnit.SECONDS.sleep(3);myData.setTo60();
        System.out.println(Thread.currentThread().getName()+"\t update number value: "+myData.number);}catch (InterruptedException e){e.printStackTrace();}
    },"AAA").start();
    while (myData.number==0){
     //main线程持有共享数据的拷贝,一直为0
    }
    System.out.println(Thread.currentThread().getName()+"\t mission is over. main get number value: "+myData.number);
}

原子性

原子性:保持数据的完整性。不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整。要么同时成功,要么同时失败。

volatile不保证原子性

private static void atomicDemo() {
    System.out.println("原子性测试");
    MyData myData=new MyData();
    for (int i = 1; i <= 20; i++) {
        new Thread(()->{
            for (int j = 0; j <1000 ; j++) {
                myData.addPlusPlus();

            }
        },String.valueOf(i)).start();
    }
    while (Thread.activeCount()>2){
        Thread.yield();
    }
    System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);//答案小于20000

}

解决方式,使用JUC的AtomicInteger类,保证原子性

    public void addAtomic(){
        atomicInteger.getAndIncrement();
    }

有序性

多线程环境中线程交替执行,由于编译器会指令重排,多线程情况下会乱序

代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。

volatile就是用来保证特定操作的执行顺序,保证某些变量的内存可见性的

在满足JMM的三大特性后,线程安全性能够获得保证

2.5 happens-before原则

1、程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
2、锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作,比如说在代码里有先对一个lock.lock(),lock.unlock(),lock.lock()
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个volatile变量的读操作,volatile变量写,再是读,必须保证是先写,再读
4、传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作c,则可以得出操作A先行发生于操作Ce
5、线程启动规则: Thread对象的start()方法先行发生于此线程的每个一个动作, thread.start(),thread.interrupt()

6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
7、线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
8、对象终结规则:一个对象的初始化完成先行发生于他的 finalize()方法的开始

规则制定了在一些特殊情况下,不允许编译器、指令器对你写的代码进行指令重排,必须保证你的代码的有序性

3.单例模式DCL(Double Check Lock 双端检锁机制)

在加锁前和加锁后都进行一次判断

DCL (双端检锁)机制不一定线程安全,原因是有指令重排序的存在,加入volatile可以禁止指令重排
原因在于某一个线程执行到第一次检测,读取到的instance不 为null时,instance的引用对象可能没有完成初始化

instance = new SingletonDemo();可以分为以下3步完成(伪代码)
memory = allocate(); //1.分配对象内存空间
instance(memory); //2. 初始化对象
instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance! =null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory; //3. 设置instance指向刚分配的内存地址,此时instance! =nill, 但是对象还没有初始化完成
instance(memory); //2. 初始化对象

但是指令重排只会保证串行语义的执行的一-致性(单线程), 但并不会关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance实例未必己初始化完成,也就造成了线程安全问题。

volitile可以禁止指令重排,保证多线程下的线程安全问题

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;
    private SingletonDemo() {};
    public static SingletonDemo getInstance() {
        if(instance==null) {
            synchronized (SingletonDemo.class) {
                if(instance==null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

13.CAS

1.CAS是什么?

CAS,compare and set,就是比较并交换

compareAndSet方法

如果多个线程更新同一个变量,只有一个线程可以成功,其他全部失败。失败的线程可以再次尝试更新。

2.底层原理

CAS思想自旋锁、unsafe

AtomicInteger内部维护了volatile int valueprivate static final Unsafe unsafe两个比较重要的参数。

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

AtomicInteger.getAndIncrement()调用了Unsafe.getAndAddInt()方法。Unsafe类的大部分方法都是native的,用来像C语言一样从底层操作内存。

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

这个方法的var1和var2,就是根据对象偏移量得到在主内存的快照值var5。然后compareAndSwapInt方法通过var1和var2得到当前主内存的实际值。如果这个实际值快照值相等,那么就更新主内存的值为var5+var4。如果不等,那么就一直循环,一直获取快照,一直对比,直到实际值和快照值相等为止。

使用自旋锁相对于synchronized来说,提高了并发性,没必要锁住对象,大大提高了效率

比如有A、B两个线程,一开始都从主内存中拷贝了原值为3,A线程执行到var5=this.getIntVolatile,即var5=3。此时A线程挂起,B修改原值为4,B线程执行完毕,由于加了volatile,所以这个修改是立即可见的。A线程被唤醒,执行this.compareAndSwapInt()方法,发现这个时候主内存的值不等于快照值3,所以继续循环,重新从主内存获取。

3.CAS缺点

自旋锁过程中,如果CAS一直不成功,会给CPU带来很大的开销

14.谈谈原子类AromicInteger的ABA问题?原子更新引用知道吗?

ABA

ABA也就是“狸猫换太子”

首先是线程1,线程2都要获取内存中的A去修改,但是存在一个调用差,线程1要10秒调用,线程2只需要2秒。在线程2调用的过程中,他将A改成了B,然后又改成了A,这个对于线程1来说,他发现A的值并没有改变,于是对A进行了修改。尽管顺利修改,但是里面是存在问题的。
在这里插入图片描述

AtomicReference

AtomicInteger对整数进行原子操作,如果是一个POJO呢?可以用AtomicReference来包装这个POJO,使其操作原子化。

User user1 = new User("Jack",25);
User user2 = new User("Lucy",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); //false
AtomicStampedReference和ABA问题的解决

修改版本号

使用AtomicStampedReference类可以解决ABA问题。这个类维护了一个“版本号”Stamp,在进行CAS操作的时候,不仅要比较当前值,还要比较版本号。只有两者都相等,才执行更新操作。

AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp,newStamp);
package thread;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {
        System.out.println("======ABA问题的产生======");

        new Thread(() -> {
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();

            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get().toString());
        }, "t2").start();

        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("======ABA问题的解决======");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第二次版本号: " + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,
                    atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第三次版本号: " + atomicStampedReference.getStamp());
        }, "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号: " + stamp);
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result=atomicStampedReference.compareAndSet(100,2019,
                    stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t修改成功与否:"+result+"  当前最新版本号"+atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName()+"\t当前实际值:"+atomicStampedReference.getReference());
        }, "t4").start();
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGcwcwF0-1617160289491)(D:\学习笔记\面试题复习\imgs\java基础\ABA.png)]

15.我们都知道ArrayList是线程不安全的,写一个例子并给出解决方案

public class ListNotSafe {
    public static void main(String[] args) {
        List<String> list =new ArrayList<>();

        for (int i = 0; i < 30; i++) {
            new Thread(()->{
                list.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(list);
            },String.valueOf(i)).start();
        }

    }
}

故障java.util.ConcurrentModificationException并发修改异常

解决方法:
1.使用Vector

2.使用Collections.synchronizedList();

3.使用new CopyOnWriteArrayList<>(new ArrayList<>());

使用写时复制原理,就是写的时候,复制容器,将容器+1,写入,将指针引用指向新容器,然后再返回

16.公平锁/非公平锁/可重入锁/递归锁/自旋锁的理解?手写一个自旋锁?

1.公平和非公平锁

ReentrantLock的构造函数传递boolean值表示是公平或者非公平锁,默认是非公平锁

概念:所谓公平锁,就是多个线程按照申请锁的顺序来获取锁,类似排队,先到先得。而非公平锁,则是多个线程抢夺锁,有可能后申请的线程比先申请的线程先获取锁

在高并发情况下会导致优先级反转饥饿现象

区别:公平锁在获取锁时先查看此锁维护的等待队列为空或者当前线程是等待队列的队首,则直接占有锁,否则插入到等待队列,FIFO原则。非公平锁比较粗鲁,上来直接先尝试占有锁,失败则采用公平锁方式。非公平锁的优点是吞吐量比公平锁更大。

synchronizedjuc.ReentrantLock默认都是非公平锁ReentrantLock在构造的时候传入true则是公平锁

2.可重入锁(递归锁ReentrantLock)

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码

同一个线程在外层方法获得锁时,进入内层方法会自动获取锁。也就是说,线程可以进入任何一个它已经拥有锁的代码块。

比如get方法里面有set方法,两个方法都有同一把锁,得到了get的锁,就自动得到了set的锁。

ReentrantLock/Synchronized就是经典的可重入锁,可以防止死锁

3.自旋锁

所谓自旋锁,就是尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取。自己在那儿一直循环获取,就像“自旋”一样。这样的好处是减少线程切换的上下文开销,缺点是会消耗CPU。CAS底层的getAndAddInt就是自旋锁思想。

public class SpinLock {
    AtomicReference<Thread> atomicReference = new AtomicReference<> ();
    public void mylock() {
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"\t come in ~~~~~");
        while(!atomicReference.compareAndSet(null,thread)){}//

    }
    public void myunlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(thread.getName()+"\t exit !!!! ~~~~~");
    }

    public static void main(String[] args) {
        SpinLock spinLock =new SpinLock();
        new Thread(()->{
           spinLock.mylock();
           try {
               TimeUnit.SECONDS.sleep(5);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           spinLock.myunlock();
        },"AA").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            spinLock.mylock();
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLock.myunlock();
        },"BB").start();
    }
}
4.读写锁(共享锁、独占锁)

读锁共享的写锁独占的juc.ReentrantLocksynchronized都是独占锁,独占锁就是一个锁只能被一个线程所持有。共享锁,指该所困有被多个线程多持有。有的时候,需要读写分离,那么就要引入读写锁,即juc.ReentrantReadWriteLock

读-读可以共存

读-写不能共存

写-写不能共存

缓存一般都用volatile来修饰,保证可见性

比如缓存,就需要读写锁来控制。缓存就是一个键值对,以下Demo模拟了缓存的读写操作,读的get方法使用了ReentrantReadWriteLock.ReadLock(),写的put方法使用了ReentrantReadWriteLock.WriteLock()。这样避免了写被打断,实现了多个线程同时读。

17.谈谈CountDownLatch/CyclicBarrier/Semaphore

1.CountDownLatch

CountDownLatch内部维护了一个计数器,只有当计数器==0时,某些线程才会停止阻塞,开始执行。

CountDownLatch主要有两个方法,countDown()来让计数器-1,await()来让线程阻塞。当count==0时,阻塞线程自动唤醒。

2.CyclicBarrier

和CountDownLatch不同的是,他是计算加

3.Semaphore

原理就是抢车位,他的值是伸缩的

Semaphore semaphore=new Semaphore(permits);

Semaphore semaphore=new Semaphore(permits,fair);//可以修改是公平/非公平锁

Semaphore semaphore=new Semaphore(3);
for (int i = 1; i <=6 ; i++) {
    new Thread(()->{
        try {
            //占有资源
            semaphore.acquire();
            System.out.println(Thread.currentThread().getName()+"\t抢到车位");
            try{ TimeUnit.SECONDS.sleep(3);} catch (Exception e){e.printStackTrace(); }
	    System.out.println(Thread.currentThread().getName()+"\t停车3秒后离开车位");
	    } 
	    catch (InterruptedException e) {e.printStackTrace();} 
	    //释放资源
	    finally {semaphore.release();}
    },String.valueOf(i)).start();
}

18.阻塞队列

阻塞:所谓阻塞,就是在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒

概念:当阻塞队列为空时,获取(take)操作是阻塞的;当阻塞队列为满时,添加(put)操作是阻塞的。
在这里插入图片描述

好处:阻塞队列不用手动控制什么时候该被阻塞,什么时候该被唤醒,简化了操作。

体系CollectionQueueBlockingQueue→七个阻塞队列实现类。

类名作用
ArrayBlockingQueue数组构成的有界阻塞队列
LinkedBlockingQueue链表构成的有界阻塞队列
PriorityBlockingQueue支持优先级排序的无界阻塞队列
DelayQueue支持优先级的延迟无界阻塞队列
SynchronousQueue单个元素的阻塞队列
LinkedTransferQueue由链表构成的无界阻塞队列
LinkedBlockingDeque由链表构成的双向阻塞队列

粗体标记的三个用得比较多,许多消息中间件底层就是用它们实现的。

需要注意的是LinkedBlockingQueue虽然是有界的,但有个巨坑,其默认大小是Integer.MAX_VALUE,高达21亿,一般情况下内存早爆了(在线程池的ThreadPoolExecutor有体现)。

API:抛出异常是指当队列满时,再次插入会抛出异常;返回布尔是指当队列满时,再次插入会返回false;阻塞是指当队列满时,再次插入会被阻塞,直到队列取出一个元素,才能插入。超时是指当一个时限过后,才会插入或者取出。

方法类型抛出异常返回布尔阻塞超时
插入add(E e)offer(E e)put(E e)offer(E e,Time,TimeUnit)
取出remove()poll()take()poll(Time,TimeUnit)
队首element()peek()

在这里插入图片描述

SynchronousQueue

SynchronousQueue与其他的BlockingQueue不一样,他是一个不存储元素的BlockingQueue

每个put操作必须要等待一个take操作,负责不能继续添加元素,反之亦然

也就是说,队列只有一个元素,如果想插入多个,必须等队列元素取出后,才能插入,只能有一个“坑位”,用一个插一个。

public class SynchronousQueueDemo {
    public static void main(String[] args) {
        BlockingQueue<String> blockingQueue=new SynchronousQueue<String>();
        new Thread(()->{
            try {
                System.out.println(Thread.currentThread().getName()+"\t put 1");
                blockingQueue.put("1");
                System.out.println(Thread.currentThread().getName()+"\t put 2");
                blockingQueue.put("2");
                System.out.println(Thread.currentThread().getName()+"\t put 3");
                blockingQueue.put("3");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"AAA").start();

        new Thread(()->{
            try {
                try{ TimeUnit.SECONDS.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t take "+blockingQueue.take());
                try{ TimeUnit.SECONDS.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t take "+blockingQueue.take());
                try{ TimeUnit.SECONDS.sleep(5); }catch (InterruptedException e){ e.printStackTrace(); }
                System.out.println(Thread.currentThread().getName()+"\t take"+blockingQueue.take());
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"BBB").start();
    }
}

线程会等待take操作完成后,再进行put操作

19.synchronized和lock的区别?用新的lock有什么好处

1.原始构成

synchronized是关键字,属于JVM层面
monitorenter(底层是通monitor对象来完成,其实wait/notify 等方法也依赖monitor对象只有在同步块或方法中才能调wait/notify等方法
monitorexit
Lock是具体类(java. util. concurrent. locks.Lock)是api层面的锁

2.使用方法
synchronized不需要用户去手动释放锁,当synchronized 代码执行完后系统会自动让线程释放对锁的占用

Reentrantlock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象。
需要lock() 和unlock()方法配合try/finally语旬块来完成。
3.等待是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断:

  1. 设置超时方法trylock(long timeout, TimeUnit unit)
  2. lockInterruptibly()放代码块中,调用interrupt() 方法可中断

4.加锁是否公平
synchronized非公平锁
Reentrantlock两者都可以,默认公平锁,构透方法可以传入boolean值,true为公平锁,false为非公平锁

5.锁绑定多个条件Condition

synchronized没有

Reentrantlock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。

20.为什么要用线程池?优势是什么?

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
他的主要特点为:线程复用;控制最大并发数:管理线程。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

21.线程池如何使用?

底层使用ThreadPoolExector

ExecutorService threadPool = Executors.方法()

常用三种方式进行创建

newFixedThreadPool:一池固定线程数,使用LinkedBlockingQueue实现,定长线程池。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor:使用LinkedBlockingQueue实现,一池只有一个线程。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

newCachedThreadPool:一池多线程,使用SynchronousQueue实现,变长线程池。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                 60L, TimeUnit.SECONDS,
                                 new SynchronousQueue<Runnable>());
}

22.线程池创建的七个参数

参数意义
corePoolSize线程池常驻核心线程数
maximumPoolSize能够容纳的最大线程数
keepAliveTime空闲线程存活时间
unit存活时间单位
workQueue存放提交但未执行任务的队列
threadFactory创建线程的工厂类
handler等待队列满后的拒绝策略

理解:线程池的创建参数,就像一个银行

corePoolSize就像银行的“当值窗口“,比如今天有2位柜员在受理客户请求(任务)。如果超过2个客户,那么新的客户就会在等候区(等待队列workQueue)等待。当等候区也满了,这个时候就要开启“加班窗口”,让其它3位柜员来加班,此时达到最大窗口maximumPoolSize,为5个。如果开启了所有窗口,等候区依然满员,此时就应该启动”拒绝策略handler,告诉不断涌入的客户,叫他们不要进入,已经爆满了。由于不再涌入新客户,办完事的客户增多,窗口开始空闲,这个时候就通过keepAlivetTime将多余的3个”加班窗口“取消,恢复到2个”当值窗口“。

23.线程池底层原理

原理图:上面银行的例子,实际上就是线程池的工作原理。

在这里插入图片描述

流程图

在这里插入图片描述

新任务到达→

如果正在运行的线程数小于corePoolSize,创建核心线程;大于等于corePoolSize,放入等待队列。

如果等待队列已满,但正在运行的线程数小于maximumPoolSize,创建非核心线程;大于等于maximumPoolSize,启动拒绝策略。

当一个线程无事可做一段时间keepAliveTime后,如果正在运行的线程数大于corePoolSize,则关闭非核心线程。

24.线程池的拒绝策略

当等待队列满时,且达到最大线程数,再有新任务到来,就需要启动拒绝策略。JDK提供了四种拒绝策略,分别是。

  1. AbortPolicy:默认的策略,直接抛出RejectedExecutionException异常,阻止系统正常运行。
  2. CallerRunsPolicy:既不会抛出异常,也不会终止任务,而是将任务返回给调用者。
  3. DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交任务。
  4. DiscardPolicy:直接丢弃任务,不做任何处理。
  5. 自定义拒绝策略,将任务持久化到磁盘中,等待队列执行后在重新执行

25.实际生产使用哪一个线程池?

单一、可变、定长都不用!原因就是FixedThreadPoolSingleThreadExecutor底层都是用LinkedBlockingQueue实现的,这个队列最大长度为Integer.MAX_VALUE,显然会导致OOM。所以实际生产一般自己通过ThreadPoolExecutor的7个参数,自定义线程池。

ExecutorService threadPool=new ThreadPoolExecutor(2,5,
                        1L,TimeUnit.SECONDS,
                        new LinkedBlockingQueue<>(3),
                        Executors.defaultThreadFactory(),
                        new ThreadPoolExecutor.AbortPolicy());
自定义线程池参数选择

对于CPU密集型任务,最大线程数是CPU线程数+1。对于IO密集型任务,尽量多配点,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。

26.死锁编码及定位分析

定位到第七题

27.线程调度算法及其优缺点

在这里插入图片描述

上下文切换:就是如果就绪状态转成了其他状态,然后在回归就绪状态,需要之前保存的信息,这就是上下文切换

1.先进先出(队列)

优点:任务切换开销小;吞吐量大;公平性高,先来先做

缺点:响应时间高

2.最短耗时任务优先

特点:优先调度耗时时间短的,需要提前预知任务耗时情况,任务剩余时间

优点:响应时间比较低

缺点:耗时时间长的任务一直在等,形成饥饿;频繁切换线程上下文,额外开销增大

3.时间片轮转

特点:给每个队列任务都分时间片,任务完成时间是以耗时时间从小到大排列

优点:每个任务都是公平调度;哪怕耗时段的任务在耗时长的任务后面,都可以快速调度耗时短的任务

缺点:上下文切换开销大,尤其是CPU缓存;时间片设置困难

使用场景:耗时差不多的任务

不适用场景:任务需要大量计算,对IO需要读写

4.最大最小公平算法

28.什么是CAS,什么是AQS?

CAS见13

AQS是一个抽象类,是一个访问共享资源的框架,同步类有reentrantlock,countdownlatch等等.

抽象队列同步器

定义一个volatile status
在这里插入图片描述

29.TreadLocal使用的注意事项

本地线程变量,其为变量在每个线程中都创建了一个副本,每个线程都访问和修改本线程中变量的副本。

线程之间的threadLocal变量是互不影响的,

使用private final static进行修饰,防止多实例时内存的泄露问题
线程池环境下使用后将threadLocal变量remove掉或设置成一个初始值,否则会产生内存泄露

30.synchronized,volatile和lock的区别

synchronized和volatile的区别与联系?

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
2.volatile仅能使用在变量级别;synchronized则可以使用在方法和代码块;
3.volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
4.volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
5.volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。

synchronized和lock的区别与联系?

synchronized是java中的一个关键字,也就是说是Java语言内置的特性。那么为什么会出现Lock呢?

如果一个代码块被synchronized修饰了,当一个线程获取了对应的锁,并执行该代码块时,其他线程便只能一直等待,等待获取锁的线程释放锁,而这里获取锁的线程释放锁只会有两种情况:

1)获取锁的线程执行完了该代码块,然后线程释放对锁的占有;

2)线程执行发生异常,此时JVM会让线程自动释放锁。

那么如果这个获取锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,其他线程便只能等待,试想一下,这多么影响程序执行效率。

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。(摘自博客)

区别

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

31.线程池的状态

参考博客讲的非常详细

线程池有五种状态,分别是RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED

通过runStateOf(int)方法获得池状态:

通过workerCountOf(int)方法获得工作线程数:

  • 运行(RUNNING):该状态下的线程池接收新任务并处理队列中的任务;线程池创建完毕就处于该状态,也就是正常状态;

    五种状态中他的值为负数

  • 关机(SHUTDOWN):线程池不接受新任务,但处理队列中的任务;线程池调用shutdown()之后的池状态;

    1.只能由RUNNING状态改变来,其他状态不改变

    2.中断闲置线程,继续处理队列中的任务,当前池内只剩下一个线程并且队列为空的时候进入TERMINATED

    总而言之就是将runnable线程直接用拒绝策略处理,并销毁空闲线程

  • 停止(STOP):线程池不接受新任务,也不处理队列中的任务,并中断正在执行的任务;线程池调用shutdownNow()之后的池状态;

    1.该状态是RUNNING或者SHUTDOWN状态下调用shutdownNow()后的结果

    2.会中断所有线程,包括活动

    3.将所有任务移动到另一个队列中,并由该方法返回

    4.如果当前线程数为0进入TIDYING

  • 清理(TIDYING):线程池所有任务已经终止,workCount(当前线程数)为0;过渡到清理状态的线程将运行terminated()钩子方法;

  • 终止(TERMINATED):terminated()方法结束后的线程池状态;

32.调用Shutdown方法以后,如何检测任务是否完成?(探活机制)

参考博客https://www.freesion.com/article/3476688817/

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
        	// 检查权限, 是否可以终止线程池
            checkShutdownAccess();
            // 自旋设置线程池状态为SHUTDOWN
            // 在shutDownNow里面这里设置的是STOP, 并且会返回一个Runnable的List
            advanceRunState(SHUTDOWN);
            // 将workers集合中的所有线程标记为interrupt
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

这里也需要获得锁,保证只有同一线程进入

    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                // 这里获取w的锁失败就表示w还有任务正在运行
                // 只有获取到任务的线程才会上锁防止被interrupt
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }

我们可以看见getTask中如果返回的是null, 那么在runWorker中的自旋也会结束, 当线程的run方法运行完毕之后, 就会终止

private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            // 如果使用的是shutDownNow这里就会直接返回null
            // 如果是shutDown那么阻塞队列为空时才会返回null, 即执行完毕阻塞队列中的任务
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
			// 省略
        }
    }

我们知道在设置线程池状态为SHUTDOWN之后 execute方法就无法添加新任务给线程池, 所以SHUTDOWN了之后, 阻塞队列中的任务不会再添加, 执行完现有任务之后线程池就会关闭

不论是shutDown还是shutDownNow正在执行的任务都会被执行完毕, 因为在设置interrupt的时候需要先获取worker的锁, 如果获取失败则会让worker通过自旋的getTask获取null来终止, 如果获取成功那么正在阻塞的线程都会抛出InterruptedException异常然后将终止标记清空继续运行

            try {
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }

阻塞队列中无任务的时候, 线程就会被阻塞在取阻塞队列任务的地方, 被interrupt抛出异常之后, 这里会捕获到异常, 然后timedOut设置为false重新进行自旋, 之后就会根据上述的线程池状态判断, 返回null

也就是说 其实底层是通过获取锁,还有task的返回来进行CAS操作

我看了别的博客也有用countdowmlatch来进行实现的

33.三个线程交替打印字符

 private int count = 0;
private final Object lock1 = new Object();
private final Object lock2 = new Object();
private final Object lock3 = new Object();

public static void main(String[] args) throws InterruptedException {
    new MultiTurningThread().multiTurning();
}

public void multiTurning() throws InterruptedException {
    Thread t1 = new Thread(new MultiTurningRunner(lock2, lock1, "线程1: 1"));
    Thread t2 = new Thread(new MultiTurningRunner(lock3, lock2, "线程2: 2"));
    Thread t3 = new Thread(new MultiTurningRunner(lock1, lock3, "线程3: 3"));
    t1.start();
    t2.start();
    t3.start();
}

class MultiTurningRunner implements Runnable {
    private final Object nextLock;
    private final Object currentLock;
    private final String content;

    public MultiTurningRunner(Object nextLock, Object currentLock, String content) {
        this.nextLock = nextLock;
        this.currentLock = currentLock;
        this.content = content;
    }

    @Override
    public void run() {
        while (count <= 100) {
            synchronized (nextLock) {
                synchronized (currentLock) {
                    System.out.println(content);
                    count++;
                    // 唤醒等待当前锁的线程
                    currentLock.notifyAll();
                }
                try {
                    // 如果还需要继续执行,则让出下一个线程对应的锁并进入等待状态
                    if (count <= 100) {
                        nextLock.wait();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值