Skip to content

java并发编程之美

线程 VS 进程

线程

线程是进程的一个执行路径,是CPU分配的基本单位。

进程

进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。

关系

线程是进程中的一个实体,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

线程创建与运行

线程创建方式

  1. 实现 Runnable接口的run方法;
  2. 继承Thread类,重写run方法; 调用start方法后,线程进入就绪状态(已获取除CPU资源外的其他资源),获取到CPU资源后进入运行状态,run方法执行完毕后,线程处于终止状态。
  3. 使用FutureTask;
Java
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方法是,调用线程会被阻塞挂起,直到发生以下情况之一:

  1. 其他线程调用了该共享对象的notify或norifyAll方法;
  2. 其他线程调用了该线程的interrupt方法,该线程抛出InterruptedException异常。

使用方式

使用synchronized包裹共享变量,或使用synchronized修饰方法,否则会抛出IllegalMonitorStateException。调用wait方法后,会释放共享变量的监视器锁。

Java
synchronized( 共享变量){
    // code block...
 
}
 
 
synchronized void methodName(){
    // code block...
 
}

虚假唤醒

虚假唤醒指线程未调用共享变量的notify、notifyAll方法,也没调用线程的interrupt方法就由挂起状态变为可运行状态。 防范方式为把wait方法放到循环体中,循环条件为不满足唤醒条件。

Java
synchronized (共享变量){
    while (不满足唤醒的条件){
        共享变量.wait();
 
    }
 
}

sleep、yield

sleep与yield方法的区别在于,当线程调用sleep方法时调用线程会被阻塞挂起指定时间,这期间线程调度器不会去调度该线程,且不会释放监视器锁。 而调用yield方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。

线程中断

Java中线程中断是一种线程间的协作模式,通过设置线程的中断标志并不能直接终止该线程的执行,而是被中断的线程根据中断状态自行处理。

void interrupt方法

中断线程。设置被中断线程的中断位并立即返回。被中断线程实际并未中断。如果被中断线程因为调用了wait、join、sleep等方法被阻塞挂起,这时调用被中断线程的interrupt方法,被中断线程会在阻塞处抛出InterruptedException异常。

boolean isInterrupted方法

检查被执行的该方法的线程是否被中断。

boolean interrupted方法

返回当前线程是否被中断,并重置中断标志为false。

线程死锁

死锁概念

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直互相等待而无法继续运行下去。

死锁条件

  1. 互斥条件:线程对已经获取到的资源进行排他性使用,即该资源同时只能由一个线程占用。如果此时还有其他资源请求获取该资源,则请求者只能等待,直至占有资源的线程释放该资源。
  2. 请求并持有条件:一个线程已经持有了至少一个资源,但又提出了新的资源请求,而新资源已被其他线程占有,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  3. 不可剥夺条件:线程获取到的资源在自己使用完之前不能被其他线程抢占,只有在自己使用完毕后才由自己释放该资源。
  4. 环路等待条件:在发生死锁时,必然存在一个线程-资源的环形链,即线程集合{T0,T1,T2,……,Tn}中的T0正在等待一个T1占用的资源,T1正在等待T2占用的资源,……Tn正在等待已被T0占用的资源。

避免方法

只有请求并持有和环路等待条件是可以被破坏的。造成死锁的原因和申请资源的顺序有很大的关系,使用资源申请的有序性原则就可以避免死锁。即不同线程申请资源的顺序保持一致。

守护线程和用户线程

Java中的线程分为两类,分别是daemon线程和user线程。

区别

当最后一个非守护线程结束时,JVM会正常退出,不管当前是否有守护线程。

守护线程设置方式

只需要设置线程的daemon参数为true。

Java
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或其他业务字段来实现。

公平锁、非公平锁

公平锁:线程获取锁的顺序是按照线程请求锁的时间早晚来决定。

Java
// 公平锁
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方法

如果调用线程已经持有许可证,则马上返回。否则挂起参数指定时间后自动返回。