Rumah  >  Artikel  >  Java  >  Bagaimana untuk melaksanakan kunci putaran manual menggunakan Java

Bagaimana untuk melaksanakan kunci putaran manual menggunakan Java

WBOY
WBOYke hadapan
2023-05-09 18:31:191544semak imbas

    Prakata

    Apabila kita menulis atur cara serentak, keperluan yang sangat biasa ialah memastikan bahawa hanya satu utas melaksanakan sekeping kod tertentu pada masa tertentu, seperti Kod jenis ini dipanggil bahagian kritikal, dan cara biasa untuk memastikan bahawa hanya satu utas melaksanakan kod dalam bahagian kritikal pada satu masa adalah dengan mengunci. Dalam artikel ini, kami akan menganalisis dan mempelajari dengan teliti tentang kunci putaran Apa yang dipanggil kunci putaran dilaksanakan melalui gelung sementara, yang membolehkan benang yang telah memperoleh kunci memasuki bahagian kritikal untuk melaksanakan kod, dan membenarkan utas. yang belum mendapat kunci untuk terus mati semasa Gelung, ini sebenarnya adalah benang "berpusing" itu sendiri dalam gelung while, jadi kunci jenis ini dipanggil kunci putaran.

    Kunci putaran

    Atomicity

    Sebelum bercakap tentang kunci putaran, kita perlu bercakap tentang atomicity. Apa yang dipanggil atomicity hanya bermaksud bahawa setiap operasi sama ada tidak dilakukan atau melakukan semua bermakna ia tidak boleh diganggu semasa operasi Contohnya, menambah satu pada data pembolehubah mempunyai tiga langkah berikut:

    • Muatkan data dari memori untuk mendaftar.

    • Tambahkan satu pada nilai data.

    • Tulis hasilnya kembali ke ingatan.

    Atomicity bermaksud bahawa apabila thread melakukan operasi kenaikan, ia tidak boleh diganggu oleh thread lain Hanya apabila thread ini melengkapkan ketiga-tiga proses ini, thread lain boleh memanipulasi data.

    Mari kita rasainya sekarang dengan kod Di Java, kita boleh menggunakan AtomicInteger untuk melaksanakan operasi atom pada data integer:

    import java.util.concurrent.atomic.AtomicInteger;
     
    public class AtomicDemo {
     
      public static void main(String[] args) throws InterruptedException {
        AtomicInteger data = new AtomicInteger();
        data.set(0); // 将数据初始化位0
        Thread t1 = new Thread(() -> {
          for (int i = 0; i < 100000; i++) {
            data.addAndGet(1); // 对数据 data 进行原子加1操作
          }
        });
        Thread t2 = new Thread(() -> {
          for (int i = 0; i < 100000; i++) {
            data.addAndGet(1);// 对数据 data 进行原子加1操作
          }
        });
        // 启动两个线程
        t1.start();
        t2.start();
        // 等待两个线程执行完成
        t1.join();
        t2.join();
        // 打印最终的结果
        System.out.println(data); // 200000
      }
    }

    Daripada analisis kod di atas, kita boleh tahu bahawa jika ia adalah umum. integer Jika dua benang beroperasi pada pembolehubah pada masa yang sama, keputusan akhir akan kurang daripada 200,000.

    Sekarang mari kita simulasi proses masalah dengan pembolehubah integer am:

    Nilai awal data memori utama adalah bersamaan dengan 0, dan nilai awal data yang diperolehi oleh kedua-dua utas adalah sama dengan 0 .

    Bagaimana untuk melaksanakan kunci putaran manual menggunakan Java

    Sekarang benang satu menambah satu pada data, dan kemudian benang satu menyegerakkan nilai data kembali ke memori utama Data dalam keseluruhan memori berubah seperti berikut:

    Bagaimana untuk melaksanakan kunci putaran manual menggunakan Java

    Sekarang tambahkan satu pada data benang dua, dan kemudian segerakkan nilai data kembali ke memori utama (menimpa nilai asal memori utama):

    Bagaimana untuk melaksanakan kunci putaran manual menggunakan Java

    Kami pada asalnya berharap nilai data akan menjadi 2 selepas perubahan di atas, tetapi benang dua menimpa nilai kami, jadi dalam situasi berbilang benang, hasil akhir kami akan menjadi lebih kecil.

    Tetapi dalam program di atas, hasil keluaran akhir kami adalah sama dengan 20000. Ini kerana operasi +1 kepada data adalah atom dan tidak boleh dibahagikan Semasa proses operasi, utas lain tidak boleh menjalankan operasi pada data . Ini adalah kelebihan atomicity.

    Tulis kunci putaran anda sendiri

    Kelas AtomicInteger

    Sekarang kita telah memahami peranan atomicity, mari kita fahami kelas AtomicInteger Operasi atom lain - compareAndSet, operasi ini dipanggil compare and swap (CAS), ia adalah atom.

    public static void main(String[] args) {
      AtomicInteger atomicInteger = new AtomicInteger();
      atomicInteger.set(0);
      atomicInteger.compareAndSet(0, 1);
    }

    Maksud fungsi compareAndSet: Pertama, ia akan membandingkan parameter pertama (sepadan dengan kod di atas ialah 0) dan nilai atomicInteger Jika ia sama, ia akan ditukar, iaitu, nilai atomicInteger akan ditetapkan kepada parameter kedua (Kod yang sepadan di atas ialah 1 Jika operasi ini berjaya, fungsi compareAndSet akan kembali benar nilai parameter pertama (nilai dijangka) tidak sama dengan atomicInteger Jika ia sama, Ia mungkin kerana ia gagal apabila menukar nilai atomicInteger (kerana mungkin terdapat berbilang benang yang beroperasi, dan kerana kewujudan atomicity, hanya satu benang boleh beroperasi dengan jayanya).

    Prinsip pelaksanaan kunci putaran

    Kita boleh menggunakan kelas AtomicInteger untuk melaksanakan kunci putaran Kita boleh menggunakan nilai 0 untuk menunjukkan bahawa ia tidak dikunci, dan nilai 1 untuk menunjukkan bahawa ia dikunci.

    Nilai awal kelas AtomicInteger ialah 0.

    Apabila mengunci, kita boleh menggunakan kod atomicInteger.compareAndSet(0, 1) untuk melaksanakannya Kami telah menyebut sebelum ini bahawa hanya satu utas boleh melengkapkan operasi ini, yang bermaksud bahawa hanya satu utas boleh memanggil baris ini kod kemudian mengembalikan benar dan utas lain mengembalikan palsu ini tidak boleh memasuki bahagian kritikal, jadi kami memerlukan utas ini untuk berhenti di atomicInteger.compareAndSet(0, 1. Kita boleh melakukannya gunakan Gelung while memastikan utas ini diletakkan di sini sementara (!value.compareAndSet(0, 1)); Kunci jenis ini juga dipanggil kunci putaran.

    线程在出临界区的时候需要重新将锁的状态调整为未上锁的上状态,我们使用代码value.compareAndSet(1, 0);就可以实现,将锁的状态还原为未上锁的状态,这样其他的自旋的线程就可以拿到锁,然后进入临界区了。

    自旋锁代码实现

    import java.util.concurrent.atomic.AtomicInteger;
     
    public class SpinLock {
        
      // 0 表示未上锁状态
      // 1 表示上锁状态
      protected AtomicInteger value;
     
      public SpinLock() {
        this.value = new AtomicInteger();
        // 设置 value 的初始值为0 表示未上锁的状态
        this.value.set(0);
      }
     
      public void lock() {
        // 进行自旋操作
        while (!value.compareAndSet(0, 1));
      }
     
      public void unlock() {
        // 将锁的状态设置为未上锁状态
        value.compareAndSet(1, 0);
      }
     
    }

    上面就是我们自己实现的自旋锁的代码,这看起来实在太简单了,但是它确实帮助我们实现了一个锁,而且能够在真实场景进行使用的,我们现在用代码对上面我们写的锁进行测试。

    测试程序:

    public class SpinLockTest {
     
      public static int data;
      public static SpinLock lock = new SpinLock();
     
      public static void add() {
        for (int i = 0; i < 100000; i++) {
          // 上锁 只能有一个线程执行 data++ 操作 其余线程都只能进行while循环
          lock.lock();
          data++;
          lock.unlock();
        }
      }
     
      public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[100];
        // 设置100个线程
        for (int i = 0; i < 100; i ++) {
          threads[i] = new Thread(SpinLockTest::add);
        }
        // 启动一百个线程
        for (int i = 0; i < 100; i++) {
          threads[i].start();
        }
        // 等待这100个线程执行完成
        for (int i = 0; i < 100; i++) {
          threads[i].join();
        }
        System.out.println(data); // 10000000
      }
    }

    在上面的代码单中,我们使用100个线程,然后每个线程循环执行100000data++操作,上面的代码最后输出的结果是10000000,和我们期待的结果是相等的,这就说明我们实现的自旋锁是正确的。

    自己动手写可重入自旋锁

    可重入自旋锁

    在上面实现的自旋锁当中已经可以满足一些我们的基本需求了,就是一个时刻只能够有一个线程执行临界区的代码。但是上面的的代码并不能够满足重入的需求,也就是说上面写的自旋锁并不是一个可重入的自旋锁,事实上在上面实现的自旋锁当中重入的话就会产生死锁。

    我们通过一份代码来模拟上面重入产生死锁的情况:

    public static void add(int state) throws InterruptedException {
      TimeUnit.SECONDS.sleep(1);
      if (state <= 3) {
        lock.lock();
        System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
        for (int i = 0; i < 10; i++)
          data++;
        add(state + 1); // 进行递归重入 重入之前锁状态已经是1了 因为这个线程进入了临界区
        lock.unlock();
      }
    }

    在上面的代码当中加入我们传入的参数state的值为1,那么在线程执行for循环之后再次递归调用add函数的话,那么state的值就变成了2。

    if条件仍然满足,这个线程也需要重新获得锁,但是此时锁的状态是1,这个线程已经获得过一次锁了,但是自旋锁期待的锁的状态是0,因为只有这样他才能够再次获得锁,进入临界区,但是现在锁的状态是1,也就是说虽然这个线程获得过一次锁,但是它也会一直进行while循环而且永远都出不来了,这样就形成了死锁了。

    可重入自旋锁思想

    针对上面这种情况我们需要实现一个可重入的自旋锁,我们的思想大致如下:

    • 在我们实现的自旋锁当中,我们可以增加两个变量,owner一个用于存当前拥有锁的线程,count一个记录当前线程进入锁的次数。

    • 如果线程获得锁,owner = Thread.currentThread()并且count = 1。

    • 当线程下次再想获取锁的时候,首先先看owner是不是指向自己,则一直进行循环操作,如果是则直接进行count++操作,然后就可以进入临界区了。

    • 我们在出临界区的时候,如果count大于一的话,说明这个线程重入了这把锁,因此不能够直接将锁设置为0也就是未上锁的状态,这种情况直接进行count--操作,如果count等于1的话,说明线程当前的状态不是重入状态(可能是重入之后递归返回了),因此在出临界区之前需要将锁的状态设置为0,也就是没上锁的状态,好让其他线程能够获取锁。

    可重入锁代码实现

    实现的可重入锁代码如下:

    public class ReentrantSpinLock extends SpinLock {
     
      private Thread owner;
      private int count;
     
      @Override
      public void lock() {
        if (owner == null || owner != Thread.currentThread()) {
          while (!value.compareAndSet(0, 1));
          owner = Thread.currentThread();
          count = 1;
        }else {
          count++;
        }
     
      }
     
      @Override
      public void unlock() {
        if (count == 1) {
          count = 0;
          value.compareAndSet(1, 0);
        }else
          count--;
      }
    }

    下面我们通过一个递归程序去验证我们写的可重入的自旋锁是否能够成功工作。

    测试程序:

    import java.util.concurrent.TimeUnit;
     
    public class ReentrantSpinLockTest {
     
      public static int data;
      public static ReentrantSpinLock lock = new ReentrantSpinLock();
     
      public static void add(int state) throws InterruptedException {
        TimeUnit.SECONDS.sleep(1);
        if (state <= 3) {
          lock.lock();
          System.out.println(Thread.currentThread().getName() + "\t进入临界区 state = " + state);
          for (int i = 0; i < 10; i++)
            data++;
          add(state + 1);
          lock.unlock();
        }
      }
     
      public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
          threads[i] = new Thread(new Thread(() -> {
            try {
              ReentrantSpinLockTest.add(1);
            } catch (InterruptedException e) {
              e.printStackTrace();
            }
          }, String.valueOf(i)));
        }
        for (int i = 0; i < 10; i++) {
          threads[i].start();
        }
        for (int i = 0; i < 10; i++) {
          threads[i].join();
        }
        System.out.println(data);
      }
    }

    上面程序的输出:

    Thread-3 Memasuki keadaan keratan kritikal = 1
    Thread-3 Memasuki keadaan keratan kritikal = 2
    Thread-3 Memasuki keadaan keratan kritikal = 3
    Thread-0 Memasuki keadaan keratan kritikal = 1
    Benang-0 Memasuki keadaan keratan kritikal = 2
    Benang-0 Memasuki keadaan keratan kritikal = 3
    Benang-9 Memasuki keadaan keratan kritikal = 1
    Benang-9 Memasuki keadaan keratan kritikal = 2
    Benang-9 Memasuki keadaan keratan kritikal = 3
    Benang-4 Memasuki keadaan keratan kritikal = 1
    Benang-4 Memasuki keadaan keratan kritikal = 2
    Benang -4 Memasuki keadaan keratan kritikal = 3
    Benang-7 Memasuki keadaan keratan kritikal = 1
    Benang-7 Memasuki keadaan keratan kritikal = 2
    Benang-7 Memasuki keadaan keratan kritikal = 3
    Benang-8 Memasuki keadaan keratan kritikal = 1
    Benang- 8 Memasuki keadaan keratan kritikal = 2
    Benang-8 Memasuki keadaan keratan kritikal = 3
    Benang-5 Memasuki keadaan keratan kritikal = 1
    Benang-5 Memasuki keadaan keratan kritikal = 2
    Benang-5 Memasuki keadaan keratan kritikal = 3
    Benang-2 Masukkan keadaan keratan kritikal = 1
    Benang-2 Masukkan keadaan keratan kritikal = 2
    Thread-2 Masukkan keadaan keratan kritikal = 3
    Thread-6 Masukkan keadaan keratan kritikal = 1
    Thread-6 Memasuki keadaan keratan kritikal = 2
    Thread-6 Memasuki bahagian kritikal keadaan = 3
    Benang-1 Memasuki keadaan bahagian kritikal = 1
    Benang-1 Memasuki keadaan bahagian kritikal = 2
    Benang-1 Masukkan keadaan bahagian kritikal = 3
    300

    Daripada hasil keluaran di atas, kita boleh tahu bahawa apabila benang boleh memperoleh kunci, ia boleh masuk semula Dan hasil keluaran akhir juga betul, jadi ia disahkan bahawa kunci putaran masuk semula yang kita tulis adalah berkesan!

    Atas ialah kandungan terperinci Bagaimana untuk melaksanakan kunci putaran manual menggunakan 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