6 线程安全性

1️⃣定义

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


2️⃣线程安全性的体现

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


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


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


3️⃣原子性 : Atomic包(在Atomic包中都是使用CAS来保证原子性的)

① 我们之前做的累加计算的demo我们现在使用Atomic对它进行一下简单的改造;

@Slf4j
@ThreadSafe
public class AtomicExample1 {

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

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

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        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 (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

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

大家可以看到,我们通过简单的改造将原来count的类型由int修改为AtomicInteger这个类就由线程不安全编程了线程安全,那么这是为什么呢?

② AtomicInteger源码解析

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    1.  在源码中我们可以看到incrementAndGet使用了一个unsage的类,
        然后调用的是unsafe.getAndAddInt方法,接下来我们看一下这个
        方法的源码;
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
         其实这个方法在每次执行的时候都会去判断当前的值与底层的值是否一致,
         如果一致才会执行加1的操作,如果不一致则重新循环取值然后接着判断进行
         加1运算;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        compareAndSwapInt这个方法的核心就是CAS的核心;
        return var5;
    }
    2. 在这个方法中主体是通过一个do while语句来进行实现的,核心的逻辑  
       是compareAndSwapInt这个方法,接下来我们看一下这个方法的源码;
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    3. 可以看到这是一个被native修饰的方法;

刚才我们处理的是int类型的,但是其他类型也是这样处理的,比如下面的AtomicLong 以及 LongAdder;

@Slf4j
@ThreadSafe
public class AtomicExample2 {

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

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

    public static AtomicLong count = new AtomicLong(0);

    public static void main(String[] args) throws Exception {
        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 (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
        // count.getAndIncrement();
    }
}
@Slf4j
@ThreadSafe
public class AtomicExample3 {

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

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

    public static LongAdder count = new LongAdder();

    public static void main(String[] args) throws Exception {
        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 (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

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

其实他们两个大体上是差不多的, LongAdder是1.8新增加的,这里我们就说一下他们两个之间的优缺点:
我们刚才在分析AtomicInteger源码时我们看到,它底层其实就是在一个死循环内进行循环的比较和运算不断的尝试修改目标值,如果竞争不激烈一般都是能成功的,但是如果竞争激烈的情况下就会容易会修改失败并且浪费性能;
这里有一个小的知识点,对于64位的(Long和Double)操作JVM允许拆分成两个32位的操作, LongAdder的思想是将value拆分成数组,然后进行运算,通过这样的操作可以很大程度上提升性能;


4️⃣ Atomic包中的其他类

① AtomicReference

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

② AtomicIntegerFieldUpdater

@Slf4j
@ThreadSafe
public class AtomicExample5 {

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

    @Getter
    public volatile int count = 100;

    public static void main(String[] args) {

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

③ 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 Exception {
        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 (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("isHappened:{}", isHappened.get());
    }

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

小结 : Atomic包中的大部分类原理都是差不多,核心就是借助CAS的思想来保证原子性;


4️⃣原子性: 锁

synchronized : 依赖JVM实现;
Lock : 依赖特殊的CPU指令,代码实现(后续会进行详细讲解)
本篇笔记先重点讲解一下synchronized;


概览: synchronized是一个关键字,它是同步锁,它修饰的对象有四种;

① 修饰代码块:大括号括起来的代码,作用于调用对象;
② 修饰方法: 整个方法,作用于调用对象;
③ 修饰静态方法: 整个静态方法,作用于所有对象;
④ 修饰类: 括号括起来的部分,作用于所有对象;


①代码演示

@Slf4j
public class SynchronizedExample1 {

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

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

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

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

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

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

小结: 如果使用synchronized进行修饰的类或者方法,那么当前类或者方法只能有一个线程去执行,会导致其他的线程阻塞;synchronized是不可中断的锁,适合竞争不激烈的情况使用,可读性较好;


5️⃣原子性:对比

synchronized:不可中断的锁,适合竞争不激烈的情况,可读性较好;
lock:可中断锁,多样化同步,竞争激烈时能维持常态;
Atomic:竞争激烈时能维持常态,比Lock性能好,只能同步一个值;


6️⃣可见性

可见性 : 是一个线程对主内存的修改可以及时的被其他线程观察到;
说起可见性我们就需要说一下什么时候不可见,下面我们就简单说一下导致不可见的常见原因:
① 线程交叉执行;
② 重排序结合线程交叉执行;
③ 共享变量更新后的值没有在工作内存与主内存间及时更新;


① 可见性:synchronized

JMM关于synchronized的两条规定:

  1. 线程解锁前,必须把共享变量的最新值刷新到主内存;
  2. 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意:加锁与解锁是同一把锁);

② 可见性: volatile

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

  1. 对volatile变量写操作时,会在写操作后加入一条store屏障指令,将本地内存中的共享变量值刷新到主内存;
  2. 对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 Exception {
        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 (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count);
    }

    private static void add() {
        count++;
        // 1、count
        // 2、+1
        // 3、count
    }
}

当我们使用volatile优化代码后,这个类仍然不是线程安全的,这是为什么呢?我们来分析一下,当执行count++操作的时候,第一步先从主存中获取最新的值,第二步执行+1操作,第三步将新值从新写入主存;那么此时问题就来了,如果同时有两个线程执行,那么他们获取的都是最新的值,但是当他们执行+1以后的写入时,写入的值是相同的,就会丢失一次操作;
这也从侧面证明了volatile不具备原子性;


7️⃣有序性

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


我们可以通过volatile synchronized lock来保证线程的有序性;


happens-before原则:

① 程序次序规则 : 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作;
② 锁定规则 : 一个unlock操作先行发生于后面对同一个锁的lock操作;
③ volatile变量规则 : 对一个变量的写操作先行发生于后面对这个变量的读操作;
④ 传递规则 : 如果操作A先行发生于操作B,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C;
⑤ 线程启动规则 : Thread对象的start()方法先行发生于此线程的每一个动作;
⑥ 线程中断规则 : 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;
⑦ 线程终结规则 : 线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束 Thread.isAlive()的返回值手段检测到线程已经终止执行;
⑧ 对象终结规则 : 一个对象的初始化完成先行发生于它的finalize()方法的开始;


如果线程执行的结果不能通过happens-before原则推导出来,那么就不能保证他们的有序性,虚拟机就可以随意的对他们进行重排序;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容