高并发多线程总结
1.多线程基本概念
首先,我们要理解多线程编程,必须清楚几个基本概念:
进程——进程是操作系统层面的概念,它是操作系统进行资源分配和调度的基本单位
线程——线程是进程内部的程序流,每个进程内部都会有多个线程,所有线程共享进程的内部资源,所以,一个进程可以有多个线程,多个线程采用时间片轮转的方式并发执行,
并发——所谓并发,就是指宏观上并行微观上串行机制,一个CPU执行多个任务
并行——多个CPU执行多个任务
2.线程实现方式
//继承Thread类
public class Multithreading extends Thread{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
//实现Runnable接口
public class Multithreading implements Runnable{
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
//使用Future类
public class Multithreading{
public static void main(String[] args) {
new CompletableFuture<String>().thenRunAsync(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}).join();
}
}
三种方法看情况使用,这里推荐使用CompletableFuture类,内置了很多使用方法,基本可以满足多线程要求,有关线程池部分,后面会讲到!
3.锁机制
3.1并发三大特性——原子性、有序性、可见性
可见性——一个线程修改了共享变量的值,另一个线程可以立刻感知到,这是由CPU高速缓存造成的,高速缓存与内存数据不一致
原子性——一个操作或多次操作,要么全部执行、要么全部不执行
有序性——正常编译代码是按顺序执行的,不过有时候,在代码顺序对程序结果无影响时,会对代码进行重排序执行
3.2解决方式
volatile——解决可见性、有序性
那么volatile是如何保证可见性和有序性的呢?
这就涉及volatile的原理:
第一,基于happens-before规则保证有序性,凡是volatile修饰的变量的写操作都是在多操作之前,第二,设置内存屏障,禁止指令重排保证有序性;
基于lock前缀指令和MESI缓存一致性协议保证可见性(不用深究,操作系统的指令)
synhonzied,加锁Lock——解决原子性
核心思想:对需要修改的共享资源进行上锁,让所有线程进行串行化修改变量
Synchronzied的底层原理:对所有关键字修饰的代码段(或者说线程),都会封装成ObjectMoniter对象进行处理;在jvm中,每一个对象都会相应的对象头信息,这其中就包括了指向monior的指针,线程会通过这个monitor指针去对象对应的monitor对象上进行加锁,这是该对象就是加锁状态;
ObjectMonitor对象四大核心组件:
EntryList——想获取该对象的阻塞线程、
waitSet——进入等待资源的线程、
owner——当前加锁线程、
count——0 当前对象无人加锁 1 当前对象已经加锁
ReentrantLock锁
与Synchronzied相比,Synchronzied属于重量级锁,而ReentrantLock是通过对象加锁,二者都是可重入锁,但是ReentrantLock需要认为加锁
底层原理:AQS(抽象队列同步器)+CAS(CompareAndSet)
AQS组件介绍:
state:与count类似,加锁标记 0 无锁状态 1 有锁状态
任务队列:没有获取锁的阻塞线程队列——双向链表
当前加锁线程:记录当前加锁的线程
实现思路:首先通过CAS判断state是否上锁,只有一个线程能够上锁成功,其他线程则进入任务队列,需要注意的是,因为ReenrantLock四可重入锁,所以state可以无线增大,所以只有一层一层解锁直至state=0时才会解锁
3.3锁的级别
偏向锁
轻量级锁
重量级锁
自旋锁
4.线程池
概念:使用缓存的思想将线程放入线程池中,避免重复创建线程减少系统资源消耗,使用线程池也可以更加方便的管理线程
实现方式:
![PXGNUG5WSQ`ZZ5NZ{O~3DI.png
核心参数介绍:
corePoolSize——核心线程数量
maximumPoolSize——最大线程数量
workQueue——工作队列
handle——处理策略
keepAliveTime——非核心线程的存活时间
实现思想:当线程进入线程池,首先判断核心线程数是否空余,如果核心线程池已满,则进入工作队列。当工作队列也填满时,这时如果最大线程数未满,则创建新线程来执行任务,如果已经满了,则执行饱和策略(常见的策略有中断抛出异常、丢弃任务、丢弃队列中存在时间最久的任务、让提交任务的线程去执行任务)
线程池实现复用的原理
线程池中执行的是一个一个的队列,
核心逻辑是ThreadPoolExecutor类中的execute方法,其本身维护了一个HashSet<Worker> workers;Worker对象实现了Runnable,本质上也是任务,核心在run方法里面,话不多说,上代码~
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
// 该worker正在运行的线程
final Thread thread;
// 将要运行的初始任务
Runnable firstTask;
// 每个线程的任务计数器
volatile long completedTasks;
// 构造方法
Worker(Runnable firstTask) {
setState(-1); // 调用runWorker()前禁止中断
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); // 通过ThreadFactory创建一个线程
}
// 实现了Runnable接口的run方法
public void run() {
runWorker(this);
}
... // 此处省略了其他方法
private boolean addWorker(Runnable firstTask, boolean core) {
retry: // 循环退出标志位
for (;;) { // 无限循环
int c = ctl.get();
int rs = runStateOf(c); // 线程池状态
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()) // 换成更直观的条件语句
// (rs != SHUTDOWN || firstTask != null || workQueue.isEmpty())
)
// 返回false的条件就可以分解为:
//(1)线程池状态为STOP,TIDYING,TERMINATED
//(2)线程池状态为SHUTDOWN,且要执行的任务不为空
//(3)线程池状态为SHUTDOWN,且任务队列为空
return false;
// cas自旋增加线程个数
for (;;) {
int wc = workerCountOf(c); // 当前工作线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize)) // 工作线程数>=线程池容量 || 工作线程数>=(核心线程数||最大线程数)
return false;
if (compareAndIncrementWorkerCount(c)) // 执行cas操作,添加线程个数
break retry; // 添加成功,退出外层循环
// 通过cas添加失败
c = ctl.get();
// 线程池状态是否变化,变化则跳到外层循环重试重新获取线程池状态,否者内层循环重新cas
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
// 简单总结上面的CAS过程:
//(1)内层循环作用是使用cas增加线程个数,如果线程个数超限则返回false,否者进行cas
//(2)cas成功则退出双循环,否者cas失败了,要看当前线程池的状态是否变化了
//(3)如果变了,则重新进入外层循环重新获取线程池状态,否者重新进入内层循环继续进行cas
// 走到这里说明cas成功,线程数+1,但并未被执行
boolean workerStarted = false; // 工作线程调用start()方法标志
boolean workerAdded = false; // 工作线程被添加标志
Worker w = null;
try {
w = new Worker(firstTask); // 创建工作线程实例
final Thread t = w.thread; // 获取工作线程持有的线程实例
if (t != null) {
final ReentrantLock mainLock = this.mainLock; // 使用全局可重入锁
mainLock.lock(); // 加锁,控制并发
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get()); // 获取当前线程池状态
// 线程池状态为RUNNING或者(线程池状态为SHUTDOWN并且没有新任务时)
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // 检查线程是否处于活跃状态
throw new IllegalThreadStateException();
workers.add(w); // 线程加入到存放工作线程的HashSet容器,workers全局唯一并被mainLock持有
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock(); // finally块中释放锁
}
if (workerAdded) { // 线程添加成功
t.start(); // 调用线程的start()方法
workerStarted = true;
}
}
} finally {
if (! workerStarted) // 如果线程启动失败,则执行addWorkerFailed方法
addWorkerFailed(w);
}
return workerStarted;
}
}
从上面可以看到,在addwork()中,进行了一系列校验,代码逻辑就是上文提到的实现思想。
5.ThreadLocal详解
1.理解:可以把ThreadLocal看做是存在于Thread类的一个属性字段,提供一个只有Thread才能访问的局部变量。
2.使用方式(见下方代码)
public class GCTest {
public ThreadLocal<Integer> intLocal = new ThreadLocal<Integer>();
public ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set(int i){
intLocal.set(i);
stringLocal.set(Thread.currentThread().getName());
}
public int getInt(){
return intLocal.get();
}
public String getString(){
return stringLocal.get();
}
public static void main(String[] args) {
GCTest gcTest = new GCTest();
gcTest.set(1);
System.out.println(gcTest.getString());
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
gcTest.set(1);
System.out.println(gcTest.getString()+":"+ gcTest.getInt()+i);
}
}
}).start();
//注意新线程的打印与第一个线程,看数据是否隔离
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
gcTest.set(1);
System.out.println(gcTest.getString()+":"+ gcTest.getInt()+i);
String name = Thread.currentThread().getName();
System.out.println(name);
}
}
}).start();
}
}
3.内存泄漏
首先解释一下什么是内存泄漏,在jvm中,存在四种对象引用方式——强引用、软引用、弱引用、虚引用
强引用:把对象赋给一个引用变量,只要对象不为null,在GC时就不会被回收
软引用:需要继承softReference,会在内存不足是进行回收
弱引用:需要继承WeakReference,只要发生GC就会被回收
虚引用:需要继承PhantomReference,监控通知时使用,跟踪对象垃圾回收的状态
简而言之,内存泄漏就是对象不被程序调用,但是GC又回收不了时产生的。
接下来,再说说ThreadLocal与内存泄漏的关系,我们知道ThreadLocal的设计里面,其实内部的一个Map对象的包装,key为ThreadLocal对象本身,value为存入的值,所以这就存在一个问题了,当ThreadLoacl对象被回收之后,但是线程还是存在一个弱引用通过ThreadLocalMap指向ThreeadLocal对象的,这时ThreadLocalMap的value一直无法回收。
解释:这种概率非常低,我们知道只要ThreadLocal没有被回收,那就没有内存泄漏的风险,在这,我们也知道其实ThreadLocalMap是依附在Thread上的,只要Thread销毁,那么ThreadLocaMap也会销毁,所以在非线程池的环境下,也不会有内存泄漏的风险,而且ThreadLocal本身也做了一些保护措施,在线程池的环境下,如果发现ThreadLocalMap的key为null时,则会将其清除。
综上:要存在长期内存泄漏,要满足三个条件——ThreadLocal被回收、线程被复用、线程复用后不再调用set/get/remove方法
面试总结系列第一面——请大家多多关照
有关AQS与ObjectMonitor的底层原理,这块内容有些枯燥,光讲解的话有点难理解,如果大家有兴趣,欢迎留言评论区!