进程的状态
创建状态(new) :进程正在被创建,尚未到就绪状态。
就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运
行。
线程的生命周期和状态
NEW(新建)
RUNNING(运行)
BLOCKED(阻塞)
WAITING(等待)
TIMED_WAITING(超时等待)
TERMINATED(终止)
上下文切换
上下文:线程在执行过程中自己的运行条件和状态
线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场
死锁例子
两个共享资源,两个线程错位争夺共享资源
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
死锁的四个条件
互斥条件:该资源任意一个时刻只由一个线程占用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
sleep() 方法和 wait() 方法
两者都可以暂停线程的执行。
wait() 通常被用于线程间交互/通信, sleep() 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。
sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
锁的分类
https://mp.weixin.qq.com/s?__biz=MjM5NjQ5MTI5OA==&mid=2651749434&idx=3&sn=5ffa63ad47fe166f2f1a9f604ed10091&chksm=bd12a5778a652c61509d9e718ab086ff27ad8768586ea9b38c3dcf9e017a8e49bcae3df9bcc8&scene=38#wechat_redirect
synchronized 关键字
概念:synchronized 关键字解决的是多个线程之间访问资源的同步性,保证被它修饰的方法或者代码块
在任意时刻只能有一个线程执行。
使用方式:
1.修饰实例方法: 作用于当前对象实例加锁,进入同步代码前要获得 当前对象实例的锁
2.修饰静态方法:给当前类加锁,会作用于类的所有对象实例
3.修饰代码块:指定加锁对象,对给定对象/类加锁。
底层原理:javac -》javap -c -s -v -l
1.代码块:实现使用的是 monitorenter 和 monitorexit 指令。
当执行 monitorenter 指令时,线程获取 对象监视器 monitor 的持有权。获取锁后将锁计数器+1;
在执行 monitorexit 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。
2.方法: ACC_SYNCHRONIZED 标识
对象实例
对象在内存中分为三块区域
对象头:由 Mark Word (标记字段)和 Klass Point (类型指针)构成。
Mark Word(标记字段):用于存储对象自身的运行时数据,例如存储对象的HashCode,分
代年龄、锁标志位等信息。
Klass Point(类型指针):对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对
象是哪个类的实例。
实例数据:这部分主要是存放类的数据信息,父类的信息。
字节对齐:为了内存的IO性能,JVM要求对象起始地址必须是8字节的整数倍。
synchronized 的优化机制
锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞
争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来
解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。
双重校验锁实现对象单例
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;
}
}
volatile 关键字
原因: CPU 缓存模型:CPU 缓存则是为了解决 CPU 处理速度和内存处理速度不对等的问题。内
存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题
CPU 缓存 的工作方式:先复制一份数据到 CPU 缓存,当 CPU 需要用到的时候就可以直
接从 CPU 缓存中读取数据,当运算完成后,再将运算得到的数据写回 主内存 中。CPU 为了解决内存缓
存不一致性问题可以通过制定缓存一致协议或者其他手段来解决
并发编程的三个重要特性:原子性;可见性;有序性
解决原理:
1.可见性原理:处理器和内存之间存在多级缓存来提升处理器速度;多级缓存导致数据不一致问题。对volatile变量进行写操作的时候,jvm会向处理器发送lock前缀指令,立刻将缓存变量写入主内存。缓存存在一致性协议,发现自己数据与主内存不一致,从主内存更新数据。
2.有序性原理:处理器优化导致指令重排,代码乱序执行。禁止指令重排优化
3.原子性原理:当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去 CPU 使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。volatile 是不能保证原子性的
volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性
synchronized 关键字和 volatile 关键字的区别
synchronized 关键字和 volatile 关键字是两个互补的存在。
作用地点:volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块。
局限性:volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
重点方向:volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
ThreadLocal
创建每个线程的变量副本,进行数据隔离。private static final ThreadLocal map=ThreadLocal.withInitial( 资源);
使用 get()和 set()方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。
ThreadLocal<String> local=new ThreadLocal();
local.set("miaoxiaowen");
local.get();
ThreadLocal 原理: ThreadLocalMap类对应的 get()、set()方法。每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
set方法:通过Thread 获取map,存在直接更新set(key经过hash定位,往后遍历找null槽,找过期key),不存在则创建新的。
Hash 算法:每当创建一个ThreadLocal对象,ThreadLocal.nextHashCode这个值就会增长0x61c88647(斐波那契数),Hash 冲突:线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,将当前元素放入此槽位中
ThreadLocal 内存泄露问题: ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而value 是强引用。如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
ThreadLocalMap的两种过期key数据清理方式:
探测式清理:从开始位置向后探测清理过期数据,将过期数据的 Entry 设置为 null ,沿途中碰到未过期的数据则将此数据 rehash 后重新在 table 数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的 Entry=null 的桶中,使 rehash 后的 Entry 数据距离正确的桶的位置更近一些
启发式清理
Atomic 原子类
基本类型:
1.AtomicInteger:整形原子类 ;
2.AtomicLong:长整型原子类;
3.AtomicBoolean:布尔型原子类
数组类型:
1.AtomicIntegerArray:整形数组原子类;
2.AtomicLongArray:长整形数组原子类;
3.AtomicReferenceArray:引用类型数组原子类
引用类型:
1.AtomicReference 引用类型原子类;
2.AtomicStampedReference 原子更新带有版本号的引用类型;
3.AtomicMarkableReference 原子更新带有标记位的引用类型
对象的属性修改类型(对引用数据类型的某个字段进行操作):
1.AtomicIntegerFieldUpdater:原子更新整形字段的更新器;
2.AtomicLongFieldUpdater:原子更新长整形字段的更新器
AtomicInteger 的使用
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式
将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能
导致其他线程在之后的一小段时间内还是可以读到旧的值。
AtomicInteger 类的原理
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。
CAS算法涉及到三个操作数:需要读写的内存值 V,进行比较的值 A,要写入的新值 B
CAS三大问题
ABA问题:在变量前面添加版本号,每次变量更新的时候都把版本号加一
循环时间长开销大问题。CAS操作如果长时间不成功,会导致其一直自旋,给CPU带来非常大的开销,采用分段CAS和自动分段迁移
多变量原子问题。采用AtomicReference,这个是封装自定义对象的,多个变量可以放一个自定义对象里
ReentrantLock
ReentrantLock的使用方式:
// 1.初始化选择公平锁、非公平锁
ReentrantLock lock = new ReentrantLock(true);
// 2.可用于代码块
lock.lock();
// 业务代码
。。。
// 释放锁
lock.unlock()
公平锁实现
ReentrantLock里面有一个内部类Sync,Sync继承AQS。其子类公平锁FairSync。公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
加锁过程:
1.通过 ReentrantLock 的加锁方法 Lock 进行加锁操作。
2.调用到内部类 Sync 的 Lock 方法,由于Lock 是抽象方法,根据 ReentrantLock 初始化选择的公平锁和非公平锁,执行相关内部类的 Lock 方法,本质上都会执行 AQS 的 Acquire 方法。
3.AQS 的 Acquire 方法会执行 tryAcquire 方法,但是由于 tryAcquire 需要自定义同步器实现,因此执行了 ReentrantLock 中的 tryAcquire 方法,由于 ReentrantLock 是通过公平锁和非公平锁内部类实现的 tryAcquire 方法,因此会根据锁类型不同,执行不同的 tryAcquire。
4.tryAcquire 是获取锁逻辑,获取失败后,会执行框架 AQS 的后续逻辑,跟 ReentrantLock 自定义同步器无关。
解锁过程:
1.通过 ReentrantLock 的解锁方法 Unlock 进行解锁。
2.Unlock 会调用内部类 Sync 的 Release 方法,该方法继承于 AQS。
3.Release 中会调用 tryRelease 方法,tryRelease 需要自定义同步器实现,tryRelease 只在ReentrantLock 中的Sync 实现,因此可以看出,释放锁的过程,并不区分是否为公平锁。释放成功后,所有处理由 AQS 框架完成,与自定义同步器无关。
非公平锁实现
ReentrantLock里面有一个内部类Sync,Sync继承AQS。其子类非公平锁NonfairSync。
公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
AQS
AQS 是一个用来构建锁和同步器的框架
AQS 核心思想:请求的共享资源=>空闲=>将线程设置为有效的工作线程=>锁定共享资源
请求的共享资源=>占用=>将线程加入 CLH 队列(FIFO )
AQS的工作原理:AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程
的排队工作。
AQS两种资源共享方式:
独占:只有一个线程能执行。 分为公平锁和非公平锁。
共享:多个线程同时执行
AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:
protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回
false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回
false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但
没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则
返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要
去实现它。
AQS同步组件:独占式如ReentrantLock,共享式如Semaphore,CountDownLatch,组合式的如
ReentrantReadWriteLock。
AQS队列数据结构
队列由一个一个的Node结点组成。
重点属性:
waitStatus(等待状态,初始为0)
Node prev(当前结点的前驱结点)
Node next(当前结点的后继结点)
Thread thread(与当前结点关联的排队中的线程)
AQS重点方法
1.获取同步状态–acquire():
首先,调用使用者重写的tryAcquire方法,若返回true,意味着获取同步状态成功,后面的逻辑不再执行;若返回false,构造独占式同步结点,通过addWatiter将此结点添加到同步队列的尾部(此时可能会有多个线程结点试图加入同步队列尾部,需要以线程安全的方式(cas自旋)添加);加入队列中的结点线程进入自旋状态,若是老二结点(即前驱结点为头结点),才有机会尝试去获取同步状态;否则,当其前驱结点的状态为SIGNAL,线程便可安心休息,进入阻塞状态,直到被中断或者被前驱结点唤醒。
2.释放同步状态–release()
找到头结点的后继结点进行唤醒,若后继结点为空或处于CANCEL状态,从后向前遍历找寻一个正常的结点,唤醒其对应线程
AQS 组件总结
Semaphore信号量:指定多个线程同时访问某个资源
public class SemaphoreExample1 {
private final static int threadCount = 20;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
//同时最多允许3个线程访问
final Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
//获取 一个 许可
semaphore.acquire();
//获取多个许可
//semaphore.acquire(3);
semaphore.tryAcquire();//尝试获取许可
test(threadNum);//最多有三个线程同时执行test()方法
//释放许可
semaphore.release();
//释放多个许可
//semaphore.release(3);
} catch (Exception e) {
log.error("exception 为 {}", e);
}
});
}
log.info("finish");
}
public static void test(int threadNum) throws Exception {
log.info("threadNum === {}", threadNum);
Thread.sleep(100);
}
}
CountDownLatch倒计时器 :让某一个线程等待直到倒计时结束,再开始执行
public class CountDownLatchExample1 {
private final static int threadCount = 200;
public static void main(String[] args) throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
// CountDownLatch倒计时器
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
exec.execute(() -> {
try {
test(threadNum);
} catch (Exception e) {
log.error("exception 为 {}", e);
} finally {
countDownLatch.countDown();//每次都是减1
}
});
}
countDownLatch.await();//保证上面的200线程都是执行完的
//第一个参数代表等待的时间,第二个参数:时间的单位,超过10毫秒就执行
countDownLatch.await(10, TimeUnit.MICROSECONDS);
log.info("finish");
}
public static void test(int threadNum) throws Exception {
Thread.sleep(100);
log.info("threadNum === {}", threadNum);
Thread.sleep(100);
}
}
CyclicBarrier循环栅栏:起跑线。是每个线程相互等待,等到达到设定的计数器时候,所有的线程在执行
public class CyclicBarrierExample1 {
//5个线程相互等待,到五个线程准备好时候在执行
private static CyclicBarrier barrier = new CyclicBarrier(5);
public static void main(String[] args) throws Exception {
Map map;
HashMap hashMap = new HashMap();
hashMap.put("key", "value");
ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();
concurrentHashMap.put("hashmap", "hashmap");
concurrentHashMap.get("hashmap");
//这个参数就是核心线程池的数量
ExecutorService executorService = Executors.newFixedThreadPool(10);
ExecutorService exec = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int threadNum = i;
Thread.sleep(1000);
exec.execute(() -> {
try {
race(threadNum);
} catch (Exception e) {
e.printStackTrace();
log.info("exception {}", e);
}
});
}
}
public static void race(int num) throws Exception {
Thread.sleep(1000);
log.info(" {}is ready", num);
barrier.await();//等待达到五个线程,在执行
log.info("{} continue", num);
}
}
线程池
execute()方法和 submit()方法的区别
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否
submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功**,并且可以通过Future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)`方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
创建线程池
方式一:通过构造方法实现
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize : 核心线程数定义了最小可以同时运行的线程数量。
maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
workQueue : 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
keepAliveTime :当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime 才会被回收销毁;
unit : keepAliveTime 参数的时间单位。
threadFactory :executor 创建新线程的时候会用到。
handler :饱和策略
饱和策略:
ThreadPoolExecutor.AbortPolicy : 抛出 RejectedExecutionException 来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy : 调用执行自己的线程运行任务,也就是直接在调用 execute 方法的线程中运行( run )被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy : 不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy : 此策略将丢弃最早的未处理的任务请求。
/通过ThreadPoolExecutor构造函数自定义参数创建
ThreadPoolExecutor executor = new ThreadPoolExecutor(.....)
//执行Runnable
executor.execute(worker);
//终止线程池
executor.shutdown();
while (!executor.isTerminated()) {}
原理:
创建核心线程数-》加入队列-》最大线程数-》饱和策略
方式二:通过 Executor 框架的工具类 Executors 来实现
FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
Executors 返回线程池对象的弊端
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量线程,从而导致 OOM。
关键方法对比
1.shutdown()和shutdownNow()
shutdown() :关闭线程池,线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列
里的任务得执行完毕。
shutdownNow() :关闭线程池,线程的状态变为 STOP。线程池会终止当前正在运行的任务,并停
止处理排队的任务并返回正在等待执行的 List
2.isTerminated()和 isShutdown()
isShutDown 当调用 shutdown() 方法后返回为 true。
isTerminated 当调用 shutdown() 方法后,并且所有提交的任务完成后返回为 true
线程池大小确定
CPU 密集型任务(N+1): 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N): 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。如何判断是 CPU 密集任务还是 IO 密集任务?CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上
并发容器
ConcurrentHashMap:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
ReentrantReadWriteLock:所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。
读操作:volatile Object[]存储元素,直接进行访问即可。
写操作:ReentrantLock进行加锁。拷贝写入。
ConcurrentLinkedQueue:非阻塞队列(通过 CAS 操作实现)
BlockingQueue:阻塞队列(当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止)
1.ArrayBlockingQueue:有界队列实现类,底层采用数组来实现。并发控制采用可重入锁ReentrantLock
ArrayBlockingQueue是通过一个循环数组的方式来实现存储元素的,这里takeIndex记录了当前可以取元素的索引位置,而putIndex则记录了下一个元素可以存储的位置。当队列满了时,takeIndex和putIndex将指向同一个元素
public void put(E e) throws InterruptedException {
checkNotNull(e); // 检查元素是否为空,不为空则抛出异常
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 锁定当前操作
try {
while (count == items.length) // 在循环中检查,以防止当前线程是被意外唤醒的
notFull.await(); // 如果当前队列已满,则当前线程进入等待状态,并释放锁
enqueue(e); // 如果通过了while判断,说明队列不满,并且当前线程获取到了锁,则元素入队
} finally {
lock.unlock(); // 操作完成,释放当前的锁
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 锁定当前的线程
try {
while (count == 0) // 在循环中检查,以防止当前线程被意外唤醒
notEmpty.await(); // 如果当前队列为空,则当前线程进入等待状态,并释放锁
return dequeue(); // 如果通过了while判断,说明队列不为空,并且当前线程获取到了锁,则元素出队
} finally {
lock.unlock(); // 操作完成,释放当前锁
}
}
2.LinkedBlockingQueue:单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使
用,同样满足 FIFO 的特性。
LinkedBlockingQueue使用一个head指针指向了队列的头节点,使用一个last指针指向了队列的尾节点,使用capacity指定了队列最多能够存储的元素个数,使用count记录当前已经存储的元素个数
3.PriorityBlockingQueue:支持优先级的无界阻塞队列
ConcurrentSkipListMap:跳表,跳表的本质是同时维护了多个链表,并且链表是分层的。跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。
CompletableFuture
创建:
CompletableFuture<String> future = CompletableFuture.completedFuture("hello!");
static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier);
static CompletableFuture<Void> runAsync(Runnable runnable);
处理:
//沿用上一个任务的线程池
thenApply()
// 不需要从回调函数中获取返回结果
thenAccept()
thenRun()
whenComplete()
异常处理:
handle()