深入java虚拟机-第五部分-高效并发
第13章 线程安全与锁优化
并发处理 的 广泛应用 是使得 Amdah1定律 代替 摩尔定律 成为 计算机性能发展源动力 的根本原因,也是人类 “压榨” 计算机运算能力 的 最有力武器。
概述
在 软件业发展 的 初期,程序编写 都是以 算法 为核心的,
程序员会把 数据 和 过程 分别作为 独立的部分 来考虑,
数据 代表 问题空间 中的 客体,程序代码 则用于 处理这些数据,
这种 思维方式 直接站在 计算机的角度 去 抽象问题和解决问题,
称为 面向过程 的 编程思想。
与此同时,
面向对象 的 编程思想 是站在 现实世界的角度 去 抽象和解决问题,
它把 数据和行为 都看做是 对象的一部分,
这样可以让程序员能以 符合现实世界的 思维方式 来 编写和组织程序。
面向过程 的 编程思想 极大地 提升了 现代软件开发 的 生产效率 和 软件可以达到的规模,
但是 现实世界与计算机世界 之间不可避免地 存在一些差异。
例如,人们很难想象现实中的 对象 在一项工作进行期间,会被不停地中断和切换,对象的属性(数据)可能会在 中断期间 被修改和变“脏”,
而这些事件 在计算机世界中 则是很正常的事情。
有时候,良好的设计原则 不得不向现实做出一些让步,我们必须让 程序 在计算机中 正确无误地运行,
然后再考虑如何将 代码组织得更好,让 程序 运行得 更快。
对于这部分的主题“高效并发”来讲,首先需要保证 并发的正确性,然后在此基础上实现 高效。
本章先从 如何保证并发的正确性 和 如何实现线程安全讲起。
线程安全
“线程安全” 这个名称,相信稍有经验的程序员都会听说过,甚至在 代码编写 和 走查的时候 可能还会将会挂在嘴边,
但是如何找到一个不太拗口的概念 来定义 线程安全 却不是一件容易的事情,
笔者尝试在Google中搜索它的概念,找到的是类似于 “如果一个对象可以安全地被多个线程同时使用,那它就是线程安全的”这样的定义------并不能说它不正确,但是人们无法从中获取到任何有用的信息。
(像我这种小白,也只是,听过,我可没有说过😶😶😶)
笔者认为《Java Concurrency In Practice》 的作者 Brian Goetz 对 “线程安全” 有一个比较恰当的定义:
“当 多个线程 访问 一个对象时,如果不用考虑这些 线程 在 运行时环境 下的 调度 和 交替执行,也不需要进行 额外的同步,或者在 调用方 进行任何其他的 协调操作,调用这个 对象的行为 都可以获得 正确的结果,那 这个对象 是 线程安全 的”。
这个定义比较 严谨,它要求 线程安全 的代码 都必须具备一个特征:
代码本身 封装 了所有必要的 正确性保障手段(如互斥同步等),
令 调用者 无需关心 多线程 的问题,更无须自己采取 任何措施 来 保证多线程的正确调用。
这点听起来简单,但其实并不容易做到,在大多数场景中,我们都会将这个定义弱化一些,如果把”调用这个对象的行为“限定为”单次调用“,
这个定义的其他描述也能够成立的话,我们就可以称它是 线程安全 了,为什么要 弱化 这个定义,现在暂且放下,稍后再详细探讨。
Java语言中的线程安全
我们已经有了 线程安全 的一个 抽象定义,
那接下来就讨论一下在 java语言 中,线程安全 具体是如何体现的?
有哪些操作是 线程安全 的?
我们这里讨论的 线程安全,就 限定与 多个线程之间 存在 共享数据访问 这个 前提,
因为如果一段代码 根本不会 与其他线程共享数据,那么从 线程安全 的 角度 来看,程序是 串行执行 还是 多线程执行 对它来说是完全没有区别的。
为了更加 深入地 理解 线程安全,在这里我们可以不把 线程安全 当做一个非真即假 的 二元排他项 来看待,
按照 线程安全 的 “安全程度” 由强至弱来排序,我们可以将Java语言中各种操作共享的数据分为以下5类:
不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
这种划分方法也是 Brian Goetz在IBM developWorkers上发表的一篇论文中提出的,
这里写 “我们” 纯粹是笔者下笔行文中的语言用法。
1.不可变
在Java语言中(特指JDK1.5以后,即 Java内存模型 被 修正以后的 Java语言),
不可变(Immutable) 的 对象 一定是 线程安全的,
无论是 对象的方法 还是 方法的调用者,都不需要再采取 任何的线程安全保障措施,
在12章我们谈到 final关键字 带来的 可见性 时曾经提到过这一点,
只要一个 不可变的对象 被正确地 构建出来(没有发生 this 引用逃逸 的情况),
那其外部的 可见状态 永远也不会改变,
永远也不会看到它在 多个线程之中 处于 不一致 的状态。
“不可变” 带来的 安全性 是 最简单和最纯粹的。
Java语言中,如果 共享数据 是一个基本数据类型,那么只要在 定义时 使用 final关键字 修饰它 就可以保证它是 不可变的。
如果 共享数据 是一个对象,那就需要保证 对象的行为 不会对其 状态 产生任何影响 才行,
如果读者还没想明白这句话,不妨想一想 java.lang.String 类的对象,它是一个典型的 不可变对象,
我们调用它的 substring()、replace()和concat() 这些方法都 不会影响它 原来的值,只会返回一个新构造的字符串对象。
保证对象行为 不影响 自己状态的途径 有很多种,
其中最简单的就是把对象中 带有状态的变量 都声明为 final,
这样在 构造函数 结束之后,它就是 不可变的,
例如代码清单13-1中java.lang.Integer 构造函数所示的,
它通过将 内部状态变量 value 定义为 final 来保障 状态不变。
代码清单 13-1 JDK中Integer类的构造函数
----------------------------------------------------------------------------------------------
/**
* The value of the <code>Integer</code>.
* @serial
*/
private final int value;
/**
* Constructs newly allocated <code>Integer</code> object that
* represents the specified <code>int</code> value.
*
* @param value the value to be represented by the <code>Integer</code> object.
*
*/
public Integer(int value){
this.value=value;
}
----------------------------------------------------------------------------------------------
在 Java API 中 符合 不可变要求 的类型,
除了上面提到的 String 之外,
常用的还有 枚举类型,以及 java.lang.Number的部分子类,
如Long和Double等数值包装类型,BigInteger和BigDecimal等大数据类型;
但同为 Number 的 子类型 的 原子类 AtomicInteger 和 AtomicLong 则并非不可变的,
读者不妨看看这两个原子类的源码,想一想为什么。
(emm,原因是不是那个value没有被标为final?Integer类当中的value被标记为final了,而AtomicInteger以及AtomicLong里面的value都没有被标记为final)
AtomicInteger类代码:
代码清单 AtomicInteger类
----------------------------------------------------------------------------------------------
/*
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
/*
* Written by Doug Lea with assistance from members of JCP JSR-166
* Expert Group and released to the public domain, as explained at
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package java.util.concurrent.atomic;
import java.util.function.IntUnaryOperator;
import java.util.function.IntBinaryOperator;
import sun.misc.Unsafe;
/**
* An {@code int} value that may be updated atomically. See the
* {@link java.util.concurrent.atomic} package specification for
* description of the properties of atomic variables. An
* {@code AtomicInteger} is used in applications such as atomically
* incremented counters, and cannot be used as a replacement for an
* {@link java.lang.Integer}. However, this class does extend
* {@code Number} to allow uniform access by tools and utilities that
* deal with numerically-based classes.
*
* @since 1.5
* @author Doug Lea
*/
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
/**
* Eventually sets to the given value.
*
* @param newValue the new value
* @since 1.6
*/
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* <p><a href="package-summary.html#weakCompareAndSet">May fail
* spuriously and does not provide ordering guarantees</a>, so is
* only rarely an appropriate alternative to {@code compareAndSet}.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful
*/
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
/**
* Atomically decrements by one the current value.
*
* @return the previous value
*/
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* Atomically decrements by one the current value.
*
* @return the updated value
*/
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the previous value
* @since 1.8
*/
public final int getAndUpdate(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the updated value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the updated value
* @since 1.8
*/
public final int updateAndGet(IntUnaryOperator updateFunction) {
int prev, next;
do {
prev = get();
next = updateFunction.applyAsInt(prev);
} while (!compareAndSet(prev, next));
return next;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the previous value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the previous value
* @since 1.8
*/
public final int getAndAccumulate(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the updated value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the updated value
* @since 1.8
*/
public final int accumulateAndGet(int x,
IntBinaryOperator accumulatorFunction) {
int prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsInt(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
/**
* Returns the String representation of the current value.
* @return the String representation of the current value
*/
public String toString() {
return Integer.toString(get());
}
/**
* Returns the value of this {@code AtomicInteger} as an {@code int}.
*/
public int intValue() {
return get();
}
/**
* Returns the value of this {@code AtomicInteger} as a {@code long}
* after a widening primitive conversion.
* @jls 5.1.2 Widening Primitive Conversions
*/
public long longValue() {
return (long)get();
}
/**
* Returns the value of this {@code AtomicInteger} as a {@code float}
* after a widening primitive conversion.
* @jls 5.1.2 Widening Primitive Conversions
*/
public float floatValue() {
return (float)get();
}
/**
* Returns the value of this {@code AtomicInteger} as a {@code double}
* after a widening primitive conversion.
* @jls 5.1.2 Widening Primitive Conversions
*/
public double doubleValue() {
return (double)get();
}
}
----------------------------------------------------------------------------------------------
AtomicLong类代码:
代码清单 AtomicLong类
----------------------------------------------------------------------------------------------
/*
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
/*
* Written by Doug Lea with assistance from members of JCP JSR-166
* Expert Group and released to the public domain, as explained at
* http://creativecommons.org/publicdomain/zero/1.0/
*/
package java.util.concurrent.atomic;
import java.util.function.LongUnaryOperator;
import java.util.function.LongBinaryOperator;
import sun.misc.Unsafe;
/**
* A {@code long} value that may be updated atomically. See the
* {@link java.util.concurrent.atomic} package specification for
* description of the properties of atomic variables. An
* {@code AtomicLong} is used in applications such as atomically
* incremented sequence numbers, and cannot be used as a replacement
* for a {@link java.lang.Long}. However, this class does extend
* {@code Number} to allow uniform access by tools and utilities that
* deal with numerically-based classes.
*
* @since 1.5
* @author Doug Lea
*/
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
// setup to use Unsafe.compareAndSwapLong for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
/**
* Records whether the underlying JVM supports lockless
* compareAndSwap for longs. While the Unsafe.compareAndSwapLong
* method works in either case, some constructions should be
* handled at Java level to avoid locking user-visible locks.
*/
static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();
/**
* Returns whether underlying JVM supports lockless CompareAndSet
* for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
*/
private static native boolean VMSupportsCS8();
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicLong.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile long value;
/**
* Creates a new AtomicLong with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicLong(long initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicLong with initial value {@code 0}.
*/
public AtomicLong() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final long get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(long newValue) {
value = newValue;
}
/**
* Eventually sets to the given value.
*
* @param newValue the new value
* @since 1.6
*/
public final void lazySet(long newValue) {
unsafe.putOrderedLong(this, valueOffset, newValue);
}
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final long getAndSet(long newValue) {
return unsafe.getAndSetLong(this, valueOffset, newValue);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* <p><a href="package-summary.html#weakCompareAndSet">May fail
* spuriously and does not provide ordering guarantees</a>, so is
* only rarely an appropriate alternative to {@code compareAndSet}.
*
* @param expect the expected value
* @param update the new value
* @return {@code true} if successful
*/
public final boolean weakCompareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final long getAndIncrement() {
return unsafe.getAndAddLong(this, valueOffset, 1L);
}
/**
* Atomically decrements by one the current value.
*
* @return the previous value
*/
public final long getAndDecrement() {
return unsafe.getAndAddLong(this, valueOffset, -1L);
}
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the previous value
*/
public final long getAndAdd(long delta) {
return unsafe.getAndAddLong(this, valueOffset, delta);
}
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final long incrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
}
/**
* Atomically decrements by one the current value.
*
* @return the updated value
*/
public final long decrementAndGet() {
return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
}
/**
* Atomically adds the given value to the current value.
*
* @param delta the value to add
* @return the updated value
*/
public final long addAndGet(long delta) {
return unsafe.getAndAddLong(this, valueOffset, delta) + delta;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the previous value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the previous value
* @since 1.8
*/
public final long getAndUpdate(LongUnaryOperator updateFunction) {
long prev, next;
do {
prev = get();
next = updateFunction.applyAsLong(prev);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function, returning the updated value. The
* function should be side-effect-free, since it may be re-applied
* when attempted updates fail due to contention among threads.
*
* @param updateFunction a side-effect-free function
* @return the updated value
* @since 1.8
*/
public final long updateAndGet(LongUnaryOperator updateFunction) {
long prev, next;
do {
prev = get();
next = updateFunction.applyAsLong(prev);
} while (!compareAndSet(prev, next));
return next;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the previous value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the previous value
* @since 1.8
*/
public final long getAndAccumulate(long x,
LongBinaryOperator accumulatorFunction) {
long prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsLong(prev, x);
} while (!compareAndSet(prev, next));
return prev;
}
/**
* Atomically updates the current value with the results of
* applying the given function to the current and given values,
* returning the updated value. The function should be
* side-effect-free, since it may be re-applied when attempted
* updates fail due to contention among threads. The function
* is applied with the current value as its first argument,
* and the given update as the second argument.
*
* @param x the update value
* @param accumulatorFunction a side-effect-free function of two arguments
* @return the updated value
* @since 1.8
*/
public final long accumulateAndGet(long x,
LongBinaryOperator accumulatorFunction) {
long prev, next;
do {
prev = get();
next = accumulatorFunction.applyAsLong(prev, x);
} while (!compareAndSet(prev, next));
return next;
}
/**
* Returns the String representation of the current value.
* @return the String representation of the current value
*/
public String toString() {
return Long.toString(get());
}
/**
* Returns the value of this {@code AtomicLong} as an {@code int}
* after a narrowing primitive conversion.
* @jls 5.1.3 Narrowing Primitive Conversions
*/
public int intValue() {
return (int)get();
}
/**
* Returns the value of this {@code AtomicLong} as a {@code long}.
*/
public long longValue() {
return get();
}
/**
* Returns the value of this {@code AtomicLong} as a {@code float}
* after a widening primitive conversion.
* @jls 5.1.2 Widening Primitive Conversions
*/
public float floatValue() {
return (float)get();
}
/**
* Returns the value of this {@code AtomicLong} as a {@code double}
* after a widening primitive conversion.
* @jls 5.1.2 Widening Primitive Conversions
*/
public double doubleValue() {
return (double)get();
}
}
----------------------------------------------------------------------------------------------
2.绝对线程安全
绝对的 线程安全 完全满足 Brian Goetz 给出的 线程安全 的定义,
这个 定义 其实是很 严格的,
一个类 要达到 “不管 运行时环境 如何, 调用者 都不需要 任何额外的 同步措施” 通常需要付出很大的,甚至有时候是不切实际的代价。
在 Java API中 标注自己是 线程安全的类,大多数都不是 绝对的线程安全。
我们可以通过 Java API中 一个不是 “绝对线程安全” 的线程安全类 来看看这里的 “绝对” 是什么意思。
如果说 java.util.Vector 是一个 线程安全 的 容器,
相信所有的Java程序员对此都不会有异议,因为它的 add()、get()和size() 这类方法都是被 synchronized 修饰的,
尽管这样 效率很低,但确实是 安全的。
但是,即使它 所有的方法 都被 修饰成 同步,也不意味着 调用它的时候永远都不需要 同步手段了,
请看一下代码清单13-2中的测试代码。
代码清单 13-2 对Vector线程安全的测试
----------------------------------------------------------------------------------------------
private static Vector<Integer> vector = new Vector<Integer>();
public static void main(String[] args){
while(true){
for( int i = 0; i < 10; i++){
vector.add(i);
}
}
Thread removeThread = new Thread(new Runnable(){
@Override
public void run(){
for(int i = 0; i < vector.size(); i++){
vector.remove(i);
}
}
});
Thread printThread = new Thread(new Runnable(){
@Override
public void run(){
for( int i = 0; i < vector.size(); i++){
System.out.println((vector.get(i)));
}
}
});
removeThread.start();
printThread.start();
//不要同时产生过多的线程,否则会导致操作系统假死
while(Thread.activeCount() > 20);
}
}
----------------------------------------------------------------------------------------------
运行结果如下:
Exception in thread "Thread-132" java.lang.ArrayIndexOutOfBoundsException:
Array index out of range:17
at java.util.Vector.remove(Vector.java:777)
at org.fenixsoft.mulithread.VectorTest$1.run(VectorTest.java:21)
at java.lang.Thread.run(Thread.java:662)
很明显,尽管这里使用到的 Vector的get()、remove()和size() 方法都是 同步的,
但是在 多线程 的环境下,如果不在 方法调用端 做额外的同步措施 的话,
使用这段代码 仍然是不安全的,因为如果另一个线程 恰好在错误的时间里 删除了一个元素,
导致序号 i 已经不再可用的话,
再用 i 访问数组就会 抛出一个ArrayIndexOutOfBoundsException
如果要保证这段代码能正确执行下去,我们不得不把 removeThread和printThread 的定义改成如代码清单13-3所示的样子。
代码清单 13-3 必须加入同步 以保证Vector访问的线程安全性
----------------------------------------------------------------------------------------------
Thread removeThread = new Thread(new Runnable(){
@Override
public void run(){
synchronized(vector){
for( int i = 0; i < vector.size(); i++){
vector.remove(i);
}
}
}
});
Thread printThread = new Thread(new Runnable(){
@Override
public void run(){
synchronized(vector){
for(int i = 0; i < vector.size(); i++){
System.out.println((vector.get(i)));
}
}
}
});
----------------------------------------------------------------------------------------------
3.相对线程安全
相对的 线程安全 就是我们 通常意义上 所讲的 线程安全,
它需要 保证 对这个对象 单独的操作 是 线程安全的,
我们在 调用的时候 不需要做 额外的保障措施,但是对于一些 特定顺序 的 连续调用,
就可能需要在 调用端 使用 额外的同步手段 来保证 调用的正确性。
上面代码清单13-2和代码清单13-3就是 相对线程安全的 明显的 案例。
在Java语言中,大部分的 线程安全类 都属于这种类型,例如 Vector、HashTable、Collections的synchronizedCollection() 方法包装的集合 等。
4.线程兼容
线程兼容 是指 对象本身 并不是 线程安全的,
但是可以通过在 调用端 正确地 使用 同步手段 来保证对象 在并发环境中 可以安全地使用,
我们平常说 一个类不是线程安全的,绝大多数时候指的是这一种情况。
Java API中大部分的类 都是属于 线程兼容的,如与前面的 Vector和HashTable 相对应的集合类 ArrayList和HashMap 等。
5.线程对立
线程对立 是指无论 调用端 是否采取了 同步措施,
都无法在 多线程环境中 并发使用的代码。
由于Java语言天生就具备 多线程特性,线程对立 这种 排斥多线程的代码 时很少出现的,而且通常都是 有害的,应当尽量避免。
一个线程对立 的例子是 Thread 类的 suspend() 和 resume() 方法,
如果有两个线程 同时持有一个线程对象,一个尝试去中断线程,另一个尝试去恢复线程,
如果并发进行的话,无论调用时 是否进行了同步,目标线程都是存在 死锁风险 的,
如果 suspend() 中断的线程 就是 即将要执行 resume() 的那个线程,那就肯定要产生 死锁 了。
也正是由于这个原因, suspend() 和 resume() 方法已经被 JDK 声明废弃(@Deprecated)了。
常见的 线程对立 的操作还有 System.setIn()、System.setOut() 和 System.runFinalizersOnExit()等。
线程安全的实现方法
了解了 什么是线程安全 之后,紧接着的一个问题就是我们应该 如何实现线程安全,
这听起来似乎是一件由代码如何编写来决定的事情,确实,如何实现线程安全 与 代码编写 有很大的关系,
但 虚拟机 提供的 同步和锁机制 也起到了非常重要的作用。
本节中,代码编写如何实现线程安全 和 虚拟机如何实现同步与锁 这两者都会有所涉及,相对而言更偏重后者一些,
只要读者了解了 虚拟机线程安全手段 的运作过程,自己去思考代码如何编写并不是一件困难的事情。
(周大大,你是认真的吗?😭😭😭 说出这句话的时候,考虑过像我这种小白的感受吗?🙃🙃🙃)
1.互斥同步
互斥同步(Mutual Exclusion & Synchronization) 是常见的一种 并发正确性保障手段。
同步 是指在 多个线程 并发访问 共享数据时,保证 共享数据 在同一个时刻 只被一个(或者是一些,使用信号量的时候)线程使用。
而 互斥 是 实现同步 的 一种手段,
临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的 互斥实现方式。
因此,在这4个字里面,互斥 是 因, 同步 是 果;互斥 是 方法,同步 是 目的。
在 Java 中,最基本的 同步互斥 手段就是 synchronized关键字,
synchronized关键字 经过 编译 之后, 会在 同步块 的前后 分别形成 monitorenter和monitorexit 这两个字节码指令,
这两个字节码 都需要一个 reference 类型的参数来指明要 锁定和解锁 的对象。
如果Java程序中的 synchronized明确指定了 对象参数,那就是 这个对象的reference;
如果没有明确指定,那就根据 synchronized 修饰的是实例方法还是类方法,去取对应的 对象实例 或 Class对象 来作为 锁对象。
根据 虚拟机规范 的要求,
在 执行 monitorenter 指令时,
首先 要尝试 获取 对象的锁。
如果 这个对象 没被 锁定,
或者 当前线程 已经拥有了 那个对象的锁,
把 锁 的 计数器 加1,
相应的,
在 执行 monitorexit 指令时 会将 锁计数器 减1,
当计数器为0时,锁 就 释放。
如果 获取 对象锁 失败,那 当前线程 就要 阻塞等待,直到 对象锁 被 另外一个线程 释放为止。
在 虚拟机规范 对 monitorenter和monitorexit 的行为描述中,有两点是需要特别注意的。
首先,synchronized同步块 对 同一条线程 来说 是可重入的,不会出现 自己把自己 锁死的问题。
其次,同步块 在 已进入的线程 执行完之前,会 阻塞 后面其他线程的进入。
第12章讲过,Java的线程 是映射到 操作系统 的 原生线程 之上的,如果要 阻塞或唤醒 一个线程,都需要 操作系统来帮忙完成,
这就需要从 用户态 转换到 核心态 中,因此 状态转换 需要耗费 很多的处理器时间。
对于 代码简单的 同步块(如被 synchronized修饰的 getter() 或 setter() 方法),
状态转换 消耗的时间 有可能比 用户代码 执行的时间 还要长。
所以 synchronized 是Java语言中一个 重量级(Heavyweight)的操作,有经验的程序员都会在 确实必要 的情况下 才使用这种操作。
而 虚拟机本身 也会进行一些 优化,
譬如在通知 操作系统阻塞线程 之前 加入一段 自旋等待过程,避免频繁地 切入到 核心态 之中。
除了 synchronized 之外,我们还可以使用 java.util.concurrent(下文称J.U.C) 包中的 重入锁(ReentrantLock) 来 实现同步,
在基本用法上, ReentrantLock与synchronized 很相似,
它们都具备一样的 线程重入特性,只是代码写法上有点区别,
一个表现为 API层面的 互斥锁( lock()和unlock()方法 配合 try/finally语句块 来完成),
另一个表现为 原生语法层面的互斥锁。
不过,相比 synchronized,ReentrantLock 增加了一些 高级功能,
主要有以下3项:
等待可中断、可实现公平锁、以及 锁可以绑定多个条件。
等待可中断 是指 当持有锁的线程 长期不释放锁 的时候,正在 等待的线程 可以选择放弃等待,改为处理其他事情,可中断特性 对处理执行时间非常长的 同步块 很有帮助。
公平锁 是指 多个线程 在等待 同一个锁 时,必须按照 申请锁 的时间顺序 来依次 获得锁;
而 非公平锁 则不保证 这一点,
在 锁 被释放时,任何一个 等待锁的线程 都有机会 获得锁。
synchronized 中 的 锁 是非公平的,ReentrantLock 默认情况下 也是非公平的,但可以通过 带布尔值 的 构造函数 要求使用 公平锁。
锁绑定多个条件 是指 一个ReentrantLock对象 可以同时 绑定多个Condition对象,
而在 synchronized中, 锁对象 的 wait()和notify()或notifyAll()方法 可以实现一个 隐含的条件,
如果要和 多于一个的条件关联的时候, 就不得不 额外地添加一个锁,
而 ReentrantLock 则无需这样做,只需要 多次调用 newCondition()方法 即可。
如果需要使用上述功能,选用 ReentrantLock 是一个很好的选择,
那如果 是基于 性能考虑呢?
关于 synchronized和ReentrantLock 的 性能问题,
Brian Goetz对这两种锁在 JDK1.5与单核处理器,以及 JDK1.5与双Xeon处理器 环境下做了一组 吞吐量对比的实验,实验结果如图13-1和图13-2所示。
本例中的数据及图片来源于 Brian Goetz 为 IBM developerWorks 撰写的论文:《Java theory and practice:More flexible, scalable locking in JDK5.0》,
原文地址是:http://www.ibm.com/developerworks/java/library/j-jtp10264/?S_TACT= 105AGX53&S_CMP=cn-a-j。
(该链接有效,但是,我看的很是艰难😭😭😭😭救救这苦命的孩子吧,就像当初接入paypal支付🙃🙃🙃🙃)
从图13-1和图13-2可以看出,
多线程环境下 synchronized的吞吐量下降得 非常严重,
而 ReentrantLock 则能基本保持在同一个 比较稳定的水平 上。
与其说 ReentrantLock 性能好,还不如说 synchronized 还有非常大的优化余地。
后续的技术发展也证明了这一点,JDK1.6中加入了很多针对 锁的优化措施(13.3节我们就会讲解这些 优化措施),
JDK1.6发布之后,人们就发现 synchronized与ReentrantLock 的 性能 基本上是 完全持平 了。
因此,如果读者的程序 是使用 JDK1.6 或以上 部署 的话,
性能因素 就不再是 选择ReentrantLock 的理由了,
虚拟机 在 未来的性能改进中 肯定也会更加偏向于 原生的synchronized,
所以还是提倡在 synchronized 能实现需求的情况下,优先考虑使用 synchronized 来进行 同步。
2.非阻塞同步
互斥同步 最主要的问题 就是 进行线程 阻塞和唤醒 所带来的的 性能问题,
因此这种 同步也称为 阻塞同步(Blocking Synchronization)。
从 处理问题的方式 上说,
互斥同步 属于一种 悲观的 并发策略,总是认为 只要不去做 正确的 同步措施(例如加锁),那就肯定会出现问题,
无论 共享数据 是否真的会出现竞争,它都要 进行加锁
(这里讨论的是 概念模型,实际上 虚拟机 会优化很大一部分 不必要的 加锁)、
用户态和心态转换、维护锁计数器 和 检查 是否有 被阻塞的线程 需要唤醒 等操作。
(这也就是所谓的 总有刁民想害朕 了)
随着 硬件指令集 的发展,我们有了另外一个选择:基于 冲突检测 的 乐观并发策略,
通俗地说,就是先进行操作,
如果没有其他线程 争用共享数据,那操作就成功了;
如果 共享数据 有争用,产生了 冲突,那就再采取其他的 补偿措施(最常见的补偿措施就是 不断地重试,直到成功为止),
这种 乐观的并发策略 的许多实现 都不需要把 线程挂起,因此这种 同步操作 称为 非阻塞同步(Non-Blocking Synchronization)。
为什么笔者说使用 乐观并发策略 需要 “硬件指令集的发展” 才能进行呢?
因为我们需要 操作和冲突检测 这两个步骤 具备 原子性,靠什么来保证呢?
如果这里 再使用 互斥同步 来保证就失去意义了,所以我们只能靠 硬件 来完成这件事情,
硬件 保证一个从 语义 上看起来需要多次操作的性能 只通过 一条处理器指令 就能完成,这类指令常用的有:
(我大概一辈子也用不到这些个东西)
- 测试并设置(Test-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap,下文称 CAS)
- 加载链接/条件存储(Load-Linked/Store-Conditional,下文称LL/SC)
(但是还是得努力去试试。不怕一万就怕万一。)
其中,前面的3条 是20世纪就已经存在于 大多数指令集之中的 处理器指令,
后面的两条是 现代处理器 新增的,而且这两条指令的 目的和功能 是类似的。
在 IA64、x86 指令集 中有 cmpxchg指令 完成 CAS功能,
在 sparc-TSO 也有 casa指令 实现,而在 ARM 和 PowerPC 架构下,则需要使用 一对 ldrex/strex 指令 来完成 LL/SC 的功能。
CAS指令 需要有3个操作数,分别是 内存位置(在Java中可以简单理解为 变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。
CAS指令 执行时,当且仅当 V 符合 旧预期值A 时,处理器 用 新值B 更新 V的值,否则它就不执行更新,
但是无论 是否更新了 V的值,都会返回 V的旧值,上述的处理过程是一个 原子操作。
在 JDK1.5 之后,Java程序 中才可以使用 CAS 操作,
该操作由 sun.misc.Unsafe类 里面的 compareAndSwapInt() 和 compareAndSwapLong() 等几个方法包装提供,
虚拟机 在 内部 对这些方法做了特殊处理,
即时编译出来的结果 就是一条 平台相关的 处理器CAS指令,没有方法调用的过程,或者可以认为是无条件 内联 进去了。
这种被 虚拟机 特殊处理 的方法称为 固有函数(Intrinsics),类似的 固有函数 还有Math.sin()等。
由于 Unsafe类 不是提供给 用户程序 调用的类(Unsafe.getUnsafe()的代码中 限制了只有 启动类加载器(Bootstrap ClassLoader) 加载的Class 才能访问它),
因此,如果不采用 反射手段 , 我们只能通过其他的 Java API来间接使用它,如 J.U.C包 里面的 整数原子类,其中的 compareAndSet()和getAndIncrement() 等方法都是用了 Unsafe类 的 CAS操作。
我们不妨拿一段在第12章中没有解决的问题代码来看看 如何使用CAS操作来避免阻塞同步 ,代码如代码清单12-1所示。
我们曾经通过这段20个线程 自增10000次 的代码来证明 volatile变量不具备原子性,那么如何才能让它 具备原子性 呢?
把 "race++"操作或increase() 方法用 同步块 包裹起来当然是一个办法,但是如果改成如代码清单13-4所示的代码,那效率将会提高许多。
代码清单 13-4 Atomic的原子自增运算
----------------------------------------------------------------------------------------------
/**
* Atomic 变量自增运算测试
*
* @author zzm
*/
public class AtomicTest{
public static AtomicInteger race = new AtomicInteger();
public static void increase(){
race.incrementAndGet();
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) throws Exception{
Thread[] threads = new Thread[THREADS_COUNT];
for(int i = 0; i < THREADS_COUNT; i++){
threads[i] = new Thread(new Runnable(){
@Override
public void run(){
for( int i = 0; i < THREADS_COUNT; i++){
increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount() > 1)
thread.yield();
System.out.println(race);
}
}
----------------------------------------------------------------------------------------------
运行结果如下:
200000
使用 AtomicInteger 代替 int 后,程序输出了正确的结果,
一切都要归功于 incrementAndGet()方法 的 原子性。
它的实现 其实非常 简单,如代码清单13-5所示。
代码清单 13-5 incrementAndGet()方法的JDK源码
----------------------------------------------------------------------------------------------
/**
* Atomically increment by one the current value.
* @return the updated value
*/
public final int incrementAndGet(){
for(;;){
int current = get();
int next = current + 1;
if(compareAndSet(current, next)){
return next;
}
}
}
----------------------------------------------------------------------------------------------
incrementAndGet()方法 在一个 无限循环 中,
不断尝试 将一个比当前值 大1的新值 赋值给自己。
如果失败了,那说明在执行 “获取-设置” 操作的时候 值已经有了修改,
于是 再次循环 进行下一次操作,直到设置成功为止。
尽管 CAS 看起来很美,但显然这种操作无法涵盖 互斥同步的所有使用场景,
并且 CAS 从语义上来说 并不是完美的,存在这样一个 逻辑漏洞:
如果一个 变量V 初次读取的时候是 A值,
并且在准备赋值的时候 检查到它仍然为A值,
那我们就能说它的值 没有被其他线程改变过了吗?
如果在这段期间 它的值曾经被改成了B, 后来又被改回A,
那 CAS操作 就会误认为它 从来没有被改变过。
这个 漏洞 称为 CAS操作 的 “ABA问题”。
J.U.C包 为了解决这个问题,提供了一个带有标记的 原子引用类“AtomicStampedReference”,
它可以通过控制 变量值的版本 来保证 CAS的正确性。
不过目前来说这个类 比较“鸡肋”,大部分情况下 ABA问题 不会影响 程序并发的正确性,
如果需要 解决ABA问题,改用传统的 互斥同步 可能会比 原子类更高效。
3.无同步方案
要保证 线程安全,并不是一定就要进行 同步,两者没有 因果关系。
同步 只是保证 共享数据争用时的正确性 的手段,
如果一个方法 本来就不涉及 共享数据,
那它自然就无需 任何同步措施 去保证 正确性,
因此会有一些代码天生就是 线程安全的,笔者简单地介绍其中的两类。
可重入代码(Reentrant Code):
这种代码也叫做 纯代码(Pure Code),
可以在 代码执行的任何时刻 中断它,
转而去 执行另外一段代码(包括 递归调用它本身),
而在 控制权 返回后,
原来的程序 不会出现任何错误。
相对 线程安全 来说,
可重入性 是更基本的特性,
它可以保证 线程安全,
即所有的 可重入 的代码都是 线程安全的,
但是并非所有的 线程安全 的代码都是 可重入的。
可重入代码 有一些 共同的特征,
例如 不依赖存储在堆上的数据 和 公用的系统资源、用到的状态量 都由参数中传入、不调用非可重入的方法等。
我们可以通过一个简单的原则来判断 代码是否具备可重入性:
如果一个方法,它的返回结果是可以 预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足 可重入性 的要求,当然也就是 线程安全的。
线程本地存储(Thread Local Storage):
如果一段代码中 所需要的数据 必须与 其他代码 共享,
那就看看这些 共享数据的代码 是否能保证 在同一个线程中 执行?
如果能保证,我们就可以把 共享数据的可见范围 限制在 同一个线程之内,
这样,无须 同步 也能保证 线程之间 不出现 数据争用 的问题。
符合这种特点的应用并不少见,
大部分使用 消费队列的架构模式(如“生产者-消费者”模式)都会将 产品的消费过程 尽量在 一个线程中 消费完,
其中最重要的一个应用实例就是 经典Web交互模型 中的 “一个请求对应一个服务器线程”(Thread-per-Request) 的处理方式,
这种 处理方式的广泛应用 使得很多 Web服务端应用 都可以使用 线程本地存储 来解决 线程安全问题。
Java语言中,
如果 一个变量 要被 多线程 访问,可以使用 volatile关键字 声明它为 “易变的”;
如果 一个变量 要被 某个线程 独享,Java中就没有类似C++中 __declspec(thread)这样的关键字,
不过还是可以通过 java.lang.ThreadLocal类 来实现 线程本地存储 的功能。
在 Visual C++ 中是 “__declspec(thread)”关键字,而在 GCC 中是“__thread”。
每一个线程的 Thread对象中都有一个 ThreadLocalMap对象,
这个对象 存储了一组 以 ThreadLocal.threadLocalHashCode 为 键 ,以 本地线程变量 为 值 的 K-V值对,
ThreadLocal对象 就是 当前线程的ThreadLocalMap的 访问入口,
每一个 ThreadLocal对象 都包含了 一个独一无二的 threadLocalHashCode值,
使用这个值 就可以在线程 K-V值对 中找回 对应的本地线程变量。
锁优化
高效并发 是从 JDK1.5到JDK1.6的一个 重要改进,
HotSpot虚拟机 开发团队在这个版本上花费了大量的精力去实现 各种锁优化技术,
如 适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等,
这些技术都是为了 在线程之间 更高效地 共享数据,
以及 解决竞争问题,
从而 提高程序的执行效率。
自旋锁与自适应自旋
前面我们讨论 互斥同步 的时候,
提到了 互斥同步 对性能最大的影响 是阻塞的实现,
挂起线程 和 恢复线程 的操作都需要 转入内核态 中完成,
这些操作给系统的 并发性能 带来了很大的压力。
同时,虚拟机 的开发团队也注意到在许多应用上,
共享数据的锁定状态 只会持续很短的一段时间,
为了这段时间 去挂起和恢复线程 并不值得。
如果 物理机器 有一个以上的 处理器,
能让 两个或两个以上的线程 同时并行执行,
我们就可以让 后面请求锁的那个线程 “稍等一下”,
但不放弃 处理器的执行时间,
看看 持有锁的线程 是否很快就会释放锁。
为了让线程等待,我们只需让 线程执行一个忙循环(自旋),这项技术就是所谓的 自旋锁。
自旋锁 在 JDK1.4.2 中就已经引入,
只不过 默认 是 关闭的,
可以使用 -XX:+UseSpinning 参数来开启,
在 JDK1.6 中就已经改为 默认开启了。
自旋等待 不能代替 阻塞,
且先不说对 处理器数量的要求,
自旋等待 本身虽然 避免了线程切换的开销,
但它是要 占用处理器时间的,
因此,如果 锁 被占用的时间 很短,自旋等待 的 效果就会非常好,
反之,如果 锁 被占用的时间 很长,那么 自旋的线程 只会 白白消耗处理器资源,
而不会做 任何有用的工作,
反而会带来 性能上的浪费。
因此,自旋等待的时间 必须要有 一定的限度,
如果 自旋超过了限定的次数 仍然没有 成功获得锁,
就应当使用 传统的方式 去挂起线程了。
自旋次数 的 默认值 是 10次,用户可以使用参数-XX:PreBlockSpin 来更改。
在 JDK1.6 中引入了 自适应的自旋锁。
自适应 意味着 自旋的时间 不再固定了,
而是由 前一次在同一个锁上的自旋时间 及 锁的拥有者的状态 来决定。
如果在 同一个锁对象上,
自旋等待 刚刚成功获得过锁,并且 持有锁的线程 正在运行中,
那么 虚拟机 就会认为 这次自旋 也很有可能再次成功,
进而它将 允许 自旋等待 持续相对更长的时间,比如100个循环。
另外,如果 对于某个锁,自旋 很少成功获得过,
随着 程序运行和性能监控信息 的 不断完善,
虚拟机 对 程序锁 的 状况预测 就会越来越准确,
虚拟机 就会变得 越来越“聪明”了。
锁消除
锁消除 是指 虚拟机 即时编译器 在运行时,
对一些代码上 要求 同步,但是被检测到 不可能存在共享数据竞争的锁 进行 消除。
锁消除 的 主要判定依据 来源于 逃逸分析 的 数据支持(第11章已经讲解过 逃逸分析技术),
如果判断 在一段代码中,堆 上的所有 数据 都不会 逃逸出去 从而被 其他的线程访问到,
那就可以把它们当做 堆上数据对待,
认为它们是 线程私有的,同步加锁 自然也就无须运行。
也许读者会有疑问, 变量 是否逃逸,
对于 虚拟机 来说需要使用 数据流分析 来确定,
但是程序员自己应该时很清楚的,怎么会在 明知道不存在数据争用的情况下 要求同步呢?
答案是有许多 同步措施 并不是程序员自己加的,
同步的代码 在java程序中的 普遍程度 也许超过了大部分读者的想象。
我们来看看代码清单13-6中的例子,这段非常简单的代码 仅仅是输出3个字符串相加的结果,
无论是 源码字面上 还是 程序语义上 都没有同步。
代码清单 13-6 一段看起来没有同步的代码
----------------------------------------------------------------------------------------------
public String concatString(String s1,String s2,String s3){
return s1+s2+s3;
}
----------------------------------------------------------------------------------------------
我们也知道,
由于 String是一个不可变的类,对字符串的连接操作 总是通过生成 新的String对象 来进行的,
因此 Javac编译器 会对 String连接 做 自动优化。
在 JDK1.5 之前,会转化为 StringBuffer对象的连续append()操作,
在 JDK1.5及以后的版本中,会转为 StringBuilder对象的连续append()操作,
即代码清单13-6中的代码 可能会变成代码清单 13-7的样子。
客观地说,既然谈到 锁消除与逃逸分析,那 虚拟机 就不可能是 JDK1.5之前的版本,实际上会转化为 非线程安全的StringBuilder 来完成字符串拼接,并不会加锁, 但这也不影响笔者用这个例子证明 Java对象中同步的普遍性。
代码清单 13-7 Javac转化后的字符串连接操作
----------------------------------------------------------------------------------------------
public String concatString(String s1,String s2,String s3){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
----------------------------------------------------------------------------------------------
现在大家还认为这段代码没有涉及同步吗?
每个 StringBuffer.append() 方法中都有一个 同步块,锁就是sb对象。
虚拟机 观察 变量sb,很快就会发现它的 动态作用域 被限制在 concatString()方法内部。
也就是说,sb的所有引用 永远不会 “逃逸” 到 concatString() 方法之外,
其他线程 无法访问 到 它,
因此,虽然这里有锁,但是可以被 安全地消除掉,
在 即时编译 之后,这段代码就会忽略掉 所有的同步块 而直接执行了。
锁粗化
原则上,我们在编写代码的时候,总是推荐将 同步块的作用范围 限制得 尽量小------只在 共享数据的实际作用域 中才进行同步,
这样是为了 使得需要 同步的操作数量 尽可能 变小,
如果存在 锁竞争,那等待 锁的线程 也能尽快拿到锁。
大部分情况下,上面的原则都是正确的,
但是如果 一系列的连续的动作 都对 同一个对象 反复加锁和解锁,
甚至 加锁操作 是出现在 循环体中的,
那即是没有 线程竞争,频繁地进行互斥同步操作 也会导致 不必要的性能损耗。
代码清单13-7中 连续的append()方法就属于这类情况。
如果 虚拟机 探测到有这样 一串零碎的操作 都对 同一个对象加锁,
将会把 加锁同步的范围 扩展(粗化)到 整个操作序列的外部,
以代码清单13-7为例,就是扩展到第一个append()操作之前 直至 最后一个append()操作之后,
这样只需要 加锁一次 就可以了。
轻量级锁
轻量级锁 是 JDK1.6之中 加入的 新型锁机制,
它名字中的 “轻量级” 是 相对于 使用 操作系统互斥量来实现的传统锁 而言的,
因此 传统的锁机制 就称为 “重量级”锁。
首先需要强调的一点是, 轻量级锁 并不是用来 代替 重量级锁 的,
它的本意 是在 没有多线程竞争的前提下,
减少 传统的重量级锁 使用 操作系统互斥量 产生的性能消耗。
要理解 轻量级锁,
以及后面会讲到的 偏向锁的原理和运作过程,
必须从 HotSpot虚拟机的对象(对象头部分)的内存布局开始介绍。
HotSpot虚拟机的对象头(Object Header)分为两部分信息,
第一部分 用于存储 对象自身的 运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,
这部分数据的长度 在32位和64位的 虚拟机中 分别为 32bit和64bit,官方称它为 “Mark Word”,
它是 实现轻量级锁 和 偏向锁 的 关键。
另一部分 用于存储 指向方法区对象 类型数据的指针,
如果是数组对象的话,还会有一个额外的部分 用于存储 数组长度。
对象头信息 是与 对象自身定义的 数据无关的 额外存储成本,
考虑到 虚拟机的空间效率,
Mark Word 被设计成一个 非固定的数据结构 以便在 极小的空间内 存储尽量多的信息,
它会根据对象的状态 复用 自己的 存储空间。
例如,在32位的HotSpot虚拟机中 对象未被锁定的状态下,
Mark Word的 32bit空间的 25bit 用于存储 对象哈希码(HashCode),
4bit 用于存储 对象分代年龄,
2bit 用于存储 锁标志位,
1bit 固定为 0,
在其他状态(轻量级锁定、重量级锁定、GC标记、可偏向)下 对象的存储内容 见表13-1。
| 表 13-1 HotSpot虚拟机对象头 Mark Word | ||
|---|---|---|
| 存储内容 | 标志位 | 状态 |
| 对象哈希码、对象分代年龄 | 01 | 未锁定 |
| 指向锁记录的指针 | 00 | 轻量级锁定 |
| 指向重量级锁的指针 | 10 | 膨胀(重量级锁定) |
| 空,不需要记录信息 | 11 | GC标记 |
| 偏向线程ID、偏向时间戳、对象分代年龄 | 01 | 可偏向 |
简单地介绍了 对象的内存布局 后,
我们把话题返回到 轻量级锁的执行过程上。
在代码进入 同步块 的时候,
如果此 同步对象 没有被 锁定(锁标志位 为“01”状态),
虚拟机 首先将 在当前线程的 栈帧 中 建立一个名为 锁记录(Lock Record)的空间,
用于存储 锁对象目前的Mark Word的拷贝
(官方把这份拷贝加了一个 Displaced前缀,即 Displaced Mark Word),
这时候 线程堆栈 与 对象头的状态 如图13-3所示。
然后,
虚拟机 将使用 CAS操作 尝试将 对象的Mark Word 更新为指向 Lock Record的指针。
如果这个更新动作 成功了,那么这个线程 就拥有了 该对象的锁,
并且 对象Mark Word 的 锁标志位(Mark Word的最后2bit)将转变为“00”,
即表示此对象处于 轻量级锁定状态,
这时候 线程堆栈与对象头的状态 如图13-4所示。
图13-3和图13-4来源于 HotSpot虚拟机的一位Senior Staff Engineer------Paul Hohensee所写的PPT “The Hotspot Java Virtual Machine”。
如果这个更新动作 失败了,虚拟机 首先会检查 对象的Mark Word 是否指向 当前线程的栈帧,
如果只说明 当前线程已经拥有了这个对象的锁,那就可以直接 进入同步块 继续执行,
否则说明 这个锁对象 已经被其他线程 抢占了。
如果有两条以上的 线程 争用同一个锁,
那 轻量级锁 就不再有效,要 膨胀 为 重量级锁,
锁标志 的 状态值 变为 “10“,
Mark Word中存储的 就是指向 重量级锁(互斥量)的指针,
后面 等待锁的线程 也要进入 阻塞状态。
上面描述的是 轻量级锁的枷锁过程,
它的 解锁过程 也是通过 CAS操作来进行的,
如果 对象的Mark Word 仍然指向这 线程的锁记录,
那就用 CAS操作 把 对象的Mark Word 和 线程中复制的Displaced Mark Word替换回来,
如果替换成功,整个同步过程就完成了。
如果替换失败,说明有其他线程 尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁 能提升 程序同步性能 的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,
这是一个经验数据。
如果没有竞争,轻量级锁使用 CAS操作 避免了使用 互斥量的开销,
但如果存在 锁竞争,除了互斥量的开销以外,还额外发生了 CAS操作,
因此在 有竞争的情况下, 轻量级锁 会比 传统的重量级锁 更慢。
偏向锁
偏向锁 也是 JDK1.6 中引入的一项 锁优化,
它的目的是 消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。
如果说 轻量级锁 是在 无竞争的情况下 使用 CAS操作 去 消除同步使用的互斥量,
那 偏向锁 就是在 无竞争的情况下 想把 整个同步都消除掉,连CAS操作都不做了。
偏向锁的 “偏”,就是偏心的“偏”、偏袒的“偏”,
它的意思是 这个锁 会偏向于 第一个 获得它的线程,
如果在接下来的执行过程中,该锁 没有被其他的 线程获取,则持有 偏向锁的 线程 将永远不需要再进行 同步。
如果读者读懂了前面 轻量级锁 中 关于对象头Mark Word与线程之间 的 操作过程,
那 偏向锁的原理 理解起来就会很简单。
假设当前 虚拟机 启用了 偏向锁(启用参数 -XX:+UseBiasedLocking,这是 JDK1.6的默认值),
那么,当 锁对象 第一次 被线程获取的时候,虚拟机 将会把 对象头中的 标志位 设为 “01”,即偏向模式。
如果使用 CAS操作 把 获取到这个锁的线程的ID 记录在 对象的Mark Word之中,
如果 CAS操作 成功
持有 偏向锁的线程 以后每次进入这个锁 相关的同步块时,虚拟机都可以不再进行 任何同步操作
(例如 LOcking、Unlocking 及对 Mark Word的Update 等)。
当有 另外一个线程 去尝试 获取这个锁时,
偏向模式 就 宣告结束。
根据 锁对象 目前是否处于 被锁定 的状态,
撤销 偏向(Revoke Bias)后 恢复到 未锁定(标志位为 “01”) 或 轻量级锁定(标志位为“00”)的状态,
后续的同步操作 就如上面介绍的 轻量级锁那样执行。
偏向锁、轻量级锁的状态转化 及 对象的Mark Word的关系如图13-5 所示。
偏向锁 可以提高 带有 同步 但 无竞争的 程序性能。
它同样是一个带有 效益权衡(Trade Off)性质的优化,
也就是说,它并不一定总是对 程序运行 有利,
如果 程序中大多数的锁 总是被 多个不同的线程 访问,
那 偏向模式就是多余的。
在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来 禁止偏向锁优化 反而可以 提升性能。
本章小结
本章介绍了 线程安全 所涉及的 概念和分类、同步实现的方式 及 虚拟机的底层运作原理,
并且介绍了 虚拟机 为了实现高效并发 所采取的的 一系列 锁优化措施。
许多资深的程序员都说过,能够写出 高伸缩性的并发程序 是一门艺术,
而 了解并发 在系统底层 是如何实现的,则是 掌握这门艺术的 前提条件,
也是成长为 高级程序员的必备知识之一。
本文深入探讨了Java中的线程安全概念,介绍了线程安全的多种级别及其实现方法,同时讲解了Java虚拟机为实现高效并发所做的锁优化技术。
3742

被折叠的 条评论
为什么被折叠?



