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

发布时间:2021-10-19 16:09:56 作者:柒染
来源:亿速云 阅读:144

这期内容当中小编将会给大家带来有关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同步锁

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

修饰代码块和方法

举例如下:

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

@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原则

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

推荐阅读:
  1. Java中线程的原理是什么
  2. java中线程调度指的是什么

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

java

上一篇:如何使用python中传递不可变对象

下一篇:如何理解排序算法

相关阅读

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

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