JUC

多线程下保证数据的一致性

使用synchronized关键字

比如对num变量进行操作,如果没有synchronized关键字,即使是使用volatile修饰变量。输出的值也会小于1000,因为volatile虽然可以保证可见性以及顺序性但是不能保证变量的原子性。

1
2
3
4
private static int num = 0;
private static synchronized void increment(){
num++;
}

使用Lock锁

1
2
3
4
5
6
7
8
9
private static int num = 0;

private static Lock lock = new ReentrantLock();

public static void increment(){
lock.lock();
num++;
lock.unlock();
}

使用Atomic原子类

1
2
3
4
5
private static AtomicInter ai = new AtomicInteger();

public static void increment(){
ai.getAndIncrement();
}

多线程情况下调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 线程调用
public static void threadUse(){
long time1 = new Data().getTime();
for(int i = 0; i < 10; i++){
new Tread(new Runnable(){
@Override
public void run(){
for(int j=0; j<10000; j++){
increment();
}
}
}).start();
}
// 确保所有线程调用结束
while(Thread.activeCount()>1)
Thread.yield();
// 打印出执行所用时间
System.out.println("time="+(new Date().getTiem - time1));
}

分别调用上述三种情况,输出的都是10000,但是执行所花费的时间是不一致的,发现synchronized和lock使用的时间是基本相同,使用Atomic原子类所花费的时间相对是两种的一半,但是当执行量越来越大的时候发现使用synchronized所花费的时间低于Atomic。

总结

  • synchronized: 重量级操作,基于悲观锁,可重入锁
  • AtomicInteger: 乐观,用CAS实现,当并发大的时候Atomic出错的概率会增大,不断校验更费时间

内存可见性

多个线程对同一个变量进行操作(称为共享变量),但是这多个线程有可能会被分配到多个处理器中运行那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主存复制一份分别存储到自己的存储器中,等到进行完操作后,再赋值回主存。

问题: 如果两个线程t1和t2分别被安排到不同的处理器上面,那么t1与t2对于变量A的操作就相对是不可见的,如果t1给A赋值,然后t2又给A赋值,那么t2的操作就会把t1的操作覆盖掉那就会产生不可预料的后果。

因此需要保证变量的可见性,一个线程对共享变量的修改能够及时的被其他线程看到

共享数据的访问权限必须定义为private

volatile关键字

当多个线程操作共享数据时,可以保证内存中的数据可见。用这个关键字修饰共享数据,就会及时的把线程缓存中的数据刷新到主存中去, 也可以理解为,就是直接操作主存中的数据。所以在不想使用锁的情况下,可以使用volatile关键字,如下:

1
private volatile boolean falg = false;

这样就可以解决内存可见性的问题了。

内存包括共享主存和高速缓存(工作内存),volatile关键字标识的变量,是指CPU从缓存读取数据时,要判断数据是否有效。如果缓存没有数据,再从主存读取。主存就不存在是否有效的说法了。而内存一致性协议也是针对缓存的协议

如何解决的内存可见性

对其他核心数立即可见,这个意思是,当一个CPU核心A修改完volatile变量,并且立即同步回主存,如果另一个CPU核心B的工作内存中也缓存了这个变量,那么B的这个变量会立即失效,当B想要修改这个变量的时候,B必须从主存中重新获取变量的值。除此之外,即便是单线程读取volatile变量,在变量值不变的情况下,也都是从主存读取。因此这里面有两种情况,一是已读取,失效;二是再读取,从主存读。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class VolatileTest implements Runnable {
// falg未使用volatile关键字修饰,内存不可见
static boolean flag = true;

@Override
public void run() {
while (flag) {
}
System.out.println("end......");
}

public static void main(String[] args) {
new Thread(new VolatileTest()).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("end main......");
}
}

上面这个例子,子线程会一直卡住,原因是flag不具备可见性。主线程和子线程刚开始都缓存了flag,并且值都是true,后来主线程把值修改为了false,但是子线程并不知道,仅此而已!!!如果flag用volatile关键字修饰,那么主线程在修改成false后,子线程再次while循环的时候,就会发现他缓存的flag已经失效了,他会从主存中重新读取flag的值。

实现原理

实现的原理是基于CPU的MESI协议的(缓存一致性协议),其中E表示独占Exclusive,S表示Shared,M表示Modify,I表示Invalid,如果一个核心修改了数据,那么其他核心的数据状态就会更新成M,同时其他核心上的状态更新成I,这个是通过CPU多核之间的嗅探机制实现的。

但是,这样是否就能保证多线程操作一个共享变量的时候,保证线程安全呢?其实不然,否则我怎么说是仅此而已呢!

volatile限定的是从缓存读取时刻的校验,如果两个CPU同时从各自缓存读取一个变量 n = 1(此时,变量n在各个CPU缓存都是有效的),并且同时修改了变量 n = n + 1,再写回缓存,这个时候n的值等于2,而不是等于3。因此,在多线程操作共享变量的时候,正确的方式是使用同步或者Atomic。

指令有序性

这个涉及到内存屏障,内存屏障有两个能力:


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!