Java多线程面试题总结

线程的生命周期,线程有哪些状态

Java多线程面试题总结_第1张图片
Java多线程面试题总结_第2张图片

  1. 线程通常有五种状态,创建,就绪,运行,阻塞和死亡状态
  2. 阻塞的情况又分为三种
    (1) 等待阻塞:运行的线程执行wait方法,该线程会释放占用的所有资源,JVM会把该线程放入"等待池"中.进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify或notifyAll方法才被唤醒,wait是object类的方法
    (2) 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入"锁池"中.
    (3)其他阻塞:运行的线程执行sleep或join方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态.当sleep状态超时,join等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态,sleep时Thread类的方法
  • 新建状态(New) : 新创建了一个线程对象
  • 就绪状态(Runnable) : 线程对象创建后,其他线程调用了该对象的start方法,该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权
  • 运行状态(Running) : 就绪状态的线程获取了CPU,执行程序代码
  • 阻塞状态(Blocked) : 阻塞状态是线程因为某种原因放弃CPU的使用权,暂时停止运行,直到线程进入就绪状态,才有机会转到运行状态
  • 死亡状态(Dead) : 线程执行完了或者因为异常退出了run方法,该线程结束生命周期

sleep(),wait(),join(),yield()的区别

  1. 锁池
    所有需要竞争同步锁的线程都会放在锁池当中,比如当前对象的锁已经被其中一个线程得到,则其他线程需要在这个锁池进行等待,当前面的线程释放同步锁后锁池中的线程去竞争同步锁,当某个线程得到后会进入就绪队列进行等待cpu资源的分配
  2. 等待池
    当我们调用wait()方法后,线程会放到等待池当中,等待池的线程是不会去竞争同步锁,只有调用了notify()或notifyAll()后等待池的线程才会开始去竞争锁,notify()是随机从等待池选出一个线程放到锁池,而notifyAll()是将等待池的所有线程放到锁池当中
  • sleep是Thread类的静态本地方法,wait则是Object类的本地方法
  • sleep方法不会释放lock,但是wait会释放,而且会加入到等待队列中

sleep就是把cpu的执行资格和执行权释放出去,不再运行此线程,当定时时间结束再取回cpu资源,参与cpu调度,获取到cpu资源后就可以继续运行了,而如果sleep时该线程有锁,那么sleep不会释放这个锁,而是把这个锁带着进入了冻结状态,也就是说其他需要这个锁的线程根本不可能获取到这个锁,也就是说无法执行程序.如果睡眠期间其他程序调用了这个线程的interrupt方法,那么这个线程就会抛出interruptexception异常返回,这点和wait是一样的

  • sleep方法不依赖于同步器synchronized,但wait需要依赖synchronized关键字
  • sleep不需要被唤醒(休眠之后退出阻塞),但是wait需要,不指定时间需要被别人中断
  • sleep一般用于当前线程休眠,或者轮循暂停操作,wait则多用于多线程之间的通信
  • sleep会让出CPU执行时间且强制上下文切换,而wait则不一定,wait后可能还是有机会重新竞争到锁继续执行的

yield()执行后线程直接进入就绪状态,马上释放了cpu执行权,但是依然保留了cpu的执行资格,所以有可能cpu下次进行线程调度还会让这个线程获取到执行权继续执行
join()执行后线程进入阻塞状态,例如在线程B中调用线程A的join(),那线程B会进入到阻塞队列,直到线程A结束或中断线程

死锁的四个必要条件和解决方法

必要条件

  1. 互斥使用,一个资源每次只能给一个进程使用,不允许多个进程同时使用一个资源。
  2. 不可强占,资源申请者不能强行的从资源占有者手中夺取资源,资源只能由占有者自愿释放,由申请者获得。
  3. 请求保持,一个进程在申请新的资源的同时保持对原有资源的占有,其他的申请者不能获取。
  4. 循环等待,存在一个进程等待队列,其中后一个执行的程序等待前一个执行的程序完成,形成一个进程等待环路。

解决方案

破坏占用并等待

  • 线程启动时拿到所有需要的资源
  • 线程启动拿到初步启动所需的资源,后续再逐步释放已有的资源,申请需要的资源

不可强占

  • 当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了

破坏循环等待

  • 可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源

Java多线程面试题总结_第3张图片

银行家算法

模拟所有资源的预定最大可能量的分配来测试安全性.

银行家算法要发挥作用,需要知道三件事:

  • 每个进程可以请求每个资源的最大值
  • 每个进程目前拥有多少分配的资源
  • 系统目前有多少资源可用

只有当请求的资源量小于或等于可用的资源量时,才能将资源分配给进程;否则,该过程将一直等到资源可用。

死锁定位分析

jps命令定位进程号
jstack找到死锁查看

说说你对线程安全的理解

不是线程安全,应该是内存安全,堆是共享内存,可以被所有线程访问

当多个线程访问一个对象时,如果不用进行额外的同步控制或其他的协调操作,调用这个对象的行为都可以获得正确的结果,我们就说这个对象是线程安全的

是进程和线程共有的空间,分全局堆和局部堆,全局堆就是所有没有分配的额外空间,局部堆就是用户分配的空间,堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是用完了要还给操作系统,要不然就是内存泄漏

在Java中,堆是Java虚拟机所管理的内存最大的一块,是所有线程共享的一块内存区域,在虚拟机启动时创建,堆所存在的内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存

是每个线程独有的,保存其运行状态和局部自动变量的,栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是线程安全的,操作系统在切换线程的时候会自动切换栈,栈空间不需要再高级语言里面显式的分配和释放

目前主流操作系统都是多任务的,即多个进程同时运行,为了保证安全,每个进程只能访问分配给自己的内存空间,而不能访问别的进程的,这是由操作系统保障的
在每个进程的内存空间中都会有一块特殊的公共区域,通常称为堆(内存),进程内的所有线程都可以访问到该区域,这就是造成问题的潜在原因.

Thread,Runnable的区别

Thread和Runnable的实质是继承关系,没有可比性,无论使用Runnable还是Thread,都会new Thread,然后执行run方法,用法上,如果有复杂的线程操作需求,那就选择继承Thread,如果只是简单的执行一个任务,那就实现runnable

说说你对守护线程的理解

守护线程:为所有非守护线程提供服务的线程,任何一个守护线程都是整个JVM中所有非守护线程的保姆;

注意:由于守护线程的终止是自身无法控制的,因此千万不要把IO,File等重要操作逻辑分给他,因为它不靠谱

守护线程的作用?
GC垃圾回收线程:就是一个经典的守护线程,当我们的程序中不再有任何运行的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是JVM仅剩的线程时,垃圾回收线程会自动离开,它始终在低级别的状态运行,用于实时监控和管理系统中的可回收资源

应用场景:

  1. 来为其他线程提供服务支持的情况
  2. 或者在任何情况下,程序结束时,这个线程必须正常且立即关闭,就可以作为守护线程来使用,反之,如果一个正在执行的某个操作的线程必须要正确的关闭,否则就会出现不好的后果,那么这个线程就不能是守护线程,而是用户线程.通常都是些关键的事务,比方说,数据库录入或者更新,这些操作都是不能中断的

ThreadLocal的原理和使用场景

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量ThreadLocal,它存储本线程所有的ThreadLocal对象及其对应的值
ThreadLocalMap由一个个的Entry对象构成

Entry继承自WeakReference>,一个Entry由ThreadLocal对象和object构成,由此可见,Entry的key是ThreadLocal对象,并且是一个弱引用,当没指向key的强引用后,该key就会被垃圾收集器回收

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中

get方法执行过程类似,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象,再以当前ThreadLocal对象为key,获取对应的value

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器互相独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性

使用场景

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束
  2. 线程间数据隔离
  3. 进行事务操作(Spring),用于存储线程事务信息
  4. 数据库连接,Session会话管理

Spring框架在事务开始时会给当前线程绑定一个JDBC Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性,Spring框架里面就是用的ThreadLocal来实现这种隔离
Java多线程面试题总结_第4张图片

ThreadLocal内存泄漏,如何避免

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏

强引用:使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收,当内存不足,java虚拟机宁愿抛出OutOfMemeryError错误,使程序异常终止,也不回收这种对象.

弱引用:JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象,在java中,用java.lang.ref.WeakReference类来表示,可以在缓存中使用弱引用

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本
Java多线程面试题总结_第5张图片
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中的key为null,而value还存在着强引用,只有thread线程推出以后,value的强引用链表才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链

key使用强引用

当ThreadLocalMap的key为强引用,回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏

key使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收,当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用
ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义称private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉

并发,并行,串行的区别

  • 串行在时间上不可能发生重叠,前一个任务没搞定,下一个任务就只能等着
  • 并行在时间上是重叠的,两个任务在同一时刻互不干扰的同时执行
  • 并发允许两个任务彼此干扰,统一时间点,只有一个任务运行,交替执行

并发的三大特性

  • 原子性
    原子性是指在一个操作中cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行
    那程序中的原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了三步,包括读取变量原始值,进行加一操作,写入工作内存,所以在多线程中,又可能一个线程还没自增完,另一个线程就已经读取了值,导致结果错误,那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据(cpu执行角度)
  • 可见性
    当一个线程修改了共享变量的值,其他线程会马上知道这个修改。当其他线程要读取这个变量的时候,最终会去内存中读取,而不是从缓存中读取。
    Java多线程面试题总结_第6张图片
  • 有序性
    虚拟机在进行代码编译时,对于那些改变顺序之后不会对最终结果造成影响的代码,虚拟机不一定会按照我们写的代码的顺序来执行,有可能将他们重排序。实际上,对于有些代码进行重排序之后,虽然对变量的值没有造成影响,但有可能会出现线程安全问题。
    volatile本身就包含了禁止指令重排序的语义,而synchronized关键字是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则明确的。

为什么用线程池?解释下线程池的参数?

线程池所做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
它的主要特点为:线程复用:控制最大并发数,管理线程

常用的线程池

 		//一池五个处理线程
       ExecutorService threadPool1 = Executors.newFixedThreadPool(1);
        //一池一个处理线程
       ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
        //一池n个处理线程
        ExecutorService threadPool3 = Executors.newCachedThreadPool();
  • newFixedThreadPool
    创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
    newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的LinkedBlockingQueue
  • newSingleThreadExecutor
    创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有的任务按照指定顺序执行
    newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的LinkedBlockingQueue
  • newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过了处理需要,可灵活回收空闲线程,若无可回收,则新建线程
    newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说任务来了就创建线程运行,当线程空闲超过60秒,就销毁线程
  1. 降低资源消耗:提高线程利用率,降低创建和销毁线程的消耗
  2. 提高响应速度:任务来了,直接有线程可用可执行,而不是先创建线程,再执行
  3. 提高线程的可管理性:线程是稀缺资源,使用线程池可以统一分配调优监控
 public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

corePoolSize:核心线程数

线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。

大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。

线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。

maximumPoolSize:最大线程数

线程池允许创建的最大线程数量。

当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。

keepAliveTime:空闲线程存活时间

当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。

可被回收的线程:

(1)设置allowCoreThreadTimeout=true的核心线程。

(2)大于核心线程数的线程(非核心线程)

unit:时间单位

keepAliveTime的时间单位:

TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.SECONDS
TimeUnit.MINUTES
TimeUnit.HOURS
TimeUnit.DAYS

workQueue:工作队列

新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。
JDK默认的工作队列有五种:
(1)ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO(先进先出),使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。

(2)LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无界),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。

(3)SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。

(4)PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。

(5)DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。

threadFactory:线程工厂

创建线程的工厂,可以设定线程名、线程编号等。

默认线程工厂:

    /**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;
 
        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }
 
        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

handler:拒绝策略

当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。

JDK默认的拒绝策略有四种:

(1)AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

(2)DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。

(3)DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。

(4)CallerRunsPolicy:由调用线程处理该任务

线程池处理流程

Java多线程面试题总结_第7张图片

线程池中阻塞队列的作用?为什么是先添加队列而不是先创建最大线程?

阻塞队列是什么?

类似于消费者,生产者模式
Java多线程面试题总结_第8张图片

  • 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素
  • 同样,试图往已满的阻塞队列中添加新元素的线程同样会被阻塞,直到其他的线程从队列中移除一个或者多个元素或者完全清空队列后重新使得空闲起来并后续新增

分类

  • ArrayBlockingQueue : 由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue : 由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
  • PriorityBlockingQueue : 支持优先级排序的无界阻塞队列
  • DelayQueue : 使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue : 不存储元素的阻塞队列,也即单个元素的队列
  • LinkedTransferQueue : 由链表结构组成的无界阻塞队列
  • LinkedBlockingDuque : 由链表结构组成的双向阻塞队列

核心方法

Java多线程面试题总结_第9张图片

作用以及为什么先加入阻塞队列

  1. 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务
    阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源
    阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu内存
  2. 在创建新线程的时候,是要获取全局锁的,这个时候其他的就得阻塞,影响了整体效率(创建新线程成本比较高)

线程池中线程复用的原理

线程池将线程和任务解耦,线程是线程,任务是任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个"循环任务",在这个"循环任务"中不停检查是否有任务需要被执行,如果有则执行,也就是调用任务中的run方法,将run方法当成一个普通方法执行,通过这种方式只需要固定的线程就将所有任务的run方法串联起来

为什么线程池不能用Executors类来创建?

使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不适用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者过度切换的问题
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险

弊端如下

  1. FixedThreadPool和SingleThreadPool:
    允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
  2. CachedThreadPool和ScheduledThreadPool
    允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

谈谈线程池的拒绝策略

当线程数达到最大线程数且都在工作,阻塞队列也已经满了的时候,如果再有新的任务进来,就会启用拒绝策略

分类

  • AbortPolicy(默认):直接抛出RejectdExecutionException异常组织系统正常运行
  • CallerRunsPolicy:"调用者运行"一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务流量
  • DiscardOldestPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛任何异常.如果允许任务丢失,这是最好的一种方案

请描述synchronized和reentrantlock的底层实现及重入的底层原理

深入Lock锁底层原理实现,手写一个可重入锁

深入理解synchronized底层原理,一篇文章就够了!

请描述锁的四种状态和升级过程

关于 锁的四种状态与锁升级过程 图文详解

CAS的ABA问题如何解决

ABA问题是怎么产生的?

  1. 线程 1 从内存位置V中取出A。
  2. 线程 2 从位置V中取出A。
  3. 线程 2 进行了一些操作,将B写入位置V。
  4. 线程 2 将A再次写入位置V。
  5. 线程 1 进行CAS操作,发现位置V中仍然是A,操作成功。

尽管线程 1 的CAS操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。

扩展:原子引用

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User z3 = new User("z3", 22);
        User l4 = new User("l4", 25);

        AtomicReference<User> atomicReference = new AtomicReference<>();

        atomicReference.set(z3);
//        atomicReference.set(l4);

        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());

    }
}

true	User{userName='l4', age=25}
false	User{userName='l4', age=25}

解决方案

修改版本号(时间戳)
通过时间戳原子类引用AtomicStampedReference来解决ABA问题

//ABA问题的解决  AtomicStampedReference
public class ABADemo {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        System.out.println("===================以下是ABA问题的产生=======================");
        new Thread(()->{
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        },"t1").start();
        new Thread(()->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
        },"t2").start();

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("=========================以下是ABA问题的解决=======================");
        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号 : " + stamp);

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第二次版本号 : " + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "\t第三次版本号 : " + atomicStampedReference.getStamp());
        },"t3").start();


        new Thread(() ->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第一次版本号 : " + stamp);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean result = atomicStampedReference.compareAndSet(100, 2021, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "\t修改成功否 : " + result + "\t当前最新实际版本号 : " + atomicStampedReference.getStamp());

        },"t4").start();
    }
}

请谈一下AQS,为什么AQS的底层是CAS和volatile

AQS抽象的队列同步器

AQS是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类型变量表示持有锁的状态

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配,这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现,它将请求共享资源的线程封装成队列的节点(Node),通过CAS,自旋以及LockSupport.part()的方式,维护state变量的状态,使并发达到同步的控制效果

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改

Java多线程面试题总结_第10张图片

AQS-hasQueuedPredecessors()解析_绅士jiejie的博客-CSDN博客_hasqueuedpredecessors

【深入AQS原理】我画了35张图就是为了让你深入 AQS - 一枝花算不算浪漫 - 博客园

请谈一下你对volatile的理解

volatile是Java虚拟机提供的轻量级的同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

谈谈JMM

JMM(保证线程安全性)
JMM(Java内存模型Java Memery Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式

JMM关于同步的规定

  1. 线程解锁前,必须把共享变量的值刷新回主内存
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存
  3. 加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,**但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,**不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
Java多线程面试题总结_第11张图片

  • 可见性
  • 原子性
  • 有序性

volatile的可见性和禁止指令重排序是如何实现的

volatile的可见性代码实例

//验证volatile的可见性
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + "\t update number value : " + myData.num);
        },"A").start();

        //第二个线程
        while (myData.num == 0){
            //main线程就一直在这里等待循环,直到num值不再等于0
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over");
    }
}

class MyData {
    volatile int num = 0;

    public void addTo60() {
        this.num = 60;
    }
}

加入volatile后,main线程感知到num值被更改

那么volatile关键字是怎么做到可见性的呢?

从语义的角度来看

  1. volatile写的内存语义:当写线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  2. volatile读的内存语义:当读线程读一个volatile变量时,JMM会把该线程对应的本地内存置为无效,线程接下来将从主内存读取共享变量。

从cpu角度来看

  1. 写volatile时处理器会将缓存写回到主内存。
  2. 一个处理器的缓存写回到内存会导致其他处理器的缓存失效。

可见性不意味着线程安全了,因为还没有实现原子性
如果两个线程A和B,他们都读取了主存中的 i 值,A进行了 i+1 的操作,之后被阻塞,B又进行了 i+1 的操作,之后将新值赋值给 i,i 值被刷新回主存,此时由于A已经执行完了 i+1 操作,所以即使主存中的i值改变了,缓存一致性原则将A中的 i 变为新值,但是这个 i 值的改变也不会影响它将之前执行完的 i+1 得到的值赋给 i这一步,同样导致了最后的结果出错

volatile关键字怎么实现禁止指令重排序?
内存屏障

  1. 在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
  2. 在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
  3. 在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
  4. 在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
    Java多线程面试题总结_第12张图片

volatile底层实现原理

CAS是什么(CompareAndSwap)

java中的atomicInteger等类是通过CAS实现的线程安全

CAS的全称为Compare-And-Swap,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的

CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法,调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令.这是一种完全依赖于硬件的功能,通过它实现原子操作.再次操作,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题

CAS底层原理

Java多线程面试题总结_第13张图片

Java多线程面试题总结_第14张图片

  1. Unsafe
    是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据.Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe方法
    注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
  2. 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
    Java多线程面试题总结_第15张图片
  3. 变量value用volatile修饰,保证了多线程之间的内存可见性
// unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // 获取对象var1,偏移量为var2地址上的值,并赋值给var5
        var5 = this.getIntVolatile(var1, var2);
        /**
         * 再次获取对象var1,偏移量var2地址上的值,并和var5进行比较:
         * - 如果不相等,返回false,继续执行do-while循环
         * - 如果相等,将返回的var5数值和var4相加并返回
         */
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    // 最终总是返回对象var1,偏移量为var2地址上的值,即上述所说的V。
    return var5;
}

跳出循环的条件就是CAS成功,期望值与真实值相同,这段代码表示程序会一直循环(读取最新值)直到两次读取到的var5相同

CAS缺点

  1. 循环时间长,开销很大
    如果CAS失败,会一直进行循环尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
  2. 只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
    但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候可以用锁来保证原子性
  3. ABA问题

DCL单例为什么要加volatile

DCL(Double Check Lock)

public class Singleton {
    private Singleton(){

    }
    private static volatile Singleton singleton;

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

    }
}

为什么要加volatile?

因为有指令重排序的存在

某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化

instance = new Singeton();可以分为以下三步完成

  1. memory = allocate();//分配内存空间
  2. instance(memory);//初始化对象
  3. instance = memory;//设置Instance指向刚分配的内存地址,此时Instance != null

步骤2步骤3不存在数据依赖关系,所以可以重排优化

请描述synchronized和ReentrantLock的异同

  1. 原始构成
    synchronized是关键字属于JVM层面
    (1)monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能调wait/notify等方法)
    (2)monitorexit(两次,正常解锁,异常解锁,保证锁解了)
    Lock是具体类(java.util.concurrent.Locks.lock)是api层面的锁
  2. 使用方法
    synchronized 不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
    ReentrantLock则需要用户去手动释放锁,就有可能导致出现死锁现象
    需要lock()和unlock()方法配合try/finally语句块来完成
  3. 等待是否可中断
    synchronized不可中断,除非抛出异常或者正常运行完成
    ReentrantLock可中断
    (1)设置超时方法tryLock(long-timeout,TimeUnit unit)
    (2)lockInterruptibly()放代码块中,调用interrupt()方法可中断
  4. 加锁是否公平
    synchronized是非公平锁
    ReetrantLock两者都可以,默认是非公平锁,构造方法可以选择
  5. 锁绑定多个条件Condition
    synchronized没有
    ReentrantLock用来实现分组唤醒需要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程

区别于AQS,Condition维护自己的队列

代码示例(ABC三个线程轮流打印)

/**
 * @author XQ
 * @description 多线程之间按顺序调用,,实现A->B->C三个线程启动,要求如下:
 * AA打印5次,BB打印10次,CC打印15次
 * 共十轮
 * @create 2021-07-03-11:52
 */
public class SyncAndReentrantLockDemo {
    public static void main(String[] args) {
        ShareResource s = new ShareResource();
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                s.print5();
            }
        },"A").start();
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                s.print10();
            }
        },"B").start();
        new Thread(() ->{
            for (int i = 0; i < 10; i++) {
                s.print15();
            }
        },"C").start();
    }
}
class ShareResource{
    private int number = 1;//A:1 B:2 C:3
    private Lock lock = new ReentrantLock();
    private Condition c1 = lock.newCondition();
    private Condition c2 = lock.newCondition();
    private Condition c3 = lock.newCondition();
    public void print5(){
        lock.lock();
        try {
             // 1 判断
            while (number != 1){
                c1.await();
            }
            // 2 业务
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 3 通知
            number = 2;
            c2.signal();

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void print10(){
        lock.lock();
        try {
             // 1 判断
            while (number != 2){
                c2.await();
            }
            // 2 业务
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 3 通知
            number = 3;
            c3.signal();

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
    public void print15(){
        lock.lock();
        try {
             // 1 判断
            while (number != 3){
                c3.await();
            }
            // 2 业务
            for (int i = 0; i < 15; i++) {
                System.out.println(Thread.currentThread().getName() + "\t" + i);
            }
            // 3 通知
            number = 1;
            c1.signal();

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}

聊聊你对as-if-serial和happens-before语义的理解

as-if-serial

不管怎么重排序,单线程下的执行结果不能被改变。

编译器、runtime和处理器都必须遵守as-if-serial语义。

happens-before

如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。

请描述一下锁的分类以及JDK中的应用

Java中的锁分类与使用 - hustzzl - 博客园

线程池有哪些?

  1. newCachedThreadPool
    创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
    这种类型的线程池特点是:
    工作线程的创建数量几乎没有限制(其实也有限制的,数目为Interger. MAX_VALUE), 这样可灵活的往线程池中添加线程。
    如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为1分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。
    在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
    执行很多短期异步的小程序或者负载较轻的服务
  2. newFixedThreadPool
    创建一个指定工作线程数量的线程池。每当提交一个任务就创建一个工作线程,如果工作线程数量达到线程池初始的最大数,则将提交的任务存入到池队列中。
    FixedThreadPool是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
    执行长期的任务,性能很好
  3. newSingleThreadExecutor
    创建一个单线程化的Executor,即只创建唯一的工作者线程来执行任务,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
    一个任务一个任务执行的场景
  4. newScheduleThreadPool
    创建一个定长的线程池,而且支持定时的以及周期性的任务执行,支持定时及周期性任务执行。
  5. newSingleThreadScheduledExecutor
    创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行并且可定时或者延迟执行线程活动。

怎么解决集合类的不安全问题?

ArrayList

  1. 用集合安全类 new Vector<>()
  2. 用Collections的工具类中的装饰器Collections.synchronizedList(new ArrayList<>());
  3. 读写分离类 new CopyOnWriteArrayList();
    读写分离,写需要复制出一个新容器

Map

  1. ConcurrentHashMap
  2. Collections
  3. CopyOnWriteMap

公平锁,非公平锁,可重入锁,递归锁,自旋锁谈谈你的理解?请手写一个自旋锁

公平和非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能申请的线程比先申请的线程优先获取锁,在高并发情况下,有可能会造成优先级反转或者饥饿现象

并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁和非公平锁,默认是非公平锁
关于两者区别
公平锁: Threads acquire a fair lock in the order in whick they requested if
公平锁,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后按照FIFO的规则从队列中取到自己(synchronized也是非公平锁)
非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁的方式(吞吐量大)

可重入锁(递归锁)

指的是同一层线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码
在同一个线程的外层方法获取锁的时候,在进入内层方法会自动获取锁
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

可重入锁最大的作用就是避免死锁

synchronized是一个典型的可重入锁
代码示例

public class ReetrantLockTest {
    public static void main(String[] args) {
        Phone phone = new Phone();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t1").start();
        new Thread(() -> {
            try {
                phone.sendSMS();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t2").start();
    }
}
class Phone{
    public synchronized void sendSMS() throws Exception{
        System.out.println(Thread.currentThread().getId() + "\t" + "invoked sendSMS()");
        sendEmail();
    }
    public synchronized void sendEmail() throws Exception{
        System.out.println(Thread.currentThread().getId() + "\t" + "invoked sendEmail()");
    }
}

输出结果

14	invoked sendSMS()
14	invoked sendEmail()
15	invoked sendSMS()
15	invoked sendEmail()

ReentrantLock也是可重入锁

自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
模拟实现自旋锁

public class T2 {
    //原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void myLock(){
        Thread thread = Thread.currentThread();

        while (!atomicReference.compareAndSet(null, thread)){
            System.out.println(Thread.currentThread().getName() + " : 尝试获取锁");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "\t lock");
    }
    public void myUnlock(){
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "\t unlock");
    }
    public static void main(String[] args) {
        T2 t = new T2();
        new Thread(() ->{
            t.myLock();
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t.myUnlock();
        },"A").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            t.myLock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t.myUnlock();
        },"B").start();
    }
}

A	 lock
B : 尝试获取锁
B : 尝试获取锁
A	 unlock
B	 lock
B	 unlock

核心就是CAS

独占锁(写锁)/共享锁(读锁)/互斥锁

ReadWriteLockDemo
独占锁:该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁

共享锁:指该锁可被多个线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁
读锁的共享锁可保证并发度是非常高效的,读写,写读,写写过程都是互斥的

线程间的通信方式(CountDownLatch、CyclicBarrier、Semaphore)

CountDownLatch

代码演示

public class T3 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 5; i++) {
            int index = i;
            new Thread(()->{
                System.out.println(index + " 线程执行");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println("最终线程执行");
    }
}
0 线程执行
3 线程执行
4 线程执行
1 线程执行
2 线程执行

线程会阻塞,因为达不到计数器的个数,但是0-4线程会执行
把i改成6,则最终线程会执行

public class T3 {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        for (int i = 0; i < 6; i++) {//改动此处
            int index = i;
            new Thread(()->{
                System.out.println(index + " 线程执行");
                countDownLatch.countDown();
            },String.valueOf(i)).start();
        }
        countDownLatch.await();
        System.out.println("最终线程执行");
    }
}
0 线程执行
3 线程执行
4 线程执行
5 线程执行
1 线程执行
2 线程执行
最终线程执行

CyclicBarrier

CyclicBarrier是加,CountDownLatch是减
CyclicBarrier会阻塞其他线程,像一道大门,当指定条件个数的线程到达门口时,一起打开

public class T3 {
    public static void main(String[] args)  {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
            System.out.println("最终线程执行");
        });
        for (int i = 0; i < 6; i++) {
            int index = i;
            new Thread(()->{
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(index + " 线程执行");
            },String.valueOf(i)).start();
        }


    }
}

最终线程执行
0 线程执行
3 线程执行
2 线程执行
5 线程执行
1 线程执行
4 线程执行

这里如果线程数不足,就不会执行任何线程的的run方法

public class T3 {
    public static void main(String[] args)  {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
            System.out.println("最终线程执行");
        });
        for (int i = 0; i < 5; i++) {//这里改动了
            int index = i;
            new Thread(()->{
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println(index + " 线程执行");
            },String.valueOf(i)).start();
        }


    }
}

Semaphore

信号量
用来控制同时访问某个特定资源的操作数量,或者同时执行某个制定操作的数量。计数信号量还可以实现某种资源池,或者对容器施加边界。

public static void main(String[] args) {
    //模拟有3个空车位
    Semaphore semaphore = new Semaphore(3);
    //6个线程来抢3个资源
    for (int i = 1; i <= 6; i++) {
        int num = i;
        new Thread(() -> {
            try {
                semaphore.acquire();
                System.out.println(Thread.currentThread().getName() + "抢到了第" + num + "个车位");
                TimeUnit.SECONDS.sleep(2);
                System.out.println(Thread.currentThread().getName() + "离开了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                semaphore.release();
            }
        }, String.valueOf(i)).start();
    }
}

1抢到了第1个车位
2抢到了第2个车位
5抢到了第5个车位
2离开了
1离开了
5离开了
3抢到了第3个车位
6抢到了第6个车位
4抢到了第4个车位
6离开了
3离开了
4离开了

写一个生产者消费者模式?

传统

/**
 * 题目: 一个初始值为零的变量,两个线程对其交替操作
 *
 * @author xq
 * @create 2021-07-03-10:08
 */
public class ProdConsumer_TraditionDemo {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();

        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.increment();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "AA").start();
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.decrement();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "BB").start();
    }
}

class ShareData {//资源类
    private int number = 0;
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void increment() throws Exception {
        lock.lock();
        try {
            //1 判断
            while (number != 0) {
                //等待.不能生产
                condition.await();
            }
            //2 增加
            number++;
            System.out.println(Thread.currentThread().getName() + "\t" + number);

            //3 通知唤醒
            this.condition.signalAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void decrement() throws Exception {
        lock.lock();
        try {
            //1 判断
            //多线程中,if被while替代是有原因的:比如有个线程进方法了,发现条件不满足,就wait,
            //然后再次被唤醒的时候,不会进行二次判断的,而是直接向前走!
            //使用while则不一样,每次都会判断,不满足条件就继续等待!
            while (number == 0) {
                //等待.不能生产
                condition.await();
            }
            //2 增加
            number--;
            System.out.println(Thread.currentThread().getName() + "\t" + number);

            //3 通知唤醒
            this.condition.signalAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }


}

进阶版(volatile + AtomicInteger + 阻塞队列)

public class ProdConsumer_BlockQueueDemo {
    public static void main(String[] args) {
        MyResource resource = new MyResource(new ArrayBlockingQueue<>(10));
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "\t生产线程启动");
            try {
                resource.myProd();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Prod").start();

        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "\t消费线程启动");
            try {
                resource.myConsumer();
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"Consumer").start();

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        resource.stop();
    }
}

class MyResource {
    private volatile boolean FLAG = true;//默认开启,进行生产+消费

    private AtomicInteger atomicInteger = new AtomicInteger();

    BlockingQueue<String> blockingQueue = null;

    public MyResource(BlockingQueue<String> blockingQueue) {
        this.blockingQueue = blockingQueue;
        System.out.println(blockingQueue.getClass().getName());
    }

    public void setFLAG(boolean FLAG) {
        this.FLAG = FLAG;
    }

    public void myProd() throws Exception {
        String data = null;
        boolean retValue;
        while (FLAG) {
            data = atomicInteger.incrementAndGet() + "";
            retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS);
            if (retValue) {
                System.out.println(Thread.currentThread().getName() + "\t插入队列 " + data + "成功");
            } else {
                System.out.println(Thread.currentThread().getName() + "\t插入队列 " + data + "失败");
            }
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(Thread.currentThread().getName() + "\t生产结束");
    }

    public void myConsumer() throws Exception {
        String result = null;
        while (FLAG) {
            result = blockingQueue.poll(2, TimeUnit.SECONDS);
            if (null == result || result.equalsIgnoreCase("")) {
                FLAG = false;
                System.out.println(Thread.currentThread().getName() + "\t 超过两秒没有取到蛋糕,消费退出");
                System.out.println();
                System.out.println();
                return;
            }
            System.out.println(Thread.currentThread().getName() + "\t消费队列 " + result + "成功");
        }
        System.out.println(Thread.currentThread().getName() + "\t消费结束");
    }

    public void stop(){
        this.FLAG = false;
    }

}

结果

Prod	生产线程启动
Consumer	消费线程启动
Prod	插入队列 1成功
Consumer	消费队列 1成功
Prod	插入队列 2成功
Consumer	消费队列 2成功
Prod	插入队列 3成功
Consumer	消费队列 3成功
Prod	插入队列 4成功
Consumer	消费队列 4成功
Prod	插入队列 5成功
Consumer	消费队列 5成功
Prod	生产结束
Consumer	 超过两秒没有取到蛋糕,消费退出

Callable接口

Java多线程带返回值的Callable接口 - 知乎

LockSupport是什么

线程等待唤醒机制的改良加强(wait->notify)

传统等待唤醒,线程先要获得并持有锁,必须在锁块中,而且必须要先等待后唤醒,线程才能够被唤醒

LockSupportAPI调用示例

public class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {

        Thread a = new Thread(() ->{
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "\t------come in");
            LockSupport.park();//被阻塞....等待通知等待放行,它要通过需要许可证
            System.out.println(Thread.currentThread().getName() + "\t------被唤醒");
        },"A");
        a.start();

        Thread B = new Thread(() ->{
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName() + "\t" + "通知了");
        },"B");
        B.start();

    }
}

线程池的submit()和execute()有什么区别?

  • execute只可接收Runnable对象,而submit既可接收Runnable对象又可接收Callable对象;因为submit方法可接收Callable对象,所以可以有通过Future获取返回结果,而execute则做不到

你可能感兴趣的:(计算机总复习,多线程)