多线程与并发编程面试题

简介: 多线程与并发编程

多线程与并发编程

多线程

  • 线程和进程的区别?

  1. 从操作系统层面上来讲:进程(process)在计算机里有单独的地址空间,而线程只有单独的堆栈和局部内存空间,线程之间是共享地址空间的,正是由于这个特性,对于同时共享变量的并发作,可以用多线程来操作;

  2. 从资源消耗的角度来说: CPU时间片是以进程来进行切换的,并且在进程间切换需要花更多时间,比如说IO阻塞,这样说来线程在消耗空间和时间上会更大;

  3. 总之,进程是资源分配的基本单元,线程是执行的基本单元。

  • Java中线程的状态图

image-20230812010026530

  • 用户线程和守卫线程的区别?

  1. 使用守卫线程通过Thread.setDaemon(boolon)命令实现。

  2. 守卫线程和用户线程的区别是,守卫线程为用户线程提供服务,等用户线程结束,守卫线程会自动结束。

  • notify()和notifyAll()的区别?

notify和notifyAll都是唤醒waiting线程,其中notify是唤醒特定条件的线程,notifyAll是唤醒所有阻塞线程;

  • Thread.sleep()和Object.wait()的区别?

  1. sleep是当前Thread的方法,其设定休眠时间后,就会自动苏醒,而wait()的话需要notify()进行唤醒。

  2. 执行Thread.sleep()方法后,线程不会释放锁;执行object.wait()方法后,线程会释放锁,意味着其他线程可以使用临界资源,等你调用notify()后会重新获得锁,重启抢占临界资源。由于Java中加锁的是对象,所以wait()和notity()是Object上的方法,由于sleep()方法不涉及释放锁,所以他是Thread上的方法。使用wait()方法,必须在处于synchronized代码块或者synchronized方法中,这是因为wait()方法的实现需要获取对象锁,或者说需要获取monitor锁。

    总结:

    JVM中会维护两个线程队列来区分加锁情况,包括: 

  3. 同步队列:保存一起竞争同一把锁对象的线程。

  4. 阻塞队列:保存调用wait()后,竞争获取锁的线程。

  • yield()和sleep()的区别?

  1. yield()是给其他线程机会,自己线程直接进入ready状态。

  2. sleep()是直接被阻塞掉,被其他任何优先级的线程抢占。

  • 多线程的创建方式?

  1. 继承Thread,调用start()启动;

  2. 实现Runnable接口,和继承Thread的区别在于由于Java是单继承,如果已经继承了其他类,只能通过Runnable的方式来创建线程;

  3. 通过Callnable接口来创建,他和Runnable其实一样,只不过由返回值;

  4. 通过Executor来创建线程池;

  • 线程池的使用和原理?

线程池的几种类型:

  1. 预置线程池:一般情况下可以通过Executors这个框架来创建,他已经封装了几种适合不同场景的线程,(1)newCachedThreadPool:这个线程的特点是没有工作队列,所有来的线程直接到工作线程池中,来一个任务创建一个线程,适合处理大量短连接类需求;(2)newFixedThreadPool:这个类型线程是创建固定大小的工作线程池,但是工作队列是无界的,这个在使用时候小心OOM问题;(3)newSingleThreadPool:这类线程只创建一个工作线程,所以进入该类线程池的线程满足FIFO特性;(4)newScheduledThreadPool,这个是定时执行线程的线程池,可以用来做定时任务。

  2. 自定义线程池:另外一种创建线程的方法就是直接调用ThreadPoolExecutor这个类,他有几个参数要注意:

public ThreadPoolExecutor(int corePoolSize,//核心线程池的大小 
int maximumPoolSize,//最大线程池的大小 
long keepAliveTime,//存活时间 
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//工作队列
ThreadFactory threadFactory,//线程产生工厂
RejectedExecutionHandler handler)//拒绝策略

Excutors框架的组成:

大概简述下这个框架的设计和运行逻辑:其中Executor和ExecutorService是接口类,实现创建和销毁线程的方法,AbstractExecutorServer实现了submit方法,这里需要了解接口和抽象类的区别了,最后ThreadPoolExecutor是线程池创建的主要类,其中调用submit就能将进程加入线程池,而Executors是线程池创建的辅导类,很多规定好的线程模版可以直接创建。

image-20230812010214990

Executor中execute()方法的逻辑:

public void execute(Runnable command) {
   
    
    //1、判断是否传进来线程 
    if (command == null) 
        throw new  NullPointerEception(); 
    int c = ctl.get(); 
    //2、判断工作线程池是否大于核心线程 
    if (workerCountOf(c) < corePoolSize) {
   
    
        if (addWorker(command, true)) 
            return; 
        c = ctl.get(); 
    } 
    //3、判断工作队列是否满了 
    if (isRunning(c) && workQueue.offer(command)) {
   
    
        int recheck = ctl.get(); 
        if (! isRunning(recheck) && remove(command)) 
            reject(command);
        else if (workerCountOf(recheck) == 0) 
            addWorker(null, false);
    } 
    //4、以上条件都不符合,直接拒绝 
    else if (!addWorker(command, false)) 
        reject(command);
}
  • ThreadPoolExecutor的主要参数和运行原理?

主要参数:

public ThreadPoolExecutor(int corePoolSize,//核心线程池的大小 
int maximumPoolSize,//最大线程池的大小 
long keepAliveTime,//存活时间 
TimeUnit unit,//时间单位
BlockingQueue<Runnable> workQueue,//工作队列
ThreadFactory threadFactory,//线程产生工厂
RejectedExecutionHandler handler)//拒绝策略

运行原理:

  1. 任务通过execute()提交到线程池中,会去判断运行线程数量是否大于corePoolSize,如果小于则会创建工作线程执行该任务;

  2. 如果运行线程数大于corePoolSize则会将任务放到阻塞队列中排队;

  3. 如果阻塞队列排队满了的话就会比较运行线程数量和maximumPoolSize的大小,如果小于则会将线程池线程数扩容,直到扩容到最大线程数;

  4. 如果扩容到maximumPoolSize最大值还是无法满足运行任务的需求,则会按照拒绝策略进行拒绝;

  5. 对于运行完的线程池,如果超过存活时间,则会被进行回收,但是存活线程数不能低于corePoolSize;

介绍了线程池原理:

https://juejin.im/entry/58fada5d570c350058d3aaad

ThreadPoolExecutor 参数详解:

https://blog.csdn.net/Jack_SivenChen/article/details/53394058

  • 执行 execute()方法和 submit()方法的区别是什么呢?

  1. execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

  2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

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

调用 start() 方法方可创建新线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行,run()方法中的逻辑是执行任务的逻辑。

并发编程

  • 并行和并发的区别?

  1. 并发(concurrent)是多个任务抢占一颗处理器,通过时间片轮转的方式来做到类实时处理任务的效果。

  2. 并行(parallel)是多个任务在多颗处理器上同时执行任务,不存在线程相互抢占的情况。

image-20230812010455188

  • 并发编程需要解决的问题?

  1. 可见性:解决主存和工作内存数据不一致的问题。在工作内存中修改的变量 不会马上写入主存,该变量被其他线程加载就会存在不一致问题。解决方案为:(1)volatile;(2)synchronized+Lock;(3)final变量;

  2. 顺序性:JVM对字节码中指令加载的顺序不一定是顺序的,在做JIT优化的时候存在顺序变化的情况。解决方案有:(1)volatile;(2)synchronized+Lock;

  3. 原子性:一个或多个操作要么全部执行要么全都不执行,和数据库中的原子性概念一样。解决方案有:(1)synchronized+Lock;(2)CAS;(3)Atomic类;

#线程同步

  • 线程同步的方法?

由于Java 内存模型决定多线程在获取内存中内容的时候可能存在数据不一致的问题,所以在进行多线程编程的时候,需要考虑线程同步的问题,线程同步主要是为了解决实例变量不一致的问题。包括以下几种解决方案:

  1. 通过互斥锁方式:包括synchronized、Lock锁等;

  2. 通过非阻塞同步方式:包括volatile、CAS,Atomic等;

  3. 使用同步工具:wait()/notify()、JUC下工具等;

  4. 通过共享变量方式:包括ThreadLocal,类变量等;

#线程通信

  • 进程通信的方法?

进程通信是让多个进程之间能够相互发送信息,主要是为了保障进程间正常的执行顺序的机制;

  1. volatile:volatile的可见性可以让两个或多个线程做到准确的通信;

  2. 通知/唤醒机制:通过wait/notify方法可以对竞争同一个加锁对象的线程进行通知和唤醒;通过park/unpark能够直接对对象进行通知和唤醒;

  3. thread.join:可以实现主线程等待子线程的效果;

  4. 管道的输入输出流:可以作用于线程之间进行数据传输。

  • 怎样控制线程的执行顺序?

想要了解线程的执行顺序,先还是需要了解线程的状态图,通过这个图可以看出哪些地方可以控制线程的执行顺序。

  1. 通过wait()和notify()的方法可以控制线程执行顺序;

  2. 可以通过join()方法来控制线程的执行顺序,他的作用就是让子线程先执行完了在执行主线程,看他的代码,其实他也是在调用wait的方法,就是只要这个线程没有执行完成,就让子线程一直执行下去;

image-20230812010527312

  1. 创建单线程的线程池,单线程的线程池是一种FIFO的方式来执行的,这样可以做到控制执行顺序;

  2. 通过堆栈数据结构来实现。

#Lock+CAS

  • Lock的分类

悲观锁&乐观锁:

  1. 悲观锁就是线程对共享资源的访问前先对共享资源加锁,禁止其他线程来访问修改;适合于写多读少的场景。

  2. 乐观锁是假设线程对共享资源的访问时其他线程并没有对其有修改,如果后续判断有修改再进行重试或者回退的机制,一般乐观锁的实现机制是CAS方案,就是判断值v修改后的预期值A是否和实际值B相等,如果A=B,则访问过程该共享资源没有进行修改。

自旋锁:

由于加锁线程从阻塞状态进入运行状态涉及到CPU的切换,所以设计了自旋锁,就是对于没有获取锁的线程,可以一直循环获取锁,通过这种方式来减少CPU切换带来的性能消耗。

公平锁&非公平锁:

公平锁是指线程想要获取锁,需要在阻塞队列后面排队,锁被释放后按照队列依次加锁;非公平锁是指线程想要获取锁,会先去请求锁,如果请求到了锁,就直接占用锁了,如果没有后去锁后再去阻塞队列后排队;

可重入锁&不可重入锁:

可重入锁是指该线程获取了该对象锁,该线程还能够继续访问该对象。可重入锁又称为递归锁,是指线程在外层方法获取了该对象的锁,方法内还能再次调用加锁方法。不可重入锁即是线程已访问该加锁对象后,无法再次进入该加锁对象进行操作。synchronized和Lock都是可重入锁。

独占锁&共享锁:

判断该锁是否独占还是可以读写区别对待。

不可不说的Java“锁”事:https://tech.meituan.com/2018/11/15/java-lock.html

  • volatile的应用和原理?

volatile是解决不同线程变量无法实时共享导致数据不一致的问题。在JMM中,不同线程变量需要先写入各自线程的私有线程缓存中,一般是在CPU寄存器中,再由寄存器写入到内存中。

  1. volatile解决可见性:

volatile关键词强制要求每次对变量的读写要写入主存中,从而保障变量对所有线程的可见性。

  1. volatile解决顺序性:

(1)通过设置内存屏障解决指令重排。Volatile会设置内存屏障,禁止指令重排,让对数据的读写不会乱序,内存屏障是一条CPU指令,实现该指令前后的指令不能重排。

(2)通过happens-before规则来实现指令的有序性。就是对于多线程执行命令需要满足一定的规则,比如wait()之后才能notify()。

总结:

volatile无法解决并发中的原子性问题。

  • CAS的原理?

概述:总的来说,Java中锁分为悲观锁和乐观锁,悲观锁是以synchronized和Lock为主的重量级锁,还有一种是以原子类Atomic为代表的轻量级锁; CAS(compare and swap)是一种乐观锁的实现方式,如其名字含义,他的核心思想就是先比较再替换,在AtomicInteger和其他原子操作的工具类中运用的比较多。

核心思路是比较三个值,CAS(V,E,N),其中V是要更新的值的地址,E是预期查询出来的值,N是更新后的值,只有当要更新的V查到的值和预期值E一致的时候,才能正常更新到N。

原理:在JDK中,Atomic类中大量使用了CAS方法来进行自增操作,他的底层是利用Unsafe类来进行操作,该类是对内存进行直接操作,保障指令的原子性。

image-20230812010607055

如何解决ABA的问题?

ABA问题是当我查询到V所对应的值A后,又有其他线程将该值改为B,后又改回A的情况,这种情况对于CAS操作来说是分辨不出来的。一种常见解决类似ABA的问题的思路是加入了version字段用来标识每次更新的版本信息,如果更新完成之后version发生了变更,就表明被其他线程进行了更改。在CAS中,是通过Atomic类下的AtomicStampedReference解决了ABA的问题,是通过加入了时间戳(stamp)来区分是否中途有更改,获取到新值之后,再通过时间戳比较是否中途被其他线程修改过。

image-20230812010617458

  • synchronized的原理?

synchronized的作用域:

  1. synchronized作用于实例方法时,锁住的是对象实例。

  2. synchronized作用于静态方法时,锁住的是Class实例,锁住了该类,所有调用该类的方法都被锁住。

  3. synchronized作用于代码块时,锁定锁对象,进入代码块前需要获取加锁对象的锁。

    image-20230812010629608

synchronized作用代码块的原理:

同步代码块是利用了JVM的monitor锁机制来实现,通过monitorEntry和monitorExit指令,当开始进入同步代码块,调用monitorEntry命令,在该对象上进行加锁操作,当退出同步代码块,调用monitorExit命令,在该对象上进行释放锁的操作。Java中每个对象都有Monitor标识字段,积累着加锁的次数。

synchronized作用方法的原理:

该种是隐式同步,不需要同步字节码命令来实现,主要在方法调用和返回操作中做标识,由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 的access flags来隐式实现的,JVM从方法常量表中标识该方法区分该方法是同步方法,当返回是取消标识。

锁的可重入性含义:

当一个线程进入其他线程加锁的共享资源时,会进入阻塞状态,当进入自己线程加锁的共享资源时,会获取锁,能够正常执行后续逻辑,这种情况就是锁的可重入性。

深入理解Java并发之synchronized实现原理:

https://blog.csdn.net/javazejian/article/details/72828483

  • synchronized锁升级的概念?

在JDK1.6以后,synchronized的锁状态会进行升级:无锁—>偏向锁—>轻量级锁—>重量级锁;

无锁:

所有线程都可以对共享资源进行访问并进行修改,但只能同时有一个线程对其进行成功修改。

偏向锁:

当共享资源进程被一个线程进行访问,那么该线程就会获取该共享资源的偏向锁,之后该线程对该资源进行访问减少加锁操作,除非有其他线程尝试竞争该偏向锁,这种方式减少CPU线程间切换,提高性能。

轻量级锁(自旋锁):

是指当线程获取偏向锁后,其他线程竞争该资源,该锁就会升级成轻量级锁,这是基于一个假设,就是大多数锁在同一个场景都不存在竞争,除工作线程外其他线程通过自旋的方式来获取锁,这种方式即为竞争线程非阻塞的多次请求锁,其他线程不会阻塞可以避免线程状态的切换,提升性能。

重量级锁:

等待的锁一直处于忙等的状态是有限度的,等待的自旋的线程自旋次数超过默认值(10次),就会从轻量级锁切换到重量级锁,减少忙等现象。

image-20230812010652480

深度分析:锁升级过程和锁状态,看完这篇你就懂了!:https://segmentfault.com/a/1190000022904663

  • synchronized锁优化的方法?

  1. 自旋锁:由于线程进行上下文切换的时候会消耗性能,对于需要抢占锁的线程可以进入自旋等待的状态,避免线程上下文切换。

  2. 锁消除:对于不会形成线程抢占的代码,会自动进行锁消除。

  3. 锁粗化:对于多个加锁逻辑同时存在的情况,会将这几个加锁逻辑合并成一个,减少加锁操作。

  • volatile和synchronized的区别?

volatile和synchronized都可以用来解决并发问题,保持数据一致性。

  1. valatile通过非阻塞同步的方式保障的是可见性和顺序性。的作用是保证线程读写的可见性,就是多个线程对于一个变量的读写是一致的,想要了解这个需要对JMM内存模型有大致的了解,其中每次线程的读写都在自己的工作内存中完成,之后再刷新到主存中,这就存在一个问题,当其他线程想要读取这个变量,就会变量不一致,volatile的作用就是让每次对这个变量进行操作是都马上刷新到主存中去,保证变量的一致性;

image-20230812010713650

  1. synchronized通过互斥加锁方式来保障并发情况的顺序性、原子性和可见性。是给对象或者类加锁,保证在没有释放锁的时候,其他进程没法进入临界资源,其实现是在锁对象加入monitor锁,就是在执行到这个代码块的时候就加上monitorentry,表示其他对象不能进来,当走了就释放这个monitor,用monitorexit表示,就表示这个锁释放,查看他的字节码就可以看到他的实现逻辑;由于在synchronized修饰的代码或方法只能单线程执行,所以一定能是有序的;对于可见性,这是由于synchronized 修饰的代码在执行进入和退出都会将工作内存中的数据刷入到主存中,这样保障其他线程读到的数据一致性。

image-20230812010722778

  • AQS的原理?

AQS的原理:对于被请求的共享资源如果是空闲的,则将请求资源的线程设置为工作线程并且将共享资源设置为锁定状态。对于被请求资源是被占用的情况,则将该线程阻塞起来放到双向同步队列中,等共享资源被释放再进行申请。

AQS的数据结构:

AQS的数据结构是一个双端队列CLH和变量state组成,每个队列元素为一个Node,Node中保存前驱后置节点、当前线程状态、当前线程以及下一个等待线程。每个Node通过获取和重置state参数来进行加锁操作。共享资源是否被占用是通过对state进行修改来实现的,但该共享资源被加锁后,就会修改state为1。

image-20230812010740662

共享锁/独占锁:

AQS分类共享锁和独占锁。其中共享锁可以多个线程共享资源,独占锁只允许单个线程独占资源。在实现上Semaphore是通过共享锁来实现的,ReentrantLock是通过独占锁来实现的。

AQS的代码实现:

  1. 对state的操作主要是通过getState()、setState()、compareAndSetState(int expect, int update)来实现的;

  2. 对同步队列主要是通过tryAcquire(int arg)、tryRelease(int arg)来实现的;

  3. 其中对CLH双向队列的操作主要是通过addWaiter(Node)和setHead(Node node)来实现的;

自定义AQS的实现方式:

AQS自定义同步器可以通过模板方法来实现,可以继承AQS的类,主要实现修改state和acquire和release方法即可。

从ReentrantLock的实现看AQS的原理及应用:

https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html

不可不说的Java“锁”事:

https://tech.meituan.com/2018/11/15/java-lock.html

  • ReentrantLock的原理?

ReentrantLock是实现AQS的悲观锁。初始化状态是state为0,当调用lock()方法时候会调用tryAcquire方法将其state设为1并且锁定,之后其他线程调用tryAcquire方法将会失败并加入到同步队列阻塞,直到该线程调用unlock()方法会将此锁释放,调用tryRelease方法释放锁,将state改为0,同时注意,该线程在释放该锁之前,可以重复获得此锁,所以ReentrantLock是可重入的。

ReentrantLock源码分析:

ReentrantLock内部有三个内部类,包括Sync—>NonfairSync、FairSync,可以分别实现公平锁和非公平锁,对ReentrantLock的操作基本是对Sync的操作,Sync分为公平实现FairSync和非公平实现NonfairSync,他们都是继承AQS接口。

image-20230812010757871

  • ReentrantLock和synchronized的区别?

相同点:

  1. synchronied和ReentrantLock都是可重入锁;

  2. synchronied是Java的关键字,是通过JVM对对象进行加Monitor锁操作实现的;而ReentrantLock通过JDK中的AQS接口来实现,提供多种加锁、解锁方法;

不同点:

  1. 特性:synchronied是非公平锁,ReentrantLock可以实现公平锁和非公平锁;ReentrantLock可以实现可中断、可重试、超时中断、多条件加锁机制;ReentrantLock可以实现等待多条件释放锁,总之ReentrantLock更近灵活;

  2. 实现:Synchronized是JVM自动隐式加解锁,执行完成他会自动释放锁;ReentrantLock通过Lock()和unLock()实现手动加锁和取消加锁。

  3. 发展历程:首先他们都是实现加锁同步的进程同步的方式,然后就是了解他们的发展历程,synchronied很早就有了,这个原理也是在上面讲到了,Lock是在jdk1.5后才有的,实现在concurrent包中,他是一个接口,有很多种实现,用的比较多的是ReentrantLock和ReadWriteLock,其中ReentrantLock表示当这个锁锁定一个对象或者类的时候,这个线程还能在进入,ReadWriteLock表示这个锁可以选择是读或写的时候进行加锁操作,同时还有公平锁和非公平锁,前者表示放在锁池中的线程是FIFO的,后者不是。

  4. 使用:如何选择哪张同步方式这个问题,首先要明确一点就是能够使用方法级别加锁的方式就不用关键词那种加锁方式,就是优先使用Lock方式,功能更多,更不容易出错。

深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理:https://blog.csdn.net/javazejian/article/details/75043422

  • ReentrantReadWriteLock的原理?

概述:ReentrantReadWriteLock是为了读写条件下加锁的效率,其中实现了ReentrantLock和ReadWriteLock两个接口。

原理:

其中核心是通过对state变量进行按位分割,将高16位表示为读状态,低16位表示为写状态。通过sharedCount()返回持有读状态的线程数,并通过HoldCounter来每个线程持有的读锁进行计数。通过exclusiveCount()返回持有写状态的线程数,写锁是一个排它锁,通过AQS的tryAcquire()和tryRelease()来实现。

总结:避免多线程写数据不一致的问题,同时相比Vector直接加锁操作,效率提高了不少。CopyOnWriteArrayList适合的常见是读多写少的场景,因为每次写操作需要加锁和复制,开销较大。

Java多线程并发读写锁ReadWriteLock实现原理剖析:

https://blog.csdn.net/SOHU_TECH/article/details/105260183?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-105260183-blog-72597192.pc_relevant_antiscanv3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-2~default~CTRLIST~Rate-1-105260183-blog-72597192.pc_relevant_antiscanv3&utm_relevant_index=1

图灵学院:

https://note.youdao.com/ynoteshare/index.html?id=0cd5654d1d4357fc84cfa84c6c753876&type=note&_time=1654498756467

#并发工具

  • JUC包里面提供了哪些工具?

  1. Lock工具,包括ReentrantLock;

  2. CAS的实现和Atomic原子类的实现:

  3. 线程安全集合类:比如ConcurrentHashmap、ConcurrentLinkedQueue、CopyOnWriteArrayList;

  4. Executor线程池工具;

  • JUC下Concurrent包下的类?

主要介绍CopyOnWriteArrayList、ConcurrentHashMap、ArrayBlockingQueue这三个类;

  1. ConcurrentHashMap是HashMap的并发包类,1.8前是通过加segment分段锁来实现的,segment内也是一个ReentrantLock;1.8后通过CAS+Lock来实现;
  2. CopyOnWriteArrayList这个是ArrayList的并发包类,主要是通过加副本来实现写时读;
  3. ArrayBlockingQueue是Queue的并发包实现类,主要也是通过加锁来实现;
  • ConcurrentHashMap的原理?

HashMap发展历程:首先还是需要了解HashMap的一个演进过程,由于HashMap不是线程安全,所以引进HashTable,这个是线程安全的,但是由于加的是对象锁,所以一旦锁定,就不能访问其他资源,之后引进了ConcurrentHashMap,这个锁的原理在于引进了分段锁segment,他只对分段的entry加锁;

在JDK1.7中是用分段锁来实现的:

分段锁就是对数组元素进行加锁操作,这样比起之前对这个对象加锁效率会高很多,这个数组元素就是segment,这个数组元素的初始数量是16。每个segment内部会保存一个hashmap,hashmap就是数组加链表的结构。其中put()操作是先找到segment,对其进行加锁操作,再根据hash值填入map中;其中get()的话也是先找到segment,在根据hash值找到对于的value值,两者区别是put()操作会对segment进行加锁操作。

image-20230812010923134

在JDK1.8后通过数组+链表+红黑树+CAS/synchronized的机制来保障安全性:

由于JDK1.7设计的ConcurrentHashMap受制于segment分段锁的数量,所以JDK1.8中还是借鉴了hashmap的设计思路,通过数组+链表的方式来实现。

  1. put()操作(写操作):先计算hash值,找到对应的槽位,如果槽位为空则直接通过CAS进行元素新增,如果其中有值的话会通过synchronized进行加锁新增。在链表数量达到一定的时候,会将链表转化为红黑树。

  2. get()操作(读操作):通过key值计算hash值来查找,其中区分了树还有链表情况;

image-20230812010933515

  • CopyOnWriteArrayList的原理?

CopyOnWriteArrayList在进行写操作的时候会通过ReentrantLock加上可重入锁,再通过Arrays.copyOf()方法复制一个副本对其操作,通过这种方式保障数据一致性。

  • JUC中BlockingQueue的原理?

定义:Java中阻塞队列是当队列满了再添加元素则会阻塞,当队列空了再获取元素则会阻塞;

实现:

  1. ArrayBlockingQueue :一个由数组结构组成的有界阻塞队列

  2. LinkedBlockingQueue :一个由链表结构组成的有界阻塞队列

  3. PriorityBlockingQueue :一个支持优先级排序的无界阻塞队列

  4. DelayQueue:一个使用优先级队列实现的无界阻塞队列

  5. SynchronousQueue:一个不存储元素的阻塞队列

  6. LinkedTransferQueue:一个由链表结构组成的无界阻塞队列(实现了继承于 BlockingQueue 的 TransferQueue)

  7. LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列

应用:阻塞队列一般应用与生产者/消费者模型和线程池的等待队列中;

  • JUC的Atomic原子操作原理?

原子操作就是不可中断的一个操作,要么全部成功,要么全部失败。之前在Java中实现原子操作是通过加锁和CAS操作,现在引入了原子类Atomic,引入了基础数据类型和数组等原子类;该类的原子操作是通过Unsafe类对内存进行原子操作。

  • JUC中CountDownLatch、CyclicBarrer、Semaphore的区别?

CountDownLatch、CyclicBarrer、Semaphore应用场景:

  1. CountDownLatch是一个计数器,用于等待约定好的线程到达某个状态后再执行,一般用于父线程等待子线程完全结束。

  2. CyclicBarrer是同步屏障,用于多个线程到达某个状态再一起进行下一步操作。

  3. Semaphore是许可证管理器,就是当需要这个许可证的时候,利用acquire()方法获取,但需要释放的时候用release()方法释放,用来做限流的。

CountDownLatch原理:

CountDownLatch是通过AQS的state来做计数器来实现的。每次初始化的时候将CountDownLatch的数量赋值到state上,同时每次调用countDown方法时state-1,并将其加入到阻塞队列中去,在调用await方法时候会查看state是否为0,为0则唤起所有线程继续执行下去;

Java 并发 - 理论基础:

https://www.pdai.tech/md/java/thread/java-thread-x-overview.html

目录
相关文章
|
6天前
|
Java
并发编程之线程池的底层原理的详细解析
并发编程之线程池的底层原理的详细解析
16 0
|
6天前
|
Java
并发编程之线程池的应用以及一些小细节的详细解析
并发编程之线程池的应用以及一些小细节的详细解析
17 0
|
27天前
|
算法 数据处理 Python
Python并发编程:解密异步IO与多线程
本文将深入探讨Python中的并发编程技术,重点介绍异步IO和多线程两种常见的并发模型。通过对比它们的特点、适用场景和实现方式,帮助读者更好地理解并发编程的核心概念,并掌握在不同场景下选择合适的并发模型的方法。
|
30天前
|
Java 程序员
java线程池讲解面试
java线程池讲解面试
53 1
|
1天前
|
SQL 开发框架 .NET
高级主题:Visual Basic 中的多线程和并发编程
【4月更文挑战第27天】本文深入探讨了Visual Basic中的多线程和并发编程,阐述了其基本概念,如何使用`System.Threading.Thread`类创建线程,以及借助`ThreadPool`、`Monitor`和`SyncLock`进行同步管理。文章还提到了多线程编程面临的挑战如竞态条件、死锁和资源竞争,并介绍了VB的异步编程、TPL和并发集合等高级技术。通过实例展示了多线程在文件处理、网络通信和图像处理中的应用,并给出了多线程编程的最佳实践。总之,理解并掌握VB的多线程和并发编程能有效提升应用程序的性能和响应能力。
|
2天前
|
安全 Go 开发者
Golang深入浅出之-Go语言并发编程面试:Goroutine简介与创建
【4月更文挑战第22天】Go语言的Goroutine是其并发模型的核心,是一种轻量级线程,能低成本创建和销毁,支持并发和并行执行。创建Goroutine使用`go`关键字,如`go sayHello(&quot;Alice&quot;)`。常见问题包括忘记使用`go`关键字、不正确处理通道同步和关闭、以及Goroutine泄漏。解决方法包括确保使用`go`启动函数、在发送完数据后关闭通道、设置Goroutine退出条件。理解并掌握这些能帮助开发者编写高效、安全的并发程序。
13 1
|
4天前
|
监控 测试技术 Linux
线程死循环是并发编程中常见的问题之一
【4月更文挑战第24天】线程死循环是并发编程中常见的问题之一
10 1
|
4天前
|
Java 调度
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
Java面试必考题之线程的生命周期,结合源码,透彻讲解!
32 1
|
5天前
|
Java
Java中的并发编程:理解和应用线程池
【4月更文挑战第23天】在现代的Java应用程序中,性能和资源的有效利用已经成为了一个重要的考量因素。并发编程是提高应用程序性能的关键手段之一,而线程池则是实现高效并发的重要工具。本文将深入探讨Java中的线程池,包括其基本原理、优势、以及如何在实际开发中有效地使用线程池。我们将通过实例和代码片段,帮助读者理解线程池的概念,并学习如何在Java应用中合理地使用线程池。
|
6天前
|
监控 Java
并发编程之线程池的详细解析
并发编程之线程池的详细解析
8 0

热门文章

最新文章