温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

java高并发中线程安全性是什么

发布时间:2021-10-19 16:09:56 来源:亿速云 阅读:140 作者:柒染 栏目:大数据

这期内容当中小编将会给大家带来有关java高并发中线程安全性是什么,文章内容丰富且以专业的角度为大家分析和叙述,阅读完这篇文章希望大家可以有所收获。

定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。

线程安全性体现在以下三个方面:

  1.  原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。

  2. 可见性:一个线程对主内存的修改可以及时的被其他线程观察到。

  3. 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。

原子性 Atomic包

新建一个测试类,内容如下:

@Slf4j
@ThreadSafe
public class CountExample2 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    // 工作内存
    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        //线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    public static void add() {
        count.incrementAndGet();
    }

}

使用了AtomicInteger类,这个类的incrementAndGet方法底层使用的unsafe.getAndAddInt(this, valueOffset, 1) + 1;方法,而底层使用了this.compareAndSwapInt方法。这个compareAndSwapInt方法(CAS)是用当前值与主内存的值进行对比,如果值相等则进行相应的操作。

count变量就是工作内存,它与主内存中的数据不一定是一样的,因此需要做同步操作才可以。 

AtomicLong与LongAdder

我们将上面的count用AtomicLong来修饰,同样可以输出正确的效果:

public static AtomicLong count = new AtomicLong(0);

我们为什么要单独说一下AtomicLong?因为JDK8中新增了一个类,与AtomicLong十分像,即LongAdder类。将上面的代码用LongAdder实现一下:

public static LongAdder count = new LongAdder();

public static void add() {
        count.increment();
    }

log.info("count:{}", count);

同样也可以输出正确的结果。

为什么有了AtomicLong后还要新增一个LongAdder?

原因是AtomicLong底层使用CAS来保持同步,是在一个死循环内不断尝试比较值,当工作内存与主内存数据一致的情况下才执行后续操作,竞争不激烈的时候成功几率高,竞争激烈时也就是并发量高时性能就会降低。对于Long和Double变量来说,jvm会将64位的Long或Double变量的读写操作拆分成两个32位的读写操作。因此实际使用过程中可以优先使用LongAdder,而不是继续使用AtomicLong,当竞争比较低的时候可以继续使用AtomicLong。

查看atomic包:

java高并发中线程安全性是什么

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。

AtomicIntegerFieldUpdater

原子性更新某个类的实例的某个字段的值,并且这个字段必须用volatile关键字修饰同时不能是static修饰的。

private static AtomicIntegerFieldUpdater<AtomicExample5> updater = AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class, "count");
    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {
        private AtomicExample5 example5 = new AtomicExample5();
        if (updater.compareAndSet(example5, 100, 120)){
            log.info("update success 1, {}", example5.getCount());
        }
        if(updater.compareAndSet(example5, 100, 120)){
            log.info("update success 2 ,{}", example5.getCount());
        }else {
            log.info("update failed, {}", example5.getCount());
        }
    }

AtomicStampReference:CAS的ABA问题

ABA问题是:在CAS操作的时候,其他线程将变量的值A改成了B,随后又改成了A,CAS就会被误导。所以ABA问题的解决思路就是将版本号加一,当一个变量被修改,那么这个变量的版本号就增加1,从而解决ABA问题。

AtomicBoolean

@Slf4j
@ThreadSafe
public class AtomicExample6 {

    private static AtomicBoolean isHappened = new AtomicBoolean(false);

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static void main(String[] args) throws InterruptedException {
        //线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    test();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened);
    }

    private static void test() {
        if (isHappened.compareAndSet(false, true)){
            log.info("excute");
        }
    }

}

这段代码test()方法只会被执行5000次而进入log.info("excute")只会被执行一次,因为isHappened变量执行一次之后就变为true了。

这个方法可以保证变量isHappened从false变成true只会执行一次。

这个例子可以解决让一段代码只执行一次绝对不会重复。

原子性 锁

  • synchronized:synchronized关键字主要是依赖JVM实现锁,因此在这个关键字作用对象的作用范围内都是同一时刻只能有一个线程可以进行操作的。

  • lock:依赖特殊的CPU指令,实现类中比较有代表性的是ReentrantLock。

synchronized同步锁

修饰的对象主要有一下四种:

  • 修饰代码块:大括号括起来的代码,作用于调用的对象

  • 修饰方法:整个方法,作用于调用的对象

  • 修饰静态方法:整个静态方法,作用于这个类的所有对象

  • 修饰类:括号括起来的部分,作用于所有对象

修饰代码块和方法

举例如下:

@Slf4j
public class SynchronizedExample1 {

    /**
     * 修饰一个代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用的对象是调用代码的对象
     */
    public void test1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}", i);
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
        executorService.execute(() -> {
            synchronizedExample1.test1();
        });
    }

}

为什么我们要使用线程池?如果不使用线程池的话,两次调用了同一个方法,本身就是同步执行的,因此是无法验证具体的影响,而我们加上线程池之后,相当于分别启动了两个线程去执行方法。

输出结果是连续输出两遍test1 0-9。

如果使用synchronized修饰方法:

    /**
     * 修饰一个方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用方法的对象
     */
    public synchronized void test2() {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}", i);
        }
    }

输出结果跟上面一样,是正确的。

接下来换不同的对象,然后乱序输出,因为同步代码块和同步方法作用对象是调用对象,因此使用两个不同的对象调用不同的同步代码块互相是不影响的,如果我们使用线程池,example1的test1方法和example2的test1方法是交叉执行的,而不是example1的test1执行完然后再执行example2的test1,代码如下:

 /**
     * 修饰一个代码块,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用的对象是调用代码的对象
     */
    public void test1(int flag) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}, {}", flag, i);
            }
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        SynchronizedExample1 synchronizedExample2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test1(2);
        });
    }

因此同步代码块作用于当前对象,不同调用对象之间是互相不影响的。

接下来测试同步方法:

/**
     * 修饰一个方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用方法的对象
     */
    public synchronized void test2(int flag) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}, {}", flag, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample1 synchronizedExample1 = new SynchronizedExample1();
        SynchronizedExample1 synchronizedExample2 = new SynchronizedExample1();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test2(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test2(2);
        });
    }

如果一个方法内部是一个完整的同步代码块,就像上面的test1方法一样,那么它和用synchronized修饰的方法效果是等同的。

同时需要注意的synchronized修饰是无法继承给子类的方法。

修饰静态方法和类

我们先测试修饰静态方法:

 /**
     * 修饰一个静态方法,被修饰的方法称为同步方法,作用范围是整个方法,作用的对象是调用方法的对象
     */
    public static synchronized void test2(int flag) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 - {}, {}", flag, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample2 synchronizedExample1 = new SynchronizedExample2();
        SynchronizedExample2 synchronizedExample2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test2(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test2(2);
        });
    }

修饰一个静态方法作用于这个类的所有对象。因此我们使用不同的对象调用synchronized修饰的静态方法时,同一时间只有一个线程在执行。因此上面的执行结果是:

11:31:37.447 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 0
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 1
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 2
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 3
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 4
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 5
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 6
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 7
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 8
11:31:37.451 [pool-1-thread-1] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 1, 9
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 0
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 1
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 2
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 3
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 4
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 5
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 6
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 7
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 8
11:31:37.451 [pool-1-thread-2] INFO com.vincent.example.sync.SynchronizedExample2 - test2 - 2, 9

他们不会交替执行。

然后调用修饰类的:

/**
     * 修饰一个类,被修饰的代码称为同步语句块,作用范围是大括号括起来的代码,作用的对象是调用代码的对象
     */
    public static void test1(int flag) {
        synchronized (SynchronizedExample2.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 - {}, {}", flag, i);
            }
        }
    }
    public static void main(String[] args) {
        SynchronizedExample2 synchronizedExample1 = new SynchronizedExample2();
        SynchronizedExample2 synchronizedExample2 = new SynchronizedExample2();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            synchronizedExample1.test1(1);
        });
        executorService.execute(() -> {
            synchronizedExample2.test1(2);
        });
    }

运行结果跟上面是一致的。

同样的如果一个方法内部被synchronized修饰的一个类是一个完整的同步代码块,就像上面的test1方法一样,那么它和用synchronized修饰的静态方法效果是等同的。

synchronized:不可中断锁,适合竞争不激烈,可读性好。

lock:可中断锁,多样化同步,竞争激烈时能维持常态。

Atomic:竞争激烈时能维持常态,比lock性能好;只能同步一个值。

可见性

可见性是指线程对主内存的修改可以及时的被其他线程观察到。说起可见性,我们常常去向什么时候不可见,下面介绍一下共享变量在线程间不可见的原因。

  • 线程交叉执行

  • 重排序结合线程交叉执行

  • 共享变量更新后的值没有在工作内存与主内存间及时更新。

JMM关于synchronized的两条规定:

  • 线程解锁前,必须把共享变量的最新值刷新到主内存。

  • 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁) 

 可见性 - volatile

通过加入内存屏障和禁止重排序优化来实现:

  • 对volatile变量写操作时,会在写操作后加入一条store屏障命令,将本地内存中的共享变量值刷新到主内存。

  • 对volatile变量读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。

volatile变量在每次对线程访问时,都强迫从主内存中读取该变量的值,而当该变量发生改变时,又会强迫线程将最新的值刷新到主内存,这样任何时候不同的线程总能看到该变量的最新值。 下面举例说明:

@Slf4j
@NotThreadSafe
public class CountExample4 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        //线程池
        ExecutorService executorService = Executors.newCachedThreadPool();
        //定义信号量
        final Semaphore semaphore = new Semaphore(threadTotal);
        //定义计数器
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for(int i = 0; i < clientTotal; i++) {
            executorService.execute(() ->{
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (InterruptedException e) {

                    log.error("exception", e);
                }
                countDownLatch.countDown();

            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    public static void add() {

        // 使用volatile修饰,可以保证count是主内存中的值
        count++;
    }

}

运行结果依然无法保证线程安全。为什么呢?

原因是当我们执行count++的时候呢,它其实是分了三步,1.从主内存中取出count值,这时的count值是最新的,2给count执行+1操作,3.将count值写回主内存。当多线程同时读取到count的值并且给count值+1,这样就会出现线程不安全的情况。

因此通过使用volatile修饰变量不是线程安全的。同时也说明volatile不具有原子性。

既然volatile不适合计数的场景,那么适合什么场景呢?

通常来说使用volatile必须具备 对变量的写操作不依赖与当前值。

原子性 有序性

java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

有序性 - happens-before原则

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写后面的操作。对于程序次序规则来说,一段程序代码的执行在单个线程中看起来是有序的(注意,虽然在这条规则中提到书写在前面的操作先行发生于书写后面的操作,这是程序看起来执行顺序是按照代码书写的顺序执行的,而虚拟机会对程序代码进行指令重排序,虽然进行了重排序,但是最终执行的结果是与程序顺序执行的结果是一样的, 它只会对不存在数据依赖行的指令进行重排序,因此在单个线程中,程序看起来是有序执行的),事实上这个规则是用来保证程序在单线程中执行结果的正确性。但无法保证程序在多线程中执行的正确性。

  • 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。也就是说无论在单线程中还是多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续进行lock操作。

  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。如果一个线程先去写一个变量,然后一个线程进行读取,那么写入操作肯定先行发生于读操作。

  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。

  • 线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作。

  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。

  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。

  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。

上述就是小编为大家分享的java高并发中线程安全性是什么了,如果刚好有类似的疑惑,不妨参照上述分析进行理解。如果想知道更多相关知识,欢迎关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI