首页 > 编程语言 >Java并发-理论基础

Java并发-理论基础

时间:2023-06-25 18:00:54浏览次数:38  
标签:同步 Java 理论 编译器 并发 ThreadLocal 线程 threadLocal 排序

作用

众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

线程不安全示例

多线程操作同一个共享数据而不同步操作,操作的结果不一致。

public class ThreadUnsafeExample {

    private int cnt = 0;

    public void add() {
        cnt++;
    }

    public int get() {
        return cnt;
    }
}
public static void main(String[] args) throws InterruptedException {
    final int threadSize = 1000;
    ThreadUnsafeExample example = new ThreadUnsafeExample();
    final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < threadSize; i++) {
        executorService.execute(() -> {
            example.add();
            countDownLatch.countDown();
        });
    }
    countDownLatch.await();
    executorService.shutdown();
    System.out.println(example.get());
}

并发三要素

可见性:CPU缓存引起

一个线程对共享变量修改,其他一个线程可以看到

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;

假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。

此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.

这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值

原子性:分时复用

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

有序性:重排序引起

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

Java并发-理论基础_有序性

上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

线程安全实现方案、

互斥同步(阻塞同步)

  1. 关键字:synchronized
  2. JUC锁:ReentrantLock

非阻塞同步

互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。

互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。

CAS

随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略: 先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。

乐观锁需要操作和冲突检测这两个步骤具备原子性,这里就不能再使用互斥同步来保证了,只能靠硬件来完成。硬件支持的原子性操作最典型的是: 比较并交换(Compare-and-Swap,CAS)。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时,只有当 V 的值等于 A,才将 V 的值更新为 B。

AtomicInteger(原子类)
ABA

如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。

J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。

无同步方案

要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性。

1、栈封闭

多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。

2、线程本地存储

如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。

符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完。其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式,这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。

可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。

对于以下代码,thread1 中设置 threadLocal 为 1,而 thread2 设置 threadLocal 为 2。过了一段时间之后,thread1 读取 threadLocal 依然是 1,不受 thread2 的影响。

public class ThreadLocalExample {
    public static void main(String[] args) {
        ThreadLocal threadLocal = new ThreadLocal();
        Thread thread1 = new Thread(() -> {
            threadLocal.set(1);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadLocal.get());
            threadLocal.remove();
        });
        Thread thread2 = new Thread(() -> {
            threadLocal.set(2);
           
        });
        thread1.start();
        thread2.start();
    }
}

当调用一个 ThreadLocal 的 set(T value) 方法时,先得到当前线程的 ThreadLocalMap 对象,然后将 ThreadLocal->value 键值对插入到该 Map 中。

Java并发-理论基础_有序性_02

ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。

在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。

3、可重入代码

这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。(返回结果一样不变的代码)

标签:同步,Java,理论,编译器,并发,ThreadLocal,线程,threadLocal,排序
From: https://blog.51cto.com/u_16084527/6547649

相关文章

  • Java 反序列化之 XStream 反序列化
    0x01XStream基础XStream简介XStream是一个简单的基于Java库,Java对象序列化到XML,反之亦然(即:可以轻易的将Java对象和XML文档相互转换)。使用XStream实现序列化与反序列化下面看下如何使用XStream进行序列化和反序列化操作的。先定义接口类IPerson.javapubli......
  • 利用SpringBoot和javafx进行桌面开发
    原文合集地址如下,有需要的朋友可以关注本文地址合集地址众所周知,SpringBoot是一款强大的Javaweb开发程序,这得益于其构造了一个Spring容器,然后通过依赖注入和控制反转,维护起一套Java对象和实例的管理机制,方便开发者去使用。在web应用开发的应用中,Springboot在Java层应用非常广,......
  • Java多线程-Lesson02
    一.Lamda表达式λ是希腊字母表的第十一位字母,英文名是LambdaLamda表达式的出现主要是为了避免匿名内部类过多的原因,它属于函数式编程的范畴 为什么要使用Lamda表达式?避免匿名内部类定义过多可以让你的代码看起来很简洁去掉了没有意义的代码,只留下一堆核心逻辑主要使用在......
  • 【拼多多商品详情数据】API接口获得宝贝详情数据、商品标题数据等Java调用示例
    ​拼多多商品详情API接口的作用是获取拼多多平台上某个商品的详细信息,包括商品标题、价格、图片、规格、参数、店铺信息等。开发者可以通过该接口获取到商品的原始数据,方便进行数据分析、价格比较、爬取等操作。通过该接口获取到的商品详情数据可以结合其他数据进行深度挖掘,例如......
  • java 中的多线程问题
    结论  全局变量 多线程非安全  (堆) static局部变量 多线程安全  (栈) fina也可以安全1:线程调用时,每一个线程都拥有一套完整的调用栈帧,栈帧中包含完整的调用方法名,方法的局部变量。而且栈是先入后出,调用方法时入栈,方法结束是出栈。2:使用线程池时需要注意参数问......
  • java tricky
    1、根据枚举的name获取枚举类:privatestaticSmsProviderTypefromName(StringspName){returnStream.of(SmsProviderType.values()).filter(sp->StringUtils.equals(sp.name(),spName)).findFirst().orElse(null);}2、字符串转成int(注意给默认值):NumberUtils.toInt3......
  • JAVA标签
    breakcontinue标签:```java标签(label)是在编程语言中用于标识代码的位置的符号。它通常与跳转语句(如break和continue)结合使用,用于控制程序的流程。在Java中,标签以标识符的形式出现,后面跟着一个冒号(:)。标签可以与循环语句(如for、while、do-while)和条件语句(如if、switch)一起......
  • java 中在图像上绘制文字
    privatevoidprintText(BufferedImageimage,floatx,floaty,Stringtext){Graphics2Dg=image.createGraphics();g.setColor(Color.RED);g.setFont(newFont("Arial",Font.BOLD,30));FontMetricsfm=g.getFontMet......
  • 关于Java中ArrayList类的toArray方法详解
    先上源码:publicObject[]toArray(){returnArrays.copyOf(elementData,size);}可以看到ArrayList类的toArray()方法调用了Arrays.copyOf(elementData,size)(其中的elementData是ArrayList类中用来存储对象的数组,size是数组大小),接下来进入其内部:publicsta......
  • 【淘宝商品详情数据】api接口获得宝贝详情数据、优惠价格数据Java调用示例
    淘宝详情API接口的作用是获取淘宝平台上某个商品的详细信息,包括商品标题、价格、图片、规格、参数、店铺信息等。开发者可以通过该接口获取到商品的原始数据,方便进行数据分析、价格比较、爬取等操作。通过该接口获取到的商品详情数据可以结合其他数据进行深度挖掘,例如可以将商品数......