java并发编程(二内存模型、线程安全、无锁)
内存模型
- 原子性
原子性是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就 不会被其它线程干扰。
(1) 在单线程中, 能够在单条指令中完成的操作都可以认为是” 原子操作”,因为中断只能发生于指令之间。
(2) 在多线程中,不能被其它进程(线程)打断的操作就叫原子操作。
i++是原子操作吗?
i++分为三个阶段:
内存到寄存器
寄存器自增
写回内存
这三个阶段中间都可以被中断分离开,从这个意义上讲,说i++是原子的并不对。
- 有序性
在并发时,程序的执行可能就会出现乱序
package basic;
public class OrderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1;
flag = true;
}
public void reader() {
if (flag) {
int i = a + 1;
// ……
}
}
}
writer()方法中,a = 1;flag = true;可能发生指令重排,flag = true;a = 1;
并发时,程序执行顺序出现了乱序。
一条指令的执行是可以分为很多步骤的
– 取指 IF
– 译码和取寄存器操作数 ID
– 执行或者有效地址计算 EX
– 存储器访问 MEM
– 写回 WB
下面是一段指令的执行及指令重排后的执行:
程序执行时,因为一个寄存器一次只能处理一个指令的IF(ID等),其它指令只能在下一个时间执行指令的IF,这样就会造成执行的空闲时间X。程序中通过指令重排,减少空间时间X。
但是这样的优化就造成了并发时,程序执行出现了乱序。
- 可见性
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。
package basic;
public class VisibilityTest extends Thread {
private boolean stop;
public void run() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("finish loop,i=" + i);
}
public void stopIt() {
stop = true;
}
public boolean getStop() {
return stop;
}
public static void main(String[] args) throws Exception {
VisibilityTest v = new VisibilityTest();
v.start();
Thread.sleep(1000);
v.stopIt();
Thread.sleep(2000);
System.out.println("finish main");
System.out.println(v.getStop());
}
}
-server模式运行上述代码,永远不会停止
以上代码程序在执行的时候, while(!stop) ,stop的值只会取一次存到副本中,
v.stopIt()中设置的stop的值,run方法无法感知到。
- Happen-Before规则
程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
线程安全的概念
指某个函数、函数库在多线程环境中被调用时,能够正确地处理各个线程的局部变量,使程序功能正确完成。
i++在多线程下访问的情况
package basic;
public class AccountingSync implements Runnable {
static AccountingSync instance = new AccountingSync();
static int i = 0;
@Override
public void run() {
for (int j = 0; j < 10000000; j++) {
synchronized (instance) {
i++;
}
}
}
}
无锁
无锁相关概念
我们利用volatile关键字可以保证变量的可见性,而且利用它可以实现读与写的原子操作。但是要实现一些复合的操作volatile就无能为力。
在并发的环境下,要实现数据的一致性,最简单的方式就是加锁,保证同一时刻只有一个线程可以对数据进行操作。对操作用synchronized关键字进行修饰,保证在并发环境下数据的一致性,但是由于使用了锁,锁的开销,线程的调度等等会使得程序的伸缩性受到了限制,于是就有了很多无锁的实现方式。
这些无锁的方法都利用了处理器所提供的一些CAS(compare and switch)指令:
public synchronized int compareAndSwap(int expect, int newValue) {
int old = this.a;
if (old == expect) {
this.a = newValue;
}
return old;
}
CAS算法的过程是这样:它包含3个参数CAS(V,E,N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V 值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么 都不做。最后,CAS返回当前V的真实值。CAS操作是抱着乐观的态度进行的,它总是认为自己可以成功完成 操作。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程 不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS操作即时没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
我们会发现,CAS的步骤太多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?
事实上,这个担心是多余的。CAS整一个操作过程是一个原子操作,它是由一条CPU指令完成的。
无锁的使用
无锁比阻塞效率要高得多。我们来看看Java是如何实现这些无锁类的。
- AtomicInteger
AtomicInteger和Integer一样,都继承与Number类。
AtomicBoolean,AtomicLong等,都大同小异。
public class AtomicInteger extends Number implements java.io.Serializable
AtomicInteger里面有很多CAS操作,典型的有:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
unsafe.compareAndSwapInt方法的意思是,对于this这个类上的偏移量为valueOffset的变量值如果与期望值expect相同,那么把这个变量的值设为update。
其实偏移量为valueOffset的变量就是value:
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
CAS是有可能会失败的,但是失败的代价是很小的,所以一般的实现都是在一个无限循环体内,直到成功为止。
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
- Unsafe
该类是用来执行较低级别的操作的,比如获取某个属性在内存中的位置。
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
上面这几行代码,是用来获取AtomicInteger实例中的value属性在内存中的位置。这里使用了Unsafe的objectFieldOffset方法。这个方法是一个本地方法, 该方法用来获取一个给定的静态属性的位置。
这里有个疑问,为什么需要获取属性在内存中的位置?通过查看AtomicInteger源码发现,在这样几个地方使用到了这个valueOffset值:
public final void lazySet(int newValue) {
unsafe.putOrderedInt(this, valueOffset, newValue);
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
查找资料后,发现lazySet方法大多用在并发的数据结构中,用于低级别的优化。compareAndSet这个方法多见于并发控制中,简称CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。
举个例子来说明compareAndSet的作用,如支持并发的计数器,在进行计数的时候,首先读取当前的值,假设值为a,对当前值 + 1得到b,但是+1操作完以后,并不能直接修改原值为b,因为在进行+1操作的过程中,可能会有其它线程已经对原值进行了修改,所以在更新之前需要判断原值是不是等于a,如果不等于a,说明有其它线程修改了,需要重新读取原值进行操作,如果等于a,说明在+1的操作过程中,没有其它线程来修改值,我们就可以放心的更新原值了。
- AtomicReference
AtomicReference是一种模板类,它可以用来封装任意类型的数据。比如String。
public class AtomicReference implements java.io.Serializable
package basic;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicReferenceTest {
public final static AtomicReference<String> atomicString = new AtomicReference<String>("hosee");
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
final int num = i;
new Thread() {
public void run() {
try {
Thread.sleep(Math.abs((int) Math.random() * 100));
} catch (Exception e) {
e.printStackTrace();
}
if (atomicString.compareAndSet("hosee", "ztk")) {
System.out.println(Thread.currentThread().getId() + "Change value");
} else {
System.out.println(Thread.currentThread().getId() + "Failed");
}
};
}.start();
}
}
}
打印结果:
12Failed
17Failed
20Failed
19Failed
18Failed
13Failed
15Failed
16Change value
14Failed
11Failed
可以看到只有一个线程能够修改值,并且后面的线程都不能再修改。
- AtomicStampedReference
我们会发现CAS操作还是有一个问题的
比如之前的AtomicInteger的incrementAndGet方法
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
假设当前value=1当某线程int current = get()执行后,切换到另一个线程,这个线程将1变成了2,然后又一个线程将2又变成了1。此时再切换到最开始的那个线程,由于value仍等于1,所以还是能执行CAS操作,当然加法是没有问题的,如果有些情况,对数据的状态敏感时,这样的过程就不被允许了。
此时就需要AtomicStampedReference类。
其内部实现一个Pair类来封装值和时间戳。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
这个类的主要思想是加入时间戳来标识每一次改变。
比较设置 参数依次为:期望值 写入新值 期望时间戳 新时间戳
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) {
Pair<V> current = pair;
return expectedReference == current.reference && expectedStamp == current.stamp
&& ((newReference == current.reference && newStamp == current.stamp)
|| casPair(current, Pair.of(newReference, newStamp)));
}
当期望值等于当前值,并且期望时间戳等于现在的时间戳时,才写入新值,并且更新新的时间戳。
- AtomicIntegerArray
与AtomicInteger相比,数组的实现多了一个下标,运用了二进制数的前导零来算数组中的偏移量。
public final boolean compareAndSet(int i, int expect, int update) {
return compareAndSetRaw(checkedByteOffset(i), expect, update);
}
它的内部封装了一个普通的array :
private final int[] array;
运用了二进制数的前导零来算数组中的偏移量:
shift = 31 - Integer.numberOfLeadingZeros(scale);
- AtomicIntegerFieldUpdater
AtomicIntegerFieldUpdater类的主要作用是让普通变量也享受原子操作。
比如原本有一个变量是int型,并且很多地方都应用了这个变量,但是在某个场景下,想让int型变成AtomicInteger,但是如果直接改类型,就要改其他地方的应用。AtomicIntegerFieldUpdater就是为了解决这样的问题产生的。
package basic;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
public class AtomicIntegerFieldUpdaterTest {
public static class V {
int id;
volatile int score;
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
public final static AtomicIntegerFieldUpdater<V> vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");
public static AtomicInteger allscore = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final V stu = new V();
Thread[] t = new Thread[10000];
for (int i = 0; i < 10000; i++) {
t[i] = new Thread() {
@Override
public void run() {
if (Math.random() > 0.4) {
vv.incrementAndGet(stu);
allscore.incrementAndGet();
}
}
};
t[i].start();
}
for (int i = 0; i < 10000; i++) {
t[i].join();
}
System.out.println("score=" + stu.getScore());
System.out.println("allscore=" + allscore);
}
}
上述代码将score使用 AtomicIntegerFieldUpdater变成 AtomicInteger。保证了线程安全。
这里使用allscore来验证,如果score和allscore数值相同,则说明是线程安全的。
说明:
1)Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。如果变量不可见,就会出错。比如如果某变量申明为private,就是不可行的。
2)为了确保变量被正确的读取,它必须是volatile类型的。如果我们原有代码中未申明这个类型,那么简单得申明一下就行,这不会引起什么问题。
3)由于CAS操作会通过对象实例中的偏移量直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)。