多线程
线程的基础知识
线程与进程的区别
程序由指令和数据组成,但指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令允许过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理IO的
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程
- 进程是正在运行的程序的实例。进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行与并发的区别
现在都是多核CPU,在多核CPU下
- 并发(concurrent)是同一时间应对(dealing with)多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行(parallel)是同一时间动手做(doing)多件事情的能力,4 核CPU同时执行 4 个线程
举例
- 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这多件事,这时就是并发
- 家庭主妇雇了个保姆,她们一起这些事,这时既有并发,也有并行(这时会产生竞争,例如锅只有一口,一个人用锅时,另一个人就得等待)
- 雇了3个保姆,一个专做饭、一个专打扫卫生、一个专喂奶,互不干扰,这时是并行
单核CPU
- 单核CPU下程序实力还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将CPU的时间片(windows下时间片最小约为15毫秒)分给不同的程序使用,只是由于CPU在线程间(时间片很短)的切换非常快,人类感觉是同时执行的
- 总结为一句话就是:微观串行,宏观并行
- 一般会将这种线程轮流使用CPU的做法成为并发
多核CPU
- 每个核(core)都可以调度运行线程,这时候线程可以是并行的
线程创建的方式有哪些
- 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("MyThread...run...");
}
public static void main(String[] args) {
// 创建MyThread对象
MyThread t1 = new MyThread() ;
MyThread t2 = new MyThread() ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
- 实现Runnable接口
public class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建MyRunnable对象
MyRunnable mr = new MyRunnable() ;
// 创建Thread对象
Thread t1 = new Thread(mr) ;
Thread t2 = new Thread(mr) ;
// 调用start方法启动线程
t1.start();
t2.start();
}
}
- 实现Callable接口
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
System.out.println("MyCallable...call...");
return "OK";
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建MyCallable对象
MyCallable mc = new MyCallable() ;
// 创建F
FutureTask<String> ft = new FutureTask<String>(mc) ;
// 创建Thread对象
Thread t1 = new Thread(ft) ;
Thread t2 = new Thread(ft) ;
// 调用start方法启动线程
t1.start();
// 调用ft的get方法获取执行结果
String result = ft.get();
// 输出
System.out.println(result);
}
}
- 线程池创建线程(项目中使用方式)
public class MyExecutors implements Runnable{
@Override
public void run() {
System.out.println("MyRunnable...run...");
}
public static void main(String[] args) {
// 创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(3);
threadPool.submit(new MyExecutors()) ;
// 关闭线程池
threadPool.shutdown();
}
}
Runnable和Callable的区别
Runnable接口的run方法是没有返回值Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来捕获异步执行的结果Callable接口的call方法允许抛出异常;而Runnable接口的run方法只能在内部消化,不能继续上抛
线程的 run() 和 start() 的区别
- start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
- run():封装了要被线程执行的代码,可以被调用多次
线程包括那些状态,状态之间是如何变换的
- 新建(NEW)
- 可执行(RUNNABLE)
- 阻塞(BLOCKED)
- 等待(WAITING)
- 时间等待(TIMED_WAITING)
- 死亡(TREMINATED)
public enum State {
/**
* 尚未启动的线程的线程状态
/
NEW,
/**
* 可运行线程的线程状态。处于可运行状态的线程正在 Java 虚拟机中执行,但它可能正在等待来自
* 操作系统的其他资源,例如处理器。
/
RUNNABLE,
/**
* 线程阻塞等待监视器锁的线程状态。处于阻塞状态的线程正在等待监视器锁进入同步块/方法或在调
* 用Object.wait后重新进入同步块/方法。
/
BLOCKED,
/**
* 等待线程的线程状态。由于调用以下方法之一,线程处于等待状态:
* Object.wait没有超时
* 没有超时的Thread.join
* LockSupport.park
* 处于等待状态的线程正在等待另一个线程执行特定操作。
* 例如,一个对对象调用Object.wait()的线程正在等待另一个线程对该对象调用Object.notify()
* 或Object.notifyAll() 。已调用Thread.join()的线程正在等待指定线程终止。
/
WAITING,
/**
* 具有指定等待时间的等待线程的线程状态。由于以指定的正等待时间调用以下方法之一,线程处于定 * 时等待状态:
* Thread.sleep
* Object.wait超时
* Thread.join超时
* LockSupport.parkNanos
* LockSupport.parkUntil
* </ul>
/
TIMED_WAITING,
/**
* 已终止线程的线程状态。线程已完成执行
*/
TERMINATED;
}
线程状态之间变换
- 创建线程对象是新建状态
- 调用了 start() 方法转变为可执行状态
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换到其他状态
- 如果没有获取锁(synchronized 或 lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了 wait() 方法进入等待状态,其他线程调用 notify() 唤醒后可切换为可执行状态
- 如果线程调用了 sleep(50) 方法,进入计时等待状态,到时间后可切换为可执行状态
线程怎么按顺序执行
可以使用线程中的 join() 方法,等待线程执行结束
public class JoinTest {
public static void main(String[] args) {
// 创建线程对象
Thread t1 = new Thread(() -> {
System.out.println("t1");
}) ;
Thread t2 = new Thread(() -> {
try {
t1.join(); // 加入线程t1,只有t1线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}) ;
Thread t3 = new Thread(() -> {
try {
t2.join(); // 加入线程t2,只有t2线程执行完毕以后,再次执行该线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}) ;
// 启动线程
t1.start();
t2.start();
t3.start();
}
}
notify() 和notifyAll()的区别
notifyAll()唤醒所有 wait 线程notify()随机唤醒一个 wait 线程
wait和sleep的区别
- 共同点
- wait(),wait(long) 和 sleep(long) 的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
- 不同点
- 方法归属不同
- sleep(long) 是 Thread 的静态方法
- 而 wait(),wait(long) 都是Object的成员方法,每个对象都有
- 醒来的时机不同
- 执行 sleep(long) 和 wait(long) 的线程都会在等待相应毫秒后醒来
- wait(long) 和 wait() 还可以被 notify 唤醒,wait() 如果不唤醒就一直等戏曲
- 它们都可以被打断唤醒
- 锁特性不同(重点)
- wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
- wait 方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃CPU,但你们还可以用)
- sleep 如果在 synchronized 代码块中执行,并不会释放对象锁(我放弃CPU,你们也用不了)
- 方法归属不同
停止一个正在运行的线程
- 使用退出标志,使线程正常退出,也就是当 run 方法完成后线程终止
- 使用 stop 方法强行终止(不推荐,方法已作废)
- 使用 interrupt 方法
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出
InterruptedException异常 - 打断正常的线程,可以根据打断状态来标记是否退出线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出
线程中并发安全
Synchronized关键字的底层原理
- Synchronized【对象锁】采用互斥的方法让同一时刻最多只有一个线程能持有【对象锁】
- 它的底层是有
monitor实现的,monitor是 JVM 级别的对象(c++)实现,线程获得锁需要使用对象(锁)关联monitor - 在
monitor内部由三个属性,分别是owner、entrylist、waitset - 其中
owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程
Monitor实现的锁属于重量级锁,锁升级
synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁之被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况
- 重量级锁:
- 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 轻量级锁:
- 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级锁修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性。
- 偏向锁:
- 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。
一旦锁发生了竞争,都会升级为重量级锁
JMM(Java内存模型)
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而确保指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享内存(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
CAS
- CAS的全程是:
Compart Anderson Swap(比较再交换);它体现的使用乐观锁的思想,在无所状态下保证线程操作数据的原子性 - CAS使用到的地方很多;
AQS框架、AtomicXXX类 - 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用
Unsafe类中的方法,都是操作系统提供的,其它语言实现
AQS
全称是AbstractQueuedSunchronizer,即抽象队列同步器,它是构建锁或者其他同步组件的基础框架
| synchronized | AQS |
|---|---|
| 关键字,c++语言实现 | java语言实现 |
| 悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
| 锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
- AQS是多线程中的队列同步器,是一种锁机制,它是作为一个基础框架使用的,像
RentrantLock、Semaphore都是基于AQS实现的 - AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部华友一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改成功了state为1,则当前线程就相当于获取了资源
- 在state修改的时候使用的CAS操作,保证多个线程修改的情况下原子性
ReentrantLock 的实现原理
ReentrantLock翻译过来就是可重入锁
ReentrantLock标识支持重新进入的锁,调用lock方法获取锁之后,在次调用lock,是不会再阻塞ReentrantLock主要了利用CAS+AQS队列来实现- 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁
synchronized 和 Lock 的区别
- 语法层面
synchronized是关键字,源码在JVM中,用c++语言实现Lock是接口,源码有JDK提供,由java语言实现- 使用
synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unLock方法释放锁
- 功能层面
- 二者都属于悲观锁、都具有基本的互斥、同步、锁重入功能
Lock提供了需要synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWriteLock(读写锁)
- 性能层面
- 在没有竞争时,
synchronized做了很多优化,如偏向锁、轻量级锁,性能不赖 - 在竞争激励时,
Lock的实现通常会提供更好的性能
- 在没有竞争时,
死锁产生的条件
死锁产生的条件
- 一个线程需要同时获取多个锁,这就容易发生死锁
当程序发现死锁现象,我们可以使用JDK自带的工具:jps 和 jstack
诊断死锁
JConsole:用于输出JVM中运行的进程状态信息。jstack:可以查看Java进程中线程的堆栈信息、日志等,检查是否有死锁现象。如果有死锁,需要通过分析具体的代码来确定原因并进行修复。- 可视化工具:如
jconsole和VisualVM也可以用来检查死锁问题。
如何进行死锁诊断
volatile
-
保证线程间的可见性:
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见;
-
禁止进行指令重排序:
指令重排:用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果。
ConcurrentHashMap底层原理
- 底层数据结构
- JDK1.7底层采用分段的数组+链表
- JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树
- 加锁的方式
- JDK1.7采用
Segment分段锁,底层使用的是ReentrantLock - JDK1.8采用
CAS添加新节点,采用synchronized锁定链表或红黑树的首届点,相对分段锁颗粒度更细,性能更好
- JDK1.7采用
导致并发程序出现问题的根本原因
- 原子性:synchronized、lock
- 内存可见性:volatile、synchronized、lock
- 有序性:volatile
线程池
线程池的核心参数
corePoolSize核心线程数目:这是线程池中的核心线程数量,它们在任务到来时会被创建且不会被销毁(除非设置了允许回收)。maximumPoolSize最大线程数目 = (核心线程 + 救急线程的最大数目):这是线程池能够容纳的最大线程数,包括核心线程和救急线程。keepAliveTime生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放:当线程池中的线程超过corePoolSize时,多余的线程会在keepAliveTime时间段内保持活动状态;如果没有新的任务分配给它,它将被终止以节省资源。unit时间单位 - 救急线程的生存时间单位,如秒、毫秒等:这个参数指定了keepAliveTime的单位,可以是秒、毫秒或其他时间单位。workQueue当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务:这是一个阻塞队列,用于存放等待被执行的任务。当所有核心线程都在忙于处理其他任务时,新来的任务就会被放入 workQueue 中等待。threadFactory线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等:这个接口可以用来自定义线程的创建过程,比如为每个线程设置名称或标记其为守护线程。handler拒绝策略 - 当所有线程都在繁忙,workQueue也放满时,会触发拒绝策略:当线程池已经达到最大容量,而workQueue也已满时,无法再接受新的任务,这时就会调用 handler 实现的拒绝策略来决定如何处理这种情况。常见的拒绝策略包括丢弃任务、抛出异常等。
线程池中的阻塞队列
ArrayBlockingQueue:基于数组结构的有界阻塞队列,先进先出LinkedBlockingQueue:基于链表结构的有界阻塞队列,先进先出DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
LinkedBlockingQueue | ArrayBlockingQueue |
|---|---|
| 默认无界,支持有界 | 强制有界 |
| 底层是链表 | 底层是数组 |
| 是懒惰的,创建节点的时候添加数据 | 提前初始化Node数组 |
| 入队会生成新Node | Node需要是提前创建好的 |
| 两把锁(头尾) | 一把锁 |
如何确定核心线程数
-
IO密集型任务(核心线程数大小设置为2N+1)
文件读写、DB读写、网络请求等
-
CPU密集型任务(核心线程数大小设置为N+1)
计算机代码、Bitmap 转换、Json转换等
线程池的种类有哪些
newFixedThreadPool:创建一个定长线程池,可以控制线程最大并发数,超出的线程会在队列中等待newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按指定顺序(先进先出)执行newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程newScheduledThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
为什么不建议用Executors创建线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
FixedThreadPool和SingleThreadPool:- 允许的请求队列长度为
Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)。
- 允许的请求队列长度为
CachedThreadPool:- 允许的创建线程数量为
Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM(内存溢出)。
- 允许的创建线程数量为
使用场合
线程池的使用场景
- 批量导入/同步:使用线程池+
CountDownLatch批量把数据库中的数据导入到了ES/Redis中,避免了OOM(内存溢出)- 同步订单数据
- 数据汇总:调用多个接口来汇总数据,如果接口之间没有依赖关系,就可以使用线程池+
furure来提升性能- 门户主页需要大量汇总数据,可以使用线程池+
furure提升性能
- 门户主页需要大量汇总数据,可以使用线程池+
- 异步线程(线程池):为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间
- 保存操作日志
- 保存数据日志
如何控制某个方法允许并发访问线程的数量
在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量
- 创建
Semaphore对象,可以给一个容量 acquire()方法可以请求一个信号量,这时候的信号量个数 -1release()释放一个信号量,此时信号量个数 +1
ThreadLocal底层原理
ThreadLocal可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】,避免争用引起的线程安全问题ThreadLocal同时实现了线程内的资源共享- 每个线程内有一个
ThreadLocalMap类型的成员变量,用来存储资源对象- 调用 set 方法,就是
ThreadLoacl自己作为 key,资源对象作为 value,放入当前线程的ThreadLocalMap集合中 - 调用 get 方法,就是以
ThreadLoacl自己作为 key,到当前线程中查找关联的资源值 - 调用 remove 方法,就是以
ThreadLocal自己作为 key,移除当前线程关联的资源值
- 调用 set 方法,就是
ThreadLocal内存泄漏问题ThreadLocalMap中的 key 是弱引用,值为强引用,key 会被 GC 释放内存,关联 value 的内存并不会释放,建议制动 remove 释放 key,value

3645

被折叠的 条评论
为什么被折叠?



