7.1 线程安全

Screen Shot 2021-01-20 at 9.43.43 AM.png

  • Thread中,可通过 setDefaultUncaughtExceptionHandler()的方式才能在主线程中捕捉到子线程异常
  • 保证高并发的线程安全,四个维度可以考虑:
    • 数据单线程内可见
    • 只读对象
    • 线程安全类
    • 同步和锁机制
  • java并发包JUC(作者Doug Lea)

    • 线程同步类 CountDownLatch Semaphore CyclicBarrier
    • 并发集合类 ConcurrentHashMap BlockingQueue CopyOnWriteArrayList
    • 线程管理类 Executors ThreadPoolExecutor ScheduledExecutorService
    • 锁相关类 Lock ReentrantLock

      7.2 锁

  • 锁主要提供了两种特性:互斥性和不可见性

  • Java中锁的实现
    • Lock类-ReentranLock
      • 核心是AbstractQueuedSynchronizer Screen Shot 2021-01-20 at 9.53.39 AM.png
    • synchronized, 有三种锁实现(这个机制已经不再笨重)
      • 偏向锁:在没有锁竞争情况下尽量减少加锁带来的性能开销
      • 轻量级锁:出现锁竞争,升级为轻量级锁
      • 重量级锁:出现激烈锁竞争那个,升级为重量级锁

7.3 线程同步

  • i++操作没有原子性
  • volatile关键字

    1. class LazyinitDemo {
    2. //这里没有加volatile关键字的话,有bug,有可能返回未被初始化的对象
    3. //new TransactionService()包含分配内存空间,设置默认值,和执行构造方法两个步骤
    4. //如果正好有个线程B在前一个线程A执行到第一步的时候进来了,那么就会一路进if判断,直接返回未被初始化的对象
    5. //解决办法是,给这个单例对象添加volatile关键字
    6. //避免指令重排序
    7. private static TransactionService service = null;
    8. public static TransactionService getTransactionService() {
    9. if (service == null) {
    10. synchronized (this) {
    11. if (service == null) {
    12. service = new TransactionService();
    13. return service;
    14. }
    15. }
    16. }
    17. return service;
    18. }
    19. }
    • 保证此变量对所有线程可见
    • 禁止指令重排序优化
  • volatile有可见性,但是没有原子性

    类中一个voliatile数值 ,A线程count++ 100次,B线程count—100次,最终结果大概率不是0 为什么呢?因为count++ 和count—不是原子性操作, volatile仅支持可见性,不支持原子性 解决:count用AtomicLong,或者LongAddr(后者更推荐,性能更好)

  • volatile适合一写多读场景,不适合多写场景

    • 案例:CopyOnWriteArrayList

CountdownLatch

3个事都办完之后,再统一处理
new CountdownLatch(3)

Semaphore

3个窗口,6个人来办事,任何一个窗口有空,排队的人就过去
new Semaphore(3)
如果Semaphore(1) ,就是一个互斥锁

CylicBarrier

一个安检口,6个人排队,每次放三个人一批进去,三个人安检完了 ,再放下一批3人。

AbstractQueuedSynchronizer 介绍

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLockSemaphore,其他的诸如 ReentrantReadWriteLockSynchronousQueueFutureTask 等等皆是基于 AQS 的。当然,我们自己也能利用 AQS 非常轻松容易地构造出符合我们自己需求的同步器。

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

AQS 定义两种资源共享方式

  • Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
    • 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
    • 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
  • Share(共享):多个线程可同时执行,如CountDownLatchSemaphoreCountDownLatchCyclicBarrierReadWriteLock 我们都会在后面讲到。

ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

如何自定义AQS?

实现方法 isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。

  • ReentrantLock 为例,state 初始化为 0,表示未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证 state 是能回到零态的。

  • 再以 CountDownLatch 以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后countDown() 一次,state 会 CAS(Compare and Swap)减 1。等到所有子线程都执行完后(即 state=0),会 unpark()主调用线程,然后主调用线程就会从 await() 函数返回,继续后余动作。

7.4 线程池

  • 核心类ThreadPoolExecutor
  • 核心实现-源码
  • 队列
  • 拒绝策略

7.5 ThreadLocal

  • 用于在线程并发时,解决变量共享的问题
  • 但是过度设计,如弱引用和哈希碰撞 ,反而成为一个故障高发点

    四种引用类型

  • 强引用>软引用>弱引用>虚引用

Screen Shot 2021-01-26 at 7.26.06 PM.png

  • 软引用在内存紧张情况下由更好的回收能力,可以用于在服务器上缓存中间结果。
  • 但是不建议缓存高频数据,因为一旦服务器重启或者软引用触发大规模回收,那么所有的访问都将指向数据库。

  • WeakReference弱引用,在新生代频繁的gc中会被回收掉
  • WeakReference的应用:WeakHashMap
  • ThreadLocal中也使用了WeakReference

    ThreadLocal的价值

    CopyValuelntoEveryThread
  1. //使得每个线程都可以有自己的随机数生成器
  2. //如果用Random,虽然多线程下它是线程安全的,但是会因为多线程竞争同一个seed而导致性能下降
  3. ThreadLocalRandom RANDOM = ThreadLocalRandom.current();

Screen Shot 2021-02-02 at 2.20.29 PM.png

ThreadLocal的弱引用设计

弱引用的设计

Screen Shot 2021-02-02 at 2.31.59 PM.png

  • 红虚线代表弱引用
  • ThreadLocal对象是线程共享的
  • 每个线程里面的有单独自己的ThreadLocal.ThreadLocalMap 对象
  • ThreadLocalMap存的是很多个Entry对象
  • 每个Entry是一个K-V结果,K是ThreadLocal对象的弱引用,V是真实的Value
  • 所以ThreadLocal对象不持有用户设置的值,这个值是存在每个线程里面的ThreadLocalMap中的
  • ThreadLocal近似的理解为,就是Map的一个key
    ``` /**
  • 建立ThreadLocal如下
  • shareNum在栈上,持有堆上ThreadLocal对象的引用
  • ThreadLocal **/ private static final ThreadLocal shareNum = new ThreadLocal(){
    1. @Override
    2. protected Integer initialValue(){
    3. return 100;
    4. }
    }; ```

弱引用和内存泄露

  • 弱引用的引入,是为了一定程度上解决内存泄露问题。

我们两种情况都讨论一下:

  • key 使用强引用:引用的ThreadLocal的对象被置为null,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被置为null,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏。

但是使用弱引用可以多一层保障:弱引用ThreadLocal这个key不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

内存泄露-深入

弱引用的引入,在一定程度上解决内存泄露问题。
但是不合理的使用仍然有可能导致内存泄露的问题。
原因:在threadLocal设为null和线程结束这段时间不会value被回收的(用户也不使用get set remove)

易发场景:单个线程可能因为ThreadLocal的使用产生内存泄露,但是只要线程结束,资源就释放了。
更恐怖的情况是,在线程池里,线程结束是可能不会销毁的,会再次使用的就可能出现内存泄露 。
(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

解决办法:每次用完ThreadLocal,都手动remove一下

ThreadLocal的其他坑

脏数据

  • 线程池中不正确地使用ThreadLocal,会造成脏数据问题
  • 因为线程池中的线程是复用的,前一个线程中用到了ThreadLocal
  • 如果没有手动remove,那么这个线程再次被启用时,就会发现 还遗留着上一次的的ThreadLocal数据

参考