线程安全性-原子性、可见性、有序性

简介:

线程安全性

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

  • 原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作
  • 可见性:一个线程对主内存的修改可以及时被其他线程观察到
  • 有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序


原子性


Atomic包

这里使用AtomicInteger进行计数,Java底层是使用CAS进行的悲观锁的同步。
详解CAS: https://blog.csdn.net/v123411739/article/details/79561458
Java中的CAS: https://blog.csdn.net/mmoren/article/details/79185862

上文提到的CAS都有三个操作数,内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。我再理解这个概念时遇到了一个问题,在多线程的情况下,java如何知道预期原值。这实际上和之前的JVM内存模型有关。

一个线程间共享的变量,首先在主存中会保留一份,然后每个线程的工作内存也会保留一份副本。这里说的预期值,就是线程保留的副本。当该线程从主存中获取该变量的值后,主存中该变量可能已经被其他线程刷新了,但是该线程工作内存中该变量却还是原来的值,这就是所谓的预期值了。当你要用CAS刷新该值的时候,如果发现线程工作内存和主存中不一致了,就会失败,如果一致,就可以更新成功。

package com.ice.concurrency.example.count;
import com.ice.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@ThreadSafe
public class CountExample2 {

    public static int clientTotal = 5000;

    public static int threadTotal = 200;

    public static AtomicInteger count = new AtomicInteger();

    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());
    }

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

AtomicInteger调用了java底层的unsafe的getAndAddInt方法,这里是实现CAS的关键
unsafe类下调用的方法大多是native方法,由jvm本地实现。在Windows环境下,CAS方法好像是由C++实现的。Linux的X86下主要是通过cmpxchgl这个指令在CPU级完成CAS操作的,但在多处理器情况下必须使用lock指令加锁来完成。
实际上Java底层到底是如何实现CAS的,我还不是十分清楚。但目前而言,已经大致理解了CAS的原理和优缺点即可。


AtomicReferce类
@Slf4j
@ThreadSafe
public class AtomicExample4 {

    private static AtomicReference<Integer> count = new AtomicReference<>(0);

    public static void main(String[] args) {
        count.compareAndSet(0,2); // 2
        count.compareAndSet(0,1); // no
        count.compareAndSet(1,3); // no
        count.compareAndSet(2,4); // 4
        count.compareAndSet(3,5); // no
        log.info("count:{}",count.get());
    }
}
>>> count:4
AtomicIntegerFieldUpdater类
@Slf4j
@ThreadSafe
public class AtomicExample5 {

    private static AtomicIntegerFieldUpdater<AtomicExample5> updater =
            AtomicIntegerFieldUpdater.newUpdater(AtomicExample5.class,"count");

    // count字段必须为volatile修饰且非static才能被AtomicIntegerFieldUpdater改变。
    @Getter
    public volatile int count=100;

    private static AtomicExample5 example5 = new AtomicExample5();

    public static void main(String[] args) {
        if(updater.compareAndSet(example5,100,120)){
            log.info("update success, {}",example5.getCount());
        }

        if(updater.compareAndSet(example5,100,120)){
            log.info("update success, {}",example5.getCount());
        }else{
            log.info("update failed, {}",example5.getCount());
        }
    }
}
AtomicStampReference类(解决ABA问题)

该类的核心方法compareAndSet,该类是通过一个重新定义一个stamp的值来标记当前值是否被更改过。

AtomicLongArray类

这里是通过传入一个index值来更新数组内的值

AtomicBoolean类
@Slf4j
@ThreadSafe
public class AtomicExample6 {

    public static int clientTotal = 5000;

    public static int threadTotal = 200;

    public static AtomicBoolean count = new AtomicBoolean(false);

    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:{}",count.get());
    }

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

运行这段代码,可以看到log只打印了一次execute。因为不论多少个线程同时访问AtomicBoolean,只有一个能成功修改它的值。


jdk主要提供的两种锁

java实现原子性的各种方法的对比

sychronized关键字

sychronized修饰不同目标时的作用范围

package com.ice.concurrency.example.sync;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@Slf4j
public class SynchronizedExample1 {

    public void test(){
        for(int i = 0;i<10;i++){
            log.info("test - {}",i);
        }
    }

    // synchronized修饰一个代码块
    public void test1(int j){
        synchronized (this){
            for(int i = 0;i<10;i++){
                log.info("test1 {} - {}",j,i);
            }
        }
    }

    // 修饰一个方法
    public synchronized void test2(int j){
        for(int i = 0;i<10;i++){
            log.info("test2 {} - {}",j,i);
        }
    }

    // 修饰一个类
    public static void test3(int j){
        synchronized (SynchronizedExample1.class){
            for(int i = 0;i<10;i++){
                log.info("test3 {} - {}",j,i);
            }
        }
    }

    // 修饰一个静态方法
    public static synchronized void test4(int j){
        for(int i = 0;i<10;i++){
            log.info("test4 {} - {}",j,i);
        }
    }

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




可见性


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

因为这两条规定,synchronized是具有可见性的
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);
    }

    private static void add(){
        // 虽然这里count变量被volatile修饰,是可见的,但依然没有原子性,线程不安全
        // 当两个线程同时操作count时,同时读一个数,又同时刷新进内存,依然会浪费一次操作
        count++;
    }
}

volatile的应用

虽然volatile修饰的变量时刻读取都是他的真实值,因此特别适合用于作为标示量。
这里线程1用作初始化,初始化成功则修改标志量inited,而只有初始化成功后,才会运行线程2while内的内容




有序性


volatile可以保证一定的有序性
synchronized,lock保证了单线程的运行,因此肯定时有序的
java内存模型具有一些先天的有序性(不需要通过任何手段就能得到的有序性,即happens-before原则)

happens-before原则

happens-before规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens- before 于该线程中的任意后续操作。 
  • 监视器锁规则:对一个监视器锁的解锁,happens- before 于随后对这个监视器锁的加锁。 
  • volatile变量规则:对一个volatile域的写,happens- before 于任意后续对这个volatile域的读。 
  • 传递性:如果A happens- before B,且B happens- before C,那么A happens- before C。 
  • Thread.start()的调用会happens-before于启动线程里面的动作。 
  • Thread中的所有动作都happens-before于其他线程从Thread.join中成功返回。

如果两个操作执行的次序无法从happens-before原则中推导出来,那么就不能保证他们的有序性,jvm就可以随意的对他们进行重排序

目录
相关文章
|
5月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
80 1
|
5月前
|
缓存 Java 编译器
多线程内存模型问题之保证Java中的原子性,如何解决
多线程内存模型问题之保证Java中的原子性,如何解决
|
5月前
线程可见性和关键字volatile
线程可见性和关键字volatile
|
7月前
|
缓存 安全 Java
多线程的三大特性:原子性、可见性和有序性
多线程的三大特性:原子性、可见性和有序性
148 0
|
7月前
|
缓存 算法 Java
多线程04 死锁,线程可见性
多线程04 死锁,线程可见性
41 0
|
7月前
|
Java
Java线程面试题:什么是原子性问题?如何解决?
Java线程面试题:什么是原子性问题?如何解决?
75 0
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
48 1
C++ 多线程之初识多线程
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
20 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
19 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
31 2