Java并发常见面试题

177 阅读28分钟

程序、进程、线程、管程

  • 程序(program): 为完成特定任务,用某种语言编写的一组指令的集合。即一段静态的代码,静态的对象
  • 进程(process): 程序一次执行过程,或正在内存中运行的应用程序。每个进程都有一个独立的空间,系统运行一个程序即是一个进程即是一个进程从创建、运行到消亡的过程。==生命周期。 进程是动态的,进程作为 操作系统调度和分配资源的最小单位,系统运行时会为每个进程分配不同的内存区域。
  • 线程(thread): 进程可进一步细分为线程,是一个程序内部的一条执行路径。一个进程至少有一个线程。
    • 若一个进程同时间并行执行多个线程,就是支持多线程的。
    • 线程作为CPU调度和执行的最小单位,每个线程拥有独立的运行栈和程序计数器,线程切换的开销小。
    • 一个进程中的多个线程共享相同的内存单元/内存地址空间---> 它们从同一堆中分配对象,可以访问相同的变量和对象。这使得线程间通信更简便,搞笑。但多个线程操作共享的系统资源可能会带来安全隐患。
  • 管程:Monitor(监视器),就是我们平时说的锁。

线程状态

  • 在java.lang.Thread.State的枚举类中这样定义:
    public enum State{
        NEW,
        RUNNABLE,
        BLOCKED,
        WAITING,
        TIMED_WATINIG,
        TERMINATED;
    }
    
  • BLOCKED: 指互有竞争关系的几个线程,其中一个线程占有锁对象,其他线程只能等待锁,只有获取锁对象的线程才有执行机会
  • TIMED_WAITING: 当前线程执行过程中遇到Treahd类的 sleep或 jion, Object类的wait,LockSupport类的park方法,并且在调用这些方法时,设置了时间,那么当前线程会进入 TIMED_WAITING, 直到时间到,或被中断。
  • WAITING: 当前线程执行过程中遇到Object类的wait,Thread类的jion,LockSupport类的park方法,并调用这些方法,没有指定时间,那么当前线程会进入WAITING状态,直到被唤醒。
    • 通过Object的wait- Object.notify/notifyAll 唤醒
    • Condition.await - Condition.signal 唤醒
    • LockSupport.park - LockSupport.unpark 唤醒
    • Thread.join, 只有调用join() 的线程对象结束才能让当前线程恢复。
  • 当WAITING或TIMED_WAITING恢复到RANNABLE状态时,如果发现当前线程没有得到监视器锁,那么就会立刻装入BLOCKED状态。

image-20200521184149034.png

  • 状态转换:
    • start -> RUNNABLE
    • synchronized 竞争锁失败 -> BLOCKED
    • wait -> WAITING, notify()唤醒 -> BLOCED
    • sleep(ms) -> TIMED_WAITING

线程等待和唤醒有几种实现手段

  • Object类下的wait() notify() 和 notifyAll() 方法
    • wait(): 让当前线程处于等待状态、并释放当前拥有的锁
    • notify(): 随机唤醒等待该锁的其他线程,重新获取锁,并执行后续的流程,只能唤醒一个线程
    • notifyAll(): 唤醒所有等待该锁的线程(锁只有一把,虽然所有线程被唤醒,但所有线程需要排队执行)
  • Condition 类下的 await() singal() 和 signalAll()
    • await(): 对应Object的wait(),线程等待
    • singal(): 对象Object的notify(), 随机唤醒一个线程
    • singalAll(): 对应Object的notifiAll(), 唤醒所有线程
  • LockSupport类下park() 和 unpark()
    • LockSupport.park():休眠当前线程
    • LockSupport.unpark(线程对象): 唤醒某一个指定的线程
    • LockSupport无需配锁(synchronized或lock)一起使用。

线程的创建方式

  • 继承Thread类: 重写run(),但单继承限制
  • 实现Runnable: 解耦任务与线程,推荐方式
  • 实现Callable: 支持返回值和异常,需配合FutureTask
  • 线程池:推荐使用ExecutorService.submit();

关闭线程的方式

  • 使用标志位终止线程:

    • 一般在run()方法执行完毕之后,当前线程就结束了,但是在某些特殊情况下,run()方法会一直执行,比如使用while(true) 这样的循环结构来执行某些操作时。这时候就可以通过修改标志位的方式的方式结束run()方法。
  • 使用stop()方法终止线程:

    • 虽然stop()方法可以停止一个正在运行的线程,但是这个方法不安全,已经被弃用。
    • 弃用的原因:
      • 调用stop()方法会立刻停止run()方法的剩余工作,包括catch()和finally中的,并抛出ThreadDeath异常,因此会导致一些清理性的工作未完成。
      • 调用stop()方法会释放当前持有的所有锁,导致数据得不到同步,出现数据不一致的情况。
  • 使用interrupt()中断线程:

    • interrupt() 不会立即停止当前线程,而是在当前线程打一个停止标记,目标线程知道以后如何处理,完全由目标线程自行决定
    • 可以使用 isInterrupt() 判断是否中断了线程。

死锁? 如何避免和预防死锁?

  • 线程死锁:多个线程同时被阻塞,它们中的一个或多个全部都在等待某个资源在被释放。由于线程被无限期地阻塞,因此线程不可能正常终止。

  • 产生死锁的四个必要条件

    • 互斥条件:该资源任意一个时刻只由一个线程占用
    • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己完毕后才释放资源
    • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。
  • 检测死锁的工具: 使用jmapjstack等命令查看 JVM 线程栈和堆内存的情况,如果有死锁,jstack的输出通常会有 Found one Java-level deadlock: 的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用topdffree等命令查看操作系统的基本情况,出现死锁可能会导致CPU、内存等小号过高。

  • 避免和预防死锁的方法,破环产生死锁的必要条件即可:

    • 一次性申请所有需要的资源
    • 如果已经占有资源的线程,要申请其他资源,申请不到时,便直接释放已占有的资源
    • 靠按序申请资源。按某一顺序申请资源,再反序释放资源,破环循环等待条件即可。

乐观锁和悲观锁

  • 悲观锁:总是假设最坏的情况,认为共享资源每次被访问时,就会出现问题(比如共享数据被修改),所以每次在获取资源操作时都会上锁,这样其他线程想拿到这个资源就会阻塞知道锁被上一个持有者释放。也就是说共享资源每次只个一个线程使用,其他线程阻塞,用完后再把资源转让给其他线程。 高并发场景下,激烈的锁竞争会照成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。 有可能会导致死锁问题,影响代码正常运行。
    • 通常用于写比较多的情况(多写场景,竞争激烈),可以避免频繁失败和重试影响性能,悲观锁的开销时固定的。
  • 乐观锁:总是假设最好的情况,认为共享资源每次访问时不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是提交修改时去验证对应资源是否被其他线程修改了。 高并发场景下,乐观锁相比悲观锁,不存在锁竞争造成线程阻塞,也不会有死锁问题,在性能上更胜一筹,但如果冲突频繁发生,会频繁失败并重试,这样同样会影响性能。
    • 通常用于读比较多,写比较少的场景,可以避免频繁加锁影响性能。乐观锁主要针对对象是单个共享变量(atomic的原子变量)

如何实现乐观锁?

  • 一般会采用版本号机制或者CAS算法来实现。CAS算法相对更多一些。
  • 版本号机制: 数据表中存放版本号version字段,每次修改version的值都会加1。当修改数据的时候,会先读取数据表中的数据,同时也读到了version的值,在更新值时,会将之前读到的version值与数据库的中version进行比较,如果相等才进行修改,否则进行重试操作,直到更新成功。

CAS

  • compare and swap,比较并交换,是一条CPU并发原语:判断内存某个位置的值是否为预期值,如果是更新否则什么都不做,这个过程是原子的。原语的执行必须是连续的,执行过程不允许中断,不会造成数据不一致问题。
  • 包含三个操作数---内存位置、预期原值及更新值
  • 执行CAS操作时,将内存位置与预期原值比较:
    • 匹配,将该位置值更新为新值
    • 不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功
  • 缺点:
    • 循环时间长,开销大
    • 只能保证一个共享变量的原子操作
    • 引出来ABA的问题(一个线程A正在写入时,另一个线程B对资源修改后,再改回原状,期间其他线程C读取到,B修改的值,而A使用CAS判断与预期值相同,替换更新值。)
    • ABA解决办法:使用带版本号的CAS,也称CAS(Double CAS)或版本号CAS:每次进行CAS操作时,不仅需要比较要修改的内存地址的值与期望的值是否相等,还需要比较这个内存地址的版本号是否与期望的版本号相等。如果相等才能修改。 比如可以使用AtomicStampedRefrence来解决ABA问题。

synchronized 锁升级

  • synchronized的底层: 本质上两者都是对象监视器mintor的获取

    • synchronized修饰同步代码块,底层是monitorenter代表同步代码块开始, monitorexit代表同步代码块结束

      • 执行monitorenter时,会尝试获取对象锁,当锁的计数器为0时,表示可以获取到锁,获取到锁后,锁计数器变为1
      • 拥有对象锁的线程可以执行monitorexit时,会进行释放锁操作,将锁的计数器变为0,表示锁已释放,可以被其他线程获取。
    • synchronized修饰同步方法,底层会在同步方法处增加表示ACC_SYNCHRONIZED表示,表明此方法是个同步方法。

  • 状态

    • 无锁:对于共享资源,不涉及多线程的竞争访问
    • 偏向锁: 共享资源首次被访问,JVM会对该共享资源对象做一些设置,比如将对象头中是否偏向锁标志位设置为1,对象头中的线程id设置为当前线程ID,后续当前线程再次访问这个共享资源,会根据偏向锁标识和线程ID进行对比,比对成功则直接获取到锁,进入临界区域(被锁保护的线程只能串行的代码),这也是synchronized锁的可重入功能。
    • 轻量级锁: 当多个线程同时申请共享资源的访问时,就产生了竞争,JVM会先尝试使用轻量级锁,以CAS方式获取锁(一般就是自旋加锁,不阻塞线程采用循环等待的方式), 成功获取到锁,状态为轻量级锁,失败(达到一定的自旋次数)则锁升级为重量级锁。
    • 重量级锁:如果共享资源锁已经被某个线程持有,此时是偏向锁,未释放锁前,再由其他线程来竞争,则会升级到重量级锁,另外轻量级锁状态多线程竞争锁时,也会升级到重量级锁。重量级锁由操作系统来实现,所以性能消耗相对较高。
  • 另外需要注意的是,由于硬件资源的不断升级,获取锁的成本随之下降,jdk15版本后默认关闭了偏向锁。也就是从无锁直接到了轻量级锁的状态。

  • 无锁 -> 偏向锁 -> 轻量级锁(CAS自旋) -> 重量级锁(内核态切换)

  • 使用场景:修饰方法(实例方法锁this, 静态方法锁Class对象)或代码块(显式指定锁对象)

ReentrantLock

  • 特点:API级别可重入锁,支持公平/非公平模式、可中断、超时等待
  • AQS原理: 通过stat变量和CLH队列实现锁竞争
  • 对比synchronized:
    • 灵活控制锁粒度(tryLock)
    • 需手动释放锁,避免死锁

Synchronzied 和 ReentrantLock的区别?

  • 两者都是可重入锁,可重入锁就是线程可以再次获取自己的内部锁。
  • synchronized依赖于JVM 而 ReentrantLock依赖于API
    • ReetrantLock,API层面的,通过lock和unlock再搭配try-catch-finally来完成
  • ReentrantLock比synchronized多了一些高级功能
    • 等待可中断:ReentrantLock提供了一种中断等待锁线程的机制,通过 lock.interruptibly()来实现,就是说当前线程在等待获取锁,其他线程中断此线程(interrupt()),当前线程会抛出 InterrputedException异常,捕获并进行处理。
    • 支持公平锁:synchronized是非公平锁。 ReentrantLock默认也是非公平锁。公平锁是先等待的线程获取到锁。 ReentrantLock(boolean fair)使用此构造方法来指定公平锁。
    • 支持超时:使用tryLock来获取等待获取锁,等待最大时间后,超时还没有获取到锁,就会获取锁失败,不再等待。
    • 等待唤醒机制: ReentrantLock实现Condition接口。
      • Object类下的wait() notify() 和 notifyAll() 方法
        • wait(): 让当前线程处于等待状态、并释放当前拥有的锁
        • notify(): 随机唤醒等待该锁的其他线程,重新获取锁,并执行后续的流程,只能唤醒一个线程
        • notifyAll(): 唤醒所有等待该锁的线程(锁只有一把,虽然所有线程被唤醒,但所有线程需要排队执行)
      • Condition 类下的 await() singal() 和 signalAll()
        • await(): 对应Object的wait(),线程等待
        • singal(): 对象Object的notify(), 随机唤醒一个线程
        • singalAll(): 对应Object的notifiAll(), 唤醒所有线程
      • LockSupport类下park() 和 unpark()
        • LockSupport.park():休眠当前线程
        • LockSupport.unpark(线程对象): 唤醒某一个指定的线程
        • LockSupport无需配锁(synchronized或lock)一起使用。

volatile

  • 可见性:强制写操作刷主内存,读操作从主内存读取(内存屏障实现)
  • 有序性:禁止指令重排(happens-before规则:告诉程序在什么情况下一个线程的操作结果对另一个线程可见)
  • 局限性:不保证原子性(如i++需结合AtomicInteger)

双重校验锁实现单例对象

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

Synchronized 和 volatile的区别?

  • volatile是线程同步的轻量级实现,所以性能要比synchronzied高。 而且volatile是修饰变量的,而synchronzied是修饰方法和代码块的。
  • volatile只能保证可见性,但不能保证原子性。synchronzied是两者都能保证。
  • volatile主要解决的是变量在多个线程之间的可见性。 synchronzied解决多个线程访问资源的同步性。

ThreadLocal

  • ThreadLocal有什么用?

    • 为每个线程提供独立的变量副本,解决多线程数据竞争和线程安全问题。每个线程都拥有自己的本地变量盒子,确保数据不会互相影响。可以通过get() set()方法获取和修改自己的副本,从而避免线程安全问题。
  • ThreadLocal提供线程局部变量。这些与正常变量不同,因为每个线程在访问ThreadLocal实例时(通过其get或set方法)都有自己的、独立初始化变量副本。 ThreadLocal实例通常是类中私有静态字段,使用它的目的是希望将状态(例如:用户ID或事务ID)与线程关联起来。

  • 必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally中回收

  • ThreadLocal中的ThreadLocalMap中使用的key为ThreadLocal的弱引用,而value是强应用,所以ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候,key会被清理掉,而value不会被清理掉。这样ThreadLocalMap中就会出现key为null的Entry。加入我们不做任何措施,value将永远无法被GC回收,可能会产生内存泄漏。最好在最后手动调用remove()方法。

objectThreadLocal.set(userInfo);
try{
    //..
}finally{
    objectThreadLocal.remove();
}

初始化

ThreadLocal.withInitial(Supplier<? extends S> supplier) //创建一个threadLocal变量

核心总结

  • 每个Thread内有自己的 实例副本 且该副本只由当前线程自己使用
  • 既然其他Thread不可访问,那就不存在多线程共享的问题
  • 统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的
  • 如何才能不争抢:加入synchronized或者Lock控制资源的访问顺序

ThreadLocal类

  • ThreadLocal(自己线程共享): set/get 只能设置和获取自己所在的信息,获取不到别的线程的。
  • InheritableThreadLocal: ① 全局数据,父子线程之间传递,子线程可以获取到父线程的set ②最好是新建线程,一旦遇到线程池,主数据变更、线程池内不会同步更新
  • TransmittableThreadLocal: 使用前先通过pom,引入transmittable-thread-local,可以实现线程池之间的共享
TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();
ExecutorService threadPool = Executors.newSingleThreadExecutor();
threadPool = TtlExecutors.getTtlExecutorService(threadPool);

底层结构ThreadLocalMap

  • Thread类有个类型为 ThreadLocal.ThreadLocalMap的实例变量 threadLocals, 也就是说每个线程有一个自己的ThreadLocalMap
  • ThreadLocalMapkey可以视作ThradLocal(实际上,key并不是ThreadLocal本身,而是弱引用),value为代码中放入的值。
  • 每个线程放值时,都会往自己的ThreadLocalMap中存,都也是以ThreadLocal为引用,在自己的map中找到,从而实现线程隔离。
  • ThreadLocalMap没有链表结构,是有数组, keyThreadLocal<?> k,继承自 WeakReference,是弱引用类型。

使用场景

  • 解决线程安全问题: spring项目中Dao层装配的Connection肯定是线程安全,解决方案就是采用ThreadLocal,当每个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果为null,说明没有进行过数据连接,连接后存入Thradlocal中,每个请求线程都有一份自己的Connection,于是解决了线程俺安全问题。
  • Spring的声明式事务管理,也是使用的ThreadLocal来保证线程之间变量独立的。

29.Java四种引用类型

  • 强引用:new出来的对象一般为强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference 修饰的对象被称为弱引用,只要发生垃圾税后,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在Java中使用哦个 PhantomReference进行定义。虚引用中唯一的作用就是用队列接受对象即将死亡的通知。

为什么要使用线程池?

  • 降低资源消耗:可以重复利用已经创建的线程,降低线程创建和销毁的消耗
  • 提高响应效率:不需要等待线程创建
  • 管理线程资源:线程是稀缺资源,如果无限制的创建,会浪费大量资源,降低系统稳定性。使用线程池统一分配,调度和监控。

为什么不用自带的线程池?

  • Executors的自带方法:
    • FixedThreadPool 和 SingleThreadExcutor使用的阻塞队列是 LinkedBlockingQueue, 长度为 Integer.MAX, 任务过多可能会导致OOM
    • CacheTreadPool 使用 SynchronousQueue,长度也是 Integer.MAX, 任务过多时,可能导致OOM
    • ScheduledThreadPool 和 SingleScheduledExcutor的队列是 DelayedWorkQueue, 任务长度为Integer.MAX, 任务过多可能会导致oom

线程池参数的含义

public ThreadPoolExecutor(int corePoolSize, 
int maximumPoolSize, 
long keepAliveTime,
TimeUnit unit, 
BlockingQueue<Runnable> workQueue, 
ThreadFactory threadFactory, 
RejectedExecutionHandler handler)
{ //... }
  • corePoolSize: 核心线程数, 空闲状态,如果未设置超时关闭,就会处于Waiting状态。

  • maxmumPoolSize: 最大线程数,线程池允许创建的最大线程数

  • KeepAliveTime: 空闲线程存活时间,没有任务,会清理非核心线程(存活时间)

  • TimeUnit: 时间单位

  • BlockingQueue: 线程池任务队列,阻塞队列,用来存储所有待执行任务

    • ArraryBlockingQueue: 一个有数组结构组成的有界阻塞队列
    • LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列
    • SynchronusQueu: 一个不存储元素的阻塞队列,即直接提交给线程不保持它们
    • PriorityBlockingQueue: 一个支持优先级排序的无界阻塞队列
    • DelayQueue: 一个优先级队列的无界阻塞队列,只有在延迟期满时才能提取元素
    • LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列,与SynchronusQueue类似,还有非阻塞方法
    • LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
  • ThreadFactory: 创建线程的工厂

  • RejectedExecutionHandler: 拒绝策略

    • AbortPolicy: 抛出异常拒绝新任务
    • CallRunsPolicy: 把任务交给添加此任务的主线程来执行
    • DiscardPolicy: 直接丢弃新任务
    • DiscardOldesPolicy: 丢弃最早未处理的任务
  • 清理核心线程的方法:使用 allowCoreThreadTimeOut(Boolean value) 方法设置true,允许回收核心线程,超时时间还是keepAliveTime

线程池底层的执行原理

  • 在创建了线程池之后,开始等待请求
  • 当调用execute() 方法添加一个请求任务后,线程池会做出如下判断:
    • 如果正在运行的线程数量小于corePoolSize, 那么马上会创建线程运行这个任务
    • 如果正在运行的线程数量大于或等于corePoolSize, 那么将这个任务放入队列
    • 如果这个时候队列满了且正在运行的线程数量还小于maxmumPoolSize, 那么还要创建非核心线程来立刻运行这个任务
    • 如果队列满了且正在运行的线程数量大于或等于maxmumPoolSize, 那么线程池会启动饱和拒绝策略来执行
  • 当一个线程完成任务时,会从队列中取下一个任务来执行
  • 当一个线程无事可做超过一定时间(keepAliveTime)时,线程会判断:
    • 如果当前运行的线程数大于corePoolSize, 那么这个线程就会被停掉
    • 所有线程池的所有任务完成后,最终会收缩到corePoolSize的大小。

image.png

线程池中线程异常是销毁还是复用?

  • 使用execute()提交任务: 如果线程执行过程中发生异常,未捕获到异常时,会终止线程,并打印异常到日志中。线程池会创建新的线程,代替他,从而保证线程数保持不变。
  • 使用submit()提交任务:如果线程执行过程中发生异常,不会打印异常,而是返回给submit()的Future对象。当调用future.get()时,可以捕获到ExcutionException异常。当前线程不会终止,而是存在线程池中,等待后续处理。

如何设计一个根据任务优先度执行的线程池

  • 阻塞队列采用PriorityBlockingQueue(优先级无界阻塞队列)

    • 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
      • 提交的任务实现Comparable接口,通过重写compareTo()方法,指定任务的优先级规则。
      • 创建priorityBlockingQueue时,传入一个Comparator对象,指定任务的优先级规则。
  • 注意:

    • PriorityBlokingQueue,是无界队列,当大量任务访问时,可能会导致OOm。 可以重写PriorityBlockingQueue的offer方法,如果插入超过一定数量的,就返回false,不允许添加。
    • 饥饿问题,低优先级的任务可能一直无法执行
    • 性能问题:由于需要对元素排序以及保证线程安全(使用reetantlock来保证的),降低性能。

AQS

什么是AQS?

  • AQS的全称AbstractQueuedSynchronizer, 抽象队列同步器,在java.util.concurretn.locks包下。主要用于构建锁和同步器, 比如ReentrantReadWriteLock SynchronousQueue等都是基于AQS的。

  • 核心: 利用一个双向队列来保存等待锁的的线程,同时利用一个变量state变量来表示锁的状态

  • AQS的同步器可以分为独占模式和共享模式两种。独占模式是指同一时刻只允许一个线程获取锁,常见实现类有 ReentrantLock, 共享模式是指同一个时刻允许多个线程同时获取锁,常见的实现类有:Semaphore,CountDownLatch,CyclicBarrier等。

  • CountDownLatch: 秦灭六国,一统华夏

    • CountDownLatch 主要有两个方法,当一个或多个线程调用await() 方法时,这些线程会阻塞
    • 其他线程调用 countDown() 会将计数器减1(调用countDown() 方法的线程不会阻塞)
    • 当计数器的值变为0,因await()阻塞的线程会被唤醒,继续执行。
    • 使用场景:
      • 多个线程(任务)完成后,进行汇总合并, 主线程通过CountDownLatch.await()等待,多个线程调用CountDownLatch.countDown()将基数减1,等待减为0时,对结果进行汇总合并,往下执行。 这个是多个线程到达某个点后,主线程会完成后续操作。
  • CyclicBarrier: 集齐七颗龙珠,召唤神龙

    • CyclicBarrierd的字面意思是可循环(Cyclic)使用的屏障(Barrier)。要做的事情让一组线程到达一个屏障(也叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活
    • 线程进入屏障通过 CyclicBarrier的 await()
    • 十名运动员各自准备比赛,需要等待所有运动员都准备好以后,裁判才能说开始然后所有运动员一起跑, 是多个线程通过开始执行。
  • Semaphore: 停车位:

    • acquire(获取)当一个线程调用acquire操作,要么通过成功获取信号量(信号量减1),要么一直等下去,直到有线程释放信号量或超时
    • release(释放)实际上会将信号量的值加1,然后唤醒等待的线程
    • 信号量主要用于两个目的;一个用于多个共享资源的互斥使用,另一个用于并发线程数的控制。

AQS的原理

  • 核心思想: 如果当前被请求的共享资源空闲,则将当前空闲线程设置为工作线程,并锁定共享资源。如果请求的共享资源已被占用,那么就需要一套线程阻塞等待以及被唤醒锁分配的机制,这个机制是AQS底层 CLH队列实现。即 将暂时获取不到锁的线程放入队列。
  • CLH队列是对自旋锁进行改进,是基于单链表的自旋锁。在多线程情况下,会将所有的线程编织成一个队列,等待的线程会通过自旋检查上一个线程的状态,上一个线程释放锁,当前节点才能抢到锁。

Java常见并发容器java.util.concurrent

  • ConcurrentHashMap:线程安全的HashMap
    • JDK1.7: ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
    • JDK1.8: ConcurrentHashMap 摒弃了Segment, 直接用 Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,因为1.6后,Synchronized锁做了很多优化。
  • CopyOnWriteArrayList:线程安全的List, 在读多写少的场合性能非常好,远远好于 Vector
    • 核心:采用了写时复制 的策略,当需要修改(add,set,remove等操作) CopyOnWriteArrayList的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
  • ConcurretnLinkedQueue: 高效的并发队列,使用链表实现。可以看作一个线程安全的 LinkedList,这是一个非阻塞队列
    • 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 concurrentLinkedQueue来替代。
  • BlockingQueue: 是个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞独立额,非常适合作为数据共享的通道。
    • ArrayBlockingQueue: 是BlockingQueue接口的有界队列实现类,底层采用数组类,一旦创建,容量不能改变,其并发控制采用了可重入锁 ReetrantLock, 不管是插入操作还是读取操作,都需要获取到锁才能拿操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞,尝试从一个空队列中取一个元素也会同样阻塞。 默认情况下,不能保证线程访问队列的公平性,即不能按照线程等待的绝对时间顺序。 如果保证公平性,通常会降低吞吐量,可采用以下代码
    ArrayBlokcingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);
    
    • LinkedBlockingQueue: 底层基于单项链表实现的阻塞队列。可以当作无界队列也可以当作有界队列来使用,同样满足 FIFO 的特性。为了防止LinkedBlockingQueue容量迅速增加,耗损大量内存,通常创建LinkedBlockingQueue对象时,会指定大小,如果未指定大小,容量等于 Integer.MAX_VALUE
    • PriorityBlockingQueue:支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo() 来指定元素排序规则, 或者 初始化通过构造器参数 Comparator 指定排序规则。 并发控制采用的时可重入锁ReetrantLock, 队列为无界队列。 它就是 PriorityQueue 线程安全的版本,不可以插入null值,同时,插入队列的对象必须时可比较大小的(comparable),否则报 ClassCastException异常。
  • ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。内部存储多张链表,所有的链表都是排序的,查找时,从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表,最底层存放的时原始链表。 跳表是一种利用空间换时间的算法。

Atomic 原子类

  • Atomic 指一个操作具有原子性,即该操作不可分割,不可中孤单。java.util.concurrent.atomic
  • Atomic 类依赖于 CAS 乐观锁来保证其方法的原子性,而不需要使用传统锁机制(如 synchronized 块或 ReetrantLock)

基本类型

  • AtomicInteger:整型原子类
  • AtomicLong: 长整型原子类
  • AtomicBoolean: 布尔型原子类
  • 常用方法
    • get(): 获取当前值
    • getAndSet(int newValue):获取当前的指,并设置新的值
    • getAndIncrement():获取当前的值,并自增
    • getAndDecrement(): 获取当前的值,并自减
    • getAndAdd(int delta) 获取当前的值,并加上预期的值
    • compareAndSet(int expect, int update): 如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。
    • lazySet(int newValue): 最终设置为newValue, 比set还弱的语义,可能之后一段时间还有线程可以读到旧值,但可能更高效

数组类型

  • AtomicIntegerArray: 整型数组原子类
  • AtomicLongArray: 长整型数组原子类
  • AtomicReferenceArray: 引用类型数组原子类
  • 常用方法
    get(int i): 获取 index=1 位置元素的值
    getAndSet(int i, int newValue) 返回index=1的值,并设置为新值newValue
    getAndIncrement(int i): 获取index=i 位置元素的值,并让该位置的元素自增
    getAndDecrement(int i): 获取index=i 位置元素的值,并让该位置的元素自减
    getAndAdd(int i,int expect,int update): 如果输入的数值等于预期值,则以原子的方式将i位置的元素更新为update
    lazySet(int i,int newVlaue) 最终,将index=1位置的元素设置为newValue
    

引用类型

  • AtomicReference: 引用类型原子类
  • AtomicMarkableReference原子更新带有标记的引用类型。
  • AtomicStampedReference: 原子更新带有版本号的引用类型。可以解决CAS进行原子更新时出现的ABA问题。

对象的属性修改类型

  • AtomicIntegerFieldUpdater: 原子更新整型字段的更新器
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器
  • AtomicReferenceFieldUpdater: 原子更新引用类型里的字段。