Rumah  >  Artikel  >  Java  >  Analisis senario penggunaan kunci Java di tempat kerja

Analisis senario penggunaan kunci Java di tempat kerja

PHPz
PHPzke hadapan
2023-04-28 15:34:141038semak imbas

    1. disegerakkan

    disegerakkan ialah kunci eksklusif reentrant, sama dengan fungsi kunci ReentrantLock sebaliknya digunakan . tergendala, senario kompleks yang boleh memenuhi kunci + baris gilir, dsb >

    Postur penggunaan kedua-duanya juga berbeza ReentrantLock perlu dinyatakan bahawa terdapat API untuk mengunci dan melepaskan kunci, manakala disegerakkan akan mengunci dan melepaskan kunci secara automatik pada blok kod Ia lebih mudah untuk bangun.
    • disegerakkan dan ReentrantLock mempunyai fungsi yang serupa, jadi kami akan mengambil penyegerakan sebagai contoh.
    • 1.1. Permulaan sumber dikongsi

      Dalam sistem yang diedarkan, kami ingin mengunci beberapa sumber konfigurasi mati ke dalam memori JVM apabila projek bermula, supaya permintaan boleh mendapatkannya Apabila konfigurasi sumber dikongsi, mereka boleh diambil terus dari memori tanpa perlu mengambilnya dari pangkalan data setiap kali, yang mengurangkan overhed masa.
    • Secara amnya, sumber perkongsian tersebut termasuk: konfigurasi proses perniagaan mati + konfigurasi peraturan perniagaan mati.

      Langkah-langkah untuk pemulaan sumber yang dikongsi secara amnya: permulaan projek -> tindakan pemulaan tunggal untuk mendapatkan data daripada pangkalan data -> ingatan .
    Apabila projek dimulakan, untuk mengelakkan sumber dikongsi daripada dimuatkan beberapa kali, kami sering menambah kunci eksklusif, supaya selepas satu utas selesai memuatkan sumber yang dikongsi, utas lain boleh terus dimuatkan masa, kunci eksklusif Kami boleh memilih disegerakkan atau ReentrantLock untuk kunci Kami mengambil disegerakkan sebagai contoh dan menulis kod olok-olok seperti berikut:

      // 共享资源
      private static final Map<String, String> SHARED_MAP = Maps.newConcurrentMap();
      // 有无初始化完成的标志位
      private static boolean loaded = false;
       /**
       * 初始化共享资源
       */
      @PostConstruct
      public void init(){
        if(loaded){
          return;
        }
        synchronized (this){
          // 再次 check
          if(loaded){
            return;
          }
          log.info("SynchronizedDemo init begin");
          // 从数据库中捞取数据,组装成 SHARED_MAP 的数据格式
          loaded = true;
          log.info("SynchronizedDemo init end");
        }
      }

    Saya tidak tahu sama ada anda telah menemui anotasi @PostConstruct daripada. kod di atas. Fungsi anotasi @PostConstruct ialah Apabila bekas Spring dimulakan, kaedah yang ditandakan dengan anotasi ini dilaksanakan Dengan kata lain, kaedah init yang ditunjukkan dalam rajah di atas dicetuskan apabila bekas Spring dimulakan.

    Anda boleh memuat turun kod demo, cari fail permulaan DemoApplication, klik kanan jalankan pada fail DemoApplication untuk memulakan keseluruhan projek Spring Boot, dan letakkan titik putus pada kaedah init untuk nyahpepijat.

    Kami menggunakan disegerakkan dalam kod untuk memastikan bahawa hanya satu urutan boleh melaksanakan operasi memulakan sumber dikongsi pada masa yang sama dan kami menambah bendera penyiapan pemuatan sumber dikongsi (dimuatkan) untuk menentukan sama ada pemuatan selesai . Jika pemuatan selesai, utas pemuatan lain akan kembali secara langsung.

    Jika anda menggantikan disegerakkan dengan ReentrantLock, pelaksanaannya adalah sama, tetapi anda perlu menggunakan API ReentrantLock secara eksplisit untuk mengunci dan melepaskan kunci Satu perkara yang perlu diberi perhatian apabila menggunakan ReentrantLock ialah kami perlu menambah Lock. lepaskan kunci dalam blok kaedah akhirnya Ini memastikan bahawa walaupun pengecualian berlaku selepas menambah kunci dalam percubaan, kunci boleh dilepaskan dengan betul pada akhirnya.

    Sesetengah pelajar mungkin bertanya, bolehkah kita menggunakan ConcurrentHashMap secara langsung Mengapa kita perlu menguncinya? Memang benar bahawa ConcurrentHashMap adalah selamat untuk benang, tetapi ia hanya boleh memastikan keselamatan rangkaian semasa operasi data dalaman Peta Ia tidak dapat menjamin bahawa dalam situasi berbilang benang, keseluruhan tindakan menanya pangkalan data dan memasang data hanya dilaksanakan sekali tambah kunci disegerakkan keseluruhan operasi, memastikan bahawa keseluruhan operasi hanya dilaksanakan sekali.

    2. CountDownLatch

    2.1. Senario

    1: Xiao Ming membeli produk di Taobao dan merasakan ia tidak bagus, jadi dia memulangkan produk itu (produk itu belum telah dihantar) , hanya bayaran balik), kami memanggilnya bayaran balik produk tunggal Apabila bayaran balik produk tunggal dijalankan dalam sistem latar belakang, penggunaan masa keseluruhan ialah 30 milisaat.

    2: Double 11, Xiao Ming membeli 40 item di Taobao dan menjana pesanan yang sama (sebenarnya beberapa pesanan mungkin dihasilkan, untuk kemudahan penerangan, kami memanggilnya satu), keesokan harinya Xiao Ming menemui Among mereka, 30 item telah dibeli secara impulsif, dan 30 item perlu dipulangkan bersama-sama.

    2.2. Implementasi

    Pada masa ini, backend hanya mempunyai fungsi pengembalian produk tunggal, dan tiada fungsi pengembalian produk batch (memulangkan 30 produk pada satu masa dipanggil batch) . Untuk melaksanakan fungsi ini dengan cepat, Pelajar A melakukannya mengikut pelan ini: gelung untuk dipanggil antara muka bayaran balik produk tunggal 30 kali Semasa ujian persekitaran QA, didapati bahawa jika 30 produk akan dikembalikan, ia akan dikembalikan ambil: 30 * 30 = 900 milisaat Ditambah dengan logik lain, ia mengambil masa hampir 1 saat untuk membayar balik 30 produk, yang sebenarnya mengambil masa yang lama, rakan sekelas A membangkitkan soalan ini dan berharap semua orang dapat membantu untuk mengoptimumkan masa penggunaan keseluruhan senario.

    同学 B 当时就提出,你可以使用线程池进行执行呀,把任务都提交到线程池里面去,假如机器的 CPU 是 4 核的,最多同时能有 4 个单商品退款可以同时执行,同学 A 觉得很有道理,于是准备修改方案,为了便于理解,我们把两个方案都画出来,对比一下:

    Analisis senario penggunaan kunci Java di tempat kerja

    同学 A 于是就按照演变的方案去写代码了,过了一天,抛出了一个问题:向线程池提交了 30 个任务后,主线程如何等待 30 个任务都执行完成呢?因为主线程需要收集 30 个子任务的执行情况,并汇总返回给前端。

    大家可以先不往下看,自己先思考一下,我们前几章说的那种锁可以帮助解决这个问题?

    CountDownLatch 可以的,CountDownLatch 具有这种功能,让主线程去等待子任务全部执行完成之后才继续执行。

    此时还有一个关键,我们需要知道子线程执行的结果,所以我们用 Runnable 作为线程任务就不行了,因为 Runnable 是没有返回值的,我们需要选择 Callable 作为任务。

    我们写了一个 demo,首先我们来看一下单个商品退款的代码:

    // 单商品退款,耗时 30 毫秒,退款成功返回 true,失败返回 false
    @Slf4j
    public class RefundDemo {
      /**
       * 根据商品 ID 进行退款
       * @param itemId
       * @return
       */
      public boolean refundByItem(Long itemId) {
        try {
          // 线程沉睡 30 毫秒,模拟单个商品退款过程
          Thread.sleep(30);
          log.info("refund success,itemId is {}", itemId);
          return true;
        } catch (Exception e) {
          log.error("refundByItemError,itemId is {}", itemId);
          return false;
        }
      }
    }

    接着我们看下 30 个商品的批量退款,代码如下:

    @Slf4j
    public class BatchRefundDemo {
    	// 定义线程池
      public static final ExecutorService EXECUTOR_SERVICE =
          new ThreadPoolExecutor(10, 10, 0L,
                                    TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<>(20));
      @Test
      public void batchRefund() throws InterruptedException {
        // state 初始化为 30 
        CountDownLatch countDownLatch = new CountDownLatch(30);
        RefundDemo refundDemo = new RefundDemo();
     
        // 准备 30 个商品
        List<Long> items = Lists.newArrayListWithCapacity(30);
        for (int i = 0; i < 30; i++) {
          items.add(Long.valueOf(i+""));
        }
     
        // 准备开始批量退款
        List<Future> futures = Lists.newArrayListWithCapacity(30);
        for (Long item : items) {
          // 使用 Callable,因为我们需要等到返回值
          Future<Boolean> future = EXECUTOR_SERVICE.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
              boolean result = refundDemo.refundByItem(item);
              // 每个子线程都会执行 countDown,使 state -1 ,但只有最后一个才能真的唤醒主线程
              countDownLatch.countDown();
              return result;
            }
          });
          // 收集批量退款的结果
          futures.add(future);
        }
     
        log.info("30 个商品已经在退款中");
        // 使主线程阻塞,一直等待 30 个商品都退款完成,才能继续执行
        countDownLatch.await();
        log.info("30 个商品已经退款完成");
        // 拿到所有结果进行分析
        List<Boolean> result = futures.stream().map(fu-> {
          try {
            // get 的超时时间设置的是 1 毫秒,是为了说明此时所有的子线程都已经执行完成了
            return (Boolean) fu.get(1,TimeUnit.MILLISECONDS);
          } catch (InterruptedException e) {
            e.printStackTrace();
          } catch (ExecutionException e) {
            e.printStackTrace();
          } catch (TimeoutException e) {
            e.printStackTrace();
          }
          return false;
        }).collect(Collectors.toList());
        // 打印结果统计
        long success = result.stream().filter(r->r.equals(true)).count();
        log.info("执行结果成功{},失败{}",success,result.size()-success);
      }
    }

    上述代码只是大概的底层思路,真实的项目会在此思路之上加上请求分组,超时打断等等优化措施。

    我们来看一下执行的结果:

    Analisis senario penggunaan kunci Java di tempat kerja

    从执行的截图中,我们可以明显的看到 CountDownLatch 已经发挥出了作用,主线程会一直等到 30 个商品的退款结果之后才会继续执行。

    接着我们做了一个不严谨的实验(把以上代码执行很多次,求耗时平均值),通过以上代码,30 个商品退款完成之后,整体耗时大概在 200 毫秒左右。

    而通过 for 循环单商品进行退款,大概耗时在 1 秒左右,前后性能相差 5 倍左右,for 循环退款的代码如下:

    long begin1 = System.currentTimeMillis();
    for (Long item : items) {
      refundDemo.refundByItem(item);
    }
    log.info("for 循环单个退款耗时{}",System.currentTimeMillis()-begin1);

     性能的巨大提升是线程池 + 锁两者结合的功劳。

    Atas ialah kandungan terperinci Analisis senario penggunaan kunci Java di tempat kerja. 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