乐观锁-无锁并发

无锁

问题提出

一个账户内有10000,1000个线程每个线程减去10,最后正确的结果应该是0。

public interface Account {
    Integer getAmount();
    void withdraw(Integer amount);

    public static void demo(Account account){
        ArrayList<Thread> threadList = new ArrayList<>();
        long start = System.nanoTime();
        for(int i = 0;i < 1000; i++){
            Thread t = new Thread(()->{
                account.withdraw(10);
            });
            threadList.add(t);
        }

        threadList.forEach(Thread ::start);	//遍历1000个线程,并调用start方法启动
        threadList.forEach(t->{
            try {
                t.join();								//保证1000个线程在主线程之前执行完毕以确保执行时间的正确
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getAmount()+"cost:"+TimeUnit.NANOSECONDS.toMillis(end-start)+"ms");
    }
}
为什么不安全
class SafeAccount implements Account{
    private Integer amount;
    public  SafeAccount(Integer amount) {
        this.amount = amount;
    }
    @Override
    public Integer getAmount() {
        return amount;
    }
    @Override
    public  void withdraw(Integer amount) {
        this.amount -= amount;
    }

    public static void main(String[] args) {
        Account account = new SafeAccount(10000);
        Account.demo(account);
    }
}

image-20210121205935443

最后结果并不是0,发生错误的原因是指令交错的原因造成的,比如线程一获得amout开始-10操作时另一个线程也开始同样的操作,使得结果发生错误。

解决思路-锁

使用synchronized关键字保护共享变量amount,当一个线程完成了-10操作并且释放了锁其他线程才能执行,让线程之间串行运行。这样不会造成指令交错,确保了操作的原子性

public synchronized void withdraw(Integer amount) {	
    this.amount -= amount;
}
解决思路-无锁
class UnlockAccount implements Account{
    private AtomicInteger balance;	//使用原子整数
    publicnlockAccount(Integer amount) {
        balance = new AtomicInteger(amount);
    }
    @Override
    public Integer getAmount() {
        return balance.get();
    }
    @Override
    public void withdraw(Integer amount) {
        while (true){
            int prev = balance.get();	//获取旧值
            int next = except - amount;	//操作后的新值
          //将旧值prev与此时的最新值比较如果相等则将最新设置为next返回true并结束循环,如果不相等就返回false继续尝试此操作直到成功。
            if (balance.compareAndSet(prev,next))   
                break;
        }
    }
    public static void main(String[] args) {
        Account account = new unlockAccount(10000);
        Account.demo(account);
    }
}

image-20210121215443270

无锁方式可以允许指令的交错,但是当发生指令交错后会让这次操作失效,继续尝试直到成功。

CAS与volatile
CAS

使用原子整数在不加锁的情况下可以实现对共享变量安全的操作。

以下是AtomicInteger的部分源码

public class AtomicInteger extends Number implements java.io.Serializable {
      public AtomicInteger(int initialValue) {
        value = initialValue;	//给value赋初始值
    }
 //获取Unsafe对象,提供了非常底层的,操作线程,内存的方法。
  private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;
  //获得value属性在AtomicInteger中的偏移量,用来定位value的地址,直接操作它。
  valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
  //将value设置为volatile确保线程间的可见性
  private volatile int value;
  public final boolean compareAndSet(int expect, int update) {
    /*通过vaueOffet定位到value并且比较和旧值expect是否相等。
    如果相等将value值设置为update
    如果不等表示这次操作失败,返回false。
     */ 
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update); //这个操作是原子性的,不会被其他线程干扰
    }
}

其中关键的就是compareAndSet,它的简称就是CAS,它属于原子操作。CAS的底层是lock cmpxchg指令(X86架构),在单核和多核CPU下都能保证【比较-交换】的原子性。

volatile

获取共享变量时为了保证变量的可见性需要使用volatile修饰,保证一个线程对变量的修改另一个线程可见。CAS操作必须借助volatile才能读取共享变量的最新值保证【比较-交换】的有效性。

为什么无锁效率高
  • 当cas操作不成功时使用while(true)循环不断尝试,该线程一直处于running状态不会停歇。而synchronized会让没获得锁的线程陷入等待,发生上下文切换。
  • 无锁情况下要保持线程不断的运行需要多个cpu的支持,如果cpu数小于线程数,虽然不会陷入阻塞,但由于没有分到时间片,仍然会进入Runnable状态,还是会导致上下文切换。所以在多核cpu下才由效果(线程数<cpu数)。
乐观锁和悲观锁
  • CAS基于乐观锁的思想:最客观的估计,在当前线程执行时不怕别的线程来修改共享变量,即使修该了也没关系,可以再继续重试
  • synchronized 基于悲观锁的思想:最悲观的估计,在我执行时就上锁不给其他线程修改共享变量的机会,只有我执行完解锁,其他线程才可以修改共享变量。
  • CAS体现的是无锁并发无阻塞并发
    • 没有使用synchronized,线程不会陷入阻塞,这时效率提升的因素之一。
    • 但是如果竞争激烈,重试频繁发生,反而影响效率。
原子类
1、原子引用类
  • AtomicReference
  • AtomicMarkableReference 给共享变量一个标记,记录是否被修改过
  • AtomicStampedReference 给共享变量附加一个版本号,记录被修改过的次数
2、ABA

多个线程将共享变量的值由A–>B,再由B–>A。其他线程在操作时不会知道共享变量是否被修改过。

static AtomicReference<String> ref = new AtomicReference<>("A");
public static void main(String[] args) throws InterruptedException {
    String prev = ref.get();
    other();
    Thread.sleep(2000);
    log.debug("change A->B{}",ref.compareAndSet(prev,"B"));
}
public static void other(){
    new Thread(()->{
        log.debug("change A->B {}",ref.compareAndSet(ref.get(),"B"));
    },"t1").start();

    new Thread(()->{
        log.debug("change B->A {}",ref.compareAndSet(ref.get(),"A"));
    },"t2").start();
}

image-20210127155531103

如果主线程希望只要其他线程修改了共享变量,那么就算自己CAS失败,这样仅比较值是不够的,需要再加一个版本号。

AtomicStampedReference

static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A",0);
public static void main(String[] args) throws InterruptedException {
    String prev = ref.getReference();
    int stamp = ref.getStamp();
    log.debug("start:{},stamp:{}",prev,stamp);
    other();
    Thread.sleep(2000);
    log.debug("change A->B{}",ref.compareAndSet(prev,"B",stamp,ref.getStamp()+1));
}
public static void other(){
    new Thread(()->{
        log.debug("change A->B {},stamp:{}",ref.compareAndSet(ref.getReference(),"B",ref.getStamp(),ref.getStamp()+1),ref.getStamp());
    },"t1").start();

    new Thread(()->{
        log.debug("change B->A {},stamp:{}",ref.compareAndSet(ref.getReference(),"A",ref.getStamp(),ref.getStamp()+1),ref.getStamp());
    },"t2").start();
}

image-20210127160005417

如果只关心是否被修改过,并不关系修改过几次,可以使用

AtomicMarkableReference

static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A",false);	//初始值设为false,即没有被修改过。
public static void main(String[] args) throws InterruptedException {
    String prev = ref.getReference();
    other();
    Thread.sleep(2000);
    log.debug("change A->B {}",ref.compareAndSet(prev,"B",false,true));
}
public static void other(){
    new Thread(()->{
        log.debug("change A->B {}",ref.compareAndSet(ref.getReference(),"B",false,true));
    },"t1").start();

    new Thread(()->{
        log.debug("change B->A {}",ref.compareAndSet(ref.getReference(),"A",true,true));
    },"t2").start();
}

image-20210127160642893

3、原子数组
/*10个线程对长度为10的数组的每个元素分别进行1000次累加操作,线程安全的情况下数组每个元素值是1000
参数1,提供数组。原子数组或普通数组
参数2,获得数组长度的方法
参数3,自增方法,回传array,index
参数4,打印数组的方法
*/
public class SafeArray {
    public static <T> void demo(
            Supplier<T> arraySupplier,
            Function<T, Integer> lenFun,
            BiConsumer<T, Integer> putConsumer,
            Consumer<T> printConsumer
    ) {
        ArrayList<Thread> threadList = new ArrayList<>();
        T array =  arraySupplier.get();
        int len = lenFun.apply(array);
        for(int i = 0; i < len; i++){
            threadList.add(new Thread(()->{
                for(int j = 0; j < 1000; j++) {
                    putConsumer.accept(array,j%len);
                }
            }));
        }

        threadList.forEach(Thread::start);//启动所有的线程
        threadList.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        printConsumer.accept(array);
    }

}
public static void main(String[] args) {
    SafeArray.demo(
            ()-> new AtomicIntegerArray(10),
            (array)->array.length(),
            (array,index)->array.getAndIncrement(index),	//累加操作
            (array) -> System.out.println(array)
    );
}

image-20210127183035497

Unsafe

Unsafe对象提供了非常底层的,操作内存,线程的方法,不能直接调用,只能通过反射获取。

自定义一个原子类:

class AtomicData{

    //使用volatile来保证可见性
    private volatile Integer data;
    private static Unsafe unsafe;
    //data属性的偏移量,Unsafe可以通过偏移地址直接操作data
    final static long OFFSET;

    public AtomicData(Integer data) {
        this.data = data;
    }

    static {
        unsafe =  UnsafeAccessor.getUnsafe();
        try {
            OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }

    public int getAndUpdate(ResultOperator resultOperator){
        int prev,update;
        do{
            prev = get();
            update = resultOperator.applyAsInt(prev);
            System.out.println(update);
        }while (!(unsafe.compareAndSwapInt(this,OFFSET,prev,update)));
        return prev;
      
    }
    public final int get(){
        return data;
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值