原子性
多线程访问共享数据的时候会产生竞争
多线程并发访问之下产生的不期望出现的结果
请看如下小程序:
public class T00_00_IPlusPlus {
private static long n = 0L;
public static void main(String[] args) throws Exception {
//Lock lock = new ReentrantLock();
Thread[] threads = new Thread[100];
CountDownLatch latch = new CountDownLatch(threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
// synchronized (T00_00_IPlusPlus.class) {
//lock.lock();
n++;
//lock.unlock();
//}
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
结果如下所示:
正常来说:应该显示的结果是100个线程,每个线程分别对n加了10000次,最终的结果应该是1000000次。但是由于多个线程访问共享数据n时产生了竞争,最终导致了数据的不一致,最终结果也不是我们所需要的
具体点来说: ++操作是可以被其它线程打断的,线程1从内存中读取n的值0送给寄存器计算后,然后由寄存器把计算后的新值写入到内存中。在写入之前,线程2直接从内存中读取了还未更新的n的值为0,然后由寄存器计算,再次写入内存。那么就造成了线程1与线程2对n++的操作最终的值都变为1,而不是我们想要的最终值2。
上锁的本质
上锁的本质就是把并发编程序列化
请看如下小程序:
public class T00_01_WhatIsLock {
private static Object o = new Object();
public static void main(String[] args) {
Runnable r = () -> {
//synchronized (o) { //打开注释试试看,对比结果
System.out.println(Thread.currentThread().getName() + " start!");
SleepHelper.sleepSeconds(2);
System.out.println(Thread.currentThread().getName() + " end!");
//}
};
for (int i = 0; i < 3; i++) {
new Thread(r).start();
}
}
}
结果如下所示:
两秒钟,三个线程同时开始同时结束,也就是并发操作
当加上Synchronized后,一个线程执行完毕后,下一个线程才会继续执行,也就是序列化操作,效率会比较低。结果变为:
总结:Synchronized保证可见性与原子性,但是不保证有序性
思考 为什么Synchronized不保证有序性?
如果说有序性是获取锁的公平锁概念的话,因为syn是非公平锁,无法保证获取锁的顺序
如果说的是指令的有序执行,因为这个是CPU的事,CPU底层会为了优化执行效率或者安全性考虑,从而进行指令的重排序,所以无论是否使用syn,都没有办法保证指令的有序性
如何保障操作的原子性?
- 悲观的认为这个操作会被别的线程打断(悲观锁)synchronized
- 乐观的认为这个操作不会被别的线程打断(乐观锁也称自旋锁、无锁)cas操作
CAS基础概念
将内存中指定位置的值与所期望的值比较,只有当内存当中的值和期望的值相同的时候,才会把内存中的值更新为一个新的值,这是一个原子的操作。如果内存中的值同时被其他线程操作那么此次更改将会失败。
CAS的ABA问题?
线程1从内存中拿到了n的值0,并对值0进行加1操作后,然后由线程1去查看最初的值是否还为0时,在这期间,来了线程2,把0的值改为了8,紧接着线程3把值8又改回了0。这个时候用CAS检查时值是没有发生变化的,虽然最终的值是0。但是它的引用指向发生了变化,也就是CAS经典的ABA问题。
如何解决该问题?
为CAS中的每个操作添加一个版本号,每次执行成功之后,都会对版本号进行+1操作,每次更新数据之前都会对比版本号,观察是否有其他线程进行干扰.如果版本号一制则继续,不一致进入自旋
请看如下小程序,认知CAS为何一定要保证原子性?
if(v == 0) v = 1
线程1在读取0时,然后对0进行加1操作,往回写入判断v == 0时并还没有执行v = 1操作时,突然来了一个线程2打断了该操作,把v的值该为了8,然后线程1继续操作,把v的值又改回了1,这时就出现问题了。也就是说CAS为何一定要保证原子性的原因
CAS如何保证原子性?
请看如下小程序:
使用AtomicInteger保证原子性
public class T01_AtomicInteger {
/*volatile*/ //int count1 = 0;
AtomicInteger count = new AtomicInteger(0);
/* synchronized */void m() {
for (int i = 0; i < 10000; i++)
//if count1.get() < 1000
count.incrementAndGet(); //count1++
}
public static void main(String[] args) {
T01_AtomicInteger t = new T01_AtomicInteger();
List<Thread> threads = new ArrayList<Thread>();
for (int i = 0; i < 100; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}
threads.forEach((o) -> o.start());
threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}
结果如下所示:
底层实现:调用unsafe类的CompareAndSwapInt方法,最终实现是lock cmpxchg 指令
CAS 宏观上我们认为是自旋锁、乐观锁
CAS微观上我们认为是悲观锁,lock锁,锁的是总线或者是锁的所在的缓存行
单核底层没有必要在cmpxchg指令前加lock,只有多核才有必要加lock指令
悲观锁与乐观锁的效率?
针对不同场景,有不同的选择
悲观锁:临界区执行时间比较长,等的人很多,线程都在队列里进行等待,不消耗CPU资源
乐观锁(自旋锁):时间短,等的人少,没抢占到锁,会一直重试,会进行线程的切换,自旋次数过多会大量消耗CPU资源
思考:Synchronized如何保障可见性的?
ThreadA上锁,上完锁之后执行数据,然后解锁,解锁完成之后,会将缓存中修改的内容刷新到主存中,确保了共享变量的值是最新的,这个地方底层也是使用到了lock语句,也是做了内存同步,然后ThreadB才会执行,获取到最新的x的值