juc 入门

发布时间 2023-11-27 16:10:24作者: chen1777

JUC并发快速入门

 

1. 线程池

1.1 概述

  • 什么是线程池?

    线程池和数据库连接池非常类似,可以统一管理和维护线程,减少没有必要的开销。

  • 为什么要使用线程池?

    因为频繁的开启线程或者停止线程,线程需要被cpu重新从就绪状态调度到运行状态,需要发送cpu的上下文切换,效率非常低。

    线程池是复用机制,提前创建好一些固定的线程数一直在运行状态,实现复用,从而可以减少就绪到运行状态的切换。

    image

  • 实际开发中在那些地方会用到线程池?

    在实际开发中,禁止手动新建线程(new Thread),而是必须使用线程池来维护和创建线程,限制最多创建多少个线程。

  • 线程池的作用?

    • 降低资源消耗。 通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的消耗。
    • 提高响应速度。 当任务到达时,无需等待线程创建就能立即执行。
    • 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
    • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行

1.2 线程池的创建

  • 可缓存线程池

    Executors.newCachedThreadPool()

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            final int finalI = i;
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "," + finalI);
            });
        }
    }
    
    /*
    pool-1-thread-2,1
    pool-1-thread-6,5
    pool-1-thread-3,2
    pool-1-thread-1,0
    pool-1-thread-8,7
    pool-1-thread-4,3
    pool-1-thread-10,9
    pool-1-thread-5,4
    pool-1-thread-9,8
    pool-1-thread-7,6
    */
    

    通过查阅源码发现,底层设置的最大线程数为无限,因此在实际开发中不会使用

    /**
     * Creates a thread pool that creates new threads as needed, but
     * will reuse previously constructed threads when they are
     * available.  These pools will typically improve the performance
     * of programs that execute many short-lived asynchronous tasks.
     * Calls to {@code execute} will reuse previously constructed
     * threads if available. If no existing thread is available, a new
     * thread will be created and added to the pool. Threads that have
     * not been used for sixty seconds are terminated and removed from
     * the cache. Thus, a pool that remains idle for long enough will
     * not consume any resources. Note that pools with similar
     * properties but different details (for example, timeout parameters)
     * may be created using {@link ThreadPoolExecutor} constructors.
     *
     * @return the newly created thread pool
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
  • 可定长度线程池

    Executors.newFixedThreadPool()

    public static void main(String[] args) {
        //最多创建3个线程
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            final int finalI = i;
            //复用这3个线程执行任务
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "," + finalI);
            });
        }
    }
    /*
    pool-1-thread-1,0
    pool-1-thread-2,1
    pool-1-thread-1,3
    pool-1-thread-3,2
    pool-1-thread-1,5
    pool-1-thread-1,7
    pool-1-thread-1,8
    pool-1-thread-2,4
    pool-1-thread-1,9
    pool-1-thread-3,6
    */
    

    在实际开发中可能会用到,因为最起码可以限制创建的线程数

  • 可定时线程池:支持定时以及周期性执行任务

    Executors.newScheduledThreadPool()

    public static void main(String[] args) {
        //设置最大线程数为1
        ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(1);
        //定时任务:2s后执行
        scheduledService.schedule(()->{
            System.out.println("哈哈");
        },2, TimeUnit.SECONDS);
    
        final SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        //“以固定的延时”执行定时周期任务:2s后执行第一次任务,之后每3s执行一次
        scheduledService.scheduleAtFixedRate(()->{
            System.out.println("时间:" + sf.format(new Date()) );
        },2,3,TimeUnit.SECONDS);
    
        //“以固定的频率”执行定时周期任务:2s后执行第一次任务,之后每3s执行一次
        scheduledService.scheduleWithFixedDelay(()->{
            System.out.println("时间:" + sf.format(new Date()) );
        },2,3,TimeUnit.SECONDS);
    }
    
  • 单列线程池

    Executors.newSingleThreadExecutor()

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        final int finalI = i;
        executorService.execute(() -> {
            System.out.println(Thread.currentThread().getName() + "," + finalI);
        });
    }
    

非常遗憾的是阿里巴巴开发手册是不推荐使用这四个JDK自带线程池的API,四个线程池创建底层基于ThreadPoolExecutor构造函数封装,而ThreadPoolExecutor构造函数传递的是无界队列缓存任务,可能会发生线程池溢出。

1.3 线程池底层复用的实现原理

本质思想:创建一个线程,不会立马停止或者销毁,而是一直实现复用

  1. 提前创建固定大小的线程一直保持在运行状态(通过死循环实现,可能很消耗CPU的资源)
  2. 当需要线程执行任务,将该任务提交缓存在并发队列中;如果缓存队列满了,则会执行拒绝策略
  3. 正在运行的线程从并发队列中获取任务执行从而实现多线程复用

image

1.4 纯手写线程池

使用队列存放线程任务:

public static void main(String[] args) {
    //队列:先进先出
    //无界队列:没有限制
    LinkedBlockingQueue<String> strings = new LinkedBlockingQueue<>();
    strings.add("zhangsan");
    //offer:将元素插入到末尾
    strings.offer("wangwu");
  	//poll:返回front元素并删除
    System.out.println(strings.poll());	//zhangsan
    System.out.println(strings.poll());	//lisi
    System.out.println(strings.poll());	//wangwu
    System.out.println(strings.poll());	//null
}

简单实现线程池:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * @author Mark
 * @version 1.0
 * @date 2022/11/8 13:14
 */

public class MarkExecutors {
    /**
     * 存放正在运行的工作线程
     */
    private List<WorkThread> workThreads;

    /**
     * 缓存线程任务:队列容器
     */
    private BlockingDeque<Runnable> runnableDeque;

    /**
     * 线程池运行状态
     */
    public boolean isRun = true;

    /**
     * 有参构造方法
     *
     * @param maxThreadCount 最大线程数
     * @param dequeSize      队列任务数大小
     */
    public MarkExecutors(int maxThreadCount, int dequeSize) {
        //提前创建固定大小的线程一直保持在运行状态
        workThreads = new ArrayList<>(maxThreadCount);
        //限制队列容量缓存
        runnableDeque = new LinkedBlockingDeque<Runnable>(dequeSize);
        for (int i = 0; i < maxThreadCount; i++) {
            //开启线程
            new WorkThread().start();
        }
    }

    /**
     * 线程
     */
    class WorkThread extends Thread {
        @Override
        public void run() {
            //如果运行状态为true或队列中还有任务,继续执行
            while (isRun || runnableDeque.size() > 0) {
                //取出任务
                Runnable runnable = runnableDeque.poll();
                if (runnable != null) {
                    runnable.run();
                }
            }
        }
    }

    /**
     * 传递执行任务
     *
     * @param command 执行任务
     * @return 传递是否成功
     */
    public boolean execute(Runnable command) {
        //将任务添加至队列
        return runnableDeque.offer(command);
    }
}

public class Test03 {
    public static void main(String[] args) {
        MarkExecutors markExecutors = new MarkExecutors(2, 20);
        for (int i = 0; i < 10; i++) {
            final int finalI = i;
            markExecutors.execute(() -> {
                System.out.println(Thread.currentThread().getName() +","+ finalI);
            });
        }
        markExecutors.isRun = false;
    }
}
/*
Thread-1,0
Thread-0,1
Thread-1,2
Thread-0,3
Thread-1,4
Thread-0,5
Thread-1,6
Thread-1,8
Thread-1,9
Thread-0,7
*/

1.5 ThreadPoolExcutor核心参数

  • corePoolSize:核心线程数量,一直保持运行的线程,即使它们处于空闲状态,除非设置了allowCoreThreadTimeOut
  • maximumPoolSize:池中允许的最大线程数
  • keepAliveTime:当线程数大于核心线程数量时,多余空闲线程最长存活时间。
  • unit:最长存活时间单位
  • workQueue:任务队列,用于保存执行的任务
  • threadFactory:线程池执行器创建新线程时要使用的工厂
  • handler:由于达到线程边界和队列容量而导致任务执行被阻止时要使用的处理程序

线程池创建的线程会一直在运行状态吗?

不会。例如核心线程数corePoolSize为2,最大线程数maximumPoolSize为5,我们可以通过配置超出核心线程数corePoolSize后创建的线程的存活时间;假设为60s,在60s内核心线程一直没有任务执行,则会停止该线程

1.6 线程池ThreadPoolExcutor底层实现原理

  • 当线程数小于核心线程数时,创建线程
  • 当线程数大于核心线程数,且任务队列未满时,将任务放入任务队列
  • 当线程数大于核心线程数,且任务队列已满
    • 若线程数小于最大线程数,创建线程
    • 若线程数等于最大线程数,抛出异常,拒绝任务
public class MarkExecutionHandler implements ThreadFactory, RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(r.getClass()+":自定义拒绝线程任务");
        r.run();
    }

    @Override
    public Thread newThread(Runnable r) {
        return null;
    }
}
public class MarkThreadPoolExecutor {
    public static ExecutorService newFixedThreadPool(int corePoolSize, int maximumPoolSize, int blockingQueue) {
        return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 60L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(blockingQueue), (RejectedExecutionHandler) new MarkExecutionHandler());
    }
}
public class Test04 {
    public static void main(String[] args) {
        ExecutorService executorService = MarkThreadPoolExecutor.newFixedThreadPool(2, 4, 5);
        for (int i = 1; i <= 10; i++) {
            final int finalI = i;
            executorService.execute(()->{
                System.out.println(Thread.currentThread().getName()+":"+ finalI);
            });
        }
    }
}
/*
pool-1-thread-1:1
pool-1-thread-2:2
pool-1-thread-1:3
pool-1-thread-3:4
pool-1-thread-2:5
pool-1-thread-4:6
pool-1-thread-3:7
pool-1-thread-3:8
pool-1-thread-4:9
class com.mark.myExecutor.Test04$$Lambda$1/1096979270:自定义拒绝线程任务
main:10
*/

/*
1.提交任务线程数<核心线程数 核心线程任务复用
2.提交的线程任务数>核心线程数,且队列容量未满,任务缓存到队列中
	循环3 4 5 6 7缓存到队列中
3.提交的线程任务数>核心线程数,且队列容量已满
	最多额外创建两个线程 执行8 9
  提交第十个任务:拒绝
*/

实际最多执行任务数核心线程数+缓存队列的容量+最大线程数-核心线程数

线程池队列满了,任务会丢失吗?

如果队列满了,且任务总数>最大线程数,则当前线程走拒绝策略

可以自定义拒绝异常策略,将该任务缓存到redis、本地文件或mysql,之后再实现补偿

线程池的拒绝策略:

  • AbortPolicy丟弃任务,抛运行时异常
  • CallerRunsPolicy执行任务
  • DiscardPolicy忽视,什么都不会发生
  • DiscardOldestPolicy从队列中踢出最先进入队列( 最后一个执行)的任务
  • 实现RejectedExecutionHandler接口,可自定义处理器

2. CAS

2.1 Java锁的分类

  • 悲观锁和乐观锁
  • 公平锁和非公平锁
  • 自旋锁和重入锁
  • 重量级锁和轻量级锁
  • 独占锁和共享锁

2.2 悲观锁与乐观锁

悲观锁:顾名思义,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

一般情况下不要使用悲观锁,因为使用悲观锁被阻塞后,被唤醒的成本非常高。

乐观锁:反之,总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。但是乐观锁比较消耗CPU的资源,所以需要控制乐观锁循环次数。

乐观锁实现方式

  • 基于CAS实现无锁机制
  • CAS(无锁)(juc并发包框架源自类)(结合自旋)底层基于修改内存值(v),E(旧的预期值),E == V 则表示修改

2.3 公平锁与非公平锁

公平锁:顾名思义,它是公平的,可以保证获取锁的线程按照先来后到的顺序,获取到锁。****

非公平锁:顾名思义,各个线程获取到锁的顺序,不一定和它们申请的先后顺序一致,有可能后来的线程,反而先获取到了锁。

在实现上,公平锁在进行lock时,首先会进行tryAcquire()操作。在tryAcquire()中,会判断等待队列中是否已经有别的线程在等待了。如果队列中已经有别的线程了,则tryAcquire失败,则将自己加入队列。如果队列中没有别的线程,则进行获取锁的操作。非公平锁,在进行lock时,会直接尝试进行加锁,如果成功,则获取到锁,如果失败,则进行和公平锁相同的动作。

Synchronized是非公平锁,而ReentramtLock底层基于aqs,new ReentramtLock(true)为公平锁,new ReentramtLock(false)为非公平锁。ReentramtLock()默认为非公平锁,空参构造函数相当于使用ReentramtLock(false)

从公平锁和非公平的实现上来看,他们的操作基本相同,唯一的区别在于,在lock时,非公平锁会直接先进行尝试加锁的操作。因此非公平锁的效率较高

2.4 自旋锁与重入锁

  • 什么是锁的可重入性?

    就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。(简单来说:A线程在某上下文中或得了某锁,当A线程想要再次获取该锁时,不会因为锁已经被自己占用,而需要先等到锁的释放)假使A线程即获得了锁,又在等待锁的释放,就会造成死锁。

    synchronizedreentrantlock都是可重入锁。synchronized:无需释放锁,synchronized会自动释放锁。reentrantlock:上几次锁,就需要手动释放几次。

  • CAS的原理

    CAS是compare and swap的缩写,译为比较并交换

    CAS是通过硬件指令,保证了原子性

    Unsafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native) 方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,Java中CAS操作的执行依赖于Unsafe类的方法。

    Java中的AtomicBooleanAtomicIntegerAtomicLong等使用CAS实现

    CAS有三个操作值:内存值V、预期值A与修改值B。只有当预期值A与内存值V相同时,才会将内存值替换成B。否则,会进入下一轮循环中。

    意思就是,假如有一个共享变量O=1,现在有两个线程A、B去获取修改O,线程A、B获取到O=1,A想要将O修改为2,B想要将O修改为3。A和B都会缓存O的副本,A跑得比较快,A拿自己的副本O(值为1)与真实的O(值为1)进行比较,相等,修改成功。然后线程B去将O修改为3,先比较值,B的副本O(值为1)与真实O(值为2)比较,不相等,修改失败。

    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(0);
        /**
         * V = 共享变量 = 0
         * E = 每个线程中都会缓存副本V的值
         *
         * 线程1  ---- E = 0  N = 1
         * 线程2  ---- E = 0  N = 2
         * 修改成功需要 V == E
         */
        boolean b1 = atomicInteger.compareAndSet(0, 1);
        boolean b2 = atomicInteger.compareAndSet(0, 1);
        boolean b3 = atomicInteger.compareAndSet(1, 2);
        System.out.println(b1 + " " + atomicInteger.get()); //true 1
        System.out.println(b2 + " " + atomicInteger.get()); //false 1
        System.out.println(b3 + " " + atomicInteger.get()); //true 2
    }
    
  • 什么是自旋锁,它的优缺点?

    自旋锁是一种基于CAS的锁,获取锁的线程不会被阻塞,而是循环的去获取锁。

    首先,我们要知道,线程在内核态与用户态之间切换是比较耗资源的。因此,尽可能要减少线程在阻塞、唤醒之间的切换

    如果线程A持有线程B需要的锁,线程B觉得我好不容易来一趟,又要让我进入阻塞等待,不知下次几时才能分配到 cpu 资源,我能不能再等等,说不定线程A很快就完事了

    cpu 说:可以,但有一个条件,我们不养闲线程,你必须做点事证明自己的用途。这样吧,你就来个自旋舞,给大伙助兴吧

    于是,线程B就一直在自旋,直到线程A释放资源,它马上拿到锁资源,开始干活

    • 优点

      自旋锁可以减少CPU上下文的切换,因为没有获取到锁的线程会一直在用户态,不会阻塞,没有锁的线程会一直通过循环控制重试;对于占用锁的时间非常短或锁竞争不激烈的代码块来说性能大幅度提升,因为自旋的CPU 耗时明显少于线程阻塞、挂起、再唤醒时两次CPU上下文切换所用的时间。

    • 缺点

      通过死循环控制,消耗CPU资源比较高,在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁资源,将引起CPU的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。

    public class CasCount extends Thread {
        /**
         * 原子计数器
         * AtomicInteger用于对整形数据进行原子操作,保证整形数据的加减操作线程安全。但是,它不能替代Integer类。
         */
        private AtomicInteger atomicInteger = new AtomicInteger(0);
    
        @Override
        public void run() {
            while (atomicInteger.get() <= 10000) {
                //getAndIncrement 底层利用CAS保证了线程安全 实际上执行了i++操作
                int i = atomicInteger.getAndIncrement();
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    
        public static void main(String[] args) {
            CasCount casCount1 = new CasCount();
            CasCount casCount2 = new CasCount();
            casCount1.start();
            casCount2.start();
        }
    }
    

2.5 利用CAS手写锁

CAS无锁机制原理:

  1. 定义一个锁的状态
    • 状态值 = 0 ,则表示没有线程获取到该锁
    • 状态值 = 1 , 则表示有线程已经持有该锁
  2. CAS获取锁
    1. 将该锁的状态从0修改为1,能够修改成功,则表示获取锁成功
    2. 如果修改失败,则获取锁失败,不会阻塞,而是循环获取(自选来控制重试)
  3. CAS释放锁
    • 将该锁的状态从1修改为0,能够修改成功,则表示释放锁成功
public class AtomicTryLock {
    /**
     * 原子变量:锁的状态
     */
    private AtomicLong cas = new AtomicLong(0);

    /**
     * 当前持有锁的线程
     */
    private Thread lockCurrentThread;

    /**
     * 获取锁
     * 锁是有状态的 如果为 0则表示没有线程持有该锁 如果为 1则表示该锁已经被线程持有
     *
     * @return
     */
    public boolean tryLock() {
        //获取锁,成功则设置状态为1,并返回true
        boolean result = cas.compareAndSet(0, 1);
        if (result) {
            //设置当前锁被哪个线程持有
            lockCurrentThread = Thread.currentThread();
        }
        return result;
    }

    /**
     * 释放锁
     *
     * @return
     */
    public boolean unLock() {
        //判断是否为当前线程释放锁
        if (lockCurrentThread != Thread.currentThread()) {
            return false;
        }
        //释放锁,成功则设置状态为0,并返回true
        return cas.compareAndSet(1, 0);
    }

    public static void main(String[] args) {
        AtomicTryLock atomicTryLock = new AtomicTryLock();
        IntStream.range(1, 10).forEach((i) -> new Thread(() -> {
            try {
                boolean result = atomicTryLock.tryLock();
                if (result) {
                    atomicTryLock.lockCurrentThread = Thread.currentThread();
                    System.out.println(Thread.currentThread().getName() + ",获取锁成功~");
                } else {
                    System.out.println(Thread.currentThread().getName() + ",获取锁失败~");
                }
            } catch (Exception e) {
            } finally {
                if (atomicTryLock != null) {
                    atomicTryLock.unLock();
                }
            }
        }).start());
    }
}

2.6 CAS锁避免ABA问题

ABA问题:线程A比线程B快得多,此时:

​ A将O=10修改为20

​ zA将O=20修改为10

B在修改O值时,本应该修改失败,但是因为A将值修改过之后又改回来了,导致B修改成功了,但是在有些业务场景下是不允许这么操作的。

解决办法:对内存中的O值添加一个版本号,在比较值的同时还要比较版本号。

2.7 重量级锁和轻量级锁

轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。轻量级锁对使用2者是透明的,即语法仍然是synchronized,假设有两个方法同步块,利用同一个对象加锁。

如果在尝试加轻量级锁的过程中,cas操作无法成功,这时有一种情况就是其它线程已经为这个对象加上了轻量级锁,这时就要进行锁膨胀,将轻量级锁变成重量级锁。

自旋优化(重量级锁的优化)

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即在自旋的时候持锁的线程释放了锁),那么当前线程就可以不用进行上下文切换就获得了锁

2.8 独占锁和共享锁

独占锁:独占锁也叫排他锁,是指该锁一次只能被一个线程所持有。如果线程对数据A加上排他锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程即能读数据又能修改数据。
共享锁:共享锁是指该锁可被多个线程所持有。如果线程对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。

3. ThreadLocal

3.1 概述

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量

ThreadLoal 变量即线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

image

ThreadLocal提供了线程本地实例,它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相互绑定。

ThreadLocal适用于在多线程的情况下,可以实现传递数据,实现线程隔离。

ThreadLocal基本API

  • New ThreadLocal() 创建ThreadLocal
  • set 设置当前线程绑定的局部变量
  • get 获取当前线程绑定的局部变量
  • remove 移除当前线程绑定的局部变量
public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(() -> {
            ThreadLocaDemo.localVar.set("local_A");
            print("A");
            //打印本地变量
            System.out.println("after remove : " + localVar.get());
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(() -> {
            ThreadLocaDemo.localVar.set("local_B");
            print("B");
            System.out.println("after remove : " + localVar.get());
        },"B").start();
    }
}

哪些地方使用了ThreadLocal?

  • 设计模式 模板方法
  • SpringMVC获取HttpRequest对象(SpringMVC会将HttpRequest对象缓存到当前线程中)
  • Spring事务模板类
  • AOP
  • LCN分布式事务
  • 分布式服务追踪框架源码
  • JavaWeb项目Tomcat接收请求
  • 创建一个线程接受请求:AOP目标方法

ThreadLocal和Synchronized的区别

ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问

但是ThreadLocal与synchronized有本质的区别:

  • Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离
  • Synchronized是利用锁的机制,使变量或代码块在某一时刻只能被一个线程访问,采用时间换空间的方式。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,采用空间换时间的方式。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,ThreadLocal是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(ThreadLocal,value),虽然不同的线程之间ThreadLocal这个key值是一样的,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

3.2 ThreadLocal底层实现原理

  1. 在每个线程中都有自己独立的一个名叫ThreadLocalMap集合,其中有一个Entry对象。
  2. 如果当前线程对应的的ThreadLocalMap对象为空的情况下,则创建该ThreadLocalMap对象,并且赋值键值对。key为当前newThreadLocal对象,value就是为object变量值。
 public void set(T value) {
     //1、获取当前线程
     Thread t = Thread.currentThread();
     //2、获取线程中的属性 threadLocalMap
     ThreadLocalMap map = getMap(t);
     //3、如果threadLocalMap 不为空,则直接更新要保存的变量值
     if (map != null)
       map.set(this, value);
     //4、否则创建初始化threadLocalMap,并赋值
     else
       createMap(t, value);
 }

ThreadLocal的set()方法赋值的时候首先会获取当前线程thread,并获取当前线程thread中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

那么什么是ThreadLocalMap,createMap又是怎样实现的?

static class ThreadLocalMap {
    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
  	...
}

可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

/**
* ThreadLocal的内部方法
*
* Create the map associated with a ThreadLocal. Overridden in InheritableThreadLocal.
* 创建与ThreadLocal关联的映射。在InheritableThreadLocal中被重写。
* @param t the current thread
		当前线程
* @param firstValue value for the initial entry of the map
		map初始条目的值
*/
void createMap(Thread t, T firstValue) {
  	t.threadLocals = new ThreadLocalMap(this, firstValue);
}
 
/**
* ThreadLocalMap 构造方法
*
* Construct a new map initially containing (firstKey, firstValue).
* 构造一个初始包含(firstKey,firstValue)的新映射。
* ThreadLocalMaps are constructed lazily, so we only create one when we have at least one entry to put in it.
* ThreadLocalMaps是懒加载的,因此我们只有在至少有一个条目要放入时才创建一个。
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
      m.remove(this);
}

remove方法,直接将ThrealLocal 对应的值从当前线程Thread中的ThreadLocalMap中删除

为什么线程缓存的是ThreadLocalMap对象

ThreadLocalMap可以存放n个不同的ThreadLocal对象;而每个ThreadLocal对象只能缓存一个变量值

ThreadLocalMap<ThreadLocal对象,value> threadLocalMap

ThreadLocal.get(); => threadLocalMap.get(ThreadLocal);

3.3 强、软、弱、虚引用的区别

  • 强引用(StrongReference)

    强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用(SoftReference)

    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用通常用在内存程序敏感的程序中,比如可用来实现内存敏感的高速缓存,内存够时就保留,不够时就回收
    软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

  • 弱引用(WeakReference)

    弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
    弱引用可以和一个引用队列 ReferenceQueue 联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference)

    “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
    虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 ReferenceQueue 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。

3.4 Thread内存泄漏问题

内存泄漏是开发者申请了内存,但是该内存一直无法释放

内存溢出是申请内存时,发现内存不足

在使用过ThreadLocal的set、get方法后进行了remove。那么为什么要删除,这就涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例子,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

那么如何避免Thread内存泄露?

  1. 在使用完ThreadLocal后手动调用remove方法,将不要的数据移除(强制)
  2. 尽量不要使用全局的ThreadLocal(推荐)

4. AQS

4.1 Lock和Synchronzied的区别

  • 用法上:
    • synchronized:在需要同步的对象中加入控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。
    • lock:需要显示指定起始位置和终止位置。一般使用ReentrantLock类做为锁,多个线程中必须要使用一个ReentrantLock类做为对象才能保证锁的生效。且在加锁和解锁处需要通过lock()unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。lock只能写在代码里,不能直接修改方法。
  • 性能上
    • 在Java1.5中,synchronized是性能低效的。因为这是一个重量级锁,需要调用操作接口,导致有可能加锁消耗的系统时间比加锁以外的操作还多。而在Java1.6中,synchronized在语义上很清晰,可以进行很多优化,有适应自旋,锁消除,锁粗化,轻量级锁,偏向锁等等。导致在Java1.6上synchronize的性能并不比Lock差。
    • 资源竞争激励的情况下,lock性能会比synchronized好,竞争不激励的情况下,synchronize比lock性能好,synchronized会根据锁的竞争情况,从偏向锁-->轻量级锁-->重量级锁升级,而且编程更简单。
  • 机制上:
    • synchronized是在JVM层面实现的,托管给JVM执行的,系统会监控锁的释放与否。
    • lock是JDK代码实现的,需要手动释放,在finally块中释放。可以采用非阻塞的方式获取锁。
    • Synchronized的编程更简洁,lock的功能更多更灵活,缺点是一定要在finally里面 unlock()资源才行。

4.2 AQS底层原理

AQS全称为AbstractQueuedSynchronizer是一个抽象同步队列,它提供了一个FIFO队列,可以看成是一个用来实现同步锁以及其他涉及到同步功能的核心组件,常见的有:ReentrantLock、CountDownLatch等。

AQS是一个抽象类,主要是通过继承的方式来使用,它本身没有实现任何的同步接口,仅
仅是定义了同步状态的获取以及释放的方法来提供自定义的同步组件。

AQS底层的实现结合CAS compareAndSwapInt实现

查看ReentrantLock底层源码:

image

image

可以发现sync继承了AbstractQueuedSynchronizer,即AQS

默认情况下,Lock为非公平锁,想要其变成公平锁,可以传递参数true:new ReentrantLock(true)

AQS底层原理

image

AQS就是一个类,这个类里核心的就是有一个state变量初始值是0,还有一个等待队列。多个线程来都来尝试加锁,就是执行到这段代码lock.lock();

线程1尝试通过CAS的方式更新state=1,线程2、3也尝试通过CAS的方式更新state=1,同一时间只会有一个线程能CAS成功,假如是线程1 CAS成功,线程2、3 CAS失败。

线程1加锁成功后,会更新state=1,同时更新加锁线程是线程1,加锁失败的线程2、3就会进入等待队列。

当线程1释放锁的时候,会更新state=0,把加锁线程置为null,然后唤醒所有其他线程重新争抢

以此类推

4.3 CAS+LockSupport+AQS手写Lock

/**
 * @author Mark
 * @version 1.0
 * @className MarkLock
 * @date 2022/11/9 9:39
 */
public class MarkLock {
    /**
     * 锁的状态 0=>未被线程持有 1=>被线程持有
     */
    private AtomicInteger lockState = new AtomicInteger(0);
    /**
     * 当前获取锁的线程
     */
    private Thread currentThread = null;
    /**
     * 未获取到锁的线程存放的队列
     */
    private ConcurrentLinkedDeque<Thread> queue = new ConcurrentLinkedDeque();

    /**
     * 获取锁
     */
    public void lock() {
        acquire();
    }

    /**
     * 获取锁处理方法
     *
     * @return 获取锁是否成功
     */
    public boolean acquire() {
        //CAS 自旋
        for (; ; ) {
            if (compareAndSet(0, 1)) {
                //获取锁成功
                currentThread = Thread.currentThread();
                return true;
            }
            //获取锁失败
            Thread thread = Thread.currentThread();
            //将当前线程放入队列
            queue.add(thread);
            //阻塞
            LockSupport.park(thread);
            return false;
        }
    }

    /**
     * 改变锁的状态
     *
     * @param expect 期望值
     * @param update 更新值
     * @return 修改是否成功
     */
    public boolean compareAndSet(int expect, int update) {
        return lockState.compareAndSet(expect, update);
    }

    /**
     * 释放锁
     */
    public boolean unlock() {
        if (currentThread == null) {
            return false;
        }
        if (currentThread == Thread.currentThread()) {
            boolean result = lockState.compareAndSet(1, 0);
            if (result) {
                //公平锁唤醒
                Thread first = queue.getFirst();
                LockSupport.unpark(first);
                return true;
            }
        }
        return false;
    }
}

public class Test02 {
    public static void main(String[] args) {
        MarkLock markLock = new MarkLock();
        markLock.lock();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "  start");
            markLock.lock();
            System.out.println(Thread.currentThread().getName() + "  end");
        }).start();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        markLock.unlock();
    }
}

4.4 Semaphore

Semaphore信号量,是JUC包下的一个工具类,我们可以通过其限制执行的线程数量,达到限流的效果

它维护了一个许可证集合,有多少资源需要限制就维护多少许可证集合。假设有N个资源,那就对应于N个许可证,同一时刻就只能有N个线程访问。一个线程获取许可证就调用acquire方法,用完了释放资源就调用release方法。可以简单理解为Semaphore信号量可以实现接口限流,底层基于AQS实现。

当一个线程执行时先通过其方法进行获取许可操作,获取到许可的线程继续执行业务逻辑,当线程执行完成后进行释放许可操作,未获取达到许可的线程进行等待或者直接结束。

public class Test03 {
    public static void main(String[] args) {
        Semaphore semaphore = new Semaphore(5);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            new Thread(() -> {
                try {
                    //获取票据 -1 aqs 锁的状态-1
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName() + "," + finalI);
                    //释放票据
                    semaphore.release();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }).start();
        }
    }
}
/*
Thread-0,0
Thread-3,3
Thread-2,2
Thread-1,1
Thread-6,6
Thread-7,7
Thread-4,4
Thread-5,5
Thread-8,8
Thread-9,9
*/

4.5 CountDownLatch

CountDownLatch是一个同步工具类,用来协调多个线程之间的同步,或者说起到线程之间的通信。

CountDownLatch能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行。使用一个计数器进行实现。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减一。当计数器的值为0时,表示所有的线程都已经完成一些任务,然后在CountDownLatch上等待的线程就可以恢复执行接下来的任务。和join方法相似。

CountDownLatch底层是基于AQS实现的

public class Test04 {
    public static void main(String[] args) {
        //AQS的state = 2
        CountDownLatch countDownLatch = new CountDownLatch(2);
        new Thread(()->{
            try {
                System.out.println("T1开始执行...");
                countDownLatch.await();
                System.out.println("T1结束执行...");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"T1").start();
        //state - 1
        //当state = 0 时 唤醒等待的线程
        countDownLatch.countDown();
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        countDownLatch.countDown();
    }
}