Java并发三大特性-原子性介绍(结合代码,分析源码)

Java并发三大特性-原子性介绍(结合代码,分析源码)_第1张图片

目录

一、原子性概念

1.1 概念

二、原子性代码例子

2.1 代码

2.2 执行结果

三、代码分析

3.1 编译java源文件程序

3.2 查看编译文件

3.3 分析count++操作流程

3.4 总结

四、Java 中保证原子性的手段

4.1 synchronized

4.1.1 优化代码

4.1.2 测试结果

4.1.3 分析代码

4.1.3.1 编译java源文件程序

4.1.3.2 查看编译文件

4.1.3.3 分析编译文件

4.2 CAS乐观锁

4.2.1 优化代码

4.2.2 测试结果

4.2.3 分析代码

4.2.3.1 java 层面

4.2.3.2 hotspot 层面

4.2.4 CAS的缺点

4.2.4.1 局限性

4.2.4.2 ABA问题

4.2.4.3 自旋时间过长问题

4.3 Lock锁

4.3.1 优化代码

4.3.2 测试结果

4.3.3 分析代码

4.4 ThreadLocal

4.4.1 示例代码

4.4.2 测试结果

4.4.3 分析代码

4.4.4 ThreadLocal内存泄漏问题


一、原子性概念

1.1 概念

原子性指一个操作是不可分割的,不可中断的,一个线程在执行时,另一个线程不会影响到它。

二、原子性代码例子

2.1 代码

package com.ningzhaosheng.thread.concurrency.features.atom;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 18:33:27
 * @description 原子性测试
 */
public class TestAtom {
    private static int count;
    // ++操作自增
    public static void increment(){
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

2.2 执行结果

Java并发三大特性-原子性介绍(结合代码,分析源码)_第2张图片

可见,以上执行结果在多线程环境下,多线程操作共享数据时,预期的结果,与最终执行的结果不符。

三、代码分析

3.1 编译java源文件程序

为了查看到我们的count++操作流程,我们可以将代码简化,只保留count++操作的代码,然后通过javac编译该java源程序,查看编译后的.class 文件内容,分析代码执行过程。

源码简化成 如下内容:

package com.ningzhaosheng.thread.concurrency.features.atom;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 18:33:27
 * @description 原子性测试
 */
public class TestAtom {
    private static int count;
    // ++操作自增
    public static void increment(){
  
        count++;
    }


}

编译Java源文件:

javac TestAtom.java

3.2 查看编译文件

先找到编译的.class文件,如下图:

Java并发三大特性-原子性介绍(结合代码,分析源码)_第3张图片

然后执行javap 命令查看:

 javap -v .\TestAtom.class

Java并发三大特性-原子性介绍(结合代码,分析源码)_第4张图片

3.3 分析count++操作流程

Java并发三大特性-原子性介绍(结合代码,分析源码)_第5张图片

我们主要分析上图核心的画红框的部分:

count++操作的过程如下:

  1. getstatic:获取静态字段的值,这里就是从主内存中获取count的值。
  2. iconst_1: 1(int)值入栈。这里指将获取到的count值写入CPU寄存器。
  3. iadd:将count值在CPU寄存器中进行+1操作。
  4. putstatic:给静态字段赋值。即将count值写回到主内存当中。

3.4 总结

由以上过程可知,其实count++操作,并不是一个原子性操作,它包含了四个操作步骤,在多线程执行的过程中,会出现并发问题。

四、Java 中保证原子性的手段

4.1 synchronized

4.1.1 优化代码

package com.ningzhaosheng.thread.concurrency.features.atom.syn;

/**
 * @author ningzhaosheng
 * @date 2024/2/6 19:14:17
 * @description 测试synchronized保证原子性
 */
public class TestSynchronized {
    private static int count;

    // ++操作自增
    public static void increment() {
        synchronized (TestSynchronized.class) {
            count++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

4.1.2 测试结果

Java并发三大特性-原子性介绍(结合代码,分析源码)_第6张图片

我们发现,使用synchronized关键字后,在多线程并发的情况下,执行结果和预期值一致,没有了并发问题。

4.1.3 分析代码

为什么使用的synchronized之后,能解决由于原子性问题导致的并发问题呢?要回答这个问题,我们还需要编译代码,看下字节码,看到底synchronized做了些什么操作。

4.1.3.1 编译java源文件程序
​​​​​​​#编译Java源文件
javac TestSynchronized.java

Java并发三大特性-原子性介绍(结合代码,分析源码)_第7张图片

4.1.3.2 查看编译文件
javap -v .\TestSynchronized.class

Java并发三大特性-原子性介绍(结合代码,分析源码)_第8张图片

4.1.3.3 分析编译文件

我们可以通过分析编译出来的.class字节码文件,分析添加了synchronized关键字后,做了些什么操作。

Java并发三大特性-原子性介绍(结合代码,分析源码)_第9张图片

通过上图中的字节码我们可以看到,添加synchronized关键字后,在执行count++操作的getstatic、iconst_1、iadd、putstatic等四个操作步骤指令的前后位置分别添加了monitorenter、monitorexit两个线程同步指令。monitorenter指令能使线程获得对象监视器(其实就是对象锁)。monitorexit指令释放并退出对象监视器(其实就是对象锁)。这两个指令的使用能避免多线程同时操作临街资源,并保证同一时间点,只会有一个线程正在操作临界资源。从而避免了并发安全问题。

4.2 CAS乐观锁

4.2.1 优化代码

使用atomic下的原子类AtomicInteger:

package com.ningzhaosheng.thread.concurrency.features.atom.cas;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 18:37:23
 * @description 测试CAS
 */
public class TestCas {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        test1();
    }

    public static void test1() throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                count.incrementAndGet();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }

}

4.2.2 测试结果

Java并发三大特性-原子性介绍(结合代码,分析源码)_第10张图片

从结果中我们可以看出,执行结果符合预期值,从而得出:使用CAS基础上提供的原子类AtomicInteger,从而解决并发安全问题。

4.2.3 分析代码

4.2.3.1 java 层面

Java并发三大特性-原子性介绍(结合代码,分析源码)_第11张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第12张图片

我们可以分析下AtomicInteger源码,从上图中可以看到,AtomicInteger在初始化时,会初始化一个Unsafe类,然后通过这个Unsafe类调用objectFieldOffset方法获取到AtomicInteger的初始值。其实就是我们在new AtomicInteger时初始化的值,这里是0。

Java并发三大特性-原子性介绍(结合代码,分析源码)_第13张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第14张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第15张图片

我们接着分析,通过上图,我们可以知道,我们调用AtomicInteger的incrementAndGet()方法,然后接着调用Unsafe的getAndAddInt()方法,getAndAddInt()这个方法,最终调用的是JDK 提供的native方法compareAndSwapInt(),这个方法就是基于CAS锁的实现,最终JVM会帮助我们将方法实现CAS汇编指令。

4.2.3.2 hotspot 层面
  • 首先我们打开openjdk官网

https://hg.openjdk.org/

  • 然后找到unsafe.cpp类

jdk8u/jdk8u/hotspot: 69087d08d473 src/share/vm/prims/unsafe.cpp (openjdk.org)

Java并发三大特性-原子性介绍(结合代码,分析源码)_第16张图片

通过查看hostpot 源码我们可以知道,Java中Unsafe类中的native 方法:compareAndSwapInt(),最终调用到C++层面的Unsafe_CompareAndSwapInt方法,该方法最终调用了一条CPU并发原语:cmpxchg指令。它是一个原子操作,通过这个并发指令实现了共享变量的并发访问安全。

4.2.4 CAS的缺点

4.2.4.1 局限性

CAS只能保证对一个变量的操作是原子性的,无法实现对多行代码实现原子性。

4.2.4.2 ABA问题

问题描述:

比如A、B两个线程,同时对共享变量num=5进行操作,A线程操作需要花费5s,B线程需要10s,A线程先花5s时间将共享变量num修改为了6,然后再花5s将共享变量num修改回了5,此时10s的时候,B线程将共享变量修改为10,由于它之前拿到的初始值还是5,符合CAS比较交换的定义,所以能修改成功,但是其实这个值已经被其他线程修改了两次,对于这种变化,A线程是不知道的,这就是CAS的ABA问题。

解决方案:数值追加版本号,使用AtomicStampedReference,在CAS时,不但会判断原值,还会比较版本信息。

代码示例:

package com.ningzhaosheng.thread.concurrency.features.atom.cas;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 18:37:23
 * @description 测试CAS
 */
public class TestCas {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        test_atomic_stamped();
    }


    public static void test_atomic_stamped() {
        AtomicStampedReference reference = new AtomicStampedReference<>("AAA", 1);

        String oldValue = reference.getReference();
        int oldVersion = reference.getStamp();

        boolean b = reference.compareAndSet(oldValue, "B", oldVersion, oldVersion + 1);
        System.out.println("修改1版本的:" + b);

        boolean c = reference.compareAndSet("B", "C", 1, 1 + 1);
        System.out.println("修改2版本的:" + c);
    }
}

测试结果:

Java并发三大特性-原子性介绍(结合代码,分析源码)_第17张图片

4.2.4.3 自旋时间过长问题

问题说明:

在CAS比较交换的时候,如果修改值不成功,会一直循环尝试修改,直到成功为止,这个循环就称为自旋,而循环的时间就叫自旋时间。在高并发场景下,使用CAS方式修改共享变量会有自旋时间过长的问题,而自旋本身消耗性能。

解决方案:

可以指定CAS一共循环多少次,如果超过这个次数,直接失败/或者挂起线程。(自旋锁、自适应自旋锁)

可以在CAS一次失败后,将这个操作暂存起来,后面需要获取结果时,将暂存的操作全部执行,再返回最后的结果。

4.3 Lock锁

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

4.3.1 优化代码

package com.ningzhaosheng.thread.concurrency.features.atom.lock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 18:44:27
 * @description 测试ReentrantLock锁
 */
public class TestReentrantLock {
    private static int count;

    private static ReentrantLock lock = new ReentrantLock();

    // 测试++操作自增(加ReentrantLock机制)
    public static void increment() {
        lock.lock();
        try {
            count++;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                increment();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

4.3.2 测试结果

Java并发三大特性-原子性介绍(结合代码,分析源码)_第18张图片

从结果中我们可以看出,执行结果符合预期值。那么为什么使用了ReentrantLock之后,就解决了并发安全问题呢,要回答这个问题,我们还需要分析下ReentrantLock的源码实现。

4.3.3 分析代码

Java并发三大特性-原子性介绍(结合代码,分析源码)_第19张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第20张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第21张图片

通过跟踪代码可以发现,其实ReentrantLock底层是基于AQS(AbstractQueuedSynchronizer抽象队列同步器)实现的,有一个基于CAS维护的state变量来实现锁的操作。

由于篇幅原因,这里的源码分析就先只到这个深度,后续会出专门讲解AQS和ReentrantLock的文章。

4.4 ThreadLocal

4.4.1 示例代码

package com.ningzhaosheng.thread.concurrency.features.atom.threadlocal;

/**
 * @author ningzhaosheng
 * @date 2024/2/5 19:35:09
 * @description 测试ThreadLocal
 */
public class TestThreadLocal {
    static ThreadLocal tl1 = new ThreadLocal();
    static ThreadLocal tl2 = new ThreadLocal();

    public static void main(String[] args) {
        tl1.set("123");
        tl2.set("456");
        Thread t1 = new Thread(() -> {
            System.out.println("t1:" + tl1.get());
            System.out.println("t1:" + tl2.get());
        });
        t1.start();

        System.out.println("main:" + tl1.get());
        System.out.println("main:" + tl2.get());
    }
}

4.4.2 测试结果

Java并发三大特性-原子性介绍(结合代码,分析源码)_第22张图片

使用ThreadLocal存储变量,可以杜绝变量在线程中共享,所以自然就不会有线程安全问题了。可以看到,示例中的线程t1并不能获取到主线程main中ThreadLocal中的变量值。

4.4.3 分析代码

Java并发三大特性-原子性介绍(结合代码,分析源码)_第23张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第24张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第25张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第26张图片

Java并发三大特性-原子性介绍(结合代码,分析源码)_第27张图片

跟踪源码发现,其实ThreadLocal的get()方法返回的是Thread线程类中的一个成员变量,ThreadLocalMap,而ThreadLocalMap本质上就是基于Entry[]实现的一个Map键值对数组,再基于ThreadLocal对象本身作为key,对value进行存取.

ThreadLocalMap的key是一个弱引用,弱引用的特点是,即便有弱引用,在GC时,也必须被回收。这里是为了在ThreadLocal对象失去引用后,如果key的引用是强引用,会导致ThreadLocal对象无法被回收.

4.4.4 ThreadLocal内存泄漏问题

Java并发三大特性-原子性介绍(结合代码,分析源码)_第28张图片

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

好了,本次内容就分享到这,欢迎关注本博主。如果有帮助到大家,欢迎大家点赞+关注+收藏,有疑问也欢迎大家评论留言!

你可能感兴趣的:(Java技术,java,高并发,多线程,原子性,hotspot,jvm)