目录
通过一个demo认识原子性
大家先来看一个小demo
public class Demo01 {
private static long n = 0L;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
CountDownLatch cd = new CountDownLatch(threads.length);
for (int i=0;i<threads.length;i++){
threads[i] = new Thread(()->{
for(int j=0;j<1000;j++){
n++;
}
cd.countDown();
});
}
for(Thread t: threads){
t.start();
}
cd.await();
System.out.println("n = " + n);
}
}
问:这个Demo执行完成之后打印n的值为1000000吗?
答案是:不是的
在这个demo中,我们用了100个线程去执行n++的操作,每个线程执行10000次n++的操作,理论上执行完成之后的结果应该是1000000才对,为什么不是呢?
看图:
图解:每个线程都需要从内存中读出n到寄存器中,然后执行n++的操作,执行完成之后在写入到内存中。我们都知道,多线程异步执行的。比如说thread01读到n之后,做完n++操作之后还没来得及给内存中更新,thread02读到了n,并且对n进行了n++的操作更新到了内存中,这时候thread01在去内存中更新n的值,是不是就重叠了。所以n++不是一个原子性的操作,原子性就是要么成功,要么失败,没有中间状态。
不了解寄存器概念的童鞋可以到我主页看我多线程的第一篇文章,里面通俗简答的介绍了寄存器,看懂必理解!!!
java中只有以下操作是原子性的
- lock:主内存,标识变量为线程独占
- unlock:主内存,解锁线程独占变量
- read:主内存,读取内存到线程缓存(工作内存)
- load:工作内存,read后的值放入线程本地变量副本
- use:工作内存,传值给执行引擎
- assign:工作内存,执行引擎结果赋值给线程本地变量
- store:工作内存,存值到主内存给write备用
- write:主内存,写变量值
注意:这些操作都是虚拟机级别的操作,不是语句级别的操作。
只要我们不确定这条语句是不是原子性的,上锁就可以,就这么简单!!!
所以上边那个小demo,只要我们对n++操作进行上锁,那么最终打印出来的结果肯定是1000000。
上锁的本质
小边那个demo告诉我们了多线程访问统一条数据的时候会产生竞争条件,产生竞争条件就有可能产生数据不一致的问题。
那么如何保障数据的一致性呢?
保障数据的一致性就要保证线程同步(线程执行的顺序事先安排好)。如何把线程执行的顺序安排好呢?我们就要通过上锁的方式来实现线程同步。让线程的执行顺序变的序列化起来。
如何理解锁的粒度?
上完锁之后锁定的代码我们成为临界区,临界区越大(也就是锁住的代码越多,执行代码的时间越长)锁的粒度越粗,反之,临界区越小,锁的粒度越小。
如何保障操作的原子性?
通过上锁保障操作的原子性,两种方式,一种是悲观锁,一种是乐观锁。
什么是悲观锁?
悲观的认为这个线程执行的操作会被别的线程打断(悲观锁),典型的悲观锁就是synchronized。
什么是乐观锁?
乐观的认为这个线程执行的操作不会被别的线程打断(乐观锁),乐观锁又称之为无锁、自旋锁、CAS(compare and swap)
什么是CAS?
就拿上面demo举例子,看图:
认真看完图并看完图中的文字,我想大家应该已经理解什么是CAS,当然,说到CAS,很多了解他的小伙伴就会想到其中的ABA问题了。 下面会说。
什么是ABA问题?
还拿上面的demo举例子,比如说现在有三个线程,要修改n的值
第一个线程(thread01),第二个线程(thread02),第三个线程(thread03)
现在n的值为0,thread01把a读出来为0,进行n++的操作,往回写的时候判断内存n的值依旧是0,所以看似没什么问题,直接更新过去了。但是当thread01判断内存中n的值依旧为0之前,thread02把n修改为了8,thread02修改完之后,thread03又把n修改为了0,所以thread01看到内存中n的值其实是thread03修改完成后的n的值,此0非彼0。这就是ABA问题,本来n的值为0(A),thread02把n的值修改为了8(B),thread02修改完之后,thread03又把n修改为了0(A)。
当然,这个小demo之后ABA问题我们可以忽略,因为最后它还是修改为了1,数据是一致的,但是如果n不是int类型,而是一个对象的引用类型,发生了ABA问题的话,虽然引用没有变,但是对象里面的内容已经改变了,这时候ABA问题就需要解决了。解决也非常简单,只需要在对象中增加一个字段version(版本号),每次对他修改完成之后把版本号+1,然后判断这个对象有没有改变的时候判断他的版本号有没有改变,就可以完美的解决ABA问题了。
乐观锁和悲观锁哪种效率跟高?
首先我们需要了解悲观锁实现方式,悲观锁大致实现是:
有一个队列,比如说有三个线程:thread01,thread02,thread03。thread01先来了,发现这段代码没有人正在在执行,好,这时候thread01执行,然后锁住。然后thread02和thread03来了,发现代码在上锁的状态,就会放到队列中,处于阻塞状态。注意:阻塞状态是不消耗cpu资源的。然后等thread01执行完成并且释放锁了,thread02再去,然后thread03......与此形成鲜明的对比的正是乐观锁,乐观锁会一致自旋的去判断上一个线程是否已经释放锁了,注意:自旋是消耗cpu资源的。等待队列是不一样的,他是不占用cpu的。
所以,假如锁的粒度大,执行的时间长,等待执行的线程多,用悲观锁的。反之,用乐观锁。
synchronized的三大特性
synchronized保证了原子性、一致性、可见性。
首先synchronized锁住的代码是原子性的,其次比如说有两个线程执行同一段代码,如果加了锁之后会保证他们序列化的执行,所以synchronized也保证了一致性,那它如何保证可见性的呢?synchronized会上锁,对应的也会解锁(unlock),unlock操作会把内存的状态和本地缓存的状态做一个刷新,所以也保证了可见性。