Java 多线程 volitile 和 atomic
volitile
关键字
public class MTester {
public static class TestKey{
int x = 0;
}
public static TestKey key0 = new TestKey();
public static void main(String[] args) {
Thread thread = new Thread(()->{
while (key0.x == 0){
}
System.out.println("key0"+key0.x);
});
Thread thread1 = new Thread(()->{
try {
Thread.sleep(1000);
key0.x=1;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
System.out.println("over");
}
});
thread.start();
thread1.start();
}
}
尝试运行以上代码,发现thread永远也无法发现key0的x被改变
所以这个时候需要加上volitile关键字
具体原因是java中每个线程都有工作内存,以及主存
我的理解就是不加volitile,线程读写变量是先在自己的工作内存中处理,然后再写回主存,但是有的线程处理的是工作内存,但是并没有从主存里面读取,加上volitile关键字之后,会通知其他线程,让他们强制从主存中读取数据
https://www.cnblogs.com/zhengbin/p/5654805.html
volatile还有一个特性:禁止指令重排序优化。
可以见这个文章
https://www.cnblogs.com/chengxiao/p/6528109.html
但是volitile只能保证可见性,不能保证原子性,也就是说如果多线程操作 i++还是无法保证正确
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0;i<10;i++){
service.submit(()->{
for (int j = 0;j <10;j++){
key0.x++;
System.out.println(key0.x);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
service.awaitTermination(10, TimeUnit.SECONDS);
System.out.println(key0.x);
还是无法保证原子性
这个时候可以考虑使用 java.util.concurrent.atomic;中的类
这些类里面的类大部分都是使用CAS算法进行操作的
CAS compare and set
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
这个 unsafe是unsafe类,里面的方法都是native方法
补充:
这个valueOffset可以理解为一个AtomicInteger的偏移量然后native方法然后传入native方法,这样就相当于获取了CAS需要比较对象的地址了
CAS其实就是期望的值进行比较,如果不相等,就证明有其他线程更改过了,然后不执行操作然后返回失败,CAS看起来很麻烦,但是却可以映射一些CPU指令,实际上执行起来还是很快的(参考java核心技术)
unsafe里面的方法大部分都是native方法
比如说我们想要对AtomicInteger执行一个 increase操作,就先比较自己跟期望的值,如果不等,那就在下次循环接着尝试更改,直到更改成功
//AtomicInteger.java
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
不过这种不断尝试比较,对CPU开销还是比较大,不过相对于synchronized来说更轻量级,因为synchronized需要不断尝试获取锁释放锁,而且只能独占
在并发量不是特别大的情况下,效率相对于synchronized还是很高的,当自选严重冲突的时候synchronized还是效率更高一些
CAS 算法属于自旋
不过CAS算法也有其他的缺点,常见的就是ABA问题
举个例子
线程1:从内存位置 1 取回A
线程2:从内存位置 1取到 A
线程2:做了一些操作
线程2:从内存位置 1 写入A
线程1:发现位置1还是A CAS成功但是却不知道线程2做了什么操作,可能引发一些后果
解决办法
AtomicStampedReference
可以用一个timestamp 或者mask来判断是否有其他操作
自旋锁的简单实现:
思路:每次只有一个线程进入临界区
import java.util.concurrent.atomic.AtomicReference;
public class MSpinLock {
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock(){
do {
}while (!this.reference.compareAndSet(null,Thread.currentThread()));
}
public void unlock(){
do {
}while (!this.reference.compareAndSet(Thread.currentThread(),null));
}
public static void main(String[] args) {
MSpinLock lock = new MSpinLock();
Thread thread = new Thread(()->{
while (true){
try{
lock.lock();
System.out.println("thread had lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("thread will unlock");
lock.unlock();
}
}
});
Thread thread1 = new Thread(()->{
while (true){
try{
lock.lock();
System.out.println("thread1 had lock");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("thread1 will unlock");
lock.unlock();
}
}
});
thread.start();
thread1.start();
}
}
每次都是成对出现的
如果注释掉lock
显然不对
不过我们做的自选锁不可重入
假如有个函数需要递归,那么自旋锁就会发生死锁
所以我们需要一个Integer来判断一下
public void lock(){
if(Thread.currentThread().equals(reference.get())){
atomicInteger.incrementAndGet();
return ;
}
do {
}while (!this.reference.compareAndSet(null,Thread.currentThread()));
atomicInteger.incrementAndGet();
}
public void unlock(){
if(Thread.currentThread().equals(reference.get())){
int n = atomicInteger.decrementAndGet();
if(n>0){
return;
}
}
do {
}while (!this.reference.compareAndSet(Thread.currentThread(),null));
}
这样就可重入了