资讯专栏INFORMATION COLUMN

高并发 - 基础

phpmatt / 3145人阅读

摘要:异步非阻塞方式,任务的完成的通知由其他线程发出。并发并行死锁饥饿活锁死锁线程持有,线程持有。如等,在多线程情况下,该操作不是原子级别的而是原子的,所以一般用于状态标记。

同步/异步、阻塞/非阻塞

同步/异步是 API 被调用者的通知方式。阻塞/非阻塞则是 API 调用者的等待方式(线程挂机/不挂起)。

同步非阻塞

Future方式,任务的完成要主线程自己判断。
如NIO,后台有多个任务在执行(非阻塞),主动循环查询(同步)多个任务的完成状态,只要有任何一个任务完成,就去处理它。这就是所谓的 “I/O 多路复用”。

同步非阻塞相比同步阻塞:
优点:能够在等待任务完成的时间里干其他活了(就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次,而任务可能在两次轮询之间的任意时间完成。

异步非阻塞

CompletableFuture方式,任务的完成的通知由其他线程发出。
如AIO,应用程序发起调用,而不需要进行轮询,进而处理下一个任务,只需在I/O完成后通过信号或是回调将数据传递给应用程序即可。

异步非阻塞相比同步非阻塞:
不需要主动轮询,减少CPU操作。

并发、并行

死锁、饥饿、活锁

死锁

线程A持有lock1,线程B持有lock2。当A试图获取lock2时,此时线程B也在试图获取lock1。此时二者都在等待对方所持有锁的释放,而二者却又都没释放自己所持有的锁,这时二者便会一直阻塞下去。

饥饿

对于非公平队列来说,线程有可能一直获取不到对锁的占用。

活锁

由于某些条件没有满足,导致两个线程一直互相“谦让”对锁的占用,从而一直等下去。活锁有可能自行解开,死锁则不能。

什么是线程安全

如果多个线程同时运行你的代码,每一个线程每次运行结果和单线程运行的结果是一样的,就是线程安全的。

原子性、可见性、有序性

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

即程序执行的顺序按照代码的先后顺序执行。(在单线程中,编译器对代码的重排序没有问题,但在多线程程序运行就可能有问题)

x = 10;         //语句1
y = x;         //语句2
x++;           //语句3
x = x + 1;     //语句4
上面4个语句只有语句1的操作具备原子性。也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量)才是原子操作。
Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。

重排序

编译器可能会对程序操作做重排序(为了让CPU指令处理的流水线更加高效,减少空闲时间)。编译器在重排序时,会遵守数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序。
注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
所以重排序会使得多线程不安全。

关键字volatile

volatile修饰的变量不保留拷贝,直接访问主内存中的变量,即保证可见性。
volatile前面的代码肯定在volatile之前,volatile后面的代码肯定在volatile之后,即保证有序性。
volatile修饰的变量缺少原子性的保证。如volatile n=n+1、n++、n = m + 1 等,在多线程情况下,该操作不是原子级别的;而n=false是原子的,所以volatile一般用于状态标记。如果自己没有把握,可以使用synchronized、Lock、AtomicInteger来代替volatile。

关键字synchronized

synchronized与static synchronized 的区别:

synchronized是对类的当前方法的实例进行加锁,类的两个不同实例的synchronized方法可以被两个线程分别访问。

static synchronized是类java.lang.Class对象锁。因为当虚拟机加载一个类的时候,会会为这个类实例化一个 java.lang.Class 对象。类的不同实例在执行该方法时共用一个锁。

synchronized方法只能由synchronized的方法覆盖:
继承时子类的覆盖方法必须定义成synchronized。

两个线程不能同时访问同一对象的不同synchronized方法:
因为synchronized锁是基于对象的。但同一对象的普通方法和synchronized方法能同时被两个线程分别访问。

Happen-Before

程序顺序原则:一个线程内保证语义的串行性
volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
传递性:A先于B,B先于C,那么A必然先于C
线程的start()方法先于它的每一个动作
线程的所有操作先于线程的终结(Thread.join())
线程的中断(interrupt())先于被中断线程的代码
对象的构造函数执行结束先于finalize()方法
这些原则保证了重排的语义是一致的。

CAS(Compare and swap)

CAS算法:CAS(V, E, N)。V表示要更新的变量,E表示预期值,N表示新值。仅当V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。

以AtomicInteger为例:

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
private volatile int value;    //保证线程间的数据是可见的

static {
    try {    //valueOffset就是value
        valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

public final int getAndSet(int newValue) {
    return unsafe.getAndSetInt(this, valueOffset, newValue);
}

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

public final boolean compareAndSet(int expect, int update) {
    //对于this这个类上的偏移量为valueOffset的变量值如果与期望值expect相同,那么把这个变量的值设为update。
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

以上compareAndSet方法类似以下:

if (value == expect) {
    Value = update;
    return true;
} else {
    return false;
}

那么问题来了,如果value== expect之后,正要执行value= update时,切换了线程更改了值,则会造成了数据不一致。但这个担心是多余的,因为CAS操作是原子的,中间不会有线程切换。
如何保证原子性,即一个步骤?
实际上compareAndSet()利用JNI(Java Native Interface)来执行CPU的CMPXCHG指令,从而保证比较、交换是一步操作,即原子性操作。

CAS缺点

ABA问题

    static final AtomicReference ref = new AtomicReference(1);

    public final int incrementAndGet() {
        while (true) {
            int current = ref.get();
            int next = current + 1;    // 1        
        if (ref.compareAndSet(current, next)) {    // 2
                return next;
            }
        }
    }

在代码1和代码2之间,若其他线程将value设置为3,另一个线程又将value设置1,则CAS进行检查时会错误的认为值没有发生变化,但是实际上却变化了。这就是A变成B又变成A,即ABA问题。
解决思路就是添加版本号。在变量和版本号绑定,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A,当版本号相同时才做更新值的操作。

java.util.concurrent.atomic.AtomicStampedReference可以解决ABA问题,其内部类:

private static class Pair {
    final T reference;
    final int stamp;
    ......
 }

AtomicStampedReference的compareAndSet方法会首先检查当前reference是否==预期reference(内存地址比较),并且当前stamp是否等于预期stamp,如果都相等,则执行Unsafe.compareAndSwapObject方法。

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));    //Unsafe.compareAndSwapObject
}

循环时间长开销大

自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的PAUSE指令那么效率会有一定的提升,PAUSE指令提升了自旋等待循环(spin-wait loop)的性能。

只能保证一个共享变量的原子操作

对于多个共享变量操作,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者把多个共享变量合并成一个共享变量来操作。JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

普通变量的原子操作
java.util.concurrent.atomic.AtomicIntegerFieldUpdater类的主要作用是让普通变量也享受原子操作。
就比如原本有一个变量是int型,并且很多地方都应用了这个变量,但是在某个场景下,想让int型变成AtomicInteger,但是如果直接改类型,就要改其他地方的应用。AtomicIntegerFieldUpdater就是为了解决这样的问题产生的。

public static class V {
    volatile int score;

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }
}

public final static AtomicIntegerFieldUpdater vv = AtomicIntegerFieldUpdater.newUpdater(V.class, "score");

public static void main(String[] args) {
    final V stu = new V();
    vv.incrementAndGet(stu);
}

注:
Updater只能修改它可见范围内的变量。因为Updater使用反射得到这个变量。
变量必须是volatile类型的。
由于CAS操作会通过对象实例中的偏移量(堆内存的偏移量)直接进行赋值,因此,它不支持static字段(Unsafe.objectFieldOffset()不支持静态变量)。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67261.html

相关文章

  • 【Java并发】Java并发编程与并发基础概念

    摘要:笔记来源并发编程与高并发解决方案并发基础综述多级缓存缓存一致性乱序执行优化内存模型规定抽象结构同步八种操作及规则并发的优势与风险并发与高并发基本概念基本概念并发同时拥有两个或者多个线程,如果程序在单核处理器上运行,多个线程将交替地换入或者换 笔记来源:【IMOOC】Java并发编程与高并发解决方案 并发基础 综述: CPU多级缓存:缓存一致性、乱序执行优化 Java内存模型:JM...

    stackfing 评论0 收藏0
  • 聊聊面试中关于并发问题的应对方案

    摘要:这里呢,我直接给出高并发场景通常都会考虑的一些解决思路和手段结尾如何有效的准备面试中并发类问题,我已经给出我的理解。 showImg(https://segmentfault.com/img/bV7Viy?w=550&h=405); 主题 又到面试季了,从群里,看到许多同学分享了自己的面试题目,我也抽空在网上搜索了一些许多公司使用的面试题,目前校招和社招的面试题基本都集中在几个大方向上...

    xzavier 评论0 收藏0
  • 三年Java后端面试经历

    摘要:前言三年后端开发经验,面的目标岗位是的高级后端开发。面试结束,应该没有后续。 前言 三年Java后端开发经验,面的目标岗位是20k-35k的高级后端Java开发。 第一场,基本裸面,关于曾经的项目部门答的不好,所以还是得好好准备。 某C轮在线旅游公司 笔试 先做半个小时的笔试题,一共六个题目,两道go语言的基础题,一道斐波那契相关,一道数据库行列转置,一道实现一个栈,还有一道是百万计...

    darry 评论0 收藏0
  • 《Java并发程序设计》读书笔记 第二章 并行程序基础

    showImg(https://segmentfault.com/img/bVbeIYP?w=4533&h=2073);

    elva 评论0 收藏0
  • 架构 - 收藏集 - 掘金

    摘要:浅谈秒杀系统架构设计后端掘金秒杀是电子商务网站常见的一种营销手段。这两个项目白话网站架构演进后端掘金这是白话系列的文章。 浅谈秒杀系统架构设计 - 后端 - 掘金秒杀是电子商务网站常见的一种营销手段。 不要整个系统宕机。 即使系统故障,也不要将错误数据展示出来。 尽量保持公平公正。 实现效果 秒杀开始前,抢购按钮为活动未开始。 秒杀开始时,抢购按钮可以点击下单。 秒杀结束后,按钮按钮变...

    Riddler 评论0 收藏0
  • 从小白程序员一路晋升为大厂级技术专家我看过哪些书籍?(建议收藏)

    摘要:大家好,我是冰河有句话叫做投资啥都不如投资自己的回报率高。马上就十一国庆假期了,给小伙伴们分享下,从小白程序员到大厂高级技术专家我看过哪些技术类书籍。 大家好,我是...

    sf_wangchong 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<