• 0908(050天 线程集合04 线程池、阻塞队列、线程变量)


    0908(050天 线程集合04 线程池、阻塞队列、线程变量)

    每日一狗(田园犬西瓜瓜

    在这里插入图片描述

    线程集合

    1. 问:线程池总结一下 JavaEE阶段终结版

    Spring 框架中还有一次补充

    线程是稀缺资源,如果被无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,合理的使用线程池对线程进行统一分配、调优和监控,线程池可以很好的解决这一问题。

    线程池

    引入

    Java1.5中引入的Executor框架把任务的提交和执行进行解耦,只需要定义好任务,然后提交给线程池,而不用关心该任务是如何执行、被哪个线程执行,以及什么时候执行。

    顶层接口

    Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具,而真正的线程池接口是ExecutorService。

    ExecutorService

    ExecutorService是Java中对线程池定义的一个接口,它java.util.concurrent包中。Java API对ExecutorService接口的实现有两个(ThreadPoolExecutor和ScheduledThreadPoolExecutor),所以这两个即是Java线程池具体实现类。除此之外,ExecutorService还继承了Executor接口(注意区分Executor接口Executors工厂类),这个接口只有一个execute()方法。

    好处

    • 降低资源消耗:共享资源呀,回收利用
    • 提高响应速度:没有创建的时间了,线程处于待命状态
    • 提高线程的可管理性:直接使用线程池操作池子中的线程

    七大参数

    名称类型含义
    corePoolSizeint核心线程池大小
    maximumPoolSizeint最大线程池大小
    keepAliveTimelong线程最大空闲时间
    unitTimeUnit时间单位
    workQueueBlockingQueue线程等待队列
    threadFactoryThreadFactory线程创建工厂
    handlerRejectedExecutionHandler拒绝策略

    工作队列

    • 无界队列:队列大小无限制,常用的为无界的LinkedBlockingQueue,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致OOM。
    • 有界队列:队列大有有明确的限制,
      • 遵循FIFO原则的队列如ArrayBlockingQueue与有界的LinkedBlockingQueue
      • 另一类是优先级队列如PriorityBlockingQueue(优先级由任务的Comparator决定)
    • 同步移交队列:如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用SynchronousQueue作为等待队列,SynchronousQueue并不是一个队列,他不能对元素进行存储,元素的进入必然伴随着另一个元素的退出,必须要有一个线程来接收任务。适合用在无界线程池和明确饱和策略是才是用这个玩意。

    使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低cpu使用率和上下文切换,但是可能会限制系统吞吐量。

    同步移交队列适合那些任务不能等的情况

    拒绝策略

    • AbortPolicy:直接抛出异常,丢弃任务
    • CallerRunPolicy:移交给提交任务的线程来执行
    • DiscardOldestPolicy:丢弃队列中最近的任务,并且执行当前任务
    • DuscardPolicy:直接丢弃任务,不抛出异常

    相关问题

    任务队列、核心线程数、最大线程数的逻辑关系

    • 当线程数小于核心线程数时,创建线程
    • 当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列
    • 当线程数大于等于核心线程数,且任务队列已满
      • 若线程数小于最大线程数,创建线程
      • 若线程数等于最大线程数,调用拒绝执行处理程序(默认效果为:抛出异常,拒绝任务)

    阿里开发规范为什么不允许Executors快速创建线程池

    线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方法,这样的处理方式让编程人员更加明确线程池的运行规则,规避资源耗尽的风险说明: Executors返回的线程池对象的弊端:

    • FixedThreadPool和SingleThreadPool允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
    • CachedThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

    为什么使用线程池?

    • 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
    • 可以简单的根据系统的承受能力,调整多线程的负载,不挑机子。

    如何合理配置线程池的大小

    任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。

    • 对于计算密集型的任务,一个有N cpu个处理器的系统通常通过使用一个N cpu+1个线程的线程池来得最优的利用率
    • 对于IO密集型任务,包含了I/O和其他阻塞操作的任务,可以设置为2*N cpu
    • 最优的线程池的大小等于 【((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目 】
      • 比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32

    可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

    Runtime.getRuntime().availableProcessors():int获取CPU的核数

    线程池相关的主要方法

    java.util.concurrent.ExecutorService是java线程池框架的主要接口,用Future保存任务的运行状态及计算结果,主要方法有:

    • void execute(Runnable)提交任务到线程池

    • Future submit(Runnable)提交任务到线程池并返回Future

    • Future submit(Runnable, T) 提交任务到线程池并返回Future,第二个参数会作为计算结果封装到Future

    • Future submit(Callable)提交任务到线程池并返回Future,call方法的计算结果会封装到Future

    • List invokeAll(Collection extends Callable>) 批量提交任务并返回计算结果

    • T invokeAny(Collectionextends Callable> tasks) 只执行其中一个任务并返回结果

    • void shutdown() 线程池不再接受新任务,继续运行正在执行中的任务及等待中的任务

    • List shutdownNow() 线程池不再接受新任务,继续运行正在执行中的任务,返回待执行的任务列表

    线程池的关闭

    shutdownNow:对正在执行的任务全部发出interrupt(),停止执行,对还未开始执行的任务全部取消,并且返回还没开始的任务列表,不建议使用
    shutdown:当调用shutdown后,线程池将不再接受新的任务,但也不会去强制终止已经提交或者正在执行中的任务

    几个预定义的池子

    • **可缓存线程池:**newCachedThreadPool

      • 实现:没有核心线程,工作线程数量为Integer.MAX_VALUE。阻塞队列使用同步队列SynchronousQueue没有提交了我不给分配线程执行那一说。

        默认60秒就是线程的最大空闲时间,超过即被回收。

        长期闲置不会消耗任何线程资源,因为最后活的时候,执行完毕的线程在闲置60S后就会被回收,到最后池子就剩一个空壳,没有一个线程。

      • 优点:任务来了就一定会被分配线程进行执行,会根据线程的提交速度,来动态匹配线程数量。

      • 缺点:工作线程数无上限,如果任务过多,可能会因为线程数量太多导致效率降低,毕竟线程之间的切换也是需要成本的;而且电脑很有可能因为同时存在的线程数量过多而直接瘫痪,这里就需要任务提交者自行把握,因为池子没在这里做限制。

      • 用来创建一个可以无限扩大的线程池,适用于服务器负载较轻,执行很多短期异步任务。

    • **定长池子:**newFixedThreadPool

      • 实现:核心线程数和最大线程数为固定传入参数值,阻塞队列使用LinkedBlockingQueue双链表阻塞队列(没有长度限制)。

        当线程池中的某个线程失败而终止时,新的线程会代替它执行剩下的任务,这么说的话就是这个线程可能会进行更替,并不是老是那几个线程在哪里执行任务,可以使用异常将线程给终止掉,线程池会重新创建新的线程执行剩余的任务。

        由于线程池的最大线程数量就是核心线程数的原因,这些线程都是不会被超时回收的,所以线程池中的线程只有池子调用shutdown函数式才会进行回收,程序才能结束。这可以算是一个常驻程序了。

      • 优点:线程的最大数量固定,即物理设备的运行最大负载固定。
      • 缺点:理论上讲任务的处理速度是固定的,阻塞队列存储容量没有存储上限。如果任务的平均提交速度快于任务的处理速度时,阻塞队列会无限制增长。
      • 创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于可以预测线程数量的业务中,或者服务器负载较重,对当前线程数量进行限制。
    • **周期定长池子:**newScheduledThreadPool

      • 创建的线程为定长池子,支持定时及周期性任务执行可以延时启动,定时启动的线程池。
      • 适用于需要多个后台线程执行周期任务的场景,比如说这里需要1小时后开始抢票,每隔20毫秒进行一次抢票,优缺点待补充
      • 可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
    • **单线程池子:**newSingleThreadExecutor :保证执行顺序

      • 里边就一个线程
      • 优点:可以保证执行顺序
      • 缺点:慢
      • 创建一个单线程的线程池,适用于需要保证顺序执行各个任务,并且在任意时间点,不会有多个线程是活动的场景。
    • **工作窃取算法的线程池:**newWorkStealingPool (适合将线程数量控制到于可用CPU核心树相同)

      • 为每个工作线程分配一个双端队列(本地队列)用于存放需要执行的任务,当自己的队列没有数据的时候从其它工作者队列中获得一个任务继续执行。

        这个线程池厉害了,他每一个线程都有一个阻塞队列,自己线程对应的阻塞队列中的任务完了,他会去偷其他线程对应的阻塞队列中的任务来执行(吾辈楷模)

        创建持有足够数量线程的线程池来支持给定的并行级别,该方法还会使用多个队列来减少竞争,无参则是根据CPU个数定义并行级别。

        核心思想是
        work-stealing工作窃取,ForkJoinPool提供了一个更有效的利用线程的机制,当ThreadPoolExecutor还在用单个队列存放任务时,ForkJoinPool已经分配了与线程数相等的队列,当有任务加入线程池时,会被平均分配到对应的队列上,各线程进行正常工作,当有线程提前完成时,会从队列的末端窃取其他线程未执行完的任务,当任务量特别大时,CPU多的计算机会表现出更好的性能。

      • 优点:快呀,而且拥有多个任务队列,可以减少连接数创建

      • 缺点:这种情况就是让CPU专心去做一个线程中的事,适用与大耗时并行任务的执行。老换线程就没意思了。

      • 创建一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行,适用于大耗时的操作,可以并行来执行

    2. 线程通讯

    当线程在系统内运行时,线程的调度具有一定的透明性,程序通常无法精确控制线程的轮换执行,但Java也有一些机制来保证线程协调执行。

    线程间通信的模型有两种:共享内存和消息传递

    1、使用 volatile 关键字

    关键字来实现线程间相互通信是使用共享内存的思想,大致意思就是多个线程同时监听一个变量,当这个变量发生变化的时候 ,线程能够感知并执行相应的业务,而起对volatile修饰的共享资源的修改会通知其他拿了这个资源的线程赶紧过来再来重新拿一下。这也是最简单的一种实现方式。

    2、生产者消费者模式

    Object类提供了线程间通信的方法:wait()、notify()、notifyaAll(),它们是多线程通信的基础,而这种实现方式的思想自然是线程间通信。

    注意: wait和notify必须配合synchronized使用,wait方法释放锁,sleep方法不释放锁

    public class TestSync {
        public static void main(String[] args) {
            // 定义一个锁对象,不是Lock接口
            Object lock = new Object();
            List<String>  list = new ArrayList<>();
            // 实现线程A
            Thread threadA = new Thread(() -> {
                synchronized (lock) {
                    for (int i = 1; i <= 10; i++) {
                        list.add("abc");
                        System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                        try {
                            Thread.sleep(500);  //wait
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        if (list.size() == 5)
                            lock.notify();// 唤醒B线程
                    }
                }
            });
            // 实现线程B
            Thread threadB = new Thread(() -> {
                while (true) {
                    synchronized (lock) {
                        if (list.size() != 5) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("线程B收到通知,开始执行自己的业务...");
                    }
                }
            });
            // 需要先启动线程B
            threadB.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 再启动线程A
            threadA.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    3、使用Lock和Condition控制线程通信

    • await()导致当前线程等待,知道其它线程调用该条件变量的signal方法唤醒
    • signal()唤醒Lock对象上等待的单个线程,如果有多个等待线程,则随机选中一个唤醒。只有当前线程
      unlock后才能执行唤醒线程
    • signalAll()唤醒Lock对象上所有的等待线程

    线程B在被A唤醒之后由于没有获取锁还是不能立即执行,也就是说,A在唤醒操作之后,并不释放锁。

    public class TestSync {
        public static void main(String[] args) {
            ReentrantLock lock = new ReentrantLock();
            Condition condition = lock.newCondition();
    
            List<String> list = new ArrayList<>();
            // 实现线程A
            Thread threadA = new Thread(() -> {
                lock.lock();
                for (int i = 1; i <= 10; i++) {
                    list.add("abc");
                    System.out.println("线程A向list中添加一个元素,此时list中元素个数为:" + list.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() == 5)
                        condition.signal();
    
                }
                lock.unlock();
            });
            // 实现线程B
            Thread threadB = new Thread(() -> {
                lock.lock();
                if (list.size() != 5) {
                    try {
                        condition.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("线程B收到通知,开始执行自己的业务...");
                lock.unlock();
            });
            threadB.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadA.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    4、CountDownLatch信号量

    public class TestSync {
        public static void main(String[] args) {
            CountDownLatch countDownLatch = new CountDownLatch(1);
            List<String>  list = new ArrayList<>();
            // 实现线程A
            Thread threadA = new Thread(() -> {
                for (int i = 1; i <= 10; i++) {
                    list.add("abc");
                    System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() == 5)
                        countDownLatch.countDown();
                }
            });
            // 实现线程B
            Thread threadB = new Thread(() -> {
                while (true) {
                    if (list.size() != 5) {
                        try {
                            countDownLatch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("线程B收到通知,开始执行自己的业务...");
                    break;
                }
            });
            // 需要先启动线程B
            threadB.start();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 再启动线程A
            threadA.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    5、LockSupport 实现线程间的阻塞和唤醒

    LockSupport是一种非常灵活的实现线程间阻塞和唤醒的工具,使用它不用关注是等待线程先进行还是
    唤醒线程先运行,但是得知道线程的名字。

    public class TestSync {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            // 实现线程B
            final Thread threadB = new Thread(() -> {
                if (list.size() != 5) {
                    LockSupport.park();
                }
                System.out.println("线程B收到通知,开始执行自己的业务...");
            });
            // 实现线程A
            Thread threadA = new Thread(() -> {
                for (int i = 1; i <= 10; i++) {
                    list.add("abc");
                    System.out.println("线程A向list中添加一个元素,此时list中的元素个数为:" + list.size());
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (list.size() == 5)
                        LockSupport.unpark(threadB);
                }
            });
            threadA.start();
            threadB.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    3. 线程入土套餐

    1、多线程有什么用

    1)发挥多核CPU的优势。随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至16核的也都不少见,如果是单线程的程序,那么在双核CPU上就浪费了50%,在4核CPU上就浪费了75%。单核CPU上所谓的多线程那是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程"同时"运行罢了。多核CPU上的多线程才是真正的多线程,它能让你的多段逻辑同时工作,多线程,可以真正发挥出多核CPU的优势来,达到充分利用CPU的目的。
    2)防止阻塞。从程序运行效率的角度来看,单核CPU不但不会发挥出多线程的优势,反而会因为在单核CPU上运行多线程导致线程上下文的切换,而降低程序整体的效率。但是单核CPU还是要应用多线程,就是为了防止阻塞。试想,如果单核CPU使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据吧,对端迟迟未返回又没有设置超时时间,那么你的整个程序在数据返回回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
    3)便于建模。假设有一个大的任务A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务A分解成几个小任务,任务B、任务C、任务D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。

    2、什么是线程安全

    如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。
    1)不可变:像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用。final能够做出如下保证:当你创建一个对象时,使用final关键字能够使得另一个线程不会访问到处于“部分创建”的对象。final类型的成员变量的值,包括那些用final引用指向的collections的对象,是读线程安全而无需使用synchronization的
    2)绝对线程安全:不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价,Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet
    3)相对线程安全:相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个 Vector,99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。
    4)线程非安全:ArrayList、LinkedList、HashMap等都是线程非安全的类

    4. 线程变量 ThreadLocal类

    ThreadLocal叫做线程变量,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

    ThreadLocal是除了加锁这种同步方式之外的另一种保证多线程访问变量时的线程安全的方法;如果每个线程对变量的访问都是基于线程自己的变量这样就不会存在线程不安全问题。

    ThreadLocal和synchronized

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

    但是ThreadLocal与synchronized有本质的区别

    • synchronized用于线程间的数据共享,ThreadLocal则用于线程间的数据隔离

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

    • 对于多线程资源共享的问题,同步机制采用了以时间换空间的方式,而ThreadLocal采用了以空间换时间的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    ThreadLocal应用场景

    • 每个线程需要有自己单独的实例

    • 实例需要在多个方法中共享,但不希望被多线程共享

    ThreadLocal实现原理

    早期版本的ThreadLocal可以理解为一个Map

    当工作线程Thread实例向本地变量保持某个值时,会以key-value形式保存在ThreadLocal内部的Map中,其中Key为线程Thread实例,Value为待保存的值。

    当工作线程Thread实例从ThreadLocal本地变量取值时,会以Thread实例为Key,获取其绑定的Value。

    JDK1.8每个线程拥有一个ThreadLocalMap,ThreadLocalMap中元素Entry由key-value对组成,key为ThreadLocal对象,value为Object类型的值。

    Entry继承自WeakReference,并且在Entry构造函数中,key也就是ThreadLocal被设置为弱引用,弱引用在垃圾回收时会被回收

    • 每个Thread线程内部都有一个Map,即ThreadLocalMap
    • Map里面存储ThreadLocal对象key和线程的变量副本value
    • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值
    • 对于不同的线程,每次获取副本值时,别的线程不能获取到当前线程的副本值,形成了副本的隔离

    当线程往ThreadLocal中设置值时,实际上是向自己的ThreadLocalMap中以设置值,调用ThreadLocal.get取数据时,也是以ThreadLocal为key去ThreadLocalMap中查找,查到后变返回了。

    ThreadLocal用于连接ThreadLocalMap和Thread,来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作

    ThreadLocalMap用来存储数据,采用类似hashmap机制,存储了以threadLocal为key,需要隔离的数据为value的Entry键值对数组结构。HashMap使用链表+红黑树解决hash冲突,ThreadLocalMap初始化容积16,负载因子2/3,使用线性探测算法(开放寻址)解决hash冲突

    ThreadLocal有个ThreadLocalMap类型的属性,存储的数据就放在这儿。

    1)每个Thread维护着一个ThreadLocalMap的引用

    2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
    3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

    4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在 map中数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。

    5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法

    6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

    引用分类

    • 强引用:比如Object obj = new Object(),创建一个对象obj,这属于强引用,就是平时所说的引用,只要强引用还存在,垃圾收集器就永远不会回收被引用的对象
    • 软引用SoftReference:描述一些还有用,但并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,将会把这些对象列进回收范围再次进行回收,如果回收完后还没有足够的内存,才会抛出内存溢出异常
    • 弱引用WeakReference:也是用来描述一些非必需的对象,强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象
    • 虚引用PhantomReference:最弱的引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。虚引用的功能是:为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知;

    3. 阻塞队列

    阻塞队列与平常接触的普通队列LinkedList或ArrayList等的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。

    • 阻塞添加:所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作
    • 阻塞删除:阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)
    队列是否阻塞是否有界如何保证线程安全适用场景注意事项
    ArrayBlockingQueue阻塞有界一把全局锁生产者消费者模型,平和二者处理速度先进先出,有界,不支持元素为空
    LinkedBockingQueue阻塞无界可配置存储采用两把锁生产者消费者模型,平和二者处理速度无界时注意OOM问题
    ConcurrentLinkedQueue非阻塞无界CAS对全局的几个进行操作size()需要遍历集合,慎用

    4. 两个常见工具类

    • Collections对集合提供一些常见的算法实现
    • Arrays针对数组提供一些常见的算法实现

    集合和数组之间互相转换

    List list=new ArrayList();
    Object[] arr=list.toArray(); // 将集合转换为数组
    Arrays.asList(arr):List // 可以将一个数组装换为集合,务必注意这里返回的ArrayList不是java.util.ArrayList
    
    • 1
    • 2
    • 3
    • static void sort(简单数据类型数组) 排序
    • static int binarySearch 折半查找,要求有序
    • static void fill 使用特定数据填充整个数组(每个数组的元素的值都是这个值)
    • static T[] copyOf 数组复制,返回新数组

    5. 常见面试题(未完)

    1、 Synchronized 用过吗,其原理是什么?

    Synchronized 经常在解决线程安全问题中使用。

    Java中的Synchronized底层实现使用的是系统的排它锁进行实现的。

    • 同步方法和同步静态方法是通过在方法区中的标志位access_flags进行标识
    • 同步代码块在编译的时候加入了两个机器指令monitorenter和monitorexit

    在对象头的work区域最后两个标志位标识了锁的类型,1.6后synchronized引入了偏向锁和轻量级锁的机制,最后四位可以标识区分偏向锁和无锁,最后两位可以区分轻量级锁、重量级锁、GC标志和非这仨玩意。

    2、 你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?

    这个锁是啥?资源呀,进入同步程序必须要获取对应的锁资源才能继续执行,否则会进行阻塞。

    确定对象锁首相要看锁对象是那个,这就得说道synchronized的三种使用方法了,同步代码块锁对象是指定的对象,同步方法的所对象是实例对象,同步静态方法是类对象,不管是那个同步程序,只要保证所对象是唯一的就可保证线程安全。

    3、 什么是可重入性,为什么说 Synchronized 是可重入锁?

    可重入性就是我自己有这一把锁,我在此进入其对应的同步程序的时候申请就直接拿到,就可以直接进入执行其对应的同步程序。

    Synchronized从底层上看,他申请的时候就是在其对象头monitor中的计数器count给加上1,当然第一次申请的时候这个计数器必须为0时才能尝试加一,之后的重入就是再此基础上申请一次加一次1,退出同步程序时会同步给这个计数器减1,直到计数器减到0这个锁才算是被完全释放,这样其他线程才可以申请尝试获取锁。

    而已排他锁的可重入性对其非常重要,如果不可重入,好家伙,自己申请到了锁,然后自己再此申请,自己把自己卡出,单线程写出死锁属于是有点东西。

    4、 JVM 对 Java 的原生锁做了哪些优化?

    1.6版本之前的Synchronized是直接使用的是重量级锁,但是这样基本就没人用了,因为重量级锁的线程切换需要在用户态和核心太来回切换,这个切换也是相当的耗费资源的,1.6后为了提高锁的效率引入了偏向锁和轻量级锁,就是1.6后虚拟机看到你这个synchronized同步程序,不着急给你上重量级锁,你一个线程来获取锁资源,我在锁对象的对象头中的PID位存储上你的线程ID值,象征性的标识一下这把锁是属于你的,同时这把锁也只有这个线程能够申请到,这就是偏向锁;当第二个线程也要来申请锁资源的时候,第二个线程发现来晚了呀,跑去忙等一会,此时这个锁会从偏向锁升级成轻量级锁,第二个线程忙等结束后发现第一个线程已经将锁释放了,赶紧申请;当自旋失败的次数达到一定阈值的时候时,这把锁会升级成重量级锁,这时来申请锁的线程申请失败后就去锁对象的阻塞队里中睡觉去了,等待有线程释放锁后来将其唤醒。

    这里为啥轻量级锁一定要升级成重量级锁的原因就是轻量级锁的核心操作自旋忙等,自旋忙等是不会释放CPU资源的,执行的也是无意义的操作,也就是说CPU这会还是挺忙的,那着不行呀。这种自旋就是牺牲一部分CPU资源以维持上下文状态的,一旦自旋次数过多就得不偿失了。

    5、 为什么说 Synchronized 是非公平锁?

    因为他本身就是非公平锁呀!在底层是来一个线程看所对象的计数器为0就直接尝试加锁,但是这个时候阻塞线程中如果还有等待的线程,这显然就不公平呀,要是这后来的给加上了,阻塞队列里的线程相当于是被插队了。

    这不用说就是不公平的了

    错:6、 什么是锁消除和锁粗化?

    锁消除:就是虚拟机在执行过程中发现你这个锁根本没啥用,他就给你

    锁粗化:无锁->偏向锁->轻量级锁->重量级锁

    7、 为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?

    悲观锁:我认为你这个临界资源一定会发生线程安全问题。

    synchronized就是典型的悲观锁,只要被修饰了,第一个线程来了就直接是偏向锁,这还不悲观

    乐观锁:与悲观锁刚好相反,我认为这个临界资源不会发生线程安全问题,就算发生了我自己有者能解决。

    CAS:对比和交换,这个翻译就已经说的很明白了,拿出来的时候在自己工作内存中备份一份预期值,到时候算完了要写回的时候进行一次判定,如果预期值和当前值相同,则将要写入的数据写入主内存中的共享数据;如果不是那就说明发生了线程安全问题,就不能写入,需要另行处理。

    8、 乐观锁一定就是好的吗?

    不是

    乐观锁的典型实现CAS

    • ABA问题
    • 资源竞争过大时修改成功率降低,随之而来的就是大量的无用操作

    乐观锁适合那种经常读的临界资源的线程安全问题。悲观锁的读写也是互相之间隔离的才能保证线程安全,乐观锁是只有在写入操作过多时会降低效率,读取操作相互之间并不会出现问题。

    9、 跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?

    Synchronized使用的是系统提供的排他锁来实现的,可重入锁ReentrantLock底层使用Sync(抽象队列同步器的子类来实现的)

    10、 那么请谈谈 AQS 框架是怎么回事儿?

    AQS:抽象队列同步器

    11、 请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。

    12、 ReentrantLock 是如何实现可重入性的?

    13、 除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?

  • 相关阅读:
    Qt 基于海康相机的视频绘图
    C#基础--运算符和类型强制转换
    不同路径数(冬季每日一题 4)
    yml 配置 mapper-locations 支持多级目录
    【Linux基础】进程(三)
    如何完成网课答案公众号搭建?小白教程!内附网课题库接口!
    中创新航:动力电池变局者?
    第四章:Unix时间
    LabVIEW和NIUSRP硬件加快了认知无线电开发
    直接插入排序
  • 原文地址:https://blog.csdn.net/weixin_48015656/article/details/126772940