一串代码引出的问题
public class LockDemo{
volatile int i= 0;
public void add(){
i++;//实际起码三次操作 ?
}
public static void main(String[] args) throws InterruptedException{
LockDemo Id = new LockDemo();
for(int i = 0;i < 2 ;i++){
new Thread( ()->{
for(int j = 0;j<10000 ; j++ ){
Id.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(Id.i);
}
}
理论上应该是20000
实际结果 :14500
第二次实际结果 :15400(不可预测性)
i++;//实际起码三次操作 ? 字节码角度
阅读这个类的字节码文件你会发现这些步骤
- 读取 i
- 计算 i++
- 赋值 i
这时 两个线程去走读取这个步骤(没事 ,然后自己计算) ,
写入的时候 如果同时写入 就会发生问题
概念了解
-
临界区: 部分关键代码的多线程并发执行 ,会对执行结果产生影响 .如上面的 i++;
-
竞态条件 :可能发生在临界区域内的特殊条件. 执行i++ 时产生了 竞泰条件(类似环境)
-
.共享资源
1.如果一段代码是安全的,这它不包含 竞态条件. 只有多个线程更新共享资源时 才会发生竞态条件.
2.栈封闭时,不会在线程之间共享的变量 .都是线程安全的. 你的流程没有用到共享变量 ,而是私有的.
3.局部对象引用本身不共享 ,但是引用的对象存储在共享堆中. 如果方法内创建对象 ,只是在方法中传递 ,并且 不对其他线程可用 ,呢么线程也是安全的. -
不可变的共享对象—不可变对象 —线程安全
实例被创建 ,value变量就不能被修改 ,这就是不可变性. -
原子操作(数据一致性的要求)
可以是一步 ,也可以是 多步 操作. 但是顺序不可以被打乱 ,也不可以被切割而只执行一部分.
将整个操作视为一个整体,资源在操作中保持一致 ,这就叫原子性
资源在操作中保持一致就是 我操作的这个数据 是确定的 而不是 正在或已经被修改了的样子
比较和替换 =机制[CAS机制] --提供了原子性保证 来解决这个问题
我在数据提交 ,赋值的 时候 看看 数据有没有变化 ,没变化在赋值(交换) ,变化了 就不操作
调用 硬件原语的流程:
java–jvm—c----010100010硬件驱动—内存
想要做到动内存里的信息 ,不是一步到位的 而且 我们输入两个值 这叫CAS操作(重点:硬件层次支持 )
一个旧值 A 未计算 1
一个新值 B 计算后 2
如图 两个线程都拿到A=1 操作,
结果: 两个线程 只有一个可以执行成功
compareAndSwapInt(对象 ,对象的属性偏移量 , 当前值 ,目标值);
JAVA中的sun.misc.Unsafe类提供compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS
代码演示:
JAVA中的sun.misc.Unsafe类提供compareAndSwapInt()和compareAndSwapLong()等几个方法实现CAS
//两个线程对 i 变量进行选择操作 +机制[CAS机制] 保证原子性
import java.lang.reflect.Field;
public class LockCASDemo{
volatile int i = 0;//因为加了volatile之后,会告诉我们的编译器,不要去优化它
//不能直接用 如下 会报错 所以我们用反射来写
// Unsafe unsafe = Unsafe.getUnsafe(); //翻译:不安全的类 我们就是靠他来实现CAS的
// unsafe.compareAndSwapInt(); //cas底层API 不是java 写的
private static Unsafe unsafe = null;
//i字段的偏移量
private static long valueOffset;
static {
try {
// 通过反射获取Unsafe对象 这是拿到theUnsafe属性 静态
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null); //拿到对象 但我们没办法直接set ,get操作内存 存的属性 而是通过 对象及其属性 的定位地址 来做--- 这里用"偏移量"来定位
Field fieldi = LockCASDemo.class.getDeclaredField("i");
// 通过Unsafe对象获得i的偏移量
valueOffset = unsafe.objectFieldOffset(fieldi);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
public void add() {
//i++; 这是原来的写法 本质是三次操作 下面分开写出来本质还是i++ 然后 就加入比较 替换的概念
do{
int current = i; //读取当前值
int value = current+1 ;
}while (!unsafe.compareAndSwapInt(this, valueOffset, current,value ) );
// if(current==i){
// i = value; //赋值 ---c s-这一步 的时候检查我原来取的数据是否还是原来的那个样子
// } else {//修改失败怎么办?
//失败了就不管了
// }
}
public static void main(String[] args) throws InterruptedException{
LockDemo Id = new LockDemo();
for(int i = 0;i < 2 ;i++){
new Thread( ()->{
for(int j = 0;j<10000 ; j++ ){
Id.add();
}
}).start();
}
Thread.sleep(2000L);
System.out.println(Id.i);
}
}
-
系统执行那么快,这样马上比较 拿值前 拿值后 有意义吗?
答:多线程执行 --短暂 且 无序 是有意义的 -
如何理解偏移量的概念?
偏移量用来找到属性 然后就可以改了 .
而它怎么得到就有学问了, java 通过 告知对象的地址 (类.class)和 属性名字 给JVM
然后 内存 会根据这些信息得到 这个对象的属性的相对位置 即偏移量 -
执行失败了怎么办?
答:执行失败了当然要补上啊 , 一次不行 再来一次 可以使用循环语句解决 -
这样的解决方案完美吗?还是会有新的问题产生.–消耗大, 多个就不行了 ,等
1.循环+CAS,自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗。
2.仅针对单个变量的操作,不能用于多个变量来实现原子操作。
3. ABA问题。(无法体现出数据的变动) 。
为了解决这个问题 我们写了很复杂的代码 过于繁琐(java包帮我们打包了哦)
ABA 无锁编程的缺点 如图 是一种少见的情况 只知道结果不知道过程
优化一 解决代码繁琐的问题
AutomicInteger原子操作类实现原理
public class LockAutomicDemo{
volatile AutomicInteger i = new AutomicInteger();
public void add() {
i.incrementAndGet();
}
/**源码:
* AutomicInteger的incrementAndGet()方法源码
*
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
优化二 解决 ++ – 操作消耗大的问题
说明: 线程多 竞争大 消耗就大 ,只有一个能改 ,其他人失败了 就再来
原理 提供多个变量 供 线程修改(分而治之的思路)
优化三 AtomicStampedReference (包装了一个对象)通过加版本号解决ABA问题
// 存储在栈里面元素 -- 对象
public class Node {
public final String value;
public Node next;
public Node(String value) {
this.value = value;
}
@Override
public String toString() {
return "value=" + value;
}
}
// 存在ABA问题的栈(后进先出)
public class Stack {
// top cas无锁修改
AtomicReference<Node> top = new AtomicReference<Node>();
public void push(Node node) { // 入栈
Node oldTop;
do {
oldTop = top.get();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
do {
oldTop = top.get();
if (oldTop == null) { // 如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { // 模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
} while (!top.compareAndSet(oldTop, newTop)); // 将下一个节点设置为top
return oldTop; // 将旧的Top作为值返回
}
}
// 使用AtomicStampedReference消除ABA问题的栈
public class ConcurrentStack {
AtomicStampedReference<Node> top = new AtomicStampedReference<>(null, 0);
public void push(Node node) { // 入栈
Node oldTop;
int v;
do {
v = top.getStamp();// 当前top的版本号
oldTop = top.getReference();
node.next = oldTop;
} while (!top.compareAndSet(oldTop, node, v, v + 1)); // CAS 替换栈顶
}
// 出栈 -- 取出栈顶 ,为了演示ABA效果, 增加一个CAS操作的延时
public Node pop(int time) {
Node newTop;
Node oldTop;
int v;
do {
v = top.getStamp();
oldTop = top.getReference();
if (oldTop == null) { // 如果没有值,就返回null
return null;
}
newTop = oldTop.next;
if (time != 0) { // 模拟延时
LockSupport.parkNanos(1000 * 1000 * time); // 休眠指定的时间
}
} while (!top.compareAndSet(oldTop, newTop, v, v + 1)); // 将下一个节点设置为top
return oldTop; // 将旧的Top作为值返回
}
}