Java线程安全 - 线程(3)

时间:2021-01-28 18:15:59

1. Java中的线程安全

《Java Concurrency In Practice》的作者Brian Goetz对Java中的“线程安全”有一个比较恰当的定义:

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调度方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的

原文:

A class is thread-safe if it behaves correctly when accessed from multiple threads, regardless of the scheduling or interleaving of the execution of those threads by the runtime environment, and with no additional synchronization or other coordinationg on the part of the calling code.

简单来说:

当多个线程访问一个对象时,如果不用考虑任何其它情况,调用这个对象的任何行为都会获得正确的结果,那么这个对象就是线程安全的

注意:

上面的定义自始至终都一致的是在说:一个对象
“行为”是说:这个对象暴露出来的所有可被调用的行为(是线程安全的)。

也就是说,你不能通过重新赋值来换了一个新的对象,然后这个对象说不安全。这是两个维度的事情,这种情况属于你没有理解线程安全的定义,再理解一遍定义!


2. 共享数据

讨论线程安全必须有个前提:“多个线程访问共享资源”,如果没有线程共享资源,那么没有必要讨论

可以将Java中操作的共享数据分为5类:

1. 不可变

Java中不可变 (immutable) 对象一定是线程安全的,这是因为Java语言规定了如果一个不可变的对象正确的被构建出来,那么其外部可见状态永远也不会改变————>也就是说:它自身的行为不会对对象自身造成影响

两种不可变类型如下:

  1. 基本类型:使用final关键字修饰后,再多的线程能够调用的行为也只是取到一个固定的值

  2. 对象类型:需要保证你正确构建了一个不可变对象
    比如String,Sun就保证它自身的行为不会对对象自身造成影响,因为substring()、replace()这些都没有真正改变对象原来的值,只是新构建了一个字符串对象

2. 绝对线程安全

Java中要保证绝对线程安全的话,开销太大,所以没有绝对线程安全数据。

3. 相对线程安全

相对线程安全是我们通常所说的“线程安全”

“相对”是指的“对于一些特定情况的调用,可能还需要调用端使用额外的手段来保证”

比如Vector、Hashtable等,它们的介绍说其本身是“线程安全”的,但是在一些特定情况(这些情况并不算罕见)还是会出现不安全的现象

4. 线程兼容

对象本身线程不安全,但是可以在调用端使用同步手段来保证线程安全

平时所说一个类是线程不安全的,就是说这种情况

举例和3对立的ArrayList、HashMap等

5. 线程对立

指的是在调用端也不能手段来保证线程安全,Java中有,但是很少,且应该避免出现这种代码


3. 线程安全的实现方法

1. 互斥同步

互斥同步 - Mutual Exclusion $ Synchronization,是常见的一种并发正确定保障手段

同步是指:多线程并发访问共享数据时,保证共享数据同一时刻只被一个线程使用
互斥是指:实现同步的一种手段

互斥是因,同步是果;互斥是方法,同步是目的

  1. synchronized关键字
    会在class中同步块的前后分别生成monitorenter和monitorexit两个字节码指令,这两个字节码指令都需要一个reference类型的参数来指明要锁定和解锁的对象

    1. 显示的在synchronized中指定对象
    2. 如果没有指定,实例方法就是本对象;类方法就是本Class对象
  2. java.util.concurrent.locks.ReentrantLock(重入锁)
    基本是synchronized的对象增强类型,也就是说这是对象而synchronized是原声语句,并且多了一些高级功能

优先考虑synchronized就可以了,JDK1.6以后他们性能持平,预测越往后synchronized会提供更多优化

2. 非阻塞同步

暂且不表

3. 无同步方案

保证线程安全,并不一定要同步,这两者没有因果关系

依据这个,可以将一些需要同步的共享数据,强行设计成“非共享数据”(还是共享数据)

  1. 可重入代码(Reentrant Code)
    暂且不表

  2. 线程本地存储(Thread Local Storage)
    如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?
    如果能的话,就能把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程安全

    Java中就有线程本地存储功能

    java.lang.ThreadLocal类

    我觉得叫ThreadLocalVar更合适,因为它代表的不是Thread,是一个线程局部变量)

    每一个Thread对象都有一个ThreadLocalMap对象,这个对象存储了一组以ThreadLocal.threadLocalHashCode为Key,以本地线程变量为值的K-V值对,ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,每一个ThreadLocal对象都包含了一个独一无二的threadLocalHashCode值,使用这个值就可以在线程K-V值对中找回对应的本地线程变量

    用法之后再表


4. Java中互斥同步相关概念

关于同步,以及线程中的 对象、监视器、锁、线程的关系可以看我另外一篇博文

《Java线程和线程同步 - 线程(2)》
《Java线程模型、线程状态 - 线程(1)》


参考文献:

[ 1 ] 周志明.深入理解Java虚拟机[M].第2版.北京:机械工业出版社,2015.8.
[ 2 ] Tim Lindholm,Frank Yellin,Gilad Bracha,Alex Buckley.The Java® Virtual Machine Specification . Java SE 8 Edition . 英文版[EB/OL].2015-02-13.
[ 3 ] James Gosling,Bill Joy,Guy Steele,Gilad Bracha,Alex Buckley.The Java® Language Specification . Java SE 8 Edition . 英文版[EB/OL].2015-02-13.