Java编写线程安全代码的建议

要编写线程安全的代码,核心在于对状态的访问操作进行管理,特别是对共享和可变的状态访问,主要有3种实现方式:

  • 不在线程之间共享该状态变量
  • 将状态变量修改为不可变的变量
  • 在访问状态时使用同步
1.Java语言提供的支持

Java语言提供了synchronized/final/volatile 3个关键来辅助实现编写线程安全的代码

  • sychronized关键字,既保证可见性,也保证原子性

  • volatile关键字,只能保证可见性,不能保证原子性。当且仅当满足以下条件的时候,才应该使用volatile

    • 对可变变量的写入操作不依赖变量的当前值,或者确保只有单个线程会更新变量的值
    • 该变量不会与其它状态变量一起纳入不变性条件中
    • 在访问变量时不需要加锁
  • final关键字,用于构造不可变对象,final类型的域是不能改变的(如果final域所引用的对象是可变的,则这些被引用的对象是可以修改的),针对的是第二条,不可变性

2.在程序设计的一些建议

线程封闭:是指仅在单个线程内访问数据的技术,需要在程序设计中来考虑,可以使用局部变量或ThreadLocal来实现

  • 栈封闭:只能通过局部变量才能访问对象,Java基础类型的局部变量,由其语义来保证始终封闭在线程内部;局部变量的引用,需要在程序设计的时候来确保被引用的对象不会逸出
  • ThreadLocal类:能使线程中的某个值与保存值的对象关联起来,用来防止对可变的单实体变量或全局变量进行共享

在并发程序中使用和共享对象时,可以使用一些实用的策略,包括

  • 线程封闭:线程封闭的对象只有一个线程拥有,对象封闭在线程中,并且只能由这个线程来修改
  • 只读共享:在没有额外同步的情况下,共享的只读对象可以由多个线程并发访问,但任何线程都不能修改。共享的只读对象包含不可变对象(某个对象创建后其状态不能被修改)和事实不可变对象(从技术上看是可变的,但是其状态在发布后不会再改变)
  • 线程安全共享:线程安全的对象在其内部实现同步,因此多个线程可以通过对象的公有接口来进行访问,而不需要进一步的同步
  • 保护对象:被保护的对象只有通过持有特定的锁来访问,保护对象包括封装在其它线程安全对象中的对象,以及已发布并且由某个特定锁保护的对象
3.并发编程的技巧

并发编程技巧:

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

以JVM 1.8为例说明,以下内容摘自JVM 文档
Java编写线程安全代码的建议

4.1 单个线程私有
  • pc(program counter) register:程序计数器,每个线程私有,保存着非native方法的地址
  • Natvie Method Stack:native方法栈,每个线程私有
  • JVM Stacks:每个线程都拥有一个私有的JVM栈,Java虚拟机栈是由一个个栈帧组成的,每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息,其中局部变量包括基础类型和对象引用(使用句柄或直接指针访问Heap区保存的实例对象)

Java虚拟机栈会出现2种错误:

  • StackOverflowError:如果Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的尝试超过了当前Java虚拟机栈最大尝试时,就会抛出此错误
  • OutOfMemoryError:如果Java虚拟机堆中没有空闲内存,并且垃圾回收器也无法提供更的内存,则会抛出此错误
4.2 所有线程共享
  • Heap:JVM有一个堆区,在JVM启动的时候创建,所有线程共享,保存着所有被创建的类实例和数组
  • Method Area:方法区,JVM里面定义的一个逻辑概念,保存方法的区域。JVM启动的时候创建,所有线程共享,从逻辑上说是Heap的一部分
  • Run-Time Constants Pool:运行时常量池,从方法区申请的一块区域

关于Method Area和Metaspace的一些说明,可以参考is-method-area-still-present-in-java-8
Method Area是JVM规范文档里面定义的一个概念,换句话说就是保存的non-java-objects

It stores per-class structures such as the run-time constant pool,
field and method data, and the code for methods and constructors, including
the special methods (§2.9) used in class and instance initialization and interface
initialization.

HotSpot JVM中对内存结构有明确的区分,Old Gen只包含了Java objects(Java objects formerly stored in Perm Gen have been moved to Old Gen),metaspace包含了JVM相关的数据和非java对象,Method Area里面定义的内容,只是metaspace的一部分