美文网首页
高并发多线程总结

高并发多线程总结

作者: 爱看书的独角兽 | 来源:发表于2022-06-17 11:49 被阅读0次
    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<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的底层原理,这块内容有些枯燥,光讲解的话有点难理解,如果大家有兴趣,欢迎留言评论区!

    相关文章

      网友评论

          本文标题:高并发多线程总结

          本文链接:https://www.haomeiwen.com/subject/ipuqvrtx.html