并发面试整理

1、线程的状态有哪些?

在Thread.state类中进行了定义

新建状态 表示刚刚创建的线程,这些线程还没有执行

可运行 可能正在运行,也饿能正在等待CPU时间片

阻塞 等待获取一个排他锁,其他线程释放了锁就会结束此状态

无线期等待 等待其他线程显示的唤醒,否则不会被分配CPU时间片

限期等待:无须等待其他线程显示的唤醒,在一定时间之后会被系统自动唤醒

死亡:可以是线程结束之后自己结束,或者产生了异常而结束

2、并发级别由哪些

阻塞· 无饥饿 无障碍 无锁 无等待

  1. 阻塞(BLOCKING)

    一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。当我们使用synchronized关键字,或者重入锁时就会产生阻塞的线程。无论是synchronized或者重入锁,都会视图在执行后需代码前,得到临界区的锁,如果得不到,线程就会被挂起等待,直到占有了所需要资源

  2. 无饥饿(starvation-Free)

    这个取决于线程之间是优先级的存在,如果系统允许高优先级的线程插队,这样有可能导致低优先级的线程产生饥饿

    公平锁与非公平锁

  3. 无障碍

    无障碍是一种最弱的非阻塞调度,相对来说非阻塞的调度是一种乐观策略

    从这个策略可以看到,当临街区中存在严重冲突时,所有线程可能会不断的回滚自己的操作,而没有一个线程可以走出临界区。这种情况会影响系统的正常执行

    这也是利用了CAS理论的思想:

    一种可行的无障碍实现可依赖一个“一致性标记”来实现。线程在操作之前,先读取并保存这个标记,在操作完后才能够后,再次读取,检查这个标记是否被更改过,如果两者一致,则说明资源访问没有冲突。如果不一致,则说明资源可能在操作过程中与其他写线程冲突,需要重式操作,而任何对资源有修改操作的线程,在修改数据前,都需要更新这个一致性标记,表示数据不再安全

  4. 无锁

    无锁的并行都是无障碍的。在无锁的情况下,所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区,一个典型的特点是可能包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果修改成功,程序退出,否则继续尝试修改。但无论如何,无锁的并行总能保证有一个线程是可以胜出的。至于临界区中竞争失败的线程,它则不断重试,直到自己获胜。如果总是尝试不成功,则会出现类似饥饿的现象,线程会停止不前

    5、无等待

    无锁值要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有的线程都必须在有限步内完成,这样就不会引起饥饿问题。如果限制这个步骤上线,还可以进一步分解为有界无等待和线程数无关的无等待几种,他们之间的区别只是对循环次数的限制不同。一种典型的无扥带结构是RCU(rEAD-copy-update)。它的基本思想是,对数据的读可以不加控制。因此所有的读线程都是无等待的,他们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在何时的实际回写数据

3、happen-before的原则是什么

定义了哪些指令不能重排

  • 单线程的happen-before原则:在同一个线程中,书写在前面的操作happen-bvefore后面的操作
  • 锁的happen-before原则: 解锁必然发生在随后的枷锁(lock)之前、
  • 1voliatle规则1:volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 传递性:A先于B,B先于C,则A比先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的中断先于被中断线程的代码
  • 线程的所有操作先于线程的终结
  • 对象创建的happen-before原则:一个对象的初始化完成先于它的finalize方法调用

4、创建线程的几种方式

继承Thread类型创建线程

  • 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
  • 创建Thread子类的实例,即创建了线程对象。
  • 调用线程对象的start()方法来启动该线程。

实现Runnable接口创建线程

  • 定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
  • 创建 Runnable实现类的实例,并依此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
  • 调用线程对象的start()方法来启动该线程

通过Future和Callable创建线程

  • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
  • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
  • 使用FutureTask对象作为Thread对象的target创建并启动新线程。
  • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

使用线程池例如Excutor框架(工厂的方法)

  • 创建线程池
    • 创建线程池的三大方式 Executors的静态方法
      • Executors.newSingleThreadExecutor()
      • Executors.newFixedThreadPool(n)
      • Executors.newCachedThreadPool()
    • 自行创建线程池的七大参数 new ThreadPoolExecutor
  • 执行WxecutorServices.execute(Runnable command)

创建线程的方式的对比

  1. 采用实现Runnable、callable接口的方式创建线程

线程了只是实现了Runnble接口或者Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU,代码和数据分开。但是,缺点是编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread

​ 2、使用继承Thread类的方式创建多线程

如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可 获得当前线程。缺点是线程类已经继承了Thread类,就不能再继承其他父类了

​ 3、Runnable和Callable的区别

  • Callablle规定重写的方法是call(),Runnable规定(重写的方法)是run
  • Callable的任务执行后可以返回值,而Runnavle的任务是不能返回值的
  • call的方法可以抛出异常,run方法不可以
  • 运行Callable任务可以拿到一个Future对象,表示异步计算的结果,它提供了检查计算是否完成的情况,可以取消任务的执行,还可获取执行结果

5、线程基本操作与线程协作

基本操作

  1. Daemon

    当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程

  2. sleep

    Thread.sleep(millsec)方法会休眠当前正在执行的线程,millsec单位是毫秒(==抱着锁睡觉)

  3. yield 礼让

    对静态方法thread.yield()的调用声明了当前线程已经完成了声明周期最重要的部分,可以切换给其他线程来执行

  4. join

    一直阻塞当前线程直到目标线程执行完毕

  5. start

    启动线程

  6. new

    创建线程

线程协作

  1. wait notify() 和notifyall() (工作在同步代码块中-

    使用wait()挂起期间,线程会释放锁。这是因为,如果没有释放锁,那么其他线程就无法进入对象 的同步方法或同步控制块中,那么就无法执行notify或者notifyAll来唤醒挂起的线程,造成死锁

    工作流程

    • 一个线程调用了object.wait(),它就会进入object对象的等待队列;这个等待队列中,可能会有多个线程,因为系统运行多个线程同时等待某个对象
    • 当object.notify()被调用时,它就会从这个等待队列中,随机选择一个线程,并将其环形
    • 方法不能随便调用,必须包含在对应的synchronized中
    • 会自动释放锁
  2. wait和sleep的区别

    • 来自不同的类
    • 关于锁的释放: wait会释放目标对象的锁,sleep不会释放任何资源
    • 使用的范围的不同: wait必须工作在synchromnized中

    3、await()、signal和singalAll()

    • java.util.concurrent 类库中提供快乐Condition类来实现线程之间的协调
    • 可以在Condition上调用await()来使得线程等待,其他线程调用singal()和singalAll()来唤性等待的线程
    • 相比于wait这种等待方式,await可以指定等待的条件,因此更加灵活
    class Data3{ // 资源类 Lock
    
        private Lock lock = new ReentrantLock();
        private Condition condition1 = lock.newCondition();
        private Condition condition2 = lock.newCondition();
        private Condition condition3 = lock.newCondition();
        private int number = 1; // 1A  2B  3C
    
        public void printA(){
            lock.lock();
            try {
                // 业务,判断-> 执行-> 通知
                while (number!=1){
                    // 等待
                    condition1.await();
                }
                System.out.println(Thread.currentThread().getName()+"=>AAAAAAA");
                // 唤醒,唤醒指定的人,B
                number = 2;
                condition2.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void printB(){
            lock.lock();
            try {
                // 业务,判断-> 执行-> 通知
                while (number!=2){
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
                // 唤醒,唤醒指定的人,c
                number = 3;
                condition3.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
        public void printC(){
            lock.lock();
            try {
                // 业务,判断-> 执行-> 通知
                // 业务,判断-> 执行-> 通知
                while (number!=3){
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName()+"=>BBBBBBBBB");
                // 唤醒,唤醒指定的人,c
                number = 1;
                condition1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
    }
    
    

6、volatile和synchronized有什么区别

出题:多线程对一个volitale变量进行++,最终这个变量结果准确吗?

答案:不准确,因为volatile不保证原子性,最终结果是小于真实值的。

如何保证原子性:

  • synchronized
  • 原子类 :采用CAS机制

volitale

  1. voltaile本质是在告诉jvm当亲变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问这个变量,其他线程被阻塞
  2. volitale仅能使用在变量级别,synchronized则可以使用在变量和方法上
  3. volitale仅能实现变量的修改可见性(缓存数据不对问题),原子性保证不了;而synchronzied则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程阻塞,而Synchronized会造成线程阻塞

7、synchronized关键字所有问题

简述一下

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

  • 在JDK早期版本中,synchronized属于重量级锁,效率低下,因为监事器锁是依赖于底层的操作系统的Mutewx Lock来是爱心的,Java的线程是映射到操作系统的原生线程之上的。如果想要挂起或者唤醒一个线程,都需要操左系统帮忙,而操作系统实现线程之间的切换时需要从用户态切换到内核态,这个状态之间的转换需要相对比较长的时间,这也是为什么早期synchronized效率低的原因

  • ,庆幸的是在Java6 之后,JVM对synchronized做了大量的优化,比如:自旋锁,适应性自旋锁,锁消除,锁粗化,锁偏向。轻量级锁等技术来减少锁操作的开销

如何使用

主要有三种使用方式

  • 同步方法块: 对给定对象加锁,进入同步代码前需要获得给定对象的锁
  • 同步方法: 锁是当前实例对象
  • 静态同步方法:锁是自己定义的锁对象

具体使用:单例模式

懒汉式

public class Test{
    
    private volitakle Test instance; //对象变量引用
    
    //构造函数私有化
    private Test(){}
    
    //这种存在的问题是锁的粒度太大
    public synchronized  Test  getInstacne{
        if(instance==null){
            instance = new Test();
        }
        return instance;
    }
}

DCL

public class Test{
    
    private volitale Test instance; //对象变量引用
    
    //构造函数私有化
    private Test(){}
    
  
    public Test  getInstacne{
        //先判断一下对象是否已已经实例化过,没有实例化过才进入加锁到吗
        if(instance==null){
            //对类对象进行加锁
             synchronized(Test.class){
                 if(instance==null){
                     instance = new Test();

                 }
                 
             }  
        }
        return instance;
    }
}

另外,需要注意 采用volitale关键字修饰也是很重要的

为什么

Test test = new Test()

这段代码其实是分三步执行:

  • 分配内存空间
  • 初始化unique
  • 将test指向分配的内存地址

*但是由于JVM指令重拍的特性,执行顺序可能是1-3-2,指令重拍在单线程的环境下不会出现问题,但是在多线程黄江下会获得还没有初始化的实例。例如,线程T1先执行1和3,此时T2调用getInstance()后发现test不为空,因此返回对象,但是此时test还没被初始化

使用volitale可以禁止JVM的指令重拍,保证在多线程环境下也能找那个长运行

底层原理

==Java对象头和monitor是实现synchronized的基础!==对象头内保存着无锁状态,偏向锁、轻量级锁、重量级锁等待状态位和标志为;monitor来标志一个对象的锁定状态(线程和对象之间的锁定关系)

  • Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针) Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。
  • 所有的Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者Monitor锁
  1. 同步代码块:使用monitotrnmt和monitorexi实现:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;

  2. 同步方法: 而synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。

  3. 可重入性

    重入锁实现可重入性原理或机制是:,每一个锁关联一个线程持有者和计数器,当技术器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一个线程请求成功后,JVM会记录下锁的持有线程,并且将计数器职位1,此时其他线程请求该锁的时候,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器递增;当想爱你成退出同步代码块时,技术器会递减,如果技术器为0,则释放该锁

  4. 锁的优化

    偏向锁:判断是否为偏向锁。直接判断当前线程id,减少同步时间

    轻量级锁:

    自旋锁

    重量级锁

    锁的消除技术

8、线程同步的几种方式

  1. synchronized(底层是如何实现的,上述已经讲过)

  2. 使用ThreadLocal管理

    • ThreadLocal仅仅是个变量访问的入口
    • 每一个Thread对象都有一个ThreadLocalMap对象,这个ThreadLocalMap持有对象的引用
    • ThreadLocalMap以当前的threadLocal对象为key,以真正的存储对象为value.get()方法时通过threadLocal实例就可以找到绑定在当前线程上对象的副本
  3. 并发包中的锁

    读写锁,CountDownLatch,信号量等或者采用并发集合ConcurrentHashMap,元祖类Atomic等(详见JUC并发包)

  4. ReebtraLock和synchronized右什么区别>

    • 锁的实现

      synchronized是JVM实现的,而ReentranLock是JDK实现的

    • 如何操作

       >  `sybchronized`会自动枷锁,释放锁,而ReebtranLock则需要手动加锁和释放锁
      
    • 性能

      新版本Java对synchronized进行了很多优化,如自旋锁等,synchronized与ReebtranLock大致相等

    • 等待可中断

      当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,该为处理其他事情

      ReentranLock可中断,Synchronized不行

    • 公平锁

      公平锁是值多个线程在等待桶一个锁时,必须按照申请锁的时间顺序来依次获得锁

      Synchronized中的锁是非公平的,ReebtranLock默认情况下也是非公平的,但也可以是公平的。

    • 锁绑定多个条件

      一个ReentranLock可以同时绑定多个Condition条件

    • 都是可重入锁

9、多线程锁的优化

  1. 减小锁持有时间

  2. 减小锁粒度(分隔数据结构),相对是锁的粗化

  3. 读写分离锁来替换独占锁(分割数据功能)(读和读之间不阻塞)

  4. jvm虚拟机对锁的优化(这是synchronized优化)

    • 偏向锁

      第一个线程请求锁并拿到锁之后,下次在请求锁不需要进行同步操作,直解拿到锁。当有第二个线程竞争的时候,偏向锁失效

    • 轻量级锁

      > 锁偏向失效后,使用轻量级锁。将对象头部作为指针,指向锁记录,如果成功就获得轻量级锁,如果失败则升级为自旋锁
      
    • 自旋锁

      轻量级锁失效后,java虚拟机会假设在不久的时间内,可以拿到锁,所以让线程进行几个空循环,如果可以拿到锁则进入临街区,否则阻塞等待锁

    • 锁消除

      > 取出不能可能存在共享资源竞争的锁,节省不必要的锁的请求时间
      

10CAS(不加锁)

加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。

无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突。

​ 无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

CAS的思想很简单:三个参数,一个当前内存值V,旧的预期值A,即将更新的值B,当且晋档预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false

ABA问题

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到他仍然是A值,那么我们就能说她的值没有被其他线程改变过吗

如果在这段期间它的值曾经该成了B,后来又改成了A,那么CAS操作就会无人为它没有被改变过,这个漏洞称为ABA问题。解决的核心思想是加上时间戳来标志不同阶端的数值。

比如JUC包为了解决这个问题,提供了一个带有标记的原子引用婆娘个类“AtmociSampedReference”,它可以通过控制变量值的版本来保证CAS的正确性,如果需要解决ABA问题,改用传统的互斥同步(典型的就是synchronized和Lock)可能会比原子类更高效

11、JUC并发包的队列和原子类

  1. BlockingQueue(在线程池中可以用到)

    java.util.courrent.BlockingQueue接口有以下阻塞队列的实现,基于ReentranLock

    FIFO队列 :LinkedBlockingQueue、ArrayBlockingQUeue(固定长度)

    优先级队列:PriorityBlicungQueue用于实现最小堆和最大堆

    阻塞队列提供了take()和put()方法,如果队列为空take()将阻塞,直到队列中有内容;如果队列满put将阻塞,直到队列有空位置

    2、ArrayBlockingQueue和LinkedBlocking的区别

    • 队列大小有所不同,ArrayBlocking是有解的初始化必须指定代销,而LinkedBlockiung可以是有界的也可以是无界的,对于后者而言,当添加速度大于移除速度的时,在无界的情况下,可能会造成内存溢出的情况
    • 数据存储容器的不同:ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockinbgQueue采用的则是以Node节点做维护连接对象的链表
    • 由于ArrayBlockingQUeue采用的是数组的存储容器,因此在插入或删除时不会产生或销毁任何额外的对象的实例,而LinkedBlockingQUeue则会生成一个额外的Node对象。这可能在长时间内需要高效的并发处理大批量数据时,对于GC可能存在较大的影响
    • 两者的实现队列添加货已出的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加和溢出操作采用的是同一个ReentaranLock锁,而LinkedfBlockingqUEUE实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的是TakeLock没这样能大大提高队列的吞吐量,也意味着在高并发情况下生产者和消费者可以并行的操作队列中的数据,以此来提高整个队列的并发性能

12、简述线程池(非常重要)

为什么要用线程池

降低资源的消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗

提高响应速度:当任务到大时,任务可以不需要等待线程创建就能立即执行

方便管理:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配,调优和监控

JUC里面线程池代码体系

ThreadPoolExecutor类实现了ExecutorServices接口和Executor接口,并由Executors类扮演线程池工厂的角色

ThreadpOOLeXECUTOR的七大参数

  • corePoolSize:核心线程池大小(线程池维护线程的最小数量)
  • maximumPoolSize:线程池维护线程最大数量
  • keepAliveTime:超时了没有人调用就会释放
  • unit: 线程池维护线程所允许的空闲时间单位
  • workQueue: 线程池所使用的缓冲队列
  • handler:线程池对拒绝任务的处理策略

常规实现的线程池(通过创建不同的ThreadPoolExecutor对象)

  • newFixedThreadPool 定长线程池

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

    coreSize和maximumPoolSize都是用户设定的线程数量nthreads

    keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停掉,但这里KeepALIVE无效

    阻塞队列是一个无界队列,实际线程数量将永远维持着在nthreads,因此m,aximumPoolSize和keepAliveTime将无效


  • newCachedThreadPool可缓存

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

它是一个可以无限扩大的线程池;

它比较适合处理执行时间比较小的任务;

corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;

keepAliveTime为60s,意味着线程空闲时间超过60S就会被杀死;

采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须找到一条工作线程来处理它,如果当前没有空闲的线程,那么就会再创建一条新的线程、



  • newSingleThreadExecutor单一线程

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    
    • 它指挥创建一条工作线程处理认为

讲述BlockingQueue

在线程池中的阻塞队列分为 直接提交队列有界队列无界队列,优先级任务队列

  • 直接提交队列 :设置为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,每执行一个插入操作就会阻塞,需要在执行一个删除操作才会唤醒,反之每一个删除操作也都要等待对应的插入操作

  • 有界的任务队列:有界的任务对哦咧可以使用ArrayBlockingQueue实现。若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到CorePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSIZe,则执行拒绝策略

  • 无界的任务队列:无界任务队列可以使用`LinkedBlockingQueue实现。使用无界任务多了,线程池的任务队列可以无限制的添加新的任务,而线程池创建最大线程书刘昂就是你corePoolSize设置的数量,也就是说你设置的maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池数量达到corePoolSize后,就不会再增加了;若有后需的新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调和控制,不然会出现队列中的任务由于无法及时处

    理导致一直增长,直到最后资源耗尽的问题

    • 优先任务队列:优先任务队列通过PriorityBlockingQueue实现。通过运行的代码我们可以看出PriorityQuue它其实就是一个特殊的无界队列,它其中无论添加多少个任务,线程池创建的线程数页不会超过corePoolSzie的数量,只不过其他任务队列一般是按照先进先出的规则处理任务,而PrioritYqUEWUE队列可以自定义规则根据任务的优先级顺序先后执行

线程池的拒绝策略

  • ThreadPoolExecutor.AbortPolicy():直接抛出异常,丢弃任务(当都满了)
  • new ThreadPoolExecutor.CallerRunsPolicy() :不想抛弃执行任务,但是由于池中没有任何资源了,那么就会直接使用调用该execute的线程本身来执行,很有可能造成当前线程也被阻塞
  • new ThreadPoolExecutor.DiscardOldestPolicy():若程序执行尚未关闭,则位于工作队列头部的任务将被删除,然后重复执行程序(如果再次失败,则重复过程)
  • ThreadPoolExecutor.DiscardPolicy():该策略默默的丢弃无法处理的任务,不予任何处理

线程池任务调度策略

当一个任务通过execute(Ruunabe)方法与添加到线程池时

  • 若此时线程池中的数量小于CorePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务
  • 若此时线程池的数量等于CorePoolSize,但是缓冲队列workQueue未满,那么任务被放入到缓冲队列
  • 若此时线程池中的数量大于CorePoolSize,缓冲队列满,并且线程池中数量小于maximumPoolSize,创建新的线程来处理被添加的任务
  • 若此时线程池中的数量大于CorePoolSize,缓冲队列workQuieue满,并且线程池数量等于maximumPoolSize,那么通过handler锁指定的策略来处理任务

也就是:

处理任务的优先级别为:核心线程CorePoolSize,任务队lie,最大线程数;若三者都满了,使用handler处理被决绝的策略

当线程池中的线程数量大于CorePOOLsIZE时,若某线程空闲时间超过`KEEPALIVETIME·,线程将被终止。这样,线程池就可以动态的调整池中的线程数

13 快速失败和安全失败

  1. 快速失败

    在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除和修改),则会抛出Concurent Modification Exception

    场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(在迭代过程中被修改)

  2. 安全失败

       > **采用安全失败机制的集合容器,在遍历时不时直接在集合容器上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历**
       >
       > `原理`:由于迭代时对原集合的拷贝进行遍历,锁以在遍历过程中对原集合所做的修改不能被迭代器检测到,所以不会触发`Concurrent Modification Exception`
       >
       > `缺点`:**基于拷贝内容的优点避免了`Concurrent Modification Exception`,但同杨的,迭代器并不能访问刀片修改后的内容,即迭代器遍历的是开始的那一刻拿到的集合拷贝哦,在遍历期间原集合发生的修改迭代器是不止到的**
       >
       > 场景:`java.util.concurrent`包下的容器都是安全失败的,可以在多线程下并发使用,并发修改
    

14、AQS原理

15、ConcurrentHashMap原理

发布了198 篇原创文章 · 获赞 20 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/ZHOUJIAN_TANK/article/details/104739295