Java并发面试题总结(2022版)

标题 地址
Java虚拟机面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125881033
Java集合面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126058839
Mysql数据库面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125901358
Spring面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126354473
Redis面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126111149
Java并发面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/125984564
分布式面试题总结(2022版) https://blog.csdn.net/qq_37924396/article/details/126256455

1.线程

1.线程有几种状态

在 Java 中,线程主要分为六种状态:

初始(NEW):新创建了一个线程对象,但还没有调用 start() 方法;
运行(RUNNABLE):Java 线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”;
线程对象创建后,其它线程(如 main 线程)调用了该对象的 start() 方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取 CPU的 使用权,此时处于就绪状态(ready)就绪状态的线程在获得 CPU 时间片后变为运行中状态(running)
阻塞(BLOCKED):表示线程阻塞于锁;
等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断);
超时等待(TIMED_WAITING):该状态不同于 WAITING,它可以在指定的时间后自行返回;
终止(TERMINATED):表示该线程已经执行完毕。

2. 创建线程的四种方式

1.继承Thread类创建线程
2.实现Runnable接口创建线程
3.使用Callable和Future创建线程
4.使用线程池例如用Executor框架

ref 线程创建常用的四种方式

3. 线程sleep()和wait()的区别?

1.sleep() 来自 Thread 类,和 wait() 来自 Object 类
2.sleep() 方法不会释放锁,wait() 方法会释放,而且会加入到等待队列中
3.sleep() 方法不依赖于同步器 synchronized(),但是 wait() 方法 需要依赖 synchronized 关键字。
4.线程调用 sleep() 之后不需要被唤醒(休眠时开始阻塞,线程的监控状态依然保持着,当指定的休眠时间到了就会自动恢复运行状态),但是 wait() 方法需要被重新唤醒(不指定时间需要被别人中断)。
5.wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
6.sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常

4. 线程中start()和run()方法的区别?

1.调用线程的start()方法是创建了新的线程,在新的线程中执行。
2.调用线程的run()方法是在主线程中执行该方法,和调用普通方法一样

5. 如何保证线程的顺序执行

1.使用join
2.使用线程间通信的等待/通知机制 wait() 方法
3.使用Conditon
4.使用线程池
5.使用线程的CountDownLatch
6.使用cyclicbarrier (多个线程互相等待,直到到达同一个同步点,再继续一起执行。)
7.使用信号量 Semaphore

ref:如何保证线程的顺序执行

2.线程池

1.为什么使用线程池?

线程池是一种多线程处理形式,处理过程中将任务提交到线程池,任务的执行交由线程池来管理。
因为创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。
使用线程池可以减少创建和销毁线程的次数,每个工作线程都可以被重复利用,执行多个任务。提高程序效率的同时还方便管理以及限流。

扫描二维码关注公众号,回复: 14714928 查看本文章

2 线程池的七个参数?

1.corePoolSize
线程池中的核心线程数。当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行。
2.maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize。
3.keepAliveTime
线程空闲时的存活时间。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
4.unit :keepAliveTime参数的时间单位。
5.workQueue
工作队列,用来存放等待执行的任务。如果当前线程数为corePoolSize,继续提交的任务就会被保存到任务缓存队列中,等待被执行。
一般来说,这里的BlockingQueue有以下三种选择:

同步移交队列(SynchronousQueue):一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。因此,如果线程池中始终没有空闲线程(任务提交的平均速度快于被处理的速度),可能出现无限制的线程增长。

无界队列(LinkedBlockingQueue):基于链表结构的阻塞队列,如果不设置初始化容量,其容量为Integer.MAX_VALUE,即为无界队列。因此,如果线程池中线程数达到了corePoolSize,且始终没有空闲线程(任务提交的平均速度快于被处理的速度),任务缓存队列可能出现无限制的增长。

有界队列(ArrayBlockingQueue):基于数组结构的有界阻塞队列,按FIFO排序任务。

6.threadFactory
线程工厂,创建新线程时使用的线程工厂。
7.handler
任务拒绝策略,当阻塞队列满了,且线程池中的线程数达到maximumPoolSize,如果继续提交任务,就会采取任务拒绝策略处理该任务,线程池提供了4种任务拒绝策略:
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常,默认策略;
CallerRunsPolicy:由调用execute方法的线程执行该任务;
DiscardPolicy:丢弃任务,但是不抛出异常;
DiscardOldestPolicy:丢弃阻塞队列最前面的任务,然后重新尝试执行任务(重复此过程)。
当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

3 线程池的任务流程

在这里插入图片描述
1,线程池内部会获取activeCount, 判断活跃线程的数量是否大于等于corePoolSize(核心线程数量),如果没有,会使用全局锁锁定线程池,创建工作线程,处理任务,然后释放全局锁;
2,判断线程池内部的阻塞队列是否已经满了,如果没有,直接把任务放入阻塞队列;
3,判断线程池的活跃线程数量是否大于等于maxPoolSize,如果没有,会使用全局锁锁定线程池,创建工作线程,处理任务,然后释放全局锁;
4,如果以上条件都满足,采用饱和处理策略处理任务。
说明:使用全局锁是一个严重的可升缩瓶颈,在线程池预热之后(即内部线程数量大于等于corePoolSize),任务的处理是直接放入阻塞队列,这一步是不需要获得全局锁的,效率比较高。

4 线程池的四种拒绝策略

在线程池 ThreadPoolExecutor 中,已经包含四种处理策略。

AbortPolicy:直接丢弃任务,并抛出 RejectedExecutionException 异常,也是线程池默认采用的策略;
CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的 run 方法,除非线程池已经 shutdown,则直接抛弃任务;
DiscardOleddestPolicy:抛弃进入队列最早的那个任务,也是即将被执行的任务,然后尝试把这次拒绝的任务放入队列;
DiscardPolicy:直接丢弃任务,不予任何处理。
除了 JDK 默认提供的四种拒绝策略,我们可以通过实现 RejectedExecutionHandler 接口根据自己的业务需求去自定义拒绝策略。

5.核心线程用完后会销毁吗?

核心线程通常不会回收,java核心线程池的回收由allowCoreThreadTimeOut参数控制,默认为false,若开启为true,则此时线程池中不论核心线程还是非核心线程,只要其空闲时间达到keepAliveTime都会被回收。但如果这样就违背了线程池的初衷(减少线程创建和开销),所以默认该参数为false。

6.JDK自带的线程池有哪些

newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

3.锁

1.Java中锁的种类

Java中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。
在这里插入图片描述
ref:Java中的各种锁
ref:互斥锁、自旋锁、读写锁、悲观锁、乐观锁的应用场景

2.什么是死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁

3.死锁产生的四个必要条件

1、互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
2、不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
3、请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
4、循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

4.synchronized关键字

1.synchronized 的用法

synchronized 是 Java 中的一个关键字,可以用来修饰方法或代码块,使用方法也很简单,就是在方法或代码块前加 synchronized 修饰符。只是二者的作用范围不同。

修饰普通方法
一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。普通同步方法,锁是当前实例对象;

修饰静态方法
由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。静态同步方法,锁是当前类的class对象;

修饰代码块
其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。同步方法块,锁是括号里面的对象。

2.synchronized 的实现原理

synchronized 关键字底层原理属于JVM 方面 在JVM中对象的实例存储在堆上面,对象在内存中的布局分为三部分:对象头、实例数据、对齐填充
在这里插入图片描述

Java对象头
Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word(标记字段) 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等它是实现轻量级锁和偏向锁的关键。(64bit)

Klass Point(类型指针) 是对象指向它的类元数据的指针,虚拟机通过这个指针来确定对象是那个类的实例 (32bit 或 64bit)

实例数据
存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

对齐填充(非必须)
由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

3.锁升级过程

无锁-偏向锁-轻量级锁(无锁 、自旋锁)-重量级锁
在这里插入图片描述
偏向锁 升级为 轻量级锁
当下一个线程访问锁只要存在竞争就升级为轻量级锁,
过程:撤销偏向锁状态,每个线程都有自己的线程栈,在自己的线程栈中生成自己的 lock Record,然后就去抢锁 看谁能把自己的lockRecord 指针贴到锁上 抢的过程使用自旋Cas的方式来抢

轻量级锁 升级为 重量级锁
在自旋超过 10次 或者 在这等着的自旋的线程超过CUP 核数的二分之一 (1.6之前 旧的)
加入了自适应自旋 Adapative Self Spinnging,JVM 自己控制(1.6之后)

适应自旋锁
JDK 1.6引入了更加聪明的自旋锁,即自适应自旋锁。所谓自适应就意味着自旋的次数不再是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。它怎么做呢?线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。反之,如果对于某个锁,很少有自旋能够成功的,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。
有了自适应自旋锁,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测会越来越准确,虚拟机会变得越来越聪明。

重量级锁
内核态 重量级锁 内部有队列
用户态 轻量级锁 消耗CPU

ref:synchronized的实现原理及锁优化

5.volatile关键字

1.volatile简介

我们已经知道可见性、有序性及原子性问题,通常情况下我们可以通过Synchronized关键字来解决这些个问题,不过如果对Synchronized原理有了解的话,应该知道Synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用Synchronized来解决问题。
而volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。对于原子性,需要强调一点,也是大家容易误解的一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

2.volatile的作用

作用一:防止重排序
我们从一个最经典的例子来分析重排序问题。大家应该都很熟悉单例模式的实现,而在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。其源码如下: 两次检测在加一把锁 必须声明为 volatile

package com.paddx.test.concurrent;

public class Singleton {
    
    
    
    public static volatile Singleton singleton;

    /**
     * 构造函数私有,禁止外部实例化       
     */
    private Singleton() {
    
    };

    public static Singleton getInstance() {
    
    
        if (singleton == null) {
    
    
            synchronized (singleton) {
    
    
                if (singleton == null) {
    
    
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

现在我们分析一下为什么要在变量singleton之间加上volatile关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
1.分配内存空间。
2.初始化对象。
3.将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:
1.分配内存空间。
2.将内存空间的地址赋值给对应的引用。
3.初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。

作用二:实现可见性

可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。
volatile关键字能有效的解决这个问题,我们看下下面的例子,就可以知道其作用:

package com.paddx.test.concurrent;

public class VolatileTest {
    
    
    int a = 1;
    int b = 2;

    public void change(){
    
    
        a = 3;
        b = a;
    }

    public void print(){
    
    
        System.out.println("b="+b+";a="+a);
    }

    public static void main(String[] args) {
    
    
        while (true){
    
    
            final VolatileTest test = new VolatileTest();
            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    test.change();
                }
            }).start();

            new Thread(new Runnable() {
    
    
                @Override
                public void run() {
    
    
                    try {
    
    
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                    test.print();
                }
            }).start();

        }
    }
}

直观上说,这段代码的结果只可能有两种:b=3;a=3 或 b=2;a=1。不过运行上面的代码(可能时间上要长一点),你会发现除了上两种结果之外,还出现了第三种结果:

b=2;a=1
b=2;a=1
b=3;a=3
b=3;a=3
b=3;a=1
b=3;a=3
b=2;a=1
b=3;a=3
b=3;a=3

为什么会出现b=3;a=1这种结果呢?正常情况下,如果先执行change方法,再执行print方法,输出结果应该为b=3;a=3。相反,如果先执行的print方法,再执行change方法,结果应该是 b=2;a=1。那b=3;a=1的结果是怎么出来的?
原因就是第一个线程将值a=3修改后,但是对第二个线程是不可见的,所以才出现这一结果。如果将a和b都改成volatile类型的变量再执行,则再也不会出现b=3;a=1的结果了。

3.Java内存模型(JMM)

JVM内存模型:主内存 和 线程独立的 工作内存。Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存(比如CPU的寄存器),线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的副本,线程要操作这些共享
变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。
Java 多线程内存模型跟cpu缓存模型类似,是基于cpu 缓存模型建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别
在这里插入图片描述

4.什么是缓存一致性协议(MESI)

在这里插入图片描述

现在的CPU基本都是多核CPU,服务器更是提供了多CPU的支持,而每个核心也都有自己独立的缓存,当多个核心同时操作多个线程对同一个数据进行更新时,如果核心2在核心1还未将更新的数据刷回内存之前读取了数据,并进行操作,就会造成程序的执行结果造成随机性的影响,这对于我们来说是无法容忍的。
而总线加锁是对整个内存进行加锁,在一个核心对一个数据进行修改的过程中,其他的核心也无法修改内存中的其他数据,这样会导致CPU处理性能严重下降。
缓存一致性协议提供了一种高效的内存数据管理方案,它只会对单个缓存行(缓存行是缓存中数据存储的基本单元)的数据进行加锁,不会影响到内存中其他数据的读写。因此,我们引入了缓存一致性协议来对内存数据的读写进行管理。

MESI协议
缓存一致性协议有MSI,MESI,MOSI,Synapse,Firefly及DragonProtocol等等,接下来我们主要介绍MESI协议。
MESI分别代表缓存行数据所处的四种状态,通过对这四种状态的切换,来达到对缓存数据进行管理的目的。

状态 描述 监听任务
M 修改(Modify) 该缓存行有效,数据被修改了,和内存中的数据不一致,数据只存在于本缓存行中 缓存行必须时刻监听所有试图读该缓存行相对应的内存的操作,其他缓存须在本缓存行写回内存并将状态置为E之后才能操作该缓存行对应的内存数据
E 独享、互斥(Exclusive) 该缓存行有效,数据和内存中的数据一致,数据只存在于本缓存行中 缓存行必须监听其他缓存读主内存中该缓存行相对应的内存的操作,一旦有这种操作,该缓存行需要变成S状态
S 共享(Shared) 该缓存行有效,数据和内存中的数据一致,数据同时存在于其他缓存中 缓存行必须监听其他处理器修改该缓存行相对应的本地缓存行的操作,一旦有这种操作,该缓存行需要变成I状态
I 无效(Invalid) 该缓存行数据无效

备注:

  1. MESI协议只对汇编指令中执行加锁操作的变量有效,表现到java中为使用volatile关键字定义变量或使用加锁操作
  2. 对于汇编指令中执行加锁操作的变量,MESI协议在以下两种情况中也会失效:
    1) CPU不支持缓存一致性协议。
    2) 该变量超过一个缓存行的大小,缓存一致性协议是针对单个缓存行进行加锁,此时,缓存一致性协议无法再对该变量进行加锁,只能改用总线加锁的方式。

ref:缓存一致性协议(MESI)

6.Lock

1.Lock简介

Lock接口是对锁操作的基本定义,它提供了synchronized关键字所具备的全部功能方法,另外我们可以借助Lock创建不同的Condtion对象进行多线程间的通信操作。

2.Lock方法

public interface Lock {
    
    
    //尝试获取锁,获取成功则返回,否则阻塞当前线程
    void lock();
     
    //尝试获取锁,线程在成功获取锁之前被中断,则放弃获取锁,抛出异常 
    void lockInterruptibly() throws InterruptedException; 
    
    //尝试获取锁,获取锁成功则返回true,否则返回false 
    boolean tryLock();
    
    //尝试获取锁,若在规定时间内获取到锁,则返回true,否则返回false,未获取锁之前被中断,则抛出异常 
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
 
     //释放锁
    void unlock(); 

    //返回当前锁的条件变量,通过条件变量可以实现类似notify和wait的功能,一个锁可以有多个条件变量
    Condition newCondition();
}

ref:Java中的Lock详解

3.Lock和synchronized 区别?

1)存在层面:Lock是一个接口是jdk层面的锁,而synchronized是Java中的关键字,存在于JVM 方面
2)锁释放条件:synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)锁状态:通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)锁类型:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可中断,公平锁 Lock可以提高多个线程进行读操作的效率。
6)锁性能:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:
Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;
ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等

4.ReentrantLock

1.ReentrantLock介绍

ReentrantLock表示重入锁,它是唯一一个实现了Lock接口的类。重入锁指的是线程在获得锁之后,再次获取该锁不需要阻塞,而是直接关联一次计数器增加重入次数

ReentrantLock是一种排他锁,同一时刻只允许一个线程访问,ReadWriteLock 接口的实现类 ReentrantReadWriteLock 读写锁提供了两个方法:readLock()和writeLock()用来获取读锁和写锁,也就是说将文件的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。

读写锁维护了两个锁,一个是读操作相关的锁也称为共享锁,一个是写操作相关的锁也称为排他锁。通过分离读锁和写锁,其并发性比一般排他锁有了很大提升。

多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥(只要出现写操作的过程就是互斥的)。在没有线程进行写操作时,进行读取操作的多个线程都可以获取读锁,而进行写入操作的线程只有在获取写锁后才能进行写操作。即多个线程可以同时进行读取操作,但是同一时刻只允许一个线程进行写入操作。

2.ReentrantLock的公平锁和非公平锁

ReentrantLock提供了两种锁机制:公平锁和非公平锁。公平锁通过类FairSync提供的方法实现,非公平锁通过NonfairSync提供的方法实现。

ReentrantLock:模式是非公平锁。也可通过构造方法创建公平锁;

public ReentrantLock() {
    
    
	sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
}

ReentrantReadWriteLock:默认是非公平锁,也可以通过构造方法创建公平锁;

public ReentrantReadWriteLock() {
    
    
	this(false);
}
public ReentrantReadWriteLock(boolean fair) {
    
    
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

5.ReentrantReadWriteLock

1.ReentrantReadWriteLock介绍

ReentrantReadWriteLock重入读写锁,它实现了ReadWriteLock接口,在这个类中维护了两个锁,一个是ReadLock,一个是WriteLock,他们都分别实现了Lock接口。读写锁是一种适合读多写少的场景下解决线程安全问题的工具,基本原则是:读和读不互斥、读和写互斥、写和写互斥。也就是说涉及到影响数据变化的操作都会存在互斥。

读写锁,可以分别获取读锁或写锁。也就是说将数据的读写操作分开,分成2个锁来分配给线程,从而使得多个线程可以同时进行读操作。读锁使用共享模式;写锁使用独占模式;读锁可以在没有写锁的时候被多个线程同时持有,写锁是独占的。当有读锁时,写锁就不能获得;而当有写锁时,除了获得写锁的这个线程可以获得读锁外,其他线程不能获得读锁

6.AQS

1.什么是AQS?

AQS中文名抽象队列同步器,全称为AbstractQueuedSynchronizer,它提供了一个FIFO队列(先入先出),可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。
AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。
可以这么说,只要搞懂了AQS,那么J.U.C中绝大部分的api都能轻松掌握。

ref:深入分析AQS实现原理

7.CAS?

1.什么是CAS?

CAS(Compare And Swap): 比较并替换,它是一条CPU原语,是一条原子指令(原子性)。
CAS通过比较真实值与预期值是否相同,如果是则进行修改,Atomic原子类底层就是使用了CAS,CAS属于乐观锁。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么会自动将该内存位置的值更新为新值,反之不做任何操作。

2.CAS 有哪些缺点?

1. ABA问题
     CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。

2. 只能保证一个共享变量的原子操作
     多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3. 循环时间长CPU开销较大
     在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

ref:一篇搞定CAS,深度讲解,面试实践必备

8.ThreadLocal

1. ThreadLocal 是什么?

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

2.ThreadLocal提供四个方法


//用来获取ThreadLocal在当前线程中保存的变量副本
public T get() {
    
     }

//用来设置当前线程中变量的副本
public void set(T value) {
    
     }

//用来移除当前线程中变量的副本
public void remove() {
    
     }

//是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法
protected T initialValue() {
    
     }

3.ThreadLocal 为什么会出现内存泄漏

在这里插入图片描述
运行时,会在栈中产生两个引用,指向堆中相应的对象。
可以看到,ThreadLocalMap使用ThreadLocal的弱引用作为key,这样一来,当ThreadLocal ref和ThreadLocal之间的强引用断开 时候,即ThreadLocal ref被置为null,下一次GC时,threadLocal对象势必会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,比如使用线程池,线程使用完成之后会被放回线程池中,不会被销毁,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
但是这些被动的预防措施并不能保证不会内存泄漏:
使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

猜你喜欢

转载自blog.csdn.net/qq_37924396/article/details/125984564