0%

Java 并发总结(一):线程安全的机制

1. 多线程开发的两个核心问题

  • 竞争协作
  • 管理竞争和协作是复杂的,所以 Java 提供了更高层次的服务,比如并发容器异步任务执行服务

2. 线程安全的机制有哪些

  • 线程表示一条单独的执行流,每个线程有自己的执行计数器,有自己的,但可以共享内存

  • 实现线程协作的基础是共享内存

  • 但共享内存有两个问题:竞态条件内存可见性。解决这些问题的思路有

    • synchronized

      • synchronized 只是一个关键字,简单易用。在大部分情况下,放到类的方法声明上就可以了。既可以解决竞态条件问题,也可以解决内存可见性问题
      • synchronized 保护的是对象,不是代码。只有对同一个对象的 synchronized 方法调用,synchronized 才能保证它们被顺序调用。对于实例方法,这个对象是 this;对于静态方法,这个对象是类对象;对于代码块,需要指定哪个对象
      • synchronized 不能尝试获取锁也不响应中断,还可能会死锁。不过,相比显式锁,synchronized 隐式锁简单易用,JVM 也可以不断优化它的实现。实际开发中,synchronized 应该被优先使用
    • 显式锁

      • 显式锁是相对于 synchronized 隐式锁而言的,它可以实现 synchronized 同样的功能,但需要程序员自己创建锁,调用锁相关的接口,主要接口是 Lock,主要实现类是 ReentrantLock
      • 相比 synchronized,显式锁支持以非阻塞式方式获取锁、可以响应中断、可以限时、可以指定公平性、可以解决死锁问题,这使得显式锁灵活很多
      • 读多写少读操作可以完全并行的场景中,可以使用读写锁以提高并发度,读写锁的接口是 ReadWriteLock,实现类是 ReentrantReadWriteLock
    • volatile

      • synchronized 和显式锁都是锁,使用锁可以实现安全,但使用锁是有成本的,获取不到锁的线程还需要等待,会有线程的上下文切换开销等
      • 保证安全不一定非要锁。如果共享的对象只有一个,操作也只是进行最简单的 get()/set() 操作,set() 也不依赖于之前的值,那就不存在竞态条件问题,而只有内存可见性问题。这时,在变量的声明上加上 volatile 关键字就可以了。volatile 可以解决内存可见性问题
    • 原子变量和 CAS

      • 使用 volatileset() 的新值不能依赖于旧值,但很多时候,set() 的新值与原来的值有关。这时,也不一定需要锁,如果需要同步的代码比较简单,可以考虑原子变量,它们包含了一些以原子方式实现组合操作的方法。对于并发环境中的计数产生序列号等需求,考虑使用原子变量而非锁
      • 原子变量的基础是 CAS,一般的计算机系统都在硬件层次上直接支持 CAS 指令。通过循环 CAS 的方式实现原子更新是一种重要的思维。相比 synchronized,它是乐观的,而 synchronized 是悲观的;它是非阻塞式的,而 synchronized 是阻塞式的
      • CAS 是 Java 并发包的基础,基于它可以实现高效的、乐观的、非阻塞式的数据结构和算法。CAS 也是并发包中锁、同步工具和各种容器的基础
    • 写时复制

      • 之所以会有线程安全的问题,是因为多个线程并发读写同一个对象,如果每个线程读写的对象都是不同的,或者,如果共享访问的对象是只读的、不能修改,那也就不存在线程安全问题了
      • 写时复制就是将共享访问的对象变为只读的,写的时候再使用锁,保证只有一个线程写。写的线程不是直接修改原对象,而是新创建一个对象,对该对象修改完毕后,再原子性地修改共享访问的变量,让它指向新的对象
    • ThreadLocal

      • 线程本地变量 ThreadLocal 就是让每个线程,对同一个变量,都有自己的独有副本。每个线程实际访问的对象都是自己的,自然也就不存在线程安全问题了
-------------------- 本文结束感谢您的阅读 --------------------