面经-java并发

面试题

volatile

volatile关键字保证了变量的可见性和有序性。编译器在编译后会在volatile变量使用时多一个Lock指令。Lock指令在执行时会生成内存屏障来实现可见性和指令重排序的问题。

内存屏障的作用主要有两点:

  • 虚拟机执行时不会对内存屏障后的代码进行指令重排序
  • 在有内存屏障的地方,线程修改完变量后会将变量从工作内存同步到主内存,并且通过锁缓存通知其他线程对应的变量失效,其他线程在使用这个变量时会重新从主内存中刷新,这样就保证了变量的可见性。这里锁缓存涉及到一个缓存一致性协议mesi

mesi协议

在volatile关键字中提到了其是通过工作线程在修改共享变量后对数据缓存进行加锁来通知其他线程该变量数据失效需要重新从主内存中刷新。这里加锁方式有两种,一种是实现简单的总线加锁缓存行加锁。总线加锁会阻塞其他线程对内存的访问,这样会使效率大大降低。缓存行加锁是指线程在修改数据时只对要修改的数据那一行的缓存进行加锁。这样就不会其他线程对内存数据的读写了。

缓存行加锁失效的情况:①CPU不支持缓存一致性协议②变量的大小超过了一个缓存行的大小,加锁方式也会变成总线加锁的方式

mesi协议中缓存行中的数据有四种状态

  • M:修改
  • E:互斥
  • S:共享
  • I:无效

CPU1将共享变量a加入工作内存,并将CPU1的工作内存中共享变量a设置为状态E,CPU1会通过总线嗅探技术对变量a的操作进行嗅探

CPU2将共享变量a加入自己的工作内存,设置其在CPU2中的状态为S,并且CPU1因为总线嗅探技术也会将自己的变量a状态设置为S

CPU1对变量a进行修改后会将自己工作内存中的变量a设置状态为M

CPU2也会被通知将自己的变量a设置为I无效状态

这样CPU2在使用共享变量a时就会先从主内存中刷新变量a实现可见性。

juc中常见的类

java如何优化CAS

java1.8开始提供的LongAddr就是对CAS进行了优化,使用CAS分段锁的方式来提高并发

LongAddr中包含一个base基本数值和cell数组,当存在并发时,线程尝试对cell数组中的元素进行cas增加,返回值时返回cell数组中的值(遍历求和)+base的值

乐观锁和悲观锁

乐观锁:认为外部对数据的修改是不频繁的,每次访问数据时不要求上锁,只有在修改数据时才会检查访问期间是否有其他线程对数据进行修改,如果没有则进行修改,否者放弃修改。CAS锁就是常见的乐观锁

悲观锁:认为外部对数据的修改是频繁的,要求每次访问数据时都要上锁。

乐观锁的优点:线程竞争不激烈时效率较高

乐观锁的缺点:线程竞争激烈时效率较低,并且乐观锁是一般搭配自旋使用,竞争激烈时长时间不能获取锁的话反而会加大CPU的压力。

应用场景:乐观锁适合于读多写少的情况,悲观锁适合于线程竞争比较激烈的情况

sleep和wait的区别

  • sleep方法是Thread类下的方法,主要作用是想当前线程让出CPU等待指定的时间sleep是不会释放资源的,等待时间结束后,重新开始执行。
  • wait方法是Object类下的方法,线程执行wait方法后会释放已持有的资源和CPU,只有等其他线程notify时才会重新进入同步队列成功获取锁后才会继续执行

Callable和Future

Callable和Runable类似,但它执行完方法后可以返回值。Future一般配合Callable使用,Future中提供了get方法可以获取到方法的返回值,如果方法还未执行结束将一直阻塞,直到Callable方法执行结束

使用Callable()创建线程比另外两种方式有什么优势吗

Callable的优势就是可以让异步方法有返回值

CountDownLatch和CyclicBarrier的区别

CountDownLatch是一个主线程等待其他线程执行结束,拦截的是那一个主线程,而CyclicBarrier是所有线程互相等待,阻塞的是所有线程,并且CyclicBarrier是可复用的CountDownLatch不可复用

什么是线程池

线程池是持有一些线程并且管理这些线程,执行任务时只需要向线程池中提交任务,再由线程池中的线程进行执行

线程池的优点主要是,避免了频繁创建线程的销毁线程(非常耗时),线程池也可以对线程进行管理。

创建线程池主要有以下几个重要参数:核心线程数,最大线程数,线程存活时间,任务队列,拒绝策略

核心线程数:线程池中长期持有的线程数

最大线程数:线程池最大可以创建的线程数量

线程存活时间:是指超过核心线程数的线程存活的时间

任务队列:线程池采用何种任务队列

拒绝策略:当线程池无法处理任务时采用哪种拒绝策略

多线程

并发领域中存在三大特性:原子性有序性可见性

线程创建有哪些方法

  1. 继承Thread
  2. 实现Runnable
  3. 实现Callable

线程的六种状态

  1. 新建
  2. 运行
  3. 等待
  4. 超时等待
  5. 阻塞
  6. 终止

线程池

线程池作用

ThreadPoolExecutor重要参数

  1. 核心线程数
  2. 最大线程数
  3. 线程存活时间
  4. 饱和策略
    • 直接丢弃当前任务
    • 移除任务队列队首的任务
    • 抛出异常
    • 让调用者线程来执行
  5. 任务队列
    • ArrayBlockQueue
    • LinkedBlockingQueue

手写线程池

线程池工作流程

常见的线程池

FixedThreadPool

FixedThreadPoo中的任务队列采用的是LinkedBlockingQueue,这种阻塞队列长度默认是Integer的最大值,所以这种线程池将任务堆在任务队列中

CachedThreadPool

CachedThreadPool中的最大线程数是Integer的最大值,

如何设置线程池大小

IO密集型:线程数=2*CPU

普通型:线程数=CPU+1

ThreadLocal底层原理

  1. ThreadLocal是Java提供的线程本地存储机制,该机制将数据缓存在线程内部,线程可以在任意时刻、任意方法中获取缓存的数据

    线程之间是不共享的

  2. ThreadLocal底层使用ThreadLocalMap实现,每个线程对象都有一个ThreadLocalMap,其key值就是一个ThreadLocal对象,Value就是存储的值

内存泄漏问题

在线程池中使用ThreadLocal会造成内存泄漏,因为线程池中的对象使用完后不会被回收,对应的ThreadLocal也不会被回收,解决方法:在使用完ThreadLocal对象后调用其remove方法手动清除

package com.sy.chapter;

/**
 * @author 沈洋 邮箱:1845973183@qq.com
 * @create 2021/8/18-18:26
 **/
public class Test {
    private static ThreadLocal<String> s = new ThreadLocal<>();

    public static void main(String[] args) {
        s.set("sdssd");
        new Thread(new MyThread(),"m1").start();
        new Thread(new MyThread(),"m2").start();
//        System.out.println(t);
    }
    static class MyThread implements Runnable{

        @Override
        public void run() {
            System.out.println(s.get());
            s.set(Thread.currentThread().getName());
            System.out.println(s.get());
        }
    }
}

输出结果:

null
null
m2
m1

因为这个程序中运行时一共有三个线程,在主线程中设置的值在其他线程里面是拿不到东西的

要从锁的种类切入

锁按功能分,主要分为悲观锁乐观锁可重入锁共享锁

悲观锁是数据对外界的修改很保守,认为线程修改数据的几率很大,因此数据每次被修改时都要求线程上锁,比如常见的Synchronized锁就是悲观锁

乐观锁与悲观锁相反,它认为外界对它的修改几率很小,每次线程对数据进行修改时不要求上锁,修改完成后才会检查,例如CAS锁

可重入锁是指一个线程如果已经获得该锁时,可以继续获得该锁。Synchronized和ReentryLock都是可重入锁

共享锁是指同一时间允许多个线程获得锁,例如ReadWriteLock

独占锁是指同一时间只允许一共线程获得锁,例如Synchronized就是独占锁

Synchronized

Synchronized锁的原理

synchronized是Java提供的原子性内置锁,JVM是基于进入和退出Moniter对象来实现同步的。

使用synchroinzed之后,会在编译之后的同步代码块前后加上monitorenter和moniterexit的字节码指令,它依赖的是操作系统底层的互斥锁实现。线程在执行到moniterenter指令时会尝试获得对象的锁,如果对象没有被锁定或者已经获得了锁,锁的计数器+1,此时其他竞争锁的线程会进入等待队列

获得锁的线程在退出或抛出异常时会执行moniterexit指令使计数器-1,当计数器的值为0时则锁释放

synchronized锁重量的原因

虚拟机在获取锁时是通过操作系统的一个mutex lock指令实现的,而这个指令需要从用户态切换到内核态,所以导致来回切换的效率较低,并且在线程获取锁失败后会进入阻塞状态,这里还会涉及到线程上下文的切换。

Synchronized锁优化机制有哪些

JDK1.6之后Synchronized锁进行了优化,优化机制主要包括包括自旋锁、自适应锁、锁消除、锁粗化、偏向锁和轻量级锁

偏向锁:当线程访问同步块并获得锁后,会在对象头和栈帧的锁记录中存储偏向锁的线程ID,之后这个线程再次进入时都不需要加锁和解锁。偏向锁永远只会偏向第一个获得锁的线程,如果有其他线程竞争时,持有偏向锁的线程会释放锁。

自旋锁:因为大部分时间锁被占用的时间很短,其他线程在没有竞争到锁时没有必要挂起线程(用户态和内核态的来回上下问切换影响性能)。自旋锁的概念就是让一共锁忙循环避免转入内核态,自旋次数默认是十次,可以修改

自适应锁:自适应锁就是自旋锁,只是它每次的自选次数不是固定的,是根据前一次在这个锁上的自旋时间来决定的

锁消除:锁消除指的是JVM检测到同步代码块完全不存在数据竞争的场景,会对锁进行消除,即不对对象上锁(JIT即时编译器的优化)

锁粗化:线程频繁的获取同一个锁时,虚拟机会将同步块合并为一个锁,线程在执行时只获取一次锁。(JIT即时编译器的优化)

轻量级锁:线程进入同步块的时候,JVM将会使用CAS方式来尝试获取锁(替换MarkWord为锁记录指针),如果获取失败,当前线程就尝试自旋来获得锁。

Synchronized锁升级

从偏向锁是如何升级到重量级锁的 - JavaShuo

JDK1.6引入了偏向锁、轻量级锁之后总共有四种锁状态分别是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。

无锁状态是当前没有线程持有该对象的锁

偏向锁状态是指当线程访问同步代码块并获得锁之后,在锁对象头中记录当前线程的ID,之后这个线程再次进入时不需要加锁和解锁。偏向锁只会偏向第一个获得锁的线程。偏向锁只有遇到其他线程尝试获取该锁时,偏向锁会进行撤销,撤销时根据持有线程是否存活来决定是恢复为无锁状态还是升级为轻量级锁。

轻量级锁状态是指所有需要获得锁的线程在获取失败的情况下进入自旋,此时线程不会进入阻塞状态。自旋结束后重新获取锁,如果仍然获取锁失败则锁将升级为重量级锁

ReentryLock

tryLock和lock方法

lock()是阻塞加锁,线程执行lock时如果没有获得锁将会停在这里直到获得这个锁

trylock方法是有返回值的,线程尝试加锁,成功与否都会返回且不会停在这里(一般与自旋锁配套使用)

公平锁和非公平锁

ReentryLock中有两种锁,公平锁和非公平锁。默认使用非公平锁

非公平锁

非公平锁中,线程进入时会先尝试直接上锁,竞争失败后进入AQS队列中排队

注意,非公平锁与公平锁的区别只有这里,线程进入队列中排队后,锁释放时只会唤醒排在最前面的线程,而不是刚进入的线程。

公平锁

线程进入后,先检测是否有线程在队列中排队,如果存在则直接进入AQS队列中进行排队。如果没有线程在排队则尝试加锁

ReentrantLock和Synchronized的区别

  • Synchronized是基于虚拟机实现的,ReentrantLock是基于java层面的

  • Synchronized在发生异常时会自动释放锁,而Lock不会主动释放

  • Synchronized只能支持非公平锁,ReentrantLock可以支持公平锁

  • ReentrantLock可以绑定多个Condition条件对象,Synchronized只能绑定一个(Object.wait)

    借助Condition可以实现指定线程唤醒

  • Synchronized不能响应中断,ReentrantLock是可以响应中断

Condition

condition是ReentrantLock提供的一个等待通知机制,一个ReentrantLock可以创建多个Condition对象

CAS

包含三个操作数:变量内存地址、旧的预期值、准备设置的新值

当V=A时才能将V更新为B,否者不更新

缺点:

  • ABA问题:线程在将A修改为B的过程中,当前读取到的指是A,准备修改时还是A,实际上这中间可能会出现其他线程将A修改成B,再修改成A的情况。可以使用添加版本值,每次修改时同时检查预期值和版本值。
  • 自旋CAS长时间不能获取锁,会给CPU带来很大开销
  • CAS只能修改一个变量(解决办法,以引用的方式修改)

Java的内存模型(JMM)

Java内存模型中主要分为工作内存和主内存,所有变量都存放在主内存中,每条线程拥有自己的工作内存线程将主内存中的变量拷贝到自己的工作内存中,线程对变量的操作都是在工作内存中进行的,不直接对主内存中的变量进程操作,线程之间的工作内存不能互相访问,只能通过主内存进行交互。

img

volatile

参考博客:( Java volatile 原理解析_java的平凡之路-CSDN博客_java volatile原理

volatile修饰的变量能保证其对所有线程的可见性(保证了可见性)。即每个线程在使用时都是最新的值。同时Volatile关键字通过编译时在指令间插入内存屏障来实现禁止指令重排序(保证了有序性

Volatile对于并发三大特性只支持其中两个(不支持原子性)

在解释volatile前,我们要先了解Java中的内存模型

Volatile可见性实现原理

当共享变量用volatile修饰时,它会保证修改的值会被立即更新到主存中,同时其他线程的工作内存中该共享变量的缓存将失效,当线程下次读取该变量时,将强制主存中读取最新值。

使用说明

volatile变量不能解决脏读问题,例如下面代码

对volatile变量的修改不能依赖当前变量的值

Happens-Before原则

Happens-Before规则就是虚拟机定义的一系列可见性原则

程序顺序规则

一个线程的每一个操作都对该线程中任意后续操作可见

监视器规则

对一个锁的解锁都Happens-Before这个锁的加锁

Volatile规则

对一个volatile变量的写,都Happens-Before对这个变量的读

传递性

A Happens-Before B,B Happens-Before C

则A Happens-Before C

线程启动规则

线程的start方法都Happens-Before随后的操作

线程终止规则

线程中任意操作都Happens-Before线程终止

线程中断

对线程执行的中断都Happens-Before对线程中断的检测

对象终结规则

对象的初始化Happens-Before对象的finalize方法

JUC

ReentryLock

可重入互斥锁和synchronized定位相似

tryLock和lock方法

lock()是阻塞加锁,线程执行lock时如果没有获得锁将会停在这里直到获得这个锁

trylock方法是有返回值的,线程尝试加锁,成功与否都会返回且不会停在这里(一般与自旋锁配套使用)

公平锁和非公平锁

ReentryLock中有两种锁,公平锁和非公平锁。默认使用非公平锁

非公平锁

非公平锁中,线程进入时会先尝试直接上锁,竞争失败后进入AQS队列中排队

注意,非公平锁与公平锁的区别只有这里,线程进入队列中排队后,锁释放时只会唤醒排在最前面的线程,而不是刚进入的线程。

公平锁

线程进入后,先检测是否有线程在队列中排队,如果存在则直接进入AQS队列中进行排队。如果没有线程在排队则尝试加锁

AQS同步队列

实现原理

AQS内部维护了一个state状态位,线程通过CAS修改状态位的值来获取锁,如果成功修改,则将AQS中持有锁的线程ID修改为当前线程代表加锁成功,此时其他线程将会进入AQS的阻塞队列进行自旋(不会挂起),当持有锁的线程释放锁时,会通知下一个结点的线程来获取锁

AQS节点状态

5种

  • SIGNAL:表示节点处于同步状态
  • CANCLE:
  • INITIAL
  • PROPAGATE
  • CONDITION:表示节点在等待队列中

LockSupport

LockSupport是AQS同步队列的基石,其提供线程挂起/恢复的功能

LockSupport底层是使用Unsafe类提供的park和unpark功能,AQS中节点进入队列后就会被park起来。park起来的线程不会获得CPU,只有当其他线程使用unpark时才会将该线程唤醒。

park是不会释放当前线程持有的锁的,Condition就是基于LockSupport实现的但Condition会释放锁是因为Condition在调用park前会先进行释放锁的操作。

优点:可以实现精准阻塞唤醒线程,unpark和park顺序可以交换,notify和wait不行。

注意:LockSupport和Synchronized混合使用会导致死锁。

为什么使用park unpark替换了suspend resume

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println("thread suspend start");
        // synchronized
        LockSupport.park();
        //Thread.currentThread().suspend();
        System.out.println("thread suspend finished");
    });

    thread.start();
    // 主线程休眠500毫秒,让thread抢到锁
    Thread.sleep(500);
    System.out.println("thread resume start");
    LockSupport.unpark(thread);
    //thread.resume();
    System.out.println("thread resume finished");
}

suspend和park都会挂起线程,并且挂起的线程不会释放资源,而之所以park替代了suspend是因为park和unpark的执行顺序不会导致线程死锁也就是说即使unpark先于park执行,执行park时也会直接跳过这一次park不会导致线程阻塞,而suspend不同,如果resume先于suspend执行则将会导致线程完全阻塞。

ReentrantReadWriteLock

JUC提供的读写锁,读锁写锁公用的一个AQS,默认使用非公平锁

CountDownLatch

设置一个主线程等待其余线程, 子线程到达后技数器-1,当计数器为0时,主线程才会继续向下执行。

并且CountDownLatch只能使用一次

CyclicBarrier

类似于CountDownLatch,但是CyclicBarrier是所有线程互相等待,计数器到达指定值之后开始才会继续向下执行。

CyclicBarrier是可重复使用的。

Semaphore

信号量,使用时指定信号量大小

线程通过acquire方法获取信号量,如果可用信号量为0则阻塞,获取成功后可用信号量-1

线程通过release方法释放信号量,释放后信号量+1

Atomic原子类

AtomicInteger

AtomicLong

守护线程和用户线程

用户线程

  • 运行在前台,执行具体任务的线程是独立存在的线程

守护线程

  • 运行在后台,当所有用户线程关闭后守护线程自动关闭,守护线程的存活依赖于用户线程不能独立存在

  • 例如垃圾回收线程就是经典的守护线程

  • 设置守护线程只需要在启动线程前设置setDeanmon(true)即可

  • 注意:守护线程中开启的新线程也是守护线程

阻塞队列

ArrayBlockingQueue

数组组成的有界阻塞队列

LinkedBlockingQueue

链表组成的有界阻塞队列,默认长度是Integer的最大值

SynchronousQueue

容量为1的阻塞队列,每个offer都必须对应一个put

PriorityBlockingQueue

优先级阻塞队列,元素按优先级排列

线程死锁

两个或两个以上的线程在竞争资源时出现阻塞,在没有外部推动下线程无法继续执行。这种状态称为线程死锁。

线程死锁必要条件有四个

  • 资源互斥

    资源同时只能被一个线程使用

  • 请求保持

    线程在至少获取一个资源后当需要获取其他资源时被阻塞,并且不放弃自己已经获得的资源

    解决办法:①线程必须一次性获取所有锁②线程在获取其他锁时先释放自己已有的锁。

  • 环路等待

    产生死锁时,比如存在一个环形链。

  • 不可剥夺

    线程在获得资源后其他线程无法干预。

预防死锁

程序运行前破坏四个条件中的一个就可以。

  • 破坏资源互斥:将资源互斥条件改变(不现实,资源的特性一开始就决定了)
  • 破坏请求保持:线程需要一次性获取所有资源
  • 破坏不可剥夺:线程获取资源失败后释放自己的已持有的资源
  • 破坏环路等待:线程获取锁时按一定顺序获取

避免死锁

避免死锁不直接破坏四个条件

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenyang1026

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值