java并发编程之美
线程 VS 进程
线程
线程是进程的一个执行路径,是CPU分配的基本单位。
进程
进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
关系
线程是进程中的一个实体,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。
线程创建与运行
线程创建方式
- 实现 Runnable接口的run方法;
- 继承Thread类,重写run方法; 调用start方法后,线程进入就绪状态(已获取除CPU资源外的其他资源),获取到CPU资源后进入运行状态,run方法执行完毕后,线程处于终止状态。
- 使用FutureTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public static class CallerTask implements Callable<String> {
@Override
public String call() throws Exception {
return "ok";
}
}
public static void main(String[] args) throws InterruptedException {
// 创建异步任务
FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
// 启动线程
new Thread(futureTask).start();
// futureTask.run();
try{
// 等待任务执行完毕,并返回结果
String result = futureTask.get();
System.out.println(result);
} catch (ExecutionException e) {
e.printStackTrace();
}
}
线程通知与等待
wait函数
当一个线程调用一个共享变量的wait方法是,调用线程会被阻塞挂起,直到发生以下情况之一:
- 其他线程调用了该共享对象的notify或norifyAll方法;
- 其他线程调用了该线程的interrupt方法,该线程抛出InterruptedException异常。
使用方式
使用synchronized包裹共享变量,或使用synchronized修饰方法,否则会抛出IllegalMonitorStateException。调用wait方法后,会释放共享变量的监视器锁。
synchronized( 共享变量){
// code block...
}
synchronized void methodName(){
// code block...
}
虚假唤醒
虚假唤醒指线程未调用共享变量的notify、notifyAll方法,也没调用线程的interrupt方法就由挂起状态变为可运行状态。 防范方式为把wait方法放到循环体中,循环条件为不满足唤醒条件。
synchronized (共享变量){
while (不满足唤醒的条件){
共享变量.wait();
}
}
sleep、yield
sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定时间,这期间线程调度器不会去调度该线程,且不会释放监视器锁。 而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。
线程中断
Java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。
void interrupt方法
中断线程。设置被中断线程的中断位并立即返回。被中断线程实际并未中断。如果被中断线程因为调用了wait、join、sleep等方法被阻塞挂起,这时调用被中断线程的interrupt方法,被中断线程会在阻塞处抛出InterruptedException异常。
boolean isInterrupted方法
检查被执行的该方法的线程是否被中断。
boolean interrupted方法
返回当前线程是否被中断,并重置中断标志为false。
线程死锁
死锁概念
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直互相等待而无法继续运行下去。
死锁条件
- 互斥条件:线程对已经获取到的资源进行排他性使用,即该资源同时只能由一个线程占用。如果此时还有其他资源请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
- 请求并持有条件:一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
- 不可剥夺条件:线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
- 环路等待条件:在发生死锁时,必然存在一个线程-资源的环形链,即线程集合{T0,T1,T2,……,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。
避免方法
只有请求并持有和环路等待条件是可以被破坏的。造成死锁的原因和申请资源的顺序有很大的关系,使用资源申请的有序性原则就可以避免死锁。即不同线程申请资源的顺序保持一致。
守护线程和用户线程
Java中的线程分为两类,分别是daemon线程和user线程。
区别
当最后一个非守护线程结束时,JVM会正常退出,不管当前是否有守护线程。
守护线程设置方式
只需要设置线程的daemon参数为true。
public static void main(String[] args) {
Thread daemonThread = new Thread(()->{});
// 设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
ThreadLocal
synchronized、volatile
synchronized和volatile都可以解决内存可见性的问题。 相同点: 被这两个关键字修饰的内容,在使用时都不会使用工作内存(CPU的一级二级缓存等)直接从主内存中读写相关内容。 不同点: synchronized使用的是独占锁,同一时刻只有一个线程能调用synchronized修饰的代码。 volatile使用的是非阻塞算法,不会造成线程上下文切换的开销。volatile也可以避免指令重排序导致的顺序问题。
伪共享
多线程操作的多个变量存放在同一个缓存行中,会出现伪共享,降低性能。 可在变量或类上添加@sun.misc.Contended注解解决伪共享问题。 Contentded注解默认只能用于Java核心类。用户代码需要使用这个注解时,需要添加-XX:-RestrictContended 参数。填充宽度参数使用-XX:ContendedPaddingWidth参数设置,默认填充宽度为128。
乐观锁、悲观锁
悲观锁:假定数据随时都会被其他线程修改,所以从一开始就将相关数据加排他锁,直到处理完毕再释放锁。 乐观锁:假定数据基本不会被其他线程修改,修改数据时才对数据冲突进行检测。可以使用version或其他业务字段来实现。
公平锁、非公平锁
公平锁:线程获取锁的顺序是按照线程请求锁的时间早晚来决定。
// 公平锁
ReentrantLock pairLock = new ReentrantLock(true);
// 非公平锁
ReentrantLock pairLock = new ReentrantLock(false);
独占锁、共享锁
独占锁:只能被一个线程持有,属于悲观锁,如ReentrantLock; 共享锁:可被多个线程持有,属于乐观锁,如ReadWriteLock。
可重入锁
线程已持有锁,该线程再次尝试获取该锁,如果不被阻塞,则为可重入锁。 锁内部维护一个线程标识,用以表示锁被哪个线程占有;再维护一个初始值为0的计数器。 当线程获取到锁后,计数器+1,其他线程获取锁时,若 尝试获取锁的线程 不是 锁的所有者 ,则被阻塞挂起。 若尝试获取锁的线程是 锁的所有者,则计数器+1,释放锁时计数器-1。 当计数器为0时,被阻塞的线程将被唤醒竞争该锁。
自旋锁
线程尝试获取锁时,若发现锁已被其他线程占有,不放弃CPU使用权(不挂起),且反复尝试获取锁,尝试次数由-XX:PreBlockSpinsh参数决定,默认为10,若超出尝试阈值后仍无法获取,才将当前线程阻塞挂起。 自旋锁使用CPU时间换取线程阻塞与调度的开销;但若未获取到锁并阻塞挂起,会浪费CPU时间。
并发相关类
ThreadLocalRandom
LongAdder
LongAccumulator
LockSupport
park 方法
park方法可以挂起当前线程,直到其他线程调用unpark方法。 其他线程调用当前线程的interrupt方法或被虚假唤醒也会返回,所以调用park方法时也需要循环判断唤醒条件。 因park方法被阻塞的线程,在被其他线程中断返回时,不会抛出InterruptedException异常。 建议调用park方法时,传入blocker参数,参数值为this,在使用jstack pid查看线程堆栈时,可以查看到有关阻塞对象的更多信息。
unpark 方法
如果线程没有持有与Lock Support关联的许可证,调用unpark时,线程会持有许可证。 如果一个线程调用了park方法,再调用unpark后线程会被唤醒; 如果一个线程没有调用park方法,直接调用了unpark方法,再调用park方法会立刻返回。
parkNanos方法
如果调用线程已经持有许可证,则马上返回。否则挂起参数指定时间后自动返回。