首页 > 其他分享 >高并发(锁)

高并发(锁)

时间:2024-04-17 16:12:01浏览次数:24  
标签:变量 并发 屏障 线程 内存 操作 排序

锁是用于控制多个线程对共享资源的访问的机制,防止出现程序对共享资源的竞态关系

线程安全

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况

线程的竞态条件

竞态条件(race condition)

竞态条件(race condition)指的是两个或者以上进程或者线程并发执行时,其最终的结果依赖于进程或者线程执行的精确时序。竞争条件会产生超出预期的情况,一般情况下我们都希望程序执行的结果是符合预期的,因此竞争条件是一种需要被避免的情形。

竞争条件分为两类:

  • Mutex(互斥):两个或多个进程彼此之间没有内在的制约关系,但是由于要抢占使用某个临界资源(不能被多个进程同时使用的资源,如打印机,变量)而产生制约关系。
  • Synchronization(同步):两个或多个进程彼此之间存在内在的制约关系(前一个进程执行完,其他的进程才能执行),如严格轮转法。

要阻止出现竞态条件的关键就是不能让多个进程/线程同时访问那块共享变量。访问共享变量的那段代码就是临界区(critical section)。所有的解决方法都是围绕这个临界区来设计的。

线程安全的三大特性

1、原子性(Atomicity):原子操作是不可分割的操作,要么全部执行成功,要么全部不执行。在多线程环境下,如果多个线程同时执行原子操作,不会出现数据不一致的情况。例如,使用synchronized关键字或者Lock接口来保证关键代码块的原子性。

2、可见性(Visibility):可见性是指当一个线程修改了共享变量的值,其他线程能够立即看到修改后的值。在多线程环境下,需要保证共享变量的修改对其他线程是可见的,通常可以使用volatile关键字来实现可见性。

3、有序性(Ordering):有序性是指程序的执行顺序按照代码的先后顺序来执行,不会因为编译器的优化或者CPU的乱序执行而导致结果的不确定。在多线程环境下,需要保证共享变量的读写操作是有序的,通常可以通过synchronized关键字或volatile关键字来实现有序性。

Java的内存模型

了解更多:https://www.51cto.com/article/658158.html

我们都知道JVM中每个线程都有自己的栈空间,共享变量会存放在主内存中。在并发修改变量的过程中线程可能会发生挂起,导致写入到主内存的值发生覆盖。导致破坏了原子性

as-if-serial语义和happen-before原则

1、as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排序

2、JVM定义的Happens-Before原则是一组偏序关系:对于两个操作A和B(共享数据),这两个操作可以在不同的线程中执行。如果A Happens-Before B,那么可以保证,当A操作执行完后,A操作的执行结果对B操作是可见的

3、as-if-serial 和 happens-before 的区别

  1. as-if-serial是针对单线程程序的执行结果的一致性,允许虚拟机对单线程程序进行指令重排序,只要不改变执行结果。
  2. happens-before是针对多线程程序的执行顺序的一致性,描述了多线程之间操作的可见性和顺序关系。
  3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度

volatile关键字

volitale是Java虚拟机提供的一种轻量级的同步机制

1、保证可见性

可见性主要存于JMM的内存模型当中,指当一个线程改变其内部的工作内存当中的变量后,其他内存是否可以感知到,因为不同的工作线程无法访问到对方的工作内存,线程间的通信必须依靠主内存进行同步

2、不保证原子性

当在多线程改变变量时,需要将变量同步到工作线程中;当线程A对变量修改时,还没同步到主内存中线程挂起,线程B也对变量进行修改,这时线程A进行执行,就会覆盖线程B的值,由于可见性,这是线程B也会变成线程A修改的值,导致一致性问题

3、禁止指令重排

在本线程内观察,所有操作都是有序的(即指令重排不会导致单线程程序执行结果与排序前有任何差别)。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

想要线程安全必须保证原子性,可见性,有序性。而volatile只能保证可见性和有序性

内存屏障

  • Memory barrier 能够让CPU或编译器在内存访问上有序。一个 Memory barrier 之前的内存访问操作必定先于其之后的完成。
  • Memory barrier是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。
  • 有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。

Memory Barrier可以被分为以下几种类型:

1、LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

2、StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

3、LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

4、StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile语义中的内存屏障

在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;

在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;

volatile的内存屏障策略非常严格保守,保证了线程可见性。

final语义中的内存屏障

新建对象过程中,构造体中对final域的初始化写入(StoreStore屏障)和这个对象赋值给其他引用变量,这两个操作不能重排序;

初次读包含final域的对象引用和读取这个final域(LoadLoad屏障),这两个操作不能重排序;

Intel 64/IA-32架构下写操作之间不会发生重排序StoreStore会被省略,这种架构下也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

锁的类型

从线程是否需要对资源加锁可以分为 悲观锁(系统)乐观锁(CAS)
从资源已被锁定,线程是否阻塞可以分为自旋锁(CAS)
从多个线程并发访问资源,也就是 Synchronized 可以分为 无锁偏向锁轻量级锁重量级锁
从锁的公平性进行区分,可以分为公平锁非公平锁
从根据锁是否重复获取可以分为 可重入锁不可重入锁
从那个多个线程能否获取同一把锁分为 共享锁(读锁)排他锁(写锁)

类锁和对象锁

类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的

类锁是指对静态方法或静态变量加锁时所产生的锁。类锁是类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的;当一个线程获取了一个类锁,其他线程就无法同时获取该类的锁,直到该线程释放了锁。

对象锁是指对非静态方法或非静态变量加锁时所产生的锁。每个对象都有自己的对象锁,当一个线程获取了某个对象的锁,其他线程就无法同时获取该对象的锁,直到该线程释放了锁

自旋锁

优点:自旋锁不会引起调用者休眠,如果自旋锁已经被别的线程保持,调用者就一直循环在那里看是否该自旋锁的保持者释放了锁。由于自旋锁不会引起调用者休眠,所以自旋锁的效率远高于互斥锁

缺点:
1、自旋锁一直占用 CPU ,在未获得锁的情况下,一直运行,如果不能在很短的时间内获得锁,会导致 CPU 效率降低。
2、试图递归地获得自旋锁会引起死锁。递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。

CAS自旋

CAS(Compare and Swap)是一种基于原子操作的并发控制机制,用于实现多线程之间的同步。CAS操作包含三个操作数,分别为内存位置V、期望值A和新值B。当且仅当V的值等于A时,CAS将V的值设为B,否则不做任何操作。

CAS 是一条 CPU 的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe类提供的 CAS 方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg

  1. AtomicInteger:用于原子性地操作整数型变量。
  2. AtomicLong:用于原子性地操作长整型变量。
  3. AtomicBoolean:用于原子性地操作布尔型变量。
  4. AtomicReference:用于原子性地操作引用类型变量。
  5. AtomicIntegerArray:用于原子性地操作整数型数组。
  6. AtomicLongArray:用于原子性地操作长整型数组。
  7. AtomicReferenceArray:用于原子性地操作引用类型数组。

ABA问题

在多线程编程中,常常会遇到ABA问题。简单来说,ABA问题就是指线程A读取了共享变量V的值,然后线程B将V的值改为了其他值,再次改回了原来的值,然后线程A又进行了写操作。这种情况下,线程A会认为V的值没有发生变化,但实际上V的值已经发生了变化。

为了解决ABA问题,Java中提供了一个带有时间戳的原子类AtomicStampedReference。AtomicStampedReference类可以通过增加时间戳来解决ABA问题,时间戳的作用是记录每一次变量的修改操作,使得在比较并交换时不仅需要比较变量的值,还需要比较变量的时间戳是否相同。这样就可以避免ABA问题的发生。

具体来说,AtomicStampedReference类中的compareAndSet方法不仅会比较当前值和期望值是否相等,还会比较当前的时间戳是否相等。只有当前值和时间戳都相等时,才会执行CAS操作,否则不会执行。

Unsafe类

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用

如何调用Unsafe类

1、从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。

java -Xbootclasspath/a: ${path}   // 其中path为调用Unsafe相关方法的类所在jar包路径 

2、通过反射获取单例对象theUnsafe。

private static Unsafe reflectGetUnsafe() {
    try {
      Field field = Unsafe.class.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      return (Unsafe) field.get(null);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
      return null;
    }
}

标签:变量,并发,屏障,线程,内存,操作,排序
From: https://www.cnblogs.com/luojw/p/18141005

相关文章

  • 基于K8s+Docker+Openresty+Lua+SpringCloudAlibaba的高并发秒杀系统——与京东淘宝同
    ​介绍基于K8s+Docker+Openresty+Lua+SpringCloudAlibaba的高并发高性能商品秒杀系统,本系统实测单台(16核32G主频2.2GHz)openresty(nginx)的QPS可高达6w并发,如果您需要应对100w的并发,则需要100w/6w=17台openresty服务器,17台服务器同时接收并处理这100w的并发流量呢?当然是商业......
  • 测试Netty高并发工具
    测试Netty应用程序的高并发性能工具JMeterJMeter:ApacheJMeter是一个功能强大的用于性能测试的工具,可以模拟大量用户对Netty服务器的并发请求。你可以创建各种测试计划来模拟不同负载条件下的性能表现。wrkwrk:wrk是一个现代的HTTP基准测试工具,它可以轻松地对Netty服务器进......
  • 我们如何实现最基础的并发?
    OS的目标是在1保持控制权下2高性能的并发。因此现在我们有两大问题需要解决:1.如何高性能地并发?2.如何保持OS对计算机的控制权?(我们姑且只讨论在单CPU的机器上,运行微内核OS) 这篇博客中,我们先来回答这个问题:我们如何实现最基础的并发?       首先我们来回答一下什......
  • QPS才算高并发
    QPS才算高并发高并发场景QPS等专业指标揭秘大全与调优实战  合集-三“高”架构设计与调优(1) 1.高并发场景QPS等专业指标揭秘大全与调优实战04-14收起 高并发场景QPS等专业指标揭秘大全与调优实战最近经常有小伙伴问及高并发场景下QPS的一些问题,特意结合......
  • 高并发 (线程)
    我们都知道现在硬件水平越来越强,执行速度也越来越快。为充分利用CPU资源,我们可以通过多线程来执行分片任务提升整体的执行性能。如何创建线程在Java中通过newThread().start()方法来开启一个线程,实际是调用系统层的native方法privatenativevoidstart0();Java使用线程常用......
  • netcore 并发锁 多线程中使用SemaphoreSlim
    SemaphoreSlim是一个用于同步和限制并发访问的类,和它类似的还有Semaphore,只是SemaphoreSlim更加的轻量、高效、好用。今天说说它,以及如何使用,在什么时候去使用,使用它将会带来什么优势。代码的业务是:在多线程下进行数据的统计工作,简单点的说就是累加数据。1.首先我们建立一个程......
  • Docker安装部署Jenkins并发布NetCore应用
    Docker安装Jenkins#拉取镜像dockerpulljenkins/jenkins#查看镜像dockerimages#运行jenkins#8080端口为jenkinsWeb界面的默认端口13152是映射到外部:前面的是映射外部#50000端口为jenkins的默认代理节点(Agent)通信端口13153是映射到外部#--restart=on-fa......
  • Java并发编程实战读书笔记
    1.线程池模型    netty实战中讲到的线程池模型可以描述为:1.从线程池中选择一个空间的线程去执行任务,2.任务完成时,把线程归还给线程池。这个模型与连接池类似。    根据jdk源码的研究,具体的实现模型是,线程池ThreadPoolExecutor中有一个静态内部类Worker,使用装饰器模式扩......
  • 深入理解并发和并行
    深入理解并发和并行1并发与并行为什么操作系统上可以同时运行多个程序而用户感觉不出来?因为操作系统营造出了可以同时运行多个程序的假象,通过调度进程以及快速切换CPU上下文,每个进程执行一会就停下来,切换到下个被调度到的进程上,这种切换速度非常快,人无法感知到,从而产生了多个任......
  • 高并发场景QPS等专业指标揭秘大全与调优实战
    高并发场景QPS等专业指标揭秘大全与调优实战最近经常有小伙伴问及高并发场景下QPS的一些问题,特意结合项目经验和网上技术贴做了一些整理和归纳,供大家参考交流。一、一直再说高并发,多少QPS才算高并发?高并发的四个角度只说并发不提高可用就是耍流氓。可以从四个角度讨论这个问......