线程方法比较
run与start
- start方法用于线程的启动;只能调用一次,底层由C实现。
- run方法用于执行线程的运行时代码(线程干的活);可重复调用。
wait,sleep与yield
- wait定义在Object中,用于线程间通信(使线程进入无限期等待),只能用于同步块/方法中;在让出CPU的同时,释放所占用的同步资源锁。
- sleep是Thread的静态方法, 可用于任何地方;只会让出CPU。
- yield是Thread的静态方法,也是让出CPU,但是不保证接下来谁能获取CPU的资源(调用yield方法的线程可能会继续获得)。
线程关键字
Synchronized
Synchronized 是Java内建机制,提供互斥语义与可见性,保证方法或代码块运行时,同时只有一个方法进入临界区,并且还能保证共享变量的内存可见性。
是非公平可重入锁。
Synchronized 与 Lock的区别
- JVM层面 - JDK层面;
- 等待锁的线程不能响应中断 - 可响应;
- Lock可以让线程尝试获取锁,无法获取时可立即返回(非阻塞);
- Lock可以实现公平锁;
- synchronized发生异常时,会自动释放线程占有的锁,不会出现死锁现象;Lock需要在finally语句块中进行锁的释放。
ReentrantLock类(再入锁)
- 提供了一个Condition类,用于实现分组唤醒需要唤醒的线程,而不是synchronized随机唤醒或全部唤醒;
- 只适用于代码块锁。
- 可重入锁(递归锁)。(线程可以进入任何一个已经拥有锁所同步的代码块)
voliate
功能:保证线程的可见性并且禁止指令的重排序,但是无法保证原子性;
底层采用内存屏障来实现的;
可见性: 多个线程同时访问同一个变量时,一个线程修改了变量值时,其它线程可以立即看到。
原子性: 一个或多个操作执行直到完成不会被中断。
有序性:程序执行的顺序按照代码先后顺序执行。
应用场景
单例模式在多线程下的安全性:
public class SingleMode {
// 需要禁止指令重排,防止指令在未初始化完成就执行后续操作,返回空指针
private static volatile SingleMode instance = null;
private SingleMode(){
System.out.println(Thread.currentThread().getName());
}
// 单线程下的单例模式
public static SingleMode getInstance(){
if( instance == null)
instance = new SingleMode();
return instance;
}
// 双端检锁机制(DCL)
// 进入同步代码块前后都进行判断
// 但是因为指令重排的机制,可能存在没有初始化完成的情况
public static SingleMode getInstanceMul(){
if(instance == null){
synchronized (SingleMode.class){
if(instance == null){
instance = new SingleMode();
}
}
}
return instance;
}
public static void main(String[] args) {
// 在单线程情况下是正确的,但是多线程会出现问题
// System.out.println(SingleMode.getInstance() == SingleMode.getInstance());
for(int i=0;i<10;i++){
new Thread(() ->{
SingleMode.getInstanceMul();
}, String.valueOf(i)).start();
}
}
}
happens-before原则
解决的问题:保证多线程处理时共享变量的可见性。
定义:对于两个操作A和B,这两个操作可以在不同线程中执行。如果A happens-before B,则可以保证A执行完后,执行结果对于B是可见的。
- 程序顺序规则:单线程时,操作顺序执行;
- 锁定规则:解锁之前的操作先行发生于同一个锁的加锁操作(保证同一时刻只能有一个线程执行锁中的操作)。
- volatile变量规则:写操作发生于读操作之前;
- 线程启动规则:start方法先行发生于其它方法;
- 线程中断规则:对线程的interrupt方法调用先行发生于中断检测之前;
- 线程终止规则:线程中所有操作发生于终止检测之前;
- 对象终结规则:初始化发生于finalize方法之前;
- 传递规则;
CAS
定义:比较并交换(Compare and Swap):判断内存某个位置的值是否为预期值,是则进行更新。过程是原子性的。
Unsafe类是CAS的核心类,存在于sun.misc包中,通过本地方法调用直接操作内存,实现原子操作。
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(5);
// Java 中的实现方法
System.out.println(atomicInteger.compareAndSet(5, 2021));
System.out.println(atomicInteger.get());
// 期望值与真实值不一致,因此修改失败
System.out.println(atomicInteger.compareAndSet(5, 1024));
System.out.println(atomicInteger.get());
atomicInteger.getAndIncrement();
}
CAS的应用
AtomicInteger 类中的 getAndIncrement方法,需要使用CAS来判断是否能进行更改(实现方式为利用unsafe类直接从内存中取值判断,如果一致则进行更改,否则一直循环/自旋锁)
自旋锁: 尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁。(减少线程切换消耗,但是消耗CPU资源)
CAS的缺点
- 如果CAS失败,会一直循环,给CPU带来很大的开销。
- 只能保证一个共享变量的原子性。(多个需要加锁)
- 会引出来ABA问题
因此,CAS适用于读多写少,并且冲突比较少的场景。
引申: ABA问题
理解:狸猫换太子: A(初始)B(更改)A(继续更改),第二个A与第一个A是不一样的。
因此,即使线程的CAS操作成功,但是不一定表示过程是没问题的。原因在于CAS只要求开始与结尾一致。(老王问题/ emm)
解决ABA问题的方式
增设版本号(类似时间戳),防止值相同引起的ABA问题。
public class ABADemo {
// 不加时间戳,无法防止ABA问题
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
// 增加时间戳(版本号),防止ABA问题
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(()->{
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
},"t1").start();
new Thread(()->{
// t1不一定先执行(代码重组)
try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }
// 此时t2会认为100没有被修改,因此会继续执行CAS操作,因此输出 true
System.out.println(atomicReference.compareAndSet(100, 2019));
},"t2").start();
new Thread(()->{
// 先睡眠一秒保证t4获取初始版本号
try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); }
atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println("当前版本号" + atomicStampedReference.getStamp()); // 2
atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
System.out.println("当前版本号" + atomicStampedReference.getStamp()); // 3
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp(); // 获取初始版本号 1
try { TimeUnit.SECONDS.sleep(3); }catch (InterruptedException e){ e.printStackTrace(); }
// 此时由于时间戳不一致,因此不会执行CAS操作,因此输出 false
System.out.println(atomicStampedReference.compareAndSet(100, 2021, stamp, stamp+1));
System.out.println("当前版本号" + atomicStampedReference.getStamp()); // 3
},"t4").start();
}
}
线程池
定义:基于池化思想管理线程的工具。通过维护多个线程,等待监督管理者分配可以并行执行的任务。
池化:为了最大化利益并最小化风险,将资源统一管理的思想。
阻塞队列
- ArrayBlockingQueue:基于数组,FIFO
- LinkedBlockingQueue:基于链表,FIFO
- SynchronousQueue:不存元素
用于保存已经提交但是尚未执行的任务。当阻塞队列满了但是线程池尚未达到最大值时,超过队列容量的后续的任务会立即被新建的线程处理。
类型
-
newFixedThreadPool
固定线程数量。 使用链表阻塞队列。 -
newSingleThreadExecutor
只有一个线程。使用链表阻塞队列。 -
newCachedThreadPool
无线程数量上限,适合小任务执行。使用同步阻塞队列。 -
newScheduledThreadPool
-
new WorkStealingPool(1.8)
-
默认实现中前两个请求队列所允许的长度为Integer.MAX_VALUE,3-4最大线程数所允许的长度为Integer.MAX_VALUE,因此会出现OOM问题。
生产中直接使用 TreadPoolExecutor创建。
优势
- 降低资源的消耗:重复利用已创建的线程;
- 提高响应速度:任务到达即可立即执行(存在空闲线程);
- 提高线程的可管理性:对线程进行统一的分配、调优与监控;
- 高拓展性;
ThreadPoolExecutor
参数介绍
- corePoolSize(int)
线程池常驻核心线程数 - maximumPoolSize(int)
线程池能容纳同时执行的最大线程数 - keepAliveTime(long)
多余的空闲线程的存活时间 - unit(TimeUnit)
keepAliveTime的单位(秒,分钟,小时) - workQueue(BlockingQueue)
任务队列,被提交但是尚未执行的任务 - threadFactory(ThreadFactory)
表示生成线程池中工作线程的线程工厂,用于创建线程 - handler(RejectedExecutionHandler)
拒绝策略,表示当队列满了并且工作线程大于等于最大线程数时启动饱和拒绝策略。
任务占用位置随着数量上升的去向
核心数 => 阻塞队列 => 非核心数 => 拒绝策略
当请求任务数量下降,出现有线程空闲时间超过规定的存活时间,并且当前线程数量超过核心数量时,该线程会被停掉,因此,当所有任务完成后,线程池中线程的数量会缩回到核心数量。
拒绝策略(RejectedExecutionHandler)
AbortPolicy(默认)
直接抛出 RejectedExecutionException 异常,并阻止系统正常运行。
CallerRunsPolicy
由调用线程池的线程来处理超出的任务。(父亲干不了,找爷爷帮忙)
DiscardOldestPolicy
丢弃阻塞队列中等待最久的任务,然后把当前任务加到队列中并再次尝试提交任务。
DiscardPolicy
直接丢弃任务,不作任何处理也不抛异常。如果允许任务丢失,这是最好的方案。
public static void main(String[] args) {
// 自定义线程池
ExecutorService threadPool = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
1L, // 非核心线程存活允许的空闲时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<Runnable>(3), // 阻塞队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.DiscardOldestPolicy() // 拒绝策略
);
try{
// 允许的最多任务数 线程池允许的最大线程数+阻塞队列容量
// 出现数量爆炸时使用拒绝策略
for(int i=0;i<10;i++){
threadPool.execute(() ->{
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
}catch (Exception e){
e.printStackTrace();
}finally {
threadPool.shutdown();
}
}
线程池最大线程数参数配置
CPU密集型
定义:任务线程需要大量计算,没有阻塞,CPU全速运行。因此,对于单核上没有加速一说。
一般设置为: CPU核心数+1
I/O密集型
定义:任务线程并不是一直都在执行。对于单核也能产生加速。(利用了被浪费的阻塞时间)
一般设置: CPU核心数/(1-阻塞系数)