保证原子性的几种方式,你都知道吗???

1. 前言

今天主要是从实战 + 浅谈原理的角度来说下,并发线程下原子性的几种处理方式,好了废话不多说了,接下来让我们看看吧

2. 开始

在开始之前需要提问下大家, 代码i ++; 能保持原子性吗??? 是不是一句话就执行ok了呢??? 接下来看下字节码分析:。

public class T01_Thread_Test01 {
    static int i = 0;
    public static void main(String[] args) {
        i ++;
    }
}

在这里插入图片描述

通过上述的截图可以看到,短短一句话i++ 会被翻译为很多条语句,在多线程的情况下,执行到任何语句时都会被别的线程打断,所以很难保证执行结果的一致性。

所以为了保证原子性,我们就要代码执行过程中不可拆分。

2.1 synchronized

public class T03_Thread_Synchronized {
    static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (T03_Thread_Synchronized.class) {
                for (int i = 0; i < 100000; i++) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (T03_Thread_Synchronized.class) {
                for (int i = 0; i < 100000; i++) {
                    count ++;
                }
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(count);
    }
}

上锁是一定能保证原子性的。在保证某个线程拿到锁后,别的线程只能处于blocked状态。是无法构成资源竞争的。
其实锁的本质也是并发线程的序列化操作

在这里插入图片描述

通过上述的截图中也可以看出。添加synchronized之后,从代码层面就相当于上锁了。等语句执行结果后会自动释放锁。

2.2 CAS

  • CAC 全称是compare and swap. 也称为自旋锁,无锁,乐观锁等。
  • 其大致的原理是:在替换内存的某个位置的值时,会判断下运算之前的值 跟 内存的值是否保持一致(为了避免别的线程也修改过此值)。 如果一致的话,直接替换。反之拿到新值继续 比较-替换
  • 但是CAS底层也是基于lock来做的。因为CAS可以分为多步,值的对比以及值的写入。有可能在写入的过程中被别的线程打断,所以必须加锁。

在这里插入图片描述

CAS在Java代码中如何体现呢??? Java中基于Unsafe的类提供了对CAS的操作的方法,JVM会帮助我们将方法实现CAS汇编指令

public class T02_Thread_CAS {
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicInteger.incrementAndGet();
            } 
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                atomicInteger.incrementAndGet();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println(atomicInteger);
    }
}
  • 通过上述的代码我们会发现,整个过程中我们并没有用到锁。而是使用Java提供的API incrementAndGet。而incrementAndGet 其实就是基于类unsafe 来实现的。
    在这里插入图片描述
  • 通过下列字节码可以看到。以下的代码其实就是自旋的过程
    在这里插入图片描述

2.2.1 CAS不足之处:

2.2.1.1 ABA 问题

ABA问题:问题如下,可以引入版本号的方式,来解决ABA的问题。Java中提供了一个类在CAS时,针对各个版本追加版本号的操作。 AtomicStampeReferenceimage.png

2.2.1.2 自旋时间过长的问题
  • 可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)
  • 可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果

2.3 lock锁

Lock锁是在JDK1.5由Doug Lea研发的,他的性能相比synchronized在JDK1.5的时期,性能好了很多多,但是在JDK1.6对synchronized优化之后,性能相差不大,但是如果涉及并发比较多时,推荐ReentrantLock锁,性能会更好

public class T05_Thread_ReentrantLock {

    static ReentrantLock lock = new ReentrantLock();
    private static int count = 0;

    public static void add() {
        try {
            lock.lock();
            for (int i = 0; i < 100000; i++) {
                count ++;
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t01 = new Thread(T05_Thread_ReentrantLock::add);
        Thread t02 = new Thread(T05_Thread_ReentrantLock::add);

        t01.start();
        t02.start();

        t01.join();
        t02.join();

        System.out.println(count);
    }
}

注意:因为lock锁无法自动释放锁。所以unlock释放锁的从操作必须用finally 包裹下,表示释放锁的操作一定会执行。

2.4 ThreadLocal

其他方式都是允许线程操作共享资源,才会形成了资源竞争。但是ThtreadLocal 是避免操作临界值的,每个值只单独在自己的线程中。这样就避免了资源竞争了

public class T06_Thread_ThreadLocal {

    static ThreadLocal t1 = new ThreadLocal();
    static ThreadLocal t2 = new ThreadLocal();

    public static void main(String[] args) {
        t1.set(123);
        t2.set(456);
        t1.set(345);

        new Thread(() -> {
            System.out.println("线程:" + t1.get());
            System.out.println("线程" + t2.get());
        }).start();

        System.out.println("main线程" + t1.get());
        System.out.println("main线程" + t2.get());
    }
}

2.4.1 ThreadLocal实现原理

  • 每个Thread中都存储着一个成员变量,ThreadLocalMap
  • ThreadLocal本身不存储数据,像是一个工具类,基于ThreadLocal去操作ThreadLocalMap
  • ThreadLocalMap本身就是基于Entry[]实现的,因为一个线程可以绑定多个ThreadLocal,这样一来,可能需要存储多个数据,所以采用Entry[]的形式实现。
  • 每一个线程都自己独立的ThreadLocalMap,再基于ThreadLocal对象本身作为key,对value进行存取
  • ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收

2.4.2 ThreadLocal内存泄漏问题

  • 如果ThreadLocal引用丢失,key因为弱引用会被GC回收掉,如果同时线程还没有被回收,就会导致内存泄漏,内存中的value无法被回收,同时也无法被获取到。
  • 只需要在使用完毕ThreadLocal对象之后,及时的调用remove方法,移除Entry即可

image.png

3. 结束

关于保证原子性的几种方式就列举这么多了。如果大家不同的看法 欢迎请在评论区内留言,欢迎一起进步

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值