CAS&Atomic

280 阅读9分钟

保证原子性--加锁 但是加锁有可能会出现死锁的现象这样就会造成很大的cpu资源浪费 简单的累加操作,比如count++用synchronized给这个操作上锁这样是很影响性能的,于是在Java中提供了Atomic原子操作的系列类,这些原子类的实现机制是借助cpu的指令来完成的,这些指令有个通用的名字就是CAS----涉及两个操作,但是通过cpu的指令可以保证这两个操作是一个原子操作。

这种操作包含三个运算符:

  • 内存地址V
  • 期望值A:即要修改的旧值A
  • 一个新值B:更新的值
public final boolean compareAndSet(int except,int update){

        return unsafe.compareAndSwapInt(this,valueOffset,except,update);
}

this,valueOffset这两个可以理解为这个变量在内存中的地址V,except就是变量现在的旧值A,update就是我们期望改成的新值B

对于Atomic原子类的操作都是基于循环CAS来实现的。

CAS.jpg

CAS实现原子操作的三大问题

  • ABA问题

AtomicMarkableRefrence、AtomicStampedRefrence,这两个原子操作解决了ABA问题

- AtomicMarkable 在解决版本更新的问题的时候只关心,原来的值有没有改过,至于改过几次是没有实现的
- AtomicStampedRefrence 既实现了是否改过的,也实现了改过几次。
  • 循环时间长开销大
  • 只能保证一个原子变量的操作

多个变量进行操作可以封装为对象,对这个封装的对象进行原子操作

jdk1.8引入LongAdder----可以进行写热点分散,来实现Java语言里面原子操作的性能,这种在架构设计中也是非常重要的思想,例如项目中针对一些高并发写的操作,可以采用数据分片的策略,即对要处理的数据或者请求分成多份并行处理(后面的ForkJoinPool分治思想也是类似的实现),比如数据库的分库分表。数据库为了应对高并发读的压力,可以加缓存、slave

LongAdder里面有两个变量:

  • base:非竞态条件下,直接累加到该变量上

并发冲突小的情况,对base这个变量直接进行读写

  • cell型的数组:非竞态条件下,直接累加到该变量上

并发上来了,做写热点的分散,cell[]是一个数组,比如cell里面有16个元素,这个时候有32个或者更多的线程过来同时读写,于是让编号1,2的线程读取第一个元素,编号3,4的线程读写第二个元素,编号5,6的线程读写地个元素。通过这种方式就把32个线程集中在base上面的读写操作,分散到了16个元素上面了。充分解决了AtomicInteger针对某一个value变量的高并发读写问题。最终会通过sum方法将结果进行汇总计算

image.png

image.png

但是sum这个值不能保证强一致,是一个近似值。因此LongAdder只能保证值是一个估计值,AtomicInteger能保证十一准确的值

并发安全问题---什么是线程安全的类

image.png

实现线程安全的方式

线程封闭

实现好的并发是一件困难的事情,所以很多时候我们都想躲避并发。避免并 发最简单的方法就是线程封闭。什么是线程封闭呢? 就是把对象封装到一个线程里,只有这一个线程能看到此对象。那么这个对 象就算不是线程安全的也不会出现任何安全问题。 能使用局部变量就使用局部变量,能够封闭在线程中的栈的内部。全局变量尽可能的少用。

栈封闭

首先我们知道,在JVM中堆和方法区是线程共享的区域,栈是每个线程私有的(本地方法栈,虚拟机栈,程序计数器也是线程私有的)

栈封闭是我们编程当中遇到的最多的线程封闭。什么是栈封闭呢?简单的说 就是局部变量。多个线程访问一个方法,此方法中的局部变量都会被拷贝一份到 线程栈中。所以局部变量是不被多个线程所共享的,也就不会出现并发问题。所 以能用局部变量就别用全局的变量,全局变量容易引起并发问题。

ThreadLocal

它是实现线程封闭最好的方法。ThreadLocal内部维护了一个Map,Map的key是每个线程的名称,而Map的值就是我们要封闭的对象。每个线程中的对象都对应着Map中一个值,即Threadlocal利用Map实现了对象的线程封闭。

实现无状态的类

面向对象--把数据以及对数的操作封装在一起,当定义一个类的时候,会去定义成员变量 ,会有对成员变量的一系列的操作,这些成员变量就可以叫做类的状态。

死锁和活锁

  • 死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信 而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去
  • 活锁:不执行业务代码,反复地执行在jdk底层实现拿锁释放锁的过程

产生死锁的四个必要的条件:

  1. 互斥条件

指进程对所分配到的资源进行排它性使用,即在一段时间内 某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待, 直至占有资源的进程用毕释放。

  1. 请求和保持条件

指进程已经保持至少一个资源,但又提出了新的资源 请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其 它资源保持不放---吃着碗里的看着锅里的,肯定会存在一定的问题

  1. 不可剥夺资源

一个进程已经获得的资源,在未使用完之前不能被剥夺,直到当前进程使用完才释放

  1. 循环等待

指在发生死锁时,必然存在一个进程——资源的环形链, 即进程集合{P0,P1,P2,···,Pn}中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

只要打破上面的四个条件之一就可避免死锁

工作中出现死锁的情况和排查

在Java世界中多线程争夺多个资源,不可避免的存在死锁问题,这些现象往往是比较隐蔽的,不定时间的。旦程序发生了发生了死锁,是没有任何的办法恢复的,只能 重启程序,对生产平台的程序来说,这是个很严重的问题。

同时,实际工作过程中,一旦出现没有任何异常信息,只知道这个应用的 所有业务越来越慢,最后停止服务,无法确定是哪个具体业务导致的问题;测试 部门也无法复现,并发量不够。 这时候可以通过,jps查看当前运行的Java进程,通过jstack id查看应用的锁的持有情况

尽量避免死锁

关键是保证拿锁的顺序一致性

1.内部通过顺序比较保证拿锁的顺序性

2.采用尝试拿锁的方式

ReentrantLock中的tryLock方法,就是尝试拿锁的机制.尝试拿锁成功则返回true,否则返回false

其他线程安全问题

  • 活锁:

两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一 个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有 的锁释放的过程。

解决办法:每个线程休眠随机数。

  • 线程饥饿

低优先级的线程总是拿不到执行时间

线程安全的单例模式

在设计模式中,单例模式是一种比较常见的设计模式,怎么实现单例呢?

public class SingleDce{
    private int a;
    private User user;//对象的域不一定赋值完成
    private static SingleDcl singleDcl;
    public static SingleDcl getInstance(){
        if(SingleDcl == null){
        Synchronized(SingleDcl.class){//类锁
        if(SingleDcl == null){//**对象的引用有了**
        singleDcl = new SingleDcl();
        
        }
        
        }
        }
        return signleDcl;
    
    }


}

上面的双重检查锁存在着线程安全的问题,因为在 singleDcl = new SignleDcl();过程中虽然只有一行代码,但是是一个对象创建的过程。在具体执行的时候有好几步操作:

  1. JVM为SingleDcl的对象实例分配空间
  2. 进行对象初始化,完成new的操作
  3. JVM把这个地址赋给我们的引用SingleDcl

因为 JVM 内部的实现原理(指并发相关的重排序等), 会产生一种情况,

第 3 步会在第 2 步之前执行。

于是在多线程下就会产生问题:

A 线程正在 syn 同步块中执行 singleDcl = new SingleDcl(),此时 B 线程也来执行 getInstance(),进行了 singleDcl == null 的检查, 因为第 3 步会在第 2 步之前执行,B 线程检查发现 singleDcl 不为 null,会直接拿 着 singleDcl 实例使用,但是这时 A 线程还在执行对象初始化,这就导致 B 线程 拿到的 singleDcl 实例可能只初始化了一半,B 线程访问 singleDcl 实例中的对象域 就很有可能出错。

怎么解决这个问题呢?

在前面声明 singleDcl 的位置: private static SingleDcl singleDcl; 加上 volatile 关键字,变成 private volatile static SingleDcl singleDcl; 即可,volatole关键字可以保证变量的可见性和有序性,Lock前缀指令禁止指令重排

单例模式推荐实现

  • 懒汉式

    类初始化模式,也叫延迟占位模式。在单例类的内部,由一个私有的静态内部类来持有这个单例类的实例。因为在JVM中对类的加载和初始化,有虚拟机保证线程安全

    public class SingleInit{
    private SingleDcl(){
    }
    //私有的静态内部类持有单例类的对象,在类的加载和初始化阶段保障这个对象是线程安全的
    private static class InstanceHolder{
        private static SingleInit instance = new SingleInit();
    }
    public static SingleInit getInstance(){
    return InstanceHolder.instance;
    }
    }
    
  • 饿汉式

在生明的时候就new这个类的实例或者使用枚举

public class SingleEhan{
private SingleEHan(){}
private static SinlgeEHan instance = new SingleInstance();
}