文章目录
原子性问题
其实多线程就只有两个重要的问题,一个是可见性问题,另一个则是原子性问题了。本章将会着重讲原子性问题。
原子操作
上面说到了的原子性问题究竟是什么?先看两段代码和输出结果就知道了。
public class Counter {
volatile int i = 0 ;
public void add(){
i++;
}
}
public class Demo1_Counter_Test {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
for(int i=0;i<10;i++){
new Thread(()->{
for(int j=0;j<10000;j++){
counter.add();
}
System.out.println("done...");
}).start();
}
Thread.sleep(6000l);
System.out.println(counter.i);
}
}
看了上述代码,相信很多人认为输出结果应该是100000,那么结果究竟是什么?来看:
wtf?为什么是87690啊,我的i明明加了volatile不应该有可见性问题啊??这就是哟啊引出的原子性问题了。
首先我们来看Counter的add方法:
public void add(){
i++;
}
这段代码乍看之下非常短,似乎只有一个操作。然而它正的就只有一步操作么?其实不然,我们只要通过反编译就能看到它真实的执行步骤了。用javap命令反编译一下即可。
javac Counter.java
javap -v -p Counter.class
这样就能得到我们想看的反编译后的字节码了:
public void add();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
LineNumberTable:
line 7: 0
line 8: 10
可以看到i++其实就是2567四步所执行的。
问题产生的原因
我们主要来看上述代码2567,这其实又要扯到Java的内存模型了,上面代码的过程其实如下图:
一个线程会有自己的一块工作空间(栈内存),以及所有线程共享的堆内存,还有共享的方法区。上面这段代码首先是从堆内存里读取i的值,再从栈内存取出1,然后iadd就是做一次运算0+1;最后的putfiled就是把i再放回堆内存。因此可以看出i++并非不可分割,一步到位堆,所以就会有原子性问题。因为当线程多的时候就会造成数据不能实时同步。
原子操作的定义
通过上面的案例可以给出原子性的定义:
原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分(不可中断性)。将整个操作视为一个整体,资源再该操作中保持一致,这是原子性的核心特征。
原子性问题解决方案
如何通过Java中的语言特性和api来解决问题呢?有以下集中方案:
synchronized
public class CounterLock {
volatile int i = 0;
public synchronized void add(){
i++;
}
}
synchronized关键字的作用相当于将代码块用synchronized(this)给同步了,下一章会详细讲解。
ReentranLock
public class CounterLock {
volatile int i = 0;
Lock lock = new ReentrantLock();
public void add(){
lock.lock();
try{
i++;
}finally {
lock.unlock();
}
}
}
加锁解锁操作,切记释放锁一定要再finally中进行,以防死锁。
AtomicInteger
上面两种写法本质上都是加锁,使得一个线程可以以单线程的方式完全执行,显然效率是偏低的,而AtomicInteger则不用加锁,其原理接下来详细讲述,先来看他的使用:
public class CounterLock {
AtomicInteger i = new AtomicInteger(0);
public void add(){
i.incrementAndGet();
}
}
再来看看输出:
可见也实现了原子性。
CAS(Compare and swap)
上文中使用了一个AtomicInteger类,我们就想,如果我们自己想实现一个原子操作应该怎么办。这时候就要用到CAS操作了。
什么是CAS操作
Compare and swap 比较和交换。属于硬件同步原语,处理器提供了基本的内存操作的原子性保证。
CAS操作需要输入两个数值,一个旧值A(期望操作前的值)和一个新值B,在操作期间先对旧值比较,若没有发生变化,才交换成新值,发生了变化则不交换。
JAVA中的sun.misc.Unsafe类,提供了compareAndSwapInt()和compareAndSwapLong()等几个方法实现了CAS。
CAS是在硬件层面上保证了同一时间只有一个线程可以访问内存,而失败的操作宁可不要了也比错了要好。
Java中的CAS操作使用
Java不能直接通过内存地址去操作内存,所以需要通过Unsafe类调用JVM去调用操作系统,如图:
Unsafe的使用及坑
首先我根据Unsafe的API去获取Unsafe,结果竟然报错,来看一下代码:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
static {
unsafe = Unsafe.getUnsafe();
}
public void add(){
}
public static void main(String[] args){
}
}
执行了一下上面这段代码,结果竟然报了这么一个错误:
这是因为Unsafe.getUnsafe这个API只能是JDK源码才能使用,而你这里不能使用,否则会有安全问题(SecurityException)。。。所以为了使用它,只能通过反射的方式去实现。
再次之前,先要了解一个概念:偏移量
偏移量
偏移量就相当于你想修改内存中的一个字段,但是这时候肯定是不能直接告诉系统该字段再哪的,这时候就要指定偏移量,这时候内存就会移动指定的偏移量到达目标字段的地址了。看图:
假设绿色的是内存,如果你想要获取j字段,这时候你就要指定到b的末尾作为偏移量,这时候就能读取j字段了。
我们将上述代码改一下:
public class CounterUnsafe {
volatile int i = 0;
private static Unsafe unsafe = null;
private static long valueOffset;
static {
//unsafe = Unsafe.getUnsafe();
try {
//通过反射获取theUnsafe这个参数;
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//获取i字段的offset
Field iField = CounterUnsafe.class.getDeclaredField("i");
valueOffset = unsafe.objectFieldOffset(iField);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add(){
for(;;) {
//获取当前对象的valueOffset偏移量处的数(i)
int current = unsafe.getIntVolatile(this, valueOffset);
//如果成功了则退出循环,不成功则会继续去执行
if (unsafe.compareAndSwapInt(this, valueOffset, current, current + 1))
break;
}
}
}
再去试验一下,得到了结果:
可见已经通过CAS实现了原子性了
J.U.C包内的原子操作封装类
除了上文中的AtomicInteger之外,JUC还提供了很多原子操作的封装类:
通过API使类的赋予成员变量原子性
我们上述已经自己手写实现了具有原子性的Integer,但是如果实际开发中已经写好了类,这时候想让它具有原子性该如何实现呢?有以下方法:
AtomicIntegerFieldUpdate
public class Demo2_AtomicIntegerFieldUpdate {
private static AtomicIntegerFieldUpdater<User> atom =
AtomicIntegerFieldUpdater.newUpdater(User.class,"id");
public static void main(String[] args) {
User user = new User(100,100,"Tong");
atom.addAndGet(user,50);
System.out.println("addAndGet(user,50) 调整后值变为:"+user);
}
}
class User{
volatile int id;
volatile int age;
private String name;
public User(int id,int age,String name){
this.id = id;
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", age=" + age +
", name='" + name + '\'' +
'}';
}
}
只需要在AtomicIntegerFieldUpdater.newUpdater中指定要执行原子修改的类的Class类以及要执行原子修改的字段即可。
CAS的三个问题
1.CPU消耗过大
循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2.只能针对单变量
仅针对单个变量的操作,不能够用于多个变量来实现原子操作
3.ABA问题
线程1和线程2都要进行CAS(0,1)操作,本来线程1执行完之后轮到线程2执行的时候预期应该是失败的,但是最后因为在它的CAS(0,1)执行之前线程1执行了一次CAS(1,0),所以最终i又变成了0,导致原来了预期失败的CAS(0,1)执行成功了,这样就带来了安全性问题。
ABA问题的解决方案
因为上述问题的主要成因即是i已经不再是原来的那个i了,所以只需要一个能够识别其身份的版本号即可加以区分。
用CAS来实现一个基本的锁
思想解析
其实就是一个原子引用AtomicRefrence<Thread>
owner,通过它的CAS操作使得当值为null的时候讲它设置为当前线程,表明锁为该线程所占用,而失败的线程则让它挂起,而释放锁的时候只要再让参数变为null即可。
代码:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.LockSupport;
/**
* 个人锁实现
*/
public class MyzLock implements Lock {
//owner参数用于通过CAS操作来实现原子性
private AtomicReference<Thread> owner = new AtomicReference<>();
//等待队列
private LinkedBlockingQueue<Thread> waiters = new LinkedBlockingQueue();
@Override
public void lock() {
if(!tryLock()){
//假如加锁失败,则进入等待队列
waiters.add(Thread.currentThread());
//用死循环防止伪唤醒问题
for(;;){
Thread head = waiters.peek();//读取头部但不出队列
if(Thread.currentThread()==head){
//如果是队列头部则再次抢锁
if(!tryLock()){
//若失败,则挂起当前线程
LockSupport.park();
}else {
//若成功,将线程出队列并退出循环
waiters.poll();
break;
}
}else {
//若不是头部,则挂起
LockSupport.park();
}
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return owner.compareAndSet(null,Thread.currentThread());
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
if(owner.compareAndSet(Thread.currentThread(),null)){
Thread head = waiters.peek();
LockSupport.unpark(head);
}
}
@Override
public Condition newCondition() {
return null;
}
}
测试结果:
这个锁就实现了。