Java高并发--原子性可见性有序性

主要是学习慕课网实战视频《Java并发编程入门与高并发面试》的笔记

  • 原子性:指一个操作不可中断,一个线程一旦开始,直到执行完成都不会被其他线程干扰。换句话说原子性保证了任何时刻只有一个线程在对共享变量进行操作。
  • 可见性:指当一个线程修改了某个共享变量的值,其他线程是否能立即知道这个修改。
  • 有序性:一个线程观察其他线程中的指令,由于指令重排序的存在,该观察结果一般杂乱无序

原子性

AtomicInteger

JDK的atomic包下提供了许多“原子类”,它们都是基于CAS操作实现的。

所谓CAS(Compare And Swap),即“比较并交换”。CAS基于乐观的态度,是无锁操作,它操作包含三个参数,当前要更新的变量、期望值、新值,仅当:当前值和预期值一样时,才会将当前值设置为新值;如果当前值和预期值不一样,说明这个变量已经被其他线程修改过了。如果有多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新。

以atomic包中最常用的AtomicInteger为例,追踪其getAndIncrement()

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看到它调用了unsafe.getAndAddInt(this, valueOffset, 1)

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

其中var1表示要被更新的对象,var2是原始值在内存中的偏移地址。通过getIntVolatile(var1, var2);拿到现在的值var5。但多个线程修改下,内存中的原始值随时都可能变化,所以现在var5是一个期望值(期望内存中的值和刚读取到的var5是相等的,因为内存中的值此时可能已经变了)。compareAndSwapInt是一个native方法,compareAndSwapInt(var1, var2, var5, var5 + var4)这句的意思是对于var1对象,根据偏移地址var2拿到的内存中的原始值,如果和期望值var5相等,则将其更新为var5 + var4。同时从while-do也可以直到,该方法在一直尝试,直到内存中的值和期望值一样时,才能进行修改,并返回修改前内存中的值。

这里只是举例解释了其中一个方法,其他方法的实现大同小异,总之都是使用了CAS操作来保证线程安全。

类似的还有AtomicLong,AtomicBoolean,值得一提的还有有一个compareAndSet方法,当且仅当期望值except和内存中的值相等时,才会执行更新操作。可以保证在多线程下同时修改共享变量,只有一个线程可以修改成功

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

举个例子

package com.shy.concurrency.count;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author Haiyu
 * @date 2018/12/17 17:33
 */
public class Test {
    public static void main(String[] args) {
        AtomicInteger a = new AtomicInteger(9);
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0;i < 10; i++) {
            executorService.execute(()-> {
                if (a.compareAndSet(9, 10)) {
                    System.out.println("更新成功");
                    System.out.println(a.get());
                }
            });
        }
        executorService.shutdown();
    }

}

上面有十个线程要修改a的值,但是最后程序只打印了一次10。因为一旦某个程序成功将a更新成10,其他线程的期望值except就和现在内存中的值10不相等了,所以都会更新失败。

LongAdder

像AtomicInteger等原子类使用CAS操作虽然没有锁,但是也可以使用减小锁粒度这种分离热点的思想。LongAdder正是这样的类,它也位于atomic包下,且有着比AtomicInteger等原子类更好的性能。LongAdder有一个称为base的变量,如果在多线程下对base的修改没有发生冲突的话,会直接操作base变量;但是如果发生了冲突,base这个热点数据会被分离成多个单元cell,每个单元独立维护内部的值(通过哈希算法定位到数组中的某个cell)。这个对象的值其实是由cell数组的求和累加得到的,这样热点就进行了有效的分离,提高了并行度。

AtomicReference

AtomicReference和AtomicInteger十分相似,不过一个是对整数的封装,一个是对普通对象的封装。

AtomicReference<Integer> money = new AtomicReference<>(); 

这样写就行了,泛型类型是Integer,因此可以实现和AtomicInteger相同的功能。

AtomicStampedReference

CAS可能引发"ABA"问题,即一个变量原来是A,先被修改成B后又修改回了A,由于CAS操作只是比较当前值和预期值是否一样(只比较结果,不在乎过程中状态的变化),在其他线程来看,该变量就好像没有发生过变化。

可以为数据添加时间戳,每次成功修改数据时,不仅更新数据的值,同时要更新时间戳的值。CAS操作时,不仅要比较当前值和预期值,还要比较当前时间戳和预期时间戳。两者都必须满足预期值才能修改成功。

AtomicStampedReference正是这样做的,它不仅维护对象值,还维护了一个时间戳(其实可就是一个版本号)。当AtomicStampedReference对应的值被修改时,不仅要更新数据本身,还要更新时间戳(版本号)。只有当数据本身和时间戳都满足期望值,写入才会成功。因此,虽然对象值被反复修改又被更新成了原来的值,但是时间戳发生了变化,就可以防止不恰当的写入。

AtomicIntegerFieldUpdater

该类可以使普通变量也拥有原子操作。首先保证该普通变量是volatile的(且不能有static修饰符)。然后像下面这样使用。

AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");

其中Money类中有一个“money”的字段,它的类型是普通的int型。通过上面的用法,使得Money中的money字段也拥有的原子性。

class Money {
    volatile int money;

    public int getMoney() {
        return money;
    }
}

运行以下程序,将总得到打印值为10,说明这是线程安全的

package com.shy.concurrency.count;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Haiyu
 * @date 2018/12/17 17:33
 */
@ThreadSafe
public class Test {
    private volatile int count;

    public int getCount() {
        return count;
    }

    static AtomicIntegerFieldUpdater<Money> updater = AtomicIntegerFieldUpdater.newUpdater(Money.class, "money");

    public static void main(String[] args) throws InterruptedException {
        final Money money = new Money();
        CountDownLatch cdl = new CountDownLatch(10);

        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                updater.incrementAndGet(money);
                cdl.countDown();
            });
        }

        cdl.await();
        System.out.println(money.getMoney());
        executorService.shutdown();
    }
}

AtomicIntegerArray

除了普通对象、基本数据类型的包装类,atomic包还提供了原子数组。如AtomicIntegerArray,AtomicLongArray,在使用上无非就是加上了索引。

比如

public final boolean compareAndSet(int i, int expect, int update) {...}

i就是数组的索引,其他API也类似,需要指定一个索引来明确表明要对哪一个变量进行原子操作。

Java中有synchronized和重入锁来保证线程的同步,以实现线程安全。

synchronized是JVM的内置锁,而重入锁是Java代码实现的。重入锁是synchronized的扩展,可以完全代替后者。重入锁可以重入,允许同一个线程连续多次获得同一把锁。其次,重入锁独有的功能有:

  • 可以相应中断,synchronized要么获得锁执行,要么保持等待。而重入锁可以响应中断,使得线程在迟迟得不到锁的情况下,可以不再等待。主要由lockInterruptibly()实现,这是一个可以对中断进行响应的锁申请动作,锁中断可以避免死锁。
  • 锁的申请可以有等待时限,用tryLock()可以实现限时等待,如果超时还未获得锁会返回false,也防止了线程迟迟得不到锁时一直等待,可避免死锁。
  • 公平锁,即锁的获得按照线程先来后到的顺序依次获得,不会产生饥饿现象。synchronized的锁默认是不公平的,重入锁可通过传入构造方法的参数实现公平锁。
  • 重入锁可以绑定多个Condition条件,这些condition通过调用await/singal实现线程间通信。

synchronized可以作用在如下四个地方:

  • 代码块,使用当前对象或其他任意对象作为锁。被代码块包围的代码会同步执行。synchronized(this)synchronized(obj)就分别使用自身和obj对象作为锁。
  • 修饰方法,使用当前对象作为锁。整个方法会同步执行
  • 修饰静态方法,使用类作为锁(因此作用于该类的所有对象)。整个方法会同步执行

注意在多线程下如果要保证synchronized的线程安全,必须使用同一把锁

可见性

导致可见性的原因:

  • 线程交叉执行
  • 指令重排结合线程交叉执行
  • 共享变量更新后没有在工作内存和主内存之间及时更新

synchronized的可见性

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

volatile的可见性

  • 对volatile变量的写操作,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新到主内存中;
  • 对volatile变量的读操作,会在读操作前加入一条load屏障指令,读主内存中读取共享变量

换句话说,volatile的作用是:在本CPU对变量的修改直接写入主内存中,同时这个写操作使得其他CPU中对应变量的缓存行无效,这样其他线程在读取这个变量时候必须从主内存中读取,所以读取到的是最新的,这就是上面说得能被立即“看到”。

volatile常使用于标志位的判断。

volatile boolean ready= false;
// 线程1
config = loadConfig(); // 语句1
ready = true;  // 语句2

// 线程2,ready为true时才停止sleep()
while (!ready) {
    sleep();
}
runWithConfig(config);

如上面的例子,如果ready变量不是volatile的,有可能因为指令重排,先执行语句2再执行语句1。由于先执行语句2,那么线程2中再config还没有初始化时就执行了runWithConfig(config),这显然是不合理的。当ready加上了volatile修饰符,禁止了指令重排,因此不会发生以上情况。

有序性

Happen-Before规则

有些指令是可以重排的,有些指令是不可重排的。下面是一些基本原则:

  • 程序顺序原则:一个线程内保证语义的串行性,比如第二条语句依赖第一条语句的结果,那么就不能先执行第二条再执行第一条。
  • volatile原则:volatile变量的写先于读,着保证了volatile变量的可见性
  • 锁规则:先解锁,后续步骤再加锁。加锁不能重排到解锁之前,这样加锁行为无法获得锁(刚加上就解了)
  • 传递性:A先于B,B先于C,那么A先于C
  • 线程的start()先于它的每个动作
  • 线程的所有操作先于线程的终结(可以通过Tread.join()方法结束、Thread.isAlive()的返回值判断一个线程是否终结)
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行、结束先于finalize()方法。

Java高并发--原子性可见性有序性的更多相关文章

  1. Java内存模型JMM 高并发原子性可见性有序性简介 多线程中篇(十)

    JVM运行时内存结构回顾 在JVM相关的介绍中,有说到JAVA运行时的内存结构,简单回顾下 整体结构如下图所示,大致分为五大块 而对于方法区中的数据,是属于所有线程共享的数据结构 而对于虚拟机栈中数据 ...

  2. java高并发系列 - 第4天:JMM相关的一些概念

    JMM(java内存模型),由于并发程序要比串行程序复杂很多,其中一个重要原因是并发程序中数据访问一致性和安全性将会受到严重挑战.如何保证一个线程可以看到正确的数据呢?这个问题看起来很白痴.对于串行程 ...

  3. Java高并发系列——检视阅读

    Java高并发系列--检视阅读 参考 java高并发系列 liaoxuefeng Java教程 CompletableFuture AQS原理没讲,需要找资料补充. JUC中常见的集合原来没讲,比如C ...

  4. Java高并发与多线程(四)-----锁

    今天,我们开始Java高并发与多线程的第四篇,锁. 之前的三篇,基本上都是在讲一些概念性和基础性的东西,东西有点零碎,但是像文科科目一样,记住就好了. 但是本篇是高并发里面真正的基石,需要大量的理解和 ...

  5. JAVA高并发程序设计笔记

    第二章 Java并行程序基础 1.join()的本质是让调用线程wait()在当前线程的对象上 2.Thread.yiedl()会使当前线程让出CPU 3.volatile保证可见性,无法保证原子性( ...

  6. java高并发编程(一)

    读马士兵java高并发编程,引用他的代码,做个记录. 一.分析下面程序输出: /** * 分析一下这个程序的输出 * @author mashibing */ package yxxy.c_005; ...

  7. 《实战Java高并发程序设计》读书笔记

    文章目录 第二章 Java并行程序基础 2.1 线程的基本操作 2.1.1 线程中断 2.1.2 等待(wait)和通知(notify) 2.1.3 等待线程结束(join)和谦让(yield) 2. ...

  8. java高并发系列 - 第21天:java中的CAS操作,java并发的基石

    这是java高并发系列第21篇文章. 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库乐观锁的一个常见示例 使用ja ...

  9. java高并发系列 - 第22天:java中底层工具类Unsafe,高手必须要了解

    这是java高并发系列第22篇文章,文章基于jdk1.8环境. 本文主要内容 基本介绍. 通过反射获取Unsafe实例 Unsafe中的CAS操作 Unsafe中原子操作相关方法介绍 Unsafe中线 ...

随机推荐

  1. 面试题5-[剑指offer] 二维数组中的查找

    题目 在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序.请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数. ...

  2. 18 ArcGIS API for JavaScript4.X 系列加载天地图(经纬度)

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. Nuget私有服务搭建实战

    最近更新了Nuget私有服务器的版本,之前是2.8.5,现在是2.11.3. Nuget服务器的搭建,这里有篇很详细的文章,跟着弄就好了: https://docs.microsoft.com/en- ...

  4. MYSQL手册

    原文出处:http://www.cnblogs.com/gaofei-1/p/7152875.html MySQL配置文件 MySQL软件使用的配置文件名为my.ini,在安装目录下. MySQL常用 ...

  5. 在vue中使用setter改写父子组件传的值

    概述 最近在用muse ui的时候碰到一个问题,简单来说是这样的,父子之间传值,父组件和子组件使用相同的props命名,并且子组件不用emit,而用等号赋值. 最后使用计算属性的setter函数解决了 ...

  6. [SQL]LeetCode176. 第二高的薪水 | Second Highest Salary

    Write a SQL query to get the second highest salary from the Employee table. +----+--------+ | Id | S ...

  7. [Swift]LeetCode936. 戳印序列 | Stamping The Sequence

    You want to form a target string of lowercase letters. At the beginning, your sequence is target.len ...

  8. Kubernetes 笔记 07 豌豆荚之旅(二)

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. Hi,大家好, ...

  9. CkEditor批量上传图片(java)

    CKEditor上传视频CKEditor批量上传图片flvplayer.swf播放器CKEditor整合包(v4.6.1) ------------------------------------ 最 ...

  10. 【Spark篇】---Spark故障解决(troubleshooting)

    一.前述 本文总结了常用的Spark的troubleshooting. 二.具体 1.shuffle file cannot find:磁盘小文件找不到. 1) connection timeout ...