程序、进程、线程、管程
- 程序(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状态。
- 状态转换:
- 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()方法。
- 一般在run()方法执行完毕之后,当前线程就结束了,但是在某些特殊情况下,run()方法会一直执行,比如使用
-
使用stop()方法终止线程:
- 虽然stop()方法可以停止一个正在运行的线程,但是这个方法不安全,已经被弃用。
- 弃用的原因:
- 调用stop()方法会立刻停止run()方法的剩余工作,包括catch()和finally中的,并抛出ThreadDeath异常,因此会导致一些清理性的工作未完成。
- 调用stop()方法会释放当前持有的所有锁,导致数据得不到同步,出现数据不一致的情况。
-
使用interrupt()中断线程:
- interrupt() 不会立即停止当前线程,而是在当前线程打一个停止标记,目标线程知道以后如何处理,完全由目标线程自行决定
- 可以使用 isInterrupt() 判断是否中断了线程。
死锁? 如何避免和预防死锁?
-
线程死锁:多个线程同时被阻塞,它们中的一个或多个全部都在等待某个资源在被释放。由于线程被无限期地阻塞,因此线程不可能正常终止。
-
产生死锁的四个必要条件
- 互斥条件:该资源任意一个时刻只由一个线程占用
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只能自己完毕后才释放资源
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源的关系。
-
检测死锁的工具: 使用
jmap、jstack等命令查看 JVM 线程栈和堆内存的情况,如果有死锁,jstack的输出通常会有Found one Java-level deadlock:的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top、df、free等命令查看操作系统的基本情况,出现死锁可能会导致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)一起使用。
- Object类下的wait() notify() 和 notifyAll() 方法
- 等待可中断:ReentrantLock提供了一种中断等待锁线程的机制,通过
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, 也就是说每个线程有一个自己的ThreadLocalMapThreadLocalMap的key可以视作ThradLocal(实际上,key并不是ThreadLocal本身,而是弱引用),value为代码中放入的值。- 每个线程放值时,都会往自己的ThreadLocalMap中存,都也是以
ThreadLocal为引用,在自己的map中找到,从而实现线程隔离。 - ThreadLocalMap没有链表结构,是有数组,
key是ThreadLocal<?> 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的大小。
线程池中线程异常是销毁还是复用?
- 使用execute()提交任务: 如果线程执行过程中发生异常,未捕获到异常时,会终止线程,并打印异常到日志中。线程池会创建新的线程,代替他,从而保证线程数保持不变。
- 使用submit()提交任务:如果线程执行过程中发生异常,不会打印异常,而是返回给submit()的Future对象。当调用future.get()时,可以捕获到ExcutionException异常。当前线程不会终止,而是存在线程池中,等待后续处理。
如何设计一个根据任务优先度执行的线程池
-
阻塞队列采用PriorityBlockingQueue(优先级无界阻塞队列)
- 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
- 提交的任务实现Comparable接口,通过重写compareTo()方法,指定任务的优先级规则。
- 创建priorityBlockingQueue时,传入一个Comparator对象,指定任务的优先级规则。
- 使用PriorityBlockingQueue,要求提交的任务必须具有排序能力
-
注意:
- PriorityBlokingQueue,是无界队列,当大量任务访问时,可能会导致OOm。 可以重写PriorityBlockingQueue的offer方法,如果插入超过一定数量的,就返回false,不允许添加。
- 饥饿问题,低优先级的任务可能一直无法执行
- 性能问题:由于需要对元素排序以及保证线程安全(使用reetantlock来保证的),降低性能。
AQS
什么是AQS?
-
AQS的全称
AbstractQueuedSynchronizer, 抽象队列同步器,在java.util.concurretn.locks包下。主要用于构建锁和同步器, 比如ReentrantReadWriteLockSynchronousQueue等都是基于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锁做了很多优化。
- JDK1.7:
CopyOnWriteArrayList:线程安全的List, 在读多写少的场合性能非常好,远远好于 Vector- 核心:采用了写时复制 的策略,当需要修改(add,set,remove等操作) CopyOnWriteArrayList的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
ConcurretnLinkedQueue: 高效的并发队列,使用链表实现。可以看作一个线程安全的 LinkedList,这是一个非阻塞队列- 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的
concurrentLinkedQueue来替代。
- 主要使用CAS非阻塞算法来实现线程安全的。 适合在性能要求比较高的,同时对队列读写存在多个线程中同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的
BlockingQueue: 是个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞独立额,非常适合作为数据共享的通道。ArrayBlockingQueue: 是BlockingQueue接口的有界队列实现类,底层采用数组类,一旦创建,容量不能改变,其并发控制采用了可重入锁ReetrantLock, 不管是插入操作还是读取操作,都需要获取到锁才能拿操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞,尝试从一个空队列中取一个元素也会同样阻塞。 默认情况下,不能保证线程访问队列的公平性,即不能按照线程等待的绝对时间顺序。 如果保证公平性,通常会降低吞吐量,可采用以下代码
ArrayBlokcingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);LinkedBlockingQueue: 底层基于单项链表实现的阻塞队列。可以当作无界队列也可以当作有界队列来使用,同样满足 FIFO 的特性。为了防止LinkedBlockingQueue容量迅速增加,耗损大量内存,通常创建LinkedBlockingQueue对象时,会指定大小,如果未指定大小,容量等于Integer.MAX_VALUEPriorityBlockingQueue:支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()来指定元素排序规则, 或者 初始化通过构造器参数Comparator指定排序规则。 并发控制采用的时可重入锁ReetrantLock, 队列为无界队列。 它就是PriorityQueue线程安全的版本,不可以插入null值,同时,插入队列的对象必须时可比较大小的(comparable),否则报ClassCastException异常。
ConcurrentSkipListMap: 跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。内部存储多张链表,所有的链表都是排序的,查找时,从顶级链表开始找,一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表,最底层存放的时原始链表。 跳表是一种利用空间换时间的算法。
Atomic 原子类
Atomic指一个操作具有原子性,即该操作不可分割,不可中孤单。java.util.concurrent.atomicAtomic类依赖于 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: 原子更新引用类型里的字段。