java 中的共享数据分为5种:不可变、绝对线程安全、相对线程安全、线程兼容与线程对立
名称 | 内容 |
---|---|
不可变 | 一个不可变的对象只要被正确地构建,则其外部可见状态永远不会改变(如果数据为一个对象,则要保证对象的行为不会对值产生任何影响) |
绝对线程安全 | 不论运行时环境如何,调用方都不需要任何额外的同步措施。此等级过于严格,API 中很多线程安全类都做不到 |
相对线程安全 | 该级别为通常意义上的线程安全。保证对对象的单独操作是线程安全的,但对于某些特定顺序的连续调用可能需要在调用端使用额外的同步手段保证调用正确性 |
线程兼容 | 该级别为通常所说的非线程安全。对象本身不是线程安全的,但是可通过在调用端正确使用同步手段保证对象在并发环境中安全使用 |
线程对立 | 指无论采取何种措施,多无法保证多线程环境下的线程安全使用。在 Java 中很少出现,且通常有害(如 suspend() 与 resume(),已经被 JDK 废弃) |
线程安全的实现方法
序号 | 内容 |
---|---|
1 | 互斥同步 |
2 | 非阻塞同步 |
3 | 无同步方案 |
互斥同步:
最基本手段:synchronized 关键字
该关键字经过编译后会在同步块前形成 monitorenter 指令,在同步块后形成 monitorexit 指令。指令需要 reference 类型的参数指定需要锁定和解锁的对象。如果 synchronized 明确指定了对象参数,就是该对象的 reference; 没有明确指定,就根据 synchronized 修饰的是实例方法还是类方法去取对应的对象实例或 class 对象作为锁对象。
执行 monitorenter 指令时,尝试获取对象锁,将锁的计数器加1;执行 monitorexit 指令时,计数器减1。计数器为0时,锁被释放。若获取锁失败,则当前线程阻塞等待,直到锁被释放为止。
synchronized 同步块对于同一线程而言可以重入,并且在一个线程执行完之前会阻塞其它线程进入。由于会频繁涉及到用户态与核心态的转换,所以 synchronized 是一个重量级操作,只有在必要情况下才建议使用。
轻量级手段: ReentrantLock(重入锁)
该方法位于 java.util.concurrent 包下,与 synchronized 用法相似,同样具备线程重入特性。区别是 synchronized 为 API 层面的互斥锁,ReentrantLock 为原生语法层面的互斥锁。
ReentrantLock 增加的高级功能为:
序号 | 名称 |
---|---|
1 | 等待可中断 |
2 | 可实现公平锁 |
3 | 锁可以绑定多个条件 |
等待可中断:持有锁的线程长期不释放锁时,等待的线程可以放弃等待,去处理其它事项。
公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序依次获得锁(synchronized 为非公平锁,锁释放时所有线程都有机会获得;ReentrantLock 默认非公平,可以通过带boolean值的构造函数要求使用公平锁)。
锁绑定多个条件:一个 ReentrantLock 可绑定多个 Condition 对象,只需要多次调用 newCondition() 方法来实现。(synchronized 中,锁对象的 notify()、wait() 以及 notifyAll() 方法能实现一个隐含条件,如果要与多于一个条件关联时,需要额外添加锁。)
JVM的性能改进偏向于优化 synchronized,且 JDK 1.6之后二者性能基本无差异,因此建议优先选择 synchronized。
非阻塞同步:
互斥同步属于悲观并发策略,在线程阻塞与唤醒之间消耗大量时间资源;而非阻塞同步属于乐观并发策略,其方式为基于冲突检测。
也就是,首先尝试进行操作,如果没有其它线程竞争,则成功;如果产生冲突,则不断重试直到成功。此种策略许多实现不需要将线程挂起,因此称为非阻塞同步。由于操作与冲突检测这两个步骤需要原子性,所以需要靠硬件指令集的发展来保证。
常见处理器指令:
序号 | 指令名称 |
---|---|
1 | 测试并设置(Test-and-Set) |
2 | 获取并增加(Fetch-and-Increment) |
3 | 交换(Swap) |
4 | 比较并交换(Compare-and-Swap)简称 CAS |
5 | 加载链接/条件存储(Load-Linked/Store-Conditional)简称 LL/SC |
CAS 需要的三个操作数:变量的内存地址(V)、旧的预期值(A)、新值(B)
指令执行时,只有 V 符合 A 时,处理器才会用 B 更新 V 的值,否则就不执行更新。当时无论更新与否,都会返回 V 的旧值。该过程为一个原子操作。
CAS 的一个缺陷是,如果 V 初次读取为 A,中途被修改为 B , 此后又被改回为 A,CAS 操作会误认为从未被修改。该漏洞称为 CAS 的“ABA”问题。
针对该漏洞, java并发包提供带标记的原子引用类 “AtomicStampedReference” 来控制变量值版本来保证 CAS 的正确性。
无同步:
可重入代码:此种亦称为“纯代码”,可在执行中的任意时刻中断,去执行其它任意代码。重新获得控制权后,程序不会出现任何错误。所有可重入代码都是线程安全的,但并非线程安全的代码都是可重入的。
此类代码的特征是:不依赖存储于堆中的数据,使用的状态量由参数中传入,不调用非可重入方法等。简单的判断原则:如果一个方法输入相同的参数返回相同的可预测结果,就满足可重入性要求,也就是线程安全。
线程本地存储:如果需要共享的数据代码能够控制在同一个线程中执行,就能够把这些数据的可见范围限制在同一线程以内,由此无需同步也可保证没有数据争夺问题。Web 交互模型中的 Thread-per-Request (一个请求对应一个服务器线程)就是使用此种方式解决线程安全问题。
java 中,变量要被线程独享时可通过 java.lang.ThreadLocal 类实现线程本地存储功能。每一个线程的Thread 对象中都有一个 ThreadLocalMap 对象,该对象存储了一组以 ThreadLocal.threadLocalHashCode 为键,以本地线程变量为值的键值对。ThreadLocal 对象就像是当前线程的 ThreadLocalMap 的访问入口,每个对象都包含了独一无二的 HashCode,通过该值可以在键值对中找回对应的本地变量值。