Java 並行プログラミング (並行プログラミングに関する 3 つの質問)

並行プログラミングとは

まず、並行性とは何か、並列性とは何かを知る必要があります。

並列性:複数のことが同時に起こっている

並行性:同時に複数のものが交互に実行される

並行プログラミング:チケット取得、seckill など. 同じシーンで同じリソースにアクセスするリクエストが多数あり、セキュリティ上の問題が発生する. そのため、リソースにアクセスするために複数のスレッドを制御する必要があります.並行プログラミングと呼ばれるプログラミングによるシーケンス

並行プログラミングの根本原因

すべての Java コードは Java 仮想マシンで実行され、Java 仮想マシンにも独自のモデルがあるため -----

Java メモリ モデル (Java メモリ モデル、JMM)

Java メモリ モデルでは、すべての変数はメイン メモリに格納され、各スレッドには独自のワーキング メモリがあり、スレッドによる変数のすべての操作はワーキング メモリで実行する必要があり、メイン メモリ内の変数を直接読み書きすることはできません。 . 、ワーキングメモリでの操作が完了した後、データはメインメモリに書き込まれます。

ここでのワーキング メモリは、ローカル メモリとも呼ばれる JMM の抽象的な概念であり、スレッドの読み取り/書き込み共有変数のコピーを格納します。これは、各プロセッサ コアにプライベート キャッシュがあり、JMM の各スレッドにプライベート ローカル メモリがあるのと同じです。

並行プログラミングの 3 つの主要な問題 (不可視性、無秩序、非原子性)

1. 不可視性

複数のスレッドが同時に共有リソースで動作し、それらは互いに見えません. 操作が完了すると、それらはメイン メモリに書き込まれます. これにより、予期した結果と一致しない問題が発生する可能性があります.

例えば:

期待される結果は count=2 になるはずです。

ただし、必ずしもそうであるとは限らず、スレッド 1 がメイン メモリからカウントを取得して ++ 操作を実行すると、1 になります。 0 と実行 ++ 操作が 1 に変更されます。2 つのスレッドがメイン メモリに書き込んだ後、最終的な結果カウントは 2 ではなく 1 になります。つまり、スレッド 1 はメイン メモリ内のデータを操作しましたが、スレッド 2 は操作を行いました。それを知らない。

2. 障害

パフォーマンスを最適化するために、一部のコード命令が再配置され、プログラムの速度が向上しました

3.原子性

原子的意思代表着——“不可分”, 原子性是拒绝多线程交叉操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作, 线程切换导致了原子性问题

CPU 能保证的原子操作是 CPU 指令级别的(i++,就只有一条语句,进行一次操作),而不是高级语言的操作符(i++,有三个操作过程), 高级语言里一条语句往往需要多条 CPU 指令完成。如 count++,至少需要三条 CPU指令。

指令 1:首先,需要把变量 count 从内存加载到工作内存;

指令 2:之后,在工作内存执行 +1 操作;

指令 3:最后,将结果写入内存;

如何解决这三个问题?

1.使用volitile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后:

1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2. 禁止进行指令重排序。

3. volatile 不能保证对变量操作的原子性。

volitile的底层实现原理

使用 Memory Barrier(内存屏障)。

内存屏障是一条指令,该指令可以对编译器(软件)和处理器(硬件)的指令重排做出一定的限制,比如,一条内存屏障指令可以禁止编译器和处理器将其后面的指令移到内存屏障指令之前。

Volatile 变量编译为汇编指令会多出Lock 前缀.

有序性实现:主要通过对 volatile 修饰的变量的读写操作前后加上各种特定的内存屏障来禁止指令重排序来保障有序性的。

可见性实现:主要是通过 Lock 前缀指令 + MESI 缓存一致性协议来实现的。对 volatiile 修饰的变量执行写操作时,JVM 会发送一个 Lock 前缀指令给CPU,CPU 在执行完写操作后,会立即将新值刷新到存,同时因为 MESI 缓存一致性协议,其他各个 CPU 都会对总线嗅探,看自己本地缓存中的数据是否被别人修改,如果发现修改了,会把自己本地缓存的数据过期掉。然后这个 CPU里的线程在读取改变量时,就会从主内存里加载最新的值了,这样就保证了可见性

可见性实现的例子:

public class ThreadDemo implements  Runnable{

    /*
       volatile 修饰的变量,在一个线程中被修改后,对其它线程立即可见
                                            禁止cpu对指令重排序
     */
    private    boolean  flag = false;//共享数据


    public void run() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.flag = true;//让一个线程修改共享变量值
        System.out.println(this.flag);
    }

    public boolean getFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
public class TestVolatile {

    public static void main(String[] args) {

        //创建线程任务
        ThreadDemo td = new  ThreadDemo();
        Thread t = new Thread(td);//创建线程
        t.start();

        //main线程中也需要使用flag变量
        while(true){
            if(td.getFlag()){//false-true
                System.out.println("main---------------");
                break;
            }
        }
    }
}

上面例子中,main主线程中执行一个子线程,还要执行一个while循环,只有当子线程中的flag为true时才可以跳出while循环

当共享数据flag没有被volitile修饰时,输出结果如下:

一直在死循环,没有跳出while循环,但是在子线程中flag已经改为了true,也已经输出了,说明main主线程和子线程彼此的操作时不可见性的, 但是有volitile修饰时,就会跳出循环.

有序性实现的例子:

/*
  模拟指令重排序
 */
public class Reorder {

    private  volatile static int x;
    private  volatile static int y;
    private  volatile static int a;
    private  volatile static int b;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();
            other.start();
            one.join();
            other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
               System.out.println(result);
            }
        }
    }

}

有volitile修饰时输出结果如下:

没有volitile修饰时:

可以看到,有volitile修饰时,代码的输出是有序的.

2.通过加锁的方式,让线程互斥执行来保证一次只有一个线程对共享资源访问,从而保证原子性.

synchronized : 关键字 修饰代码块,方法 自动获取锁,自动释放锁

ReentrantLock : 类 只能对某段代码修饰 需要手动加锁,手动释放锁.

3.在java中还提供一些原子类,在低并发情况下使用,是一种无锁实现.

原子类的原子性是通过 volatile + CAS 实现原子操作的。

AtomicInteger 类中的 value 是有 volatile 关键字修饰的,这就保证了 value的内存可见性,这为后续的 CAS 实现提供了基础。低并发情况下:使用 AtomicInteger。

4.采用CAS机制(CAS(Compare-And-Swap) 比较并交换)从而保证原子性

CAS是实现乐观锁的一种方式,是一种自旋式思想,是一种轻量级的锁机制

CAS 包含了三个操作数:

①内存值 V

②预估值 A (比较时,从内存中再次读到的值)

③更新值 B (更新后的值)

当且仅当预期值 A==V,将内存值 V=B,否则什么都不做。

这种做法的效率高于加锁,当判断不成功不能更新值时,不会阻塞,继续获得 cpu执行权,继续判断执行。

自旋思想:

第一次从主内存中获取值到工作内存中,存储作为预期值,然后将对象数据修改,将工作内存中值写入到主内存, 在写入之前需要做一个判断.用预期值与主内存中的值进行比较,如果预期值与主内存中值一致,说明没有其他线程修改, 将更新数的值,写入到主内存,如果预期值与主内存中值不一致, 说明其他线程修改了主内存的值, 这时就需要重复操作整个过程

特点:

不加锁,所有的线程都可以对共享数据操作, 适合地并发是使用,由于不加锁,其他线程不需要阻塞,效率高,但是在大并发时,不停自旋判断,导致cpu占用率高,容易导致 CPU 跑满.

ABA 问题

当某个线程将内存值由 A 改为了 B,再由 B 改为了 A。当另外一个线程使用预期值去判断时,预期值与内存值相同,当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。

如何解决ABA问题?

解决 ABA 问题的主要方式,通过使用类添加版本号,来避免 ABA 问题。如原先的内存值为(A,1),线程将(A,1)修改为了(B,2),再由(B,2)修改为(A,3)。此时另一个线程使用预期值(A,1)与内存值(A,3)进行比较,只需要比较版本号 1 和 3,即可发现该内存中的数据被更新过了

おすすめ

転載: blog.csdn.net/weixin_71243923/article/details/128906772