提升--05---并发编程之---原子性---CAS
Posted 高高for 循环
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了提升--05---并发编程之---原子性---CAS相关的知识,希望对你有一定的参考价值。
并发编程之原子性
从一个简单的小程序谈起:
需求:
- new100个线程,每个线程对共有资源n,进行+1操作,每个线程执行10000次.
- 按需求,最后共有资源n,应该是100*10000=1000000(1百万)
代码1:
import java.util.concurrent.CountDownLatch;
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++) {
//lock.lock();
n++;
//lock.unlock();
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(n);
}
}
结果分析:
一些基本概念:
- race condition => 竞争条件
指的是多个线程访问共享数据的时候产生竞争 - 数据的不一致(unconsistency),
并发访问之下产生的不期望出现的结果
- 因为代码里面n++这个操作不是原子性的,所以一个线程在进行n++操作时,被其他啊线程打断了,还没来及把内存中的值跟新.
- 此时其他线程启动,把此时内存里的数据(并未被上一个线程成功跟新)读到缓存,进行操作
- 这种情况就会造成==,n被重复的赋一样的值==.比如3个线程都把n=0,修改为n=1…
多线程的并发操作,导访问共享数据的时候,产生竞争,导致数据的不一致
原子性
定义:
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
- 原子性就像数据库里面的事务一样,他们是一个团队,同生共死。
n++案例:
public class Test02 {
static int n;
public static void main(String[] args) {
n++;
}
}
- 我们可以看到一条简单n++代码,翻译成字节码也有5条指令,
- 每一条字节码指令,也要被翻译成多条本地cpu汇编语言指令.
所有一条语句是不是原子操作,得去查cpu汇编手册
什么样的语句(指令)具备原子性?
CPU级别:
- 汇编,需要查询汇编手册!
Java中的8大原子操作:(JVM级别,不是代码级别)
- lock:主内存,标识变量为线程独占
- unlock:主内存,解锁线程独占变量
- read:主内存,读取内存到线程缓存(工作内存)
- load:工作内存,read后的值放入线程本地变量副本
- use:工作内存,传值给执行引擎
- assign:工作内存,执行引擎结果赋值给线程本地变量
- store:工作内存,存值到主内存给write备用
- write:主内存,写变量值
上锁的本质
- 我们平时所说的"上锁",一般指的是悲观锁
上锁的本质是把并发编程序列化
一些基本概念:
- monitor (管程) —> 锁 (要求是对象)
- critical section -> 临界区
- 如果临界区执行时间长,语句多,叫做 锁的粒度比较粗,反之,就是锁的粒度比较细
锁 案例:
不加锁的时候:
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的时候:
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();
}
}
}
上锁----既保证原子性,也保证可见性,但不保证有序性
注意:
- 序列化并非其他程序一直没机会执行,而是有可能会被调度,但是抢不到锁,又回到Blocked或者Waiting状态(sync锁升级)
synchronized保证可见性:
- synchronized解锁后会把所有的内存状态和我们本地的缓存做一个刷新对比
- 保证内存和缓存数据的一致性
- 然后下个一个线程才能继续
悲观锁 与 乐观锁
多线程–09–悲观锁与乐观锁
定义:
悲观锁:
- 重量级锁(经过操作系统的调度)synchronized早期都是这种锁(目前的实现中升级到最后也是这种锁)
乐观锁:
- 轻量级锁(CAS的实现,不经过OS调度)(无锁 - 自旋锁 - 乐观锁)
CAS算法
- 即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。
CAS算法涉及到三个操作数
- 需要读写的内存值 V
- 进行比较的值 A
- 拟写入的新值 B
自旋操作:
- 自旋就是cas的一个操作周期,如果一个线程特别倒霉,每次获取的值都被其他线程的修改了,那么它就会一直进行自旋比较,直到成功为止,在这个过程中cpu的开销十分的大,所以要尽量避免。
ABA 问题:
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。
- ABA如果是指向普通数字型,对结果不会影响
- ABA如果是指向一个对象的引用,就会发生问题
CAS的ABA问题解决方案
版本号机制 Version:
分为数值类型的版本号和 布尔类型的版本号
- 一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
举一个简单的例子:
假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。
- 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )
- 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20)。
- 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
- 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。
这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。
CAS操作本身的原子性保障
AtomXXX类
- AtomXXX类本身方法都是原子性的,但不能保证多个方法连续调用是原子性的
CAS案例
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
public class T00_00_IPlusPlus2 {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
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++) {
count.incrementAndGet();
}
latch.countDown();
});
}
for (Thread t : threads) {
t.start();
}
latch.await();
System.out.println(count);
}
}
分析:
AtomicInteger类
Unsafe:
最终 c++代码 调用汇编指令:
cpu底层就支持一条CAS比较指令
- lock cmpxchg 指令
lock cmpxchg 指令:
- 这条lock cmpxchg 指令不是原子的,所以还得加锁lock
- lock指令在执行的时候视情况采用缓存锁或者总线锁
- 单核cpu可以不用加lock,多核一定得加lock保证原子性
- CAS在宏观上叫乐观锁,自旋锁,但在微观上,底层实际实现上,还是用到悲观锁,lock.来保证数据的一致性
悲观锁 与 乐观锁的效率问题
悲观锁:
- 一个线程获得锁运行的时候,其他线程在队列里排队.
- 在队列里的等待的线程,是不消耗cpu资源的,他是堵塞的.
乐观锁:
- 一个线程在运行的时候,
- 此时CPU要 维持其他线程,做自旋操作,CAS循环,还要维持运行线程的切换,需要消耗CPU资源
悲观锁 与 乐观锁的使用情景:
- 悲观锁 synchronized
适用于写比较多的情况下(多写场景,冲突一般较多)
临界区执行时间比较长 , 等的人很多 - 乐观锁 CAS
适用于写比较少的情况下(多读场景,冲突一般较少),
时间短,等的人少
当然实际生产 ,要用压测比较,量化
补充:synchronized优化
- Java并发编程这个领域中synchronized关键字一直都是元老级的角色,很久之前很多人都会称它为 “重量级锁” 。但是,在JavaSE 1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁 和 轻量级锁 以及其它各种优化之后变得在某些情况下并不是那么重了。
- synchronized的底层实现主要依靠 Lock-Free 的队列,基本思路是 自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。
在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。
实战:就用synchronized ----synchronized自适应完成锁升级,效率已经很不错了.
以上是关于提升--05---并发编程之---原子性---CAS的主要内容,如果未能解决你的问题,请参考以下文章