Java并发编程实战之基础知识(Volatile 和 ThreadLocal 以及 CountDownLatch)

并发编程全景图之思维导图

在这里插入图片描述

总体设计原则

  • 尽量将域声明为 final 类型,除非需要它们是可变的。
  • 不可变对象一定是线程安全的。
  • 不可变对象能极大地降低并发编程的复杂性。它们更为简单而且安全,可以任意共享而无须使用加锁或保护性复制等机制。
  • 封装有助于管理复杂性。
  • 在编写线程安全的程序时,虽然可以将所有数据都保存在全局变量中,但为什么要这样做?将数据封装在对象中,更易于维持不变性条件。将同步机制封装在对象中,更易于遵循同步策略。
  • 锁来保护每个可变变量
  • 当保护同一个不变性条件中的所有变量时,要使用同一个锁
  • 在执行复合操作期间,要持有锁
  • 如果从多个线程中访问同一个可变变量时没有同步机制, 那么程序会出现问题 。
  • 不要故作聪明地推断出不需要使用同步。
  • 在设计过程中考虑线程安全,或者在文档中明确地指出它不是线程安全的。
  • 将同步策略文档化。
  • 可变状态是至关重要的。
    所有的并发问题都可以归结为如何协调对并发状态的访问。可变状态越少,就越容易确保线程安全性

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

  • 不在线程之间共享该状态变量。
  • 将状态变量修改为不可变的变量。
  • 在访问状态变量时使用同步。

无状态对象一定是线程安全的!

原子性

极客时间《Java并发编程实战》—并发编程BUG的源头与Java如何解决可见性和有序性问题笔记

对于可能被多个线程同时访问的可变状态变量,在访问它时都需要持有同一个锁,在这种情况下,我们称状态变量是由这个锁保护的。

每个共享的和可变的变量都应该只由一个锁来保护,从而使维护人员知道是哪一个锁。

复合操作

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护。

if(!vector.contain(key)){
	vector.add(key) ;
}

当某个变量由锁来保护时,意味着在每次访问这个变量时都需要首先获得锁,这样就确保在同一时刻只有一个线程可以访问这个变量。当类的不变性条件涉及多个状态变量时,那么还有另外一个需求:在不变性条件中的每个变量都必须由同一个锁来保护。因此可以在单个原子操作中访问或更新这些变量,从而确保不变性条件不被破坏。

对象的共享

非原子的64位操作

JVM 允许将64位的读操作和写操作分解为两个32位的操作。当读取一个非volatile类型的long变量时,如果对该变量的读操作和写操作在不同的线程中执行,那么很可能会读取到某个值的高32位和另一个值的低32位。因此,即使不考虑失效数据问题,在多线程程序中使用共享且可变的long和double等类型的变量也是不安全的,除非用关键字volatile来声明它们,或者用锁保护起来

加锁会保证互斥性和内存可见性。

Volatile 变量

当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问 volatile 变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

典型用法(经常用作一种状态的标志识别)

        volati1e boolean asleep;
        while (!as1eep) {
            countSomeSheep();
        }

错误使用 volati1e 只保证可见性,不保证原子性

        volati1e boolean asleep;
        asleep++; //不保证原子性

加锁机制既可以确保可见性又可以确保原子性,而volatile变量只能确保可见性。

当且仅当满足以下所有条件时,才应该使用 volatile 变量:

  • 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。
  • 该变量不会与其他状态变量–起纳入不变性条件中。
  • 在访问变量时不需要加锁。

发布与逸出

  • “发布(Publish)”一个对象的意思是指,使对象能够在当前作用域之外的代码中使用。

其余见:对象的安全共享

ThreadLocal 类

类似于对应线程的全局变量,但是每一个线程维护一个自己的该变量对应值。
通常用来防止对可变的单实例变量或者全局变量进行共享。
在这里插入图片描述

明确内部实现:ThreadLocal 不是一个 map,而是一个 key,真正的 map 是在 Thread 类持有的 ThreadLocalMap,这个 map 放在了 ThreadLocal 类里面去实现
在这里插入图片描述

  • ThreadLocalMap 在 get 和 set 过程发生哈希冲突时,会清理掉保存 null 引用的 entry

ThreadLocal 的内存泄漏

ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,那么 ThreadLocalMap 中保存的 key 值就变成了 null,而 Entry 又被 threadLocalMap 对象引用,threadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不终结的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

如何避免呐?

在使用完 ThreadLocal 变量后,需要我们手动 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持对 value 的强引用,导致 value 不能被回收。

1.java虽然有gc,但是内存泄漏也是十分可怕的。
将ThreadLocalMap放在Thread类中作为属性,这就是第一个典型的防止内存泄漏的例子:因为如果是在 ThreadLocal 里维护 key 为 Thread 的 map 的话,因为Thread是有生命周期,如果这个 thread 死了,则 ThreadLocal 的 map 中对应的该 thread 的对象一直占用着空间,却永远不会被用到了。其实更有甚者,由于hashmap 里的 key 指向了 thread,所以这个 thread 都不会被回收,这是对资源的最大浪费,随着越来越多的线程产生,越来越多的内存泄漏发生,这是很恐怖的。

ThreadLocalMap 特别怕内存泄漏,是因为ThreadLocalMap的生命周期和Thread一样,而很多时候Thread的生命周期很长,内存泄漏会导致很严重的问题,所以将ThreadLocalMap的key设置为ThreadLocal的弱引用,到时候如果ThreadLocal作废了,这个对象就会被gc释放,然后 ThreadLocalMap 会有一系列的释放内存的操作来防止内存泄漏。这也给我们提示,对于维护一个生命周期长的数据结构,一定要很注意内存泄漏的风险。

ThreadLocalMap 在 get 和 set 过程发生哈希冲突时,会清理掉保存 null 引用的 entry,而 HashMap 不会清理

ThreadLocal 应用场景

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:

  • 线程间数据隔离,各线程的 ThreadLocal 互不影响
  • 方便同一个线程使用某一对象,避免不必要的参数传递
  • 全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
  • Spring 事务管理器采用了 ThreadLocal
  • Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal

美团工作经验:不要滥用 threadLocal,特别是在线程池里。写代码一定注意,threadLocal的key是线程名,线程池里的线程是重用的,threadLocal也会被重用导致代码出错

对象的组合(略)

基础构建模块

同步容器类

是一类将对应容器的状态都封装起来,并对每个共有方法都进行同步(加synchronized关键字修饰)的类,相当于让所有对容器的状态的访问串行化,虽然安全但是并发性差。

并发容器类

  • map ------ ConcurrentHashMap
  • list ------ CopyOnWriteArrayList

(争取)一篇搞懂 HashMap 相关

CopyOnWriteArrayList (原理:写时复制)

只要正确发布了这个 list,它就是不可变的了,所以随便并发访问,当需要修改时,就创建一个新的容器副本替代原来的,以实现可变性;

同步工具类

闭锁(CountDownLatch)
  • await 方法等待计数器达到零,这表示所有需要等待的事件都已经发生。如果计数器的值非零,那么 await 会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。
  • CountDownLatch 主要用来解决一个线程等待多个线程的场景。可以类比旅游团团长要等待所有的游客到齐才能去下一个景点;
    在这里插入图片描述
栅栏(Barrier)

CyclicBarrier 是一组线程之间互相等待,像是几个驴友之间不离不弃。除此之外 CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但 CyclicBarrier 的计数器是可以循环利用的,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数,可以说是功能丰富

public class CyclicBarrierDemo2 {
    static CyclicBarrier barrier = new CyclicBarrier(2, new After());

    public static void main(String[] args) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("In thread");
                try {
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }.start();

        System.out.println("In main");
        try {
            barrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Finish.");
    }

    static class After implements Runnable {
        @Override
        public void run() {
            System.out.println("All reach barrier.");
        }
    }
}
/*
输出:
In main  // main线程到达屏障之后会被阻塞
In thread
All reach barrier.  // thread到达屏障之后会执行After的run
Main finish  // 然后被阻塞的main线程和thread线程才会继续执行下去
Thread finish
*/
CountDownLatch 与 CyclicBarrier 的区别
  • CountDownLatch 的计数器不能重用。
  • 自动重置,回调函数功能等等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值