高并发多线程总结

1655369765444.png

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().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 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 intLocal = new ThreadLocal();
    public ThreadLocal stringLocal = new ThreadLocal();

    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的底层原理,这块内容有些枯燥,光讲解的话有点难理解,如果大家有兴趣,欢迎留言评论区!

你可能感兴趣的:(高并发多线程总结)