Rumah  >  Artikel  >  Java  >  Analisis contoh aplikasi yang tidak menentu bagi asas Java

Analisis contoh aplikasi yang tidak menentu bagi asas Java

WBOY
WBOYke hadapan
2023-05-28 11:23:391273semak imbas

Analisis contoh aplikasi yang tidak menentu bagi asas Java

S: Sila kongsikan pemahaman anda tentang tidak menentu?
Jawapan: Volatile ialah mekanisme penyegerakan ringan yang disediakan oleh mesin maya Java Ia mempunyai tiga ciri:
1) Keterlihatan terjamin
2) Tidak menjamin atomicity Seksualiti <.> 3)
Larang penyusunan semula arahan

Anda baru sahaja mempelajari asas Java Jika seseorang bertanya kepada anda apakah yang tidak menentu? Jika ia mempunyai apa-apa fungsi, saya percaya anda pasti sangat keliru...

Mungkin selepas membaca jawapan, anda tidak faham langsung, apakah mekanisme penyegerakan? Apakah keterlihatan? Apakah atomicity? Apakah penyusunan semula arahan?

1. Meruap menjamin keterlihatan

1.1.

Untuk memahami apa itu keterlihatan, anda mesti memahami JMM terlebih dahulu.

JMM (Java Memory Model, Java Memory Model) itu sendiri adalah konsep abstrak dan tidak benar-benar wujud. Ia menerangkan satu set peraturan atau spesifikasi Melalui set spesifikasi ini, kaedah capaian pelbagai pembolehubah dalam program ditentukan. Peraturan JMM mengenai penyegerakan:

1) Sebelum benang dibuka kunci, nilai pembolehubah kongsi mesti dimuat semula ke memori utama; 2) Sebelum benang dikunci, nilai terkini memori utama mesti dibaca ke dalam memori kerjanya sendiri;
3) Mengunci dan membuka kunci adalah kunci yang sama; memori kerja (dipanggil ruang timbunan di beberapa tempat) untuknya, memori kerja ialah kawasan data peribadi setiap utas.

Model memori Java menetapkan bahawa semua pembolehubah disimpan dalam memori utama ialah kawasan memori kongsi yang boleh diakses oleh semua utas.

Tetapi operasi utas pada pembolehubah (membaca, menetapkan, dll.) mesti dilakukan dalam ingatan kerja. Mula-mula, anda perlu menyalin pembolehubah dari memori utama ke memori kerja, melakukan operasi, dan kemudian menulisnya kembali ke memori utama.

Selepas membaca pengenalan JMM di atas, saya mungkin masih keliru tentang kelebihannya Seterusnya, saya akan menggunakan sistem penjualan tiket sebagai contoh:

1) Seperti yang ditunjukkan di bawah, pada ini. masa bahagian belakang sistem penjualan tiket sahaja Tinggal 1 tiket dan telah dibaca ke dalam ingatan utama: ticketNum=1.

2) Pada masa ini, terdapat berbilang pengguna di rangkaian yang sedang merebut tiket, jadi terdapat beberapa rangkaian yang melakukan perkhidmatan pembelian tiket pada masa yang sama Andaikan terdapat 3 utas pada masa ini yang telah membacanya bilangan tiket semasa: ticketNum =1, maka anda akan membeli tiket.

3) Andaikan bahawa urutan 1 mula-mula merampas sumber CPU, membeli tiket dahulu dan menukar nilai ticketNum kepada 0 dalam ingatan kerjanya sendiri: ticketNum=0, dan kemudian menulisnya kembali ke memori utama.

Pada masa ini, pengguna dalam thread 1 telah pun membeli tiket, jadi thread 2 dan thread 3 sepatutnya tidak dapat meneruskan pembelian tiket buat masa ini, jadi sistem perlu memaklumkan thread 2 dan thread 3 ticketNum itu kini bersamaan dengan 0: ticketNum=0. Jika terdapat operasi pemberitahuan sedemikian, anda boleh memahaminya sebagai mempunyai keterlihatan.

Melalui pengenalan dan contoh JMM di atas, kita boleh merumuskannya secara ringkas.

Analisis contoh aplikasi yang tidak menentu bagi asas JavaKeterlihatan model memori JMM bermakna apabila berbilang benang mengakses sumber dalam ingatan utama, jika benang mengubah suai sumber dalam memori kerjanya sendiri dan menulisnya kembali ke memori utama, maka memori JMM Model harus memberitahu urutan lain untuk mendapatkan semula sumber terkini bagi memastikan keterlihatan sumber terkini.

1.2. Pengesahan kod keterlihatan terjamin tidak menentu

Dalam Bahagian 1.1, kami pada asasnya memahami definisi keterlihatan, dan kini kami boleh menggunakan kod untuk mengesahkan definisi. Amalan telah membuktikan bahawa menggunakan tidak menentu sememangnya boleh memastikan keterlihatan.

1.2.1. Tiada pengesahan kod keterlihatan

Pertama sekali, sahkan sama ada tiada keterlihatan jika tidak menentu tidak digunakan.

package com.koping.test;import java.util.concurrent.TimeUnit;class MyData{
    int number = 0;

    public void add10() {
        this.number += 10;
    }}public class VolatileVisibilityDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动一个线程修改myData的number,将number的值加10
        new Thread(
                () -> {
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 正在执行");
                    try{
                        TimeUnit.SECONDS.sleep(3);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    myData.add10();
                    System.out.println("线程" + Thread.currentThread().getName()+"\t 更新后,number的值为" + myData.number);
                }
        ).start();

        // 看一下主线程能否保持可见性
        while (myData.number == 0) {
            // 当上面的线程将number加10后,如果有可见性的话,那么就会跳出循环;
            // 如果没有可见性的话,就会一直在循环里执行
        }

        System.out.println("具有可见性!");
    }}

Hasil berjalan adalah seperti yang ditunjukkan di bawah Anda dapat melihat bahawa walaupun benang 0 telah menukar nilai nombor kepada 10, utas utama masih dalam gelung kerana nombor tidak kelihatan pada masa ini dan sistem. tidak akan memberitahu secara aktif.

1.2.1, Pengesahan keterlihatan jaminan tidak menentu
Analisis contoh aplikasi yang tidak menentu bagi asas Java Dalam baris 7 kod di atas, tambahkan tidak menentu pada nombor pembolehubah dan uji sekali lagi, seperti yang ditunjukkan di bawah, pada masa ini masa benang utama Gelung berjaya dikeluarkan kerana JMM secara aktif memberitahu urutan utama untuk mengemas kini nilai nombor dan nombor bukan lagi 0.

2. Meruap tidak menjamin atomicity
Analisis contoh aplikasi yang tidak menentu bagi asas Java2.1 Apakah atomicity?

Setelah memahami keterlihatan yang dinyatakan di atas, mari kita fahami apakah itu atomicity?

Atomicity merujuk kepada ciri yang tidak boleh dibahagikan atau diganggu dan mengekalkan integriti. Dalam erti kata lain, apabila benang menjalankan operasi, ia tidak boleh diganggu oleh sebarang faktor. Sama ada berjaya pada masa yang sama atau gagal pada masa yang sama.

Ia masih agak abstrak, mari kita beri contoh.

Seperti yang ditunjukkan di bawah, kelas untuk menguji atomicity dicipta: TestPragma. Kod yang disusun menunjukkan bahawa peningkatan n dalam kaedah tambah diselesaikan melalui tiga arahan.

因此可能存在线程1正在执行第1个指令,紧接着线程2也正在执行第1个指令,这样当线程1和线程2都执行完3个指令之后,很容易理解,此时n的值只加了1,而实际是有2个线程加了2次,因此这种情况就是不保证原子性。
Analisis contoh aplikasi yang tidak menentu bagi asas Java

2.2 不保证原子性的代码验证

在2.1中已经进行了举例,可能存在2个线程执行n++的操作,但是最终n的值却只加了1的情况,接下来对这种情况再用代码进行演示下。

首先给MyData类添加一个add方法

package com.koping.test;class MyData {
    volatile int number = 0;

    public void add() {
        number++;
    }}

然后创建测试原子性的类:TestPragmaDemo。验证number的值是否为20000,需要测试通过20个线程分别对其加1000次后的结果。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);

    }}

运行结果如下图,最终number的值仅为18410。
可以看到即使加了volatile,依然不保证有原子性。
Analisis contoh aplikasi yang tidak menentu bagi asas Java

2.3 volatile不保证原子性的解决方法

上面介绍并证明了volatile不保证原子性,那如果希望保证原子性,怎么办呢?以下提供了2种方法

2.3.1 方法1:使用synchronized

方法1是在add方法上添加synchronized,这样每次只有1个线程能执行add方法。

结果如下图,最终确实可以使number的值为20000,保证了原子性。

但在实际业务逻辑方法中,很少只有一个类似于number++的单行代码,通常会包含其他n行代码逻辑。现在为了保证number的值是20000,就把整个方法都加锁了(其实另外那n行代码,完全可以由多线程同时执行的)。所以就优点杀鸡用牛刀,高射炮打蚊子,小题大做了。

package com.koping.test;class MyData {
    volatile int number = 0;

    public synchronized void add() {
      // 在n++上面可能还有n行代码进行逻辑处理
        number++;
    }}

Analisis contoh aplikasi yang tidak menentu bagi asas Java

2.3.2 方法1:使用JUC包下的AtomicInteger

给MyData新曾一个原子整型类型的变量num,初始值为0。

package com.koping.test;import java.util.concurrent.atomic.AtomicInteger;class MyData {
    volatile int number = 0;

    volatile AtomicInteger num = new AtomicInteger();

    public void add() {
        // 在n++上面可能还有n行代码进行逻辑处理
        number++;
        num.getAndIncrement();
    }}

让num也同步加20000次。可以将原句重写为:使用原子整型num可以确保原子性,如下图所示:在执行number++时不会发生竞态条件。

package com.koping.test;public class TestPragmaDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();

        // 启动20个线程,每个线程将myData的number值加1000次,那么理论上number值最终是20000
        for (int i=0; i<20; i++) {
            new Thread(() -> {
                for (int j=0; j<1000; j++) {
                    myData.add();
                }
            }).start();
        }

        // 程序运行时,模型会有主线程和守护线程。如果超过2个,那就说明上面的20个线程还有没执行完的,就需要等待
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println("number值加了20000次,此时number的实际值是:" + myData.number);
        System.out.println("num值加了20000次,此时number的实际值是:" + myData.num);

    }}

Analisis contoh aplikasi yang tidak menentu bagi asas Java

3、volatile禁止指令重排

3.1 什么是指令重排?

在第2节中理解了什么是原子性,现在要理解下什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排:
源代码–>编译器优化重排–>指令并行重排–>内存系统重排–>最终执行指令

处理器在进行重排时,必须要考虑指令之间的数据依赖性。

单线程环境中,可以确保最终执行结果和代码顺序执行的结果一致。

但是多线程环境中,线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测

看了上面的文字性表达,然后看一个很简单的例子。
比如下面的mySort方法,在系统指令重排后,可能存在以下3种语句的执行情况:
1)1234
2)2134
3)1324
以上这3种重排结果,对最后程序的结果都不会有影响,也考虑了指令之间的数据依赖性。

public void mySort() {
    int x = 1;  // 语句1
    int y = 2;  // 语句2
    x = x + 3;  // 语句3
    y = x * x;  // 语句4}

3.2 单线程单例模式

看完指令重排的简单介绍后,然后来看下单例模式的代码。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试
        System.out.println("单线程的情况测试开始");
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());
        System.out.println("单线程的情况测试结束\n");
    }}

首先是在单线程情况下进行测试,结果如下图。可以看到,构造方法只执行了一次,是没有问题的。
Analisis contoh aplikasi yang tidak menentu bagi asas Java

3.3 多线程单例模式

接下来在多线程情况下进行测试,代码如下。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {
        if (instance == null) {
            instance = new SingletonDemo();
        }

        // DCL(Double Check Lock双端检索机制)//        if (instance == null) {//            synchronized (SingletonDemo.class) {//                if (instance == null) {//                    instance = new SingletonDemo();//                }//            }//        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多线程情况下的运行结果如下图。可以看到,多线程情况下,出现了构造方法执行了2次的情况。
Analisis contoh aplikasi yang tidak menentu bagi asas Java

3.4 多线程单例模式改进:DCL

在3.3中的多线程单里模式下,构造方法执行了两次,因此需要进行改进,这里使用双端检锁机制:Double Check Lock, DCL。即加锁之前和之后都进行检查。

package com.koping.test;public class SingletonDemo {
    private static SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 执行构造方法SingletonDemo()");
    }

    public static SingletonDemo getInstance() {//        if (instance == null) {//            instance = new SingletonDemo();//        }

        // DCL(Double Check Lock双端检锁机制)
        if (instance == null) {  // a行
            synchronized (SingletonDemo.class) {
                if (instance == null) {  // b行
                    instance = new SingletonDemo();  // c行
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        // 单线程测试//        System.out.println("单线程的情况测试开始");//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance());//        System.out.println("单线程的情况测试结束\n");

        // 多线程测试
        System.out.println("多线程的情况测试开始");
        for (int i=1; i<=10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }}

在多次运行后,可以看到,在多线程情况下,此时构造方法也只执行1次了。
Analisis contoh aplikasi yang tidak menentu bagi asas Java

3.5 Penambahbaikan mod singleton berbilang benang, masalah dengan versi DCL

Perlu diingat bahawa mod singleton versi DCL dalam 3.4 masih tidak 100% tepat! ! !

Tidakkah anda faham mengapa mod singleton versi 3.4DCL tidak 100% tepat?
Tidakkah anda faham kenapa tiba-tiba kita perlu bercakap tentang mod singleton berbilang benang selepas selesai memahami mudah penyusunan semula arahan dalam 3.1?

Oleh kerana versi 3.4DCL mod tunggal mungkin menyebabkan masalah disebabkan penyusunan semula arahan, walaupun kemungkinan masalah ini mungkin satu dalam sepuluh juta, kod itu masih tidak 100% tepat. Jika anda ingin memastikan ketepatan 100%, anda perlu menambah kata kunci yang tidak menentu, menambahkan tidak menentu boleh melarang penyusunan semula perintah.

Mari analisa seterusnya, mengapa mod tunggal versi 3.4DCL tidak 100% tepat?

Lihat contoh = new SingletonDemo(); arahan yang disusun boleh dibahagikan kepada tiga langkah berikut:
1) Peruntukkan ruang memori objek: memori = allocate(
2) Mulakan objek : instance(memory);
3) Tetapkan contoh untuk menunjuk ke alamat memori yang diperuntukkan: instance = memory;

Memandangkan tiada pergantungan data antara langkah 2 dan 3, langkah 132 boleh dilaksanakan.
Contohnya, urutan 1 telah melaksanakan langkah 13 tetapi belum melaksanakan langkah 2. Pada masa ini, instance!=null, tetapi objek belum dimulakan lagi
Jika thread 2 merampas CPU pada masa ini dan kemudian mencari contoh!=null, Kemudian terus kembali untuk digunakan, anda akan mendapati bahawa contoh itu kosong, dan pengecualian akan berlaku.

Ini adalah masalah yang mungkin disebabkan oleh penyusunan semula arahan. Oleh itu, jika anda ingin memastikan program itu 100% betul, anda perlu menambah tidak menentu untuk melarang penyusunan semula arahan.

3.6 Prinsip jaminan tidak menentu untuk melarang penyusunan semula arahan

Dalam 3.1, kami memperkenalkan secara ringkas maksud penyusunan semula pelaksanaan, dan kemudian melalui 3.2-3.5, kami menggunakan mod tunggal untuk menggambarkan pelbagai -situasi benang Seterusnya, sebab mengapa tidak menentu harus digunakan adalah kerana mungkin terdapat penyusunan semula arahan yang menyebabkan pengecualian program.

Seterusnya, kami akan memperkenalkan prinsip tidak menentu memastikan penyusunan semula arahan adalah dilarang.

Pertama sekali, kita perlu memahami konsep: Penghalang Memori, juga dikenali sebagai penghalang ingatan. Ia adalah arahan CPU dengan dua fungsi:
1) Menjamin susunan pelaksanaan operasi tertentu; melaksanakan penyusunan semula arahan. Jika anda memasukkan Memory Barrier antara arahan, ia akan memberitahu pengkompil dan CPU bahawa tiada arahan boleh disusun semula dengan arahan Memory Barrier ini Dalam erti kata lain,
Dengan memasukkan penghalang memori, arahan sebelum dan selepas halangan memori adalah dilarang. . Penyusunan semula pelaksanaan memerlukan pengoptimuman

.

Satu lagi fungsi penghalang memori adalah untuk memaksa data cache pelbagai CPU untuk dibuang, supaya mana-mana benang pada CPU boleh membaca versi terkini data ini .

Atas ialah kandungan terperinci Analisis contoh aplikasi yang tidak menentu bagi asas Java. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:yisu.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam