面试汇总-多线程

news2024/9/22 10:08:37

目录

1、Thread.sleep(0)的作用

2、Synchronized

2.1、特性

2.2、说一说自己对于 synchronized 关键字的了解:

2.3、synchronized关键字的底层原理(JVM如何实现重量级锁)

2.4、Jdk1.6之后对synchronized做的优化

2.4.1、锁粗化

2.4.2、锁消除

2.5、线程池的isTerminated()

2.6、如何中断线程

2.7、线程的interrupted和isInterrupted

2.8、并发竞态条件

2.9、锁的种类

3、synchronized和ReentrantLock的区别

4、锁升级策略

4.1、锁升级过程(不可逆)

4.1.1、无锁

4.1.2、无锁->偏向锁

4.1.3、偏向锁升级为轻量级锁

4.1.4、轻量级锁->重量级锁

4.2、适用场景

4.3、锁的优缺点比较

5、volatile 

5.1、Java的内存模型

5.2、volatile三重功效

5.3、说说 synchronized 关键字和 volatile 关键字的区别

5.4、volatile的原理是什么?volatile一定是线程安全的吗?

5.5、MESI(缓存一致性协议)

​​​​​​​5.6、嗅探

​​​​​​​5.7、嗅探->总线风暴

​​​​​​​5.8、Volatile防止指令重排序:内存屏障

​​​​​​​5.8.1、内存屏障作用

5.8.2、重排序规则

​​​​​​​5.8.3、内存屏障插入策略

6、线程池

6.1、如何设计线程池

​​​​​​​6.2、通过Executors创建线程池

6.3、ScheduledThreadPoolExecutor

6.3.1、延迟执行(schedule)

​​​​​​​6.3.2、周期执行(scheduleWithFixedDelay)

​​​​​​​6.4、使用ThreadPoolExecutor创建线程池

6.5、缓冲队列类型

​​​​​​​6.6、拒绝策略

6.7、线程池原理

​​​​​​​6.8、Shutdown和shutdownNow

7、AQS(抽象队列同步器)

7.1、原理

​​​​​​​7.2、State访问方式

7.3、​​​​​​​AQS主要成员变量

​​​​​​​7.4、AQS实现

​​​​​​​8、乐观锁和悲观锁

8.1、乐观锁(不加锁):

8.2、悲观锁(synchronized):

​​​​​​​9、CAS的缺陷

9.1、ABA问题:

9.2、高竞争下的开销问题:

9.3、功能受限

10、死锁

10.1、死锁的条件

10.2、如何检测Java中的死锁

10.3、如何避免线程死锁

11、​​​​​​​单例模式中volatile的作用

​​​​​​​12、为什么要使用线程池(好处)

​​​​​​​13、CAS中的ABA问题如何解决?

14、​​​​​​​ThreadLocal是什么?它的原理是什么?

15、CountDowanLatch有没有用过?适合在什么样的场景下用?

15.1、闭锁(CountDownLatch)

15.2、​​​​​​​栅栏(CyclicBarrier)

​​​​​​​15.3、信号量(Semaphore)

​​​​​​​15.4、RateLimiter(限制访问速率):令牌桶

​​​​​​​16、如何检测一个线程是否拥有锁

​​​​​​​17、同步块内的线程抛出异常会释放锁吗

​​​​​​​18、线程间如何通信

19、线程池线程数量设置

19.1、IO密集型任务

​​​​​​​19.2、计算密集型任务

20、阻塞队列

20.1、顶层接口(BlockingQueue)

20.1.1、入队操作

​​​​​​​20.1.2、出队操作

20.2、ArrayBlockingQueue

 20.3、LinkedBlockingQueue

20.4、​​​​​​​PriorityBlockingQueue

20.5、SynchronousQueue

20.6、线程池中的阻塞队列一般会选择哪种队列?为什么?

21、​​​​​​​Executors

22、wait(),notify(),notifyAll()必须加锁的原因


1、Thread.sleep(0)的作用

        触发操作系统立刻重新进行一次CPU竞争。使得各线程重新去抢占CPU的使用权,避免一个线程长时间占用CPU资源,而导致其他线程进入假死状态。

2、Synchronized

2.1、特性

  • 原子性

        synchronized作用域内的操作都是原子性的。

  • 可见性

        锁的状态对于其他线程来说是可见的,且释放锁后会立即将修改同步到主内存。

  • 有序性

        指令重排序影响的是多线程并发执行的顺序性;而synchronized保证同一时刻只有一个线程访问同步代码块,从而保证不会发生线程间的指令重排,保证有序性。

  • 可重入性

        一个线程拥有了锁后还可以重复申请当前锁。

2.2、说一说自己对于 synchronized 关键字的了解:

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

        早期的synchronized效率低下,早期,Java线程会映射到操作系统的原生线程上,线程的切换或状态改变会交由操作系统(用户态—>内核态,花费时间长)。

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

2.3、synchronized关键字的底层原理(JVM如何实现重量级锁)

        对象:对象头、实例数据、对齐填充。(申请锁、上锁、释放锁)

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

        synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

2.4、Jdk1.6之后对synchronized做的优化

锁升级策略:无锁->偏向锁->轻量级锁->重量级锁。

2.4.1、锁粗化

         对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

        此时,JVM会扩大锁的范围,把多次加锁释放锁操作,合成一次加锁释放锁的操作。

2.4.2、锁消除

        去除不可能存在竞争的锁。删除不必要的加锁操作,在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。通俗的讲,为一个不会发生并发问题的代码块或方法加了锁,代码执行时加锁只会影响性能,此时JVM就会将该锁删除。

2.5、线程池的isTerminated()

  • isShutDown当调用shutdown()或shutdownNow()方法后返回为true。
  • isTerminated当调用shutdown()方法后,并且所有提交的任务完成后返回为true;
  • isTerminated当调用shutdownNow()方法后,成功停止后返回为true;
  • 如果线程池任务正常完成,都为false

2.6、如何中断线程

  • 定义一个volatile状态变量,在存在循环的线程run方法中进行判断是否退出线程;
  • 调用线程的interrupt()方法—>仅仅只会给线程设置中断标志

        若线程处于正常状态,则会将该线程的中断标志设置为true,之后线程将继续正常运行,不受影响;若线程处于被阻塞状态,或由正常状态变为阻塞状态(sleep()、wait()),线程将立刻退出被阻塞状态,并抛出一个InterruptedException异常。可以通过捕获异常然后return。

2.7、线程的interrupted和isInterrupted

        interrupted()是static方法,调用的时候要用Thread.interrupted(),而isInterrupted()是实例方法,调用时要用线程的实例调用;

  • Thread.interrupted():测试当前线程是否已经是中断状态,执行后清除中断标志;
  • this.isInterrupted():测试线程Thread对象是否已经是中断状态,不清除状态标志。

2.8、并发竞态条件

        当某个正确性取决于多个线程的交替执行时序时,就会发生竟态条件。相当于正确的结果要取决于运气。

比如:(先检查后执行:通过一个可能失效的结果来决定下一步的动作。)

单例模式的双重锁检查,如果不加锁,可能就会发生竟态条件。是否发生取决于线程的调度状态。

2.9、锁的种类

  • 偏向锁、轻量级锁、重量级锁
  • 乐观锁(CAS)、悲观锁(synchronized)
  • 公平锁、非公平锁
  • 独占锁、共享锁

3、synchronized和ReentrantLock的区别

  • 底层原理:synchronized是JVM层面的锁,通过monitor对象来完成;

ReentrantLock 是从jdk1.5提供的API层面的锁。通过利用CAS自旋机制保证线程操作的原子性和volatile保证数据可见性以实现锁的功能。

  • 是否可手动释放:synchronized 代码执行完后系统会自动让线程释放对锁的占用;

ReentrantLock则需要手动释放锁,一般通过lock()和unlock()方法配合try/finally语句块来完成,使用释放更加灵活。如果没有手动释放锁,就可能导致死锁现象。

  • 是否可中断:synchronized是不可中断类型的锁,除非加锁的代码中出现异常或正常执行完成; ReentrantLock则可以中断,可通过trylock(long timeout,TimeUnit unit)设置超时方法或者将lockInterruptibly()放到代码块中,调用interrupt方法进行中断。
  • 是否公平锁:synchronized为非公平锁;ReentrantLock则即可以选公平锁也可以选非公平锁。
  • 锁的对象:synchronzied锁的是对象,锁是保存在对象头里面的,根据对象头数据来标识是否有线程获得锁/争抢锁;ReentrantLock锁的是线程,根据进入的线程和int类型的state标识锁的获得/争抢。

4、锁升级策略

锁升级:无锁->偏向锁->轻量级锁->重量级锁

Java对象头 = Mark Word + 指向类的指针 + 数组长度(只有数组对象才有)

Mark Word记录对象和锁有关的信息,根据锁标志位来确定当前对象是什么锁状态。

4.1、锁升级过程(不可逆)

4.1.1、无锁

        锁标志位 = 01是否偏向锁 = 0,存储对象的hashcode值。

        当对象没有被当成锁时,Mark Word记录的是对象的hashcode。

4.1.2、无锁->偏向锁

        锁标志位 = 01是否偏向锁 = 1,存储占有偏向锁线程的线程id。

        当对象被当成同步锁并且线程A通过CAS抢到锁时,进入偏向锁状态,将线程A的线程id写入Mark Word

        当线程A再次试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值等于线程A的id,就可以直接执行同步锁的代码块;

        当线程B来试图获取锁时,JVM发现同步锁为偏向锁,且偏向锁中的线程id值不等于线程B的id,线程B就会通过CAS去尝试获取锁:如果获取成功,则将Mark Word中的偏向锁的线程id修改为线程B的id;如果获取失败,则将同步锁升级为轻量级锁;

4.1.3、偏向锁升级为轻量级锁

        出现锁竞争(CAS获取锁失败)时,偏向锁升级为轻量级锁。

        锁标志位 = 00,存储指向栈中锁记录的指针。

        JVM在当前线程的栈帧中开辟一块独立空间,用以保存指向对象锁Mark Word的指针,同时Mark Word保存指向栈帧中独立空间的指针。

        如果保存成功,则当前线程获取该对象的轻量级锁,执行同步代码;

        如果保存失败,则当前线程自旋,不断尝试获取锁(尝试N次,由JVM决定),如果抢锁成功,执行同步代码;如果抢锁失败,则将轻量级锁升级为重量级锁。

4.1.4、轻量级锁->重量级锁

        轻量级锁自旋超过JVM指定次数时,升级为重量级锁。

        锁标志位 = 10,存储指向重量级锁的指针。

        未抢占到锁的线程会被阻塞,抢到锁的线程在释放锁的同时会唤醒那些阻塞的线程。

重量级锁下,线程之间的切换需要从用户态到内核态,成本较高。

4.2、适用场景

偏向锁:只有一个线程在临界区执行,无需操作系统介入;

轻量级锁:多个线程交替进入临界区,竞争不激烈,就算有冲突,线程自旋几次就能获取锁;

重量级锁:多个线程出现激烈竞争。

4.3、锁的优缺点比较

5、volatile 

5.1、Java的内存模型

        线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。(多线程环境下导致数据的不一致)。

Volatile修饰变量的内存模型

        当一个线程对volatile修饰的变量进行写操作时,JMM会立即把该线程对应的本地内存中的共享变量刷新到主内存。

        当一个线程对volatile修饰的变量进行读操作时,JMM会立即将当前线程对应的本地内存设置为无效,从主内存中读取共享变量的值。

 

5.2、volatile三重功效

        64位写入的原子性、内存可见性和禁止重排序。

5.3、说说 synchronized 关键字和 volatile 关键字的区别

  • volatile用于变量,而synchronized可以修饰方法以及代码块。
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞。
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

5.4、volatile的原理是什么?volatile一定是线程安全的吗?

保证共享变量的可见性。当一个线程修改一个共享变量时,另一个线程能读到这个修改的值。

每个线程都有自己的高速缓存区,线程本身并不与主内存进行直接交互。

保证可见性:

(1)修改volatile变量时会强制将修改后的值刷新的主内存中。

(2)修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

保证有序性:

Happen-before

如果a在b之前执行,则a的执行结果必然对b是可见的。(不代表a一定在b之前执行)

没保证原子性,不一定线程安全

​​​​​​​5.5、MESI(缓存一致性协议)

        当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取

​​​​​​​5.6、嗅探

        每个CPU通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

​​​​​​​5.7、嗅探->总线风暴

        MESI缓存一致性协议,导致CPU需要不断地从主内存嗅探和cas不断循环,会导致总线带宽达到峰值。

​​​​​​​5.8、Volatile防止指令重排序:内存屏障

        内存栅栏,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点。

        硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。

        对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;

        对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

​​​​​​​5.8.1、内存屏障作用

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

5.8.2、重排序规则

​​​​​​​5.8.3、内存屏障插入策略

        volatile写是在前面和后面分别插入内存屏障,而volatile读操作是在后面插入两个内存屏障

6、线程池

6.1、如何设计线程池

  • 考虑线程池中的个数是固定的还是动态变化的。
  • 考虑队列大小:无界队列(可能导致内存耗尽);有界队列(队列满了拒绝策略)
  • 每次提交新任务,是直接放入队列,还是开启新线程
  • 没有任务时,线程是睡眠一段时间还是进入阻塞

​​​​​​​6.2、通过Executors创建线程池

a、newFixedThreadPool(int Threads):创建固定数目线程的线程池。

b、newCachedThreadPool():创建一个可缓存的线程池,调用execute 将重用以前构造的线程(如果线程可用)。如果没有可用的线程,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

c、newSingleThreadExecutor()创建一个单线程化的Executor。

d、newScheduledThreadPool(int corePoolSize)创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

缺陷:

使用newFixedThreadPool和newSingleThreadExecutor:使用LinkedBlockingQueue无界阻塞队列,允许请求的队列长度为 Integer.MAX_VALUE,可能堆积 大量的请求,从而导致OOM。

使用newCachedThreadPoolnewScheduledThreadPool:允许创建的线程数量为 Integer.MAX_VALUE ,可能 会创建大量线程,从而导致OOM。

6.3、ScheduledThreadPoolExecutor

6.3.1、延迟执行(schedule)

​​​​​​​6.3.2、周期执行(scheduleWithFixedDelay)

​​​​​​​6.4、使用ThreadPoolExecutor创建线程池

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

->

new ThreadPoolExecutor(核心线程数,最大线程数,非核心线程存活时间,时间单位,缓冲队列,创建线程使用的工厂,任务拒绝策略)

6.5、缓冲队列类型

  • 直接提交队列SynchronousQueue队列,执行一个插入操作就会阻塞,需要再执行一个删除操作才会被唤醒,反之每一个删除操作也都要等待对应的插入操作。
  • 有界的任务队列:ArrayBlockingQueue
  • 无界的任务队列:LinkedBlockingQueue
  • 优先任务队列:PriorityBlockingQueue

​​​​​​​6.6、拒绝策略

  • 1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
  • 2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
  • 3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
  • 4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;
  • 6.7、线程池原理

new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

核心线程数->缓冲队列->最大线程数->拒绝策略。

​​​​​​​6.8、Shutdown和shutdownNow

        shutdown只是将线程池的状态设置为SHUTWDOWN状态,停止接收新线程,正在执行的任务会继续执行下去,没有被执行的则中断。

        shutdownNow则是将线程池的状态设置为STOP,停止接收新线程,正在执行的任务则被停止,没被执行任务的则返回。

        调用shutdown/shutdown方法后,如果线程再遇到wait、sleep等阻塞方法,线程会抛异常。

7、AQS(抽象队列同步器)

7.1、原理

        AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒。

        AQS是一个多线程访问同步资源的的同步器框架。该框架维护了一个volatile int state(共享资源)和一个FIFO(CLH双向队列)线程等待队列(多线程争夺共享资源时会进入此队列)

        AQS一般以继承的方式被使用,重入锁ReentrantLock、闭锁、栅栏等都用到了AQS。

        AQS的实现依赖内部的同步队列(FIFO双向队列),如果当前线程获取同步状态失败,AQS会将当前线程以及等待状态等信息构造成一个Node,将其加入同步队列的尾部,同时阻塞当前线程,当同步状态释放时,唤醒队列的头节点。

​​​​​​​7.2、State访问方式

        getState()、setState()、compareAndSetState(),线程通过尝试获取共享变量state的结果来对线程的状态作出处理。

7.3、​​​​​​​AQS主要成员变量

         ​​​​​​​State变量可以理解成同步资源的锁状态,值为0时表示当前资源未被线程占用,值不为0时表示当前共享资源已被线程获取锁。

​​​​​​​7.4、AQS实现

        实现类只需要实现共享资源state的获取和释放方式即可。

独占:只有一个线程能执行->ReentrantLock

共享:多个线程可以同时执行->Semaphore、CountDownLatch、CyclicBarrier

独占和共享:ReentrantReadWriteLock

​​​​​​​8、乐观锁和悲观锁

8.1、乐观锁(不加锁):

        在操作数据时比较乐观,认为别人不会同时修改数据,所以不会为数据加锁,只是会在更新数据时判断一下是否有其他人修改了数据,如果别人修改了数据,则放弃当前更新操作,否则执行更新。

乐观锁的两种实现方式:

CAS机制(例如AtomicIntegerAtomicBoolean等原子类):

CAS操作包括了3个操作数(多个操作,原子性由硬件层面保证)

  1. 需要读写的内存位置(V)           
  2. 进行比较的预期值(A)
  3. 拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。              

版本号机制:

给数据增加一个字段,作为判断数据变化的状态标记,读取数据的时候取出该字段,更新数据时判断数据此时的标记是否与之前取出的值相同,如果一致才进行更新操作。

8.2、悲观锁(synchronized):

        在操作数据时比较悲观,认为别人会同步修改数据,所以在更新数据时会把数据锁住,更新完后才会释放锁。

​​​​​​​9、CAS的缺陷

9.1、ABA问题:

假设有两个线程——线程1和线程2,两个线程按照顺序进行以下操作:

  • 线程1读取内存中数据为A;
  • 线程2将该数据修改为B;
  • 线程2将该数据修改为A;
  • 线程1对数据进行CAS操作

第4步中的A已经不是第1步中的A值了。

比如:栈顶问题—>一个栈的栈顶经过两次(或多次)变化又恢复了原值,但是栈可能已发生了变化

解决方法:加入版本号

9.2、高竞争下的开销问题:

在并发冲突概率大的高竞争环境下,CAS一直失败,会一直重试,导致CPU开销较大。

解决方法:增加重试阈值。

9.3、功能受限

        CAS只能保证单个变量(单个内存值)操作的原子性;

        普通Java用户不能使用,只能借助atomic下的原子类使用。

10、死锁

10.1、死锁的条件

互斥:共享资源同一时刻只能被一个线程占用。

持有并等待:线程1持有A锁,等待B锁;线程2持有B锁,等待A锁。

资源不可剥夺:已经获取到的锁,不会被其他线程强制抢占。

循环等待:线程1和线程2形成环形等待,死循环链。

10.2、如何检测Java中的死锁

  • jConsole:线程---》检测死锁
  • jstack:

        查看进程pid信息:jps

        查看指定pid的线程信息:jstack -l pid

        显示结果:

        

  • jvisualvm

10.3、如何避免线程死锁

  1. 尽量避免同一个线程对多个Lock进行锁定;
  2. 顺序加锁,锁嵌套时,保证多个Lock以相同的顺序请求加锁;
  3. 超时放弃,Lock接口的tryLock(long time, TimeUnit unit),按照固定时长等待锁,获取锁超时后,主动释放已经获得的锁。

11、​​​​​​​单例模式中volatile的作用

Volatile修饰的变量保证可见性,禁止重排序。

在第7步创建对象的时候,实际上执行了三个操作:

        1、分配对象的内存空间;

        2、初始化对象;

        3、设置instance引用指向分配的内存地址。

在2、3步可能发生指令的重排序,可能出现,instance已经指向了一个地址,但是对象还没有初始化,此时另一个线程就可能获取到一个没有初始化的对象。而volatile修饰的变量,禁止重排序,以此来解决这个问题。

​​​​​​​12、为什么要使用线程池(好处)

  1. 提高线程的利用率,降低频繁创建与销毁线程的资源消耗;
  2. 提高响应速度,可直接使用线程池中的线程,不用临时去创建;
  3. 对线程进行统一管理,统一分配调优监控。

​​​​​​​13、CAS中的ABA问题如何解决?

        CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

ABA问题:

考虑如下操作:

        并发1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功

        并发2:将数据修改成B

        并发3:将数据修改回A

        并发1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改

上述并发环境下,并发1在修改数据时,虽然还是A,但已经不是初始条件的A了,中间发生了A变B,B又变A的变化,此A已经非彼A,数据却成功修改,可能导致错误,这就是CAS引发的所谓的ABA问题。

解决:

  1. 给每个变量值增加版本号,每次变量更新的时候版本号+1;
  2. 利用jdk的Atomic包里的类AtomicStampedReference来解决ABA问题,这个类中的compareAndSet方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值更新为指定的新值

14、​​​​​​​ThreadLocal是什么?它的原理是什么?

        线程的局部变量,每一个线程所单独持有,其他线程不能对其进行访问。

        当使用ThreadLocal维护变量的时候 为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个该变量,这样同时多个线程访问该变量并不会彼此相互影响,因此他们使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

        ThreadLocal提供了set和get访问器用来访问与当前线程相关联的线程局部变量。

15、CountDowanLatch有没有用过?适合在什么样的场景下用?

15.1、闭锁(CountDownLatch)

        一个线程的继续运行需要先完成其他线程的初始化。

        Countdown()、await()

        CountDownLatch是闭锁的一个实现,允许一个或多个线程等待某个事件的发生。其内部是一个正数计数器,初始化时指定一个正数,countDown()方法对计数器进行减操作,await()方法就是这个闭锁的大门,计数器不为0时,await()方法的线程会被阻塞,当计数器为0时,await()方法的线程才会继续执行。

使用场景:

  1. 确保某个计算需要的所有资源都被初始化之后才被执行;
  2. 确保某个服务在其依赖的所有其他服务都已经启动之后再启动;
  3. 等待直到某个操作的所有参与者都就绪之后再继续执行。(多人游戏准备)

15.2、​​​​​​​栅栏(CyclicBarrier)

await()

        栅栏类似于闭锁,内部也有一个正数计数器,初始化时指定一个正数,每当有一个线程使用await()方法时,计数器加1,当前线程阻塞,直到计数器的值为初始化的正数时,打开栅栏。

区别:

        闭锁是一个或多个线程等待一个外部条件的发生,await()阻塞,countDown()为条件;而栅栏是线程与线程间相互等待,await()既是条件,也会阻塞。

举例:

        闭锁:三个工人,一个老板,三个工人都完成任务后,老板才来检查;

        栅栏:三个工人,三个工人都完成任务后,一起出去嗨皮。

​​​​​​​15.3、信号量(Semaphore)

        限流:用于限制并发访问的数量。

        信号量通过AQS的state变量去维护“许可证”的计数,而线程去访问共享资源前,必须先拿到许可证。线程可以从信号量中去“获取”一个许可证,一旦线程获取之后,信号量持有的许可证就转移过去了,所以信号量手中剩余的许可证要减一。

​​​​​​​15.4、RateLimiter(限制访问速率):令牌桶

        限流:使用访问速率进行共享资源的访问限流。

        RateLimiter会按照一定的频率往桶里扔令牌,线程拿到令牌才能执行。

        设置每秒访问速率:RateLimiter rateLimiter = RateLimiter.create(n);

        请求访问资源:rateLimiter.acquire();

​​​​​​​16、如何检测一个线程是否拥有锁

        Thread.holdsLock()

​​​​​​​17、同步块内的线程抛出异常会释放锁吗

        如果使用的synchronized,同步块内抛出异常,会自动释放锁。

        而如果是Lock,则需要在finally块里手动释放锁。

​​​​​​​18、线程间如何通信

  1. 使用volatile修饰变量,通过变量状态进行通信;
  2. 使用wait()和notify()/notifyAll()进行通信;
  3. 闭锁栅栏

19、线程池线程数量设置

19.1、IO密集型任务

        任务的主要处理为IO操作,比如文件读写、网络通信等,此类任务占用CPU资源很少,则线程数 = (CPU内核数)/(1-阻塞系数)-----阻塞系数一般为0.8或0.9;

​​​​​​​19.2、计算密集型任务

        任务的主要处理为算法计算等,占用CPU资源多,一般设置线程数=CPU核数*2。

20、阻塞队列

20.1、顶层接口(BlockingQueue)

20.1.1、入队操作

        add:队列满时抛出异常

        offer:队列满时返回false

        put:队列满时阻塞

​​​​​​​20.1.2、出队操作

        remove:队列为空抛出异常

        poll:队列为空返回null

        take:队列为空时阻塞

20.2、ArrayBlockingQueue

        一个数组 + 一把锁 + 两个条件,读写互斥。直接操作数据,性能好。

 20.3、LinkedBlockingQueue

两把锁,分别控制队头和队尾的操作。读读互斥、写写互斥。

Put通知take,take通知put。

非满时,put会通知其他put;非空时,take会通知其他take。

20.4、​​​​​​​PriorityBlockingQueue

        类似于ArrayBlockingQueue,区别在于用数组实现一个二叉堆,实现按自然顺序或者自定义比较器顺序出队列。(在插入数据时,会按照顺序写入数组)

        没有notFull条件,当元素个数超出数据长度时,执行扩容操作。

20.5、SynchronousQueue

        直接提交,不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。

        可以认为SynchronousQueue是一个缓存值为1的阻塞队列,但是 isEmpty()方法永远返回是true,remainingCapacity() 方法永远返回是0,remove()和removeAll() 方法永远返回是false,iterator()方法永远返回空,peek()方法永远返回null。

20.6、线程池中的阻塞队列一般会选择哪种队列?为什么?

  1. ArrayBlockingQueue:有界队列,基于数组结构的有界阻塞队列。
  2. LinkedBlockingQueue:无界队列,基于链表结构的阻塞队列。
  3. SynchronousQueue:直接提交,不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
  4. PriorityBlockingQueue:具有优先级的无限阻塞队列。

21、​​​​​​​Executors

        newFixedThreadPool(int nThreads):固定大小线程池;LinkedBlockingQueue;

        newCachedThreadPool():无界线程池;SynchronousQueue;

        newSingleThreadExecutor():大小为1的固定线程池;

        newScheduledThreadPool() :定长线程池,支持定时和周期性任务执行。

22、wait(),notify(),notifyAll()必须加锁的原因

        防止notify先于wait执行,导致wait线程永远处于阻塞状态(饥饿线程)。

        一个线程调用了wait()之后, 必然需要由另外一个线程调用notify()来唤醒该线程, 所以本质上, wait()notify()的成对使用, 是一种线程间的通信手段。

以上内容为个人学习理解,如有问题,欢迎在评论区指出。

部分内容截取自网络,如有侵权,联系作者删除。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/182139.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Scala运算符

算术运算符 关系运算符 Java 和 Scala 中关于“”的区别 逻辑运算符 赋值运算符 位运算符 Scala运算符总结 算术运算符 基本与Java一致 对于除号“/”&#xff0c;它的整数除和小数除是有区别的&#xff1a;整数之间做除法时&#xff0c;只保留整 数部分而舍弃小数部分…

JDBC学习笔记(黑马)

目录 一、JDBC快速入门 二、JDBC API详解 &#xff08;一&#xff09;DriverManager &#xff08;二&#xff09;Connection &#xff08;三&#xff09;Statement &#xff08;四&#xff09;ResultSet &#xff08;五&#xff09;PreparedStatement 三、数据库连接池…

前端实现水印的两种方式(DOM和Canvas)

&#x1f431; 个人主页&#xff1a;不叫猫先生 &#x1f64b;‍♂️ 作者简介&#xff1a;前端领域新星创作者、阿里云专家博主&#xff0c;专注于前端各领域技术&#xff0c;共同学习共同进步&#xff0c;一起加油呀&#xff01; &#x1f4ab;系列专栏&#xff1a;vue3从入门…

Python 海象运算符

✅作者简介&#xff1a;人工智能专业本科在读&#xff0c;喜欢计算机与编程&#xff0c;写博客记录自己的学习历程。 &#x1f34e;个人网站&#xff1a;小嗷犬的技术小站 &#x1f34a;个人信条&#xff1a;为天地立心&#xff0c;为生民立命&#xff0c;为往圣继绝学&#xf…

图纸版本混乱?BOM表管理困难?看SolidWorks PLM如何高效助力产品数据管理

“随着集团的日益壮大&#xff0c;我们越来越重视信息化系统的建设工作&#xff0c;但在研发生产的过程中我们经常会遇到图纸版本混乱、数据查找不便的问题&#xff1b;特别是在产品设计好后&#xff0c;还需要花费很多时间手动整理BOM表&#xff0c;整理期间由于数据量太大&am…

2023年,对人工智能的思考与展望

近些年来&#xff0c;人工智能的话题一次次的冲上热榜&#xff0c;而在前段时间内&#xff0c;chatgpt以及midjourney又一次冲上了热搜&#xff0c;在海内外引起广泛的讨论&#xff0c;我个人在研究了近一个多月的技术文档和文献资料后&#xff0c;也对人工智能的未来有了很多的…

STC32G 单片机通用串行通信接口及其编程

STC32G 系列单片机有4个全双工通用串行通信接口&#xff0c;串口1与串口2既有异步通信功能、又有同步通信功能&#xff0c;串口1与串口2都可进行SPI通信&#xff0c;SPI是一个全双工高速同步串行&#xff1b;通信总线串口3、串口4只有异步通信功能。本文将重点讨论其异步通信&a…

hexo个人博客搭建+butterfly主题配置(雏形版本)

前提&#xff1a; 1. 有一个属于自己的GitHub账号 2. 安装好了git 3. 安装好了node 一、安装hexo 1. 建立一个文件夹 Blog&#xff08;可以自己取名字&#xff09;&#xff0c;进入文件夹标右键打开 Git Bush Here&#xff0c;安装Hexo&#xff1a; npm install -g hexo-…

中智股份冲刺上交所上市:半年收入约87亿元,计划募资37亿元

近日&#xff0c;中智经济技术合作股份有限公司&#xff08;下称“中智股份”&#xff09;预披露招股书&#xff0c;准备在上海证券交易所主板上市&#xff0c;中信证券为其保荐机构。 本次冲刺上市&#xff0c;中智经济计划募资37.42亿元&#xff0c;将用于中智人力资源研发运…

函数的连续性和间断点——“高等数学”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是高等数学中的函数的连续性和间断点&#xff0c;好的&#xff0c;那现在就让我们进入函数的连续性和间断点的世界吧 一、函数的连续性 1.函数增量 2.连续的定义 3.单侧连续 二、例题&#xff08;函数的连续性&#xff09; …

JDK动态代理CGLIB动态代理

代理模式 是通过代理对象访问目标对象&#xff0c;这样可以在目标对象基础上增强额外的功能&#xff0c;如添加权限&#xff0c;访问控制和审计等功能。 房产中介代替业主卖房 静态代理 静态代理中代理类与被代理类都需要实现同一个接口&#xff0c;这就说明我们的一个静态代…

MFC|Toolbox内控件简单介绍

参考&#xff1a; MFC控件工具箱 &#xff08;https://blog.csdn.net/Hubz131/article/details/77684910&#xff09; 对应工具的超链接是本人搜到认为较易理解的单个控件介绍。 Pointer&#xff1a;就是普通的鼠标&#xff0c;默认状态Button&#xff1a;按钮&#xff0c;用…

stm32f091芯片的学习总结

摘自芯片手册&#xff08;外加自己的思考&#xff09; 发现网上使用这种芯片的人较少或者说解释这种芯片的电路具体怎么画的人较少&#xff0c;本来想直接借鉴的&#xff0c;发现没有找到&#xff0c;于是我自己来写一篇。 一、概述 该芯片提供标准通信接口(两个i2c&#xf…

基于STM32的FreeRTOS开发(2)----Cube工程的FreeRTOS配置

为什么使用Cube进行FreeRTOS配置 STM32Cube是STMicroelectronics提供的一种软件工具&#xff0c;用于配置和生成STM32微控制器的固件。它提供了一个图形化用户界面&#xff0c;可以轻松配置微控制器的各种功能和外设&#xff0c;并生成初始化代码。使用Cube可以大大简化微控制…

[GNN] 图神经网络入门

GNN和GCN的入门公式一、GNN的计算二、GCN的计算跟随B站课程【GNN图神经网络最牛教程】学不会up直接下跪&#xff01;图神经网络快速入门教程&#xff08;GNN/GCN&#xff09;的笔记 一、GNN的计算 对于一个图来说&#xff0c;要更新它自身的特征&#xff0c;也要更新它邻接节点…

前端websocket劫持漏洞(CSWSH)

0x00 什么是ws劫持 在Websocket的业务中&#xff0c;其中常见的漏洞是ws劫持&#xff0c;全称为跨站点CSWSH(Cross-Site WebSocket Hijacking)跨站WebSocket劫持漏洞。 WebSocket概念 WebSocket是通过HTTP启动的双向、全双工通信协议。它们通常用于流式传输数据和其他异步流量…

深度卷积神经网络、池化层、为什么使用卷积

目录1.深度卷积神经网络(a deep convolutional neural network)输入图像的维度是&#xff0c;如果&#xff0c;计算输出图像维度公式&#xff1a;。s表示步幅&#xff0c;p表示填充的层数。filters的通道数是和输入图像的通道数保持一致的。分析上图案例&#xff1a;第一层卷积…

MySQL基本查询案例练习

目录 一.案例1 需求 解决代码 二.案例2 需求 解决代码 一.案例1 创建一个学生表&#xff0c;插入以下数据 insert into student values(1,张明,男,89,78,90), (2,李静,男,77,73,60), …

golang map原理

简介本文主要通过探究在golang 中map的数据结构及源码实现来学习和了解map的特性&#xff0c;共包含map的模型探究、存取、扩容等内容。欢迎大家共同讨论。Map 的底层内存模型在 goland 的源码中表示 map 的底层 struct 是 hmap&#xff0c;其是 hashmap 的缩写type hmap struc…

“华为杯”研究生数学建模竞赛2005年-【华为杯】A题:交通网络的通行时间预测与最优路径决策(附获奖论文)

赛题描述 A: Highway Traveling time Estimate and Optimal Routing Ⅰ Highway traveling time estimate is crucial to travelers. Hence, detectors are mounted on some of the US highways. For instance, detectors are mounted on every two-way six-lane highways o…