ในบทเรียนนี้ เราจะพูดถึงโดยทั่วไปเกี่ยวกับการทำงานกับ คลาส java.lang.ThreadLocal<>และวิธีการใช้งานในสภาพแวดล้อมแบบมัลติเธรด

คลาสThreadLocalใช้สำหรับเก็บตัวแปร คุณสมบัติที่โดดเด่นของคลาสนี้คือเก็บสำเนาของค่าแยกต่างหากสำหรับแต่ละเธรดที่ใช้

เมื่อพิจารณาลึกลงไปในการทำงานของคลาส เราสามารถจินตนาการถึงแผนที่ที่แมปเธรดกับค่าต่างๆ ซึ่งเธรดปัจจุบันใช้ค่าที่เหมาะสมเมื่อจำเป็นต้องใช้

ตัวสร้างคลาส ThreadLocal

ตัวสร้าง การกระทำ
เธรดท้องถิ่น () สร้างตัวแปรว่างใน Java

วิธีการ

วิธี การกระทำ
รับ() ส่งกลับค่าของตัวแปรท้องถิ่นของเธรดปัจจุบัน
ชุด() ตั้งค่าของตัวแปรโลคัลสำหรับเธรดปัจจุบัน
ลบ() ลบค่าของตัวแปรโลคัลของเธรดปัจจุบัน
ThreadLocal.withInitial() วิธีการเพิ่มเติมจากโรงงานที่ตั้งค่าเริ่มต้น

รับ () & ชุด ()

ลองเขียนตัวอย่างที่เราสร้างสองเคาน์เตอร์ ตัวแรกเป็นตัวแปรธรรมดาสำหรับนับจำนวนเธรด ประการที่ สองเราจะห่อในThreadLocal และเราจะมาดูกันว่าพวกเขาทำงานร่วมกันอย่างไร ขั้นแรก มาเขียน คลาส ThreadDemoที่สืบทอดค่าRunnable และมีข้อมูลของเราและ เมธอดrun()ที่สำคัญทั้งหมด เราจะเพิ่มวิธีการแสดงตัวนับบนหน้าจอด้วย:


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = new ThreadLocal<>();

    public void run() {
        counter++;

        if(threadLocalCounter.get() != null) {
            threadLocalCounter.set(threadLocalCounter.get() + 1);
        } else {
            threadLocalCounter.set(0);
        }
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

ในแต่ละคลาสของเรา เราเพิ่มเคาน์เตอร์ตัวแปรเรียกเมธอดget()เพื่อรับข้อมูลจากตัวแปรThreadLocal หากเธรดใหม่ไม่มีข้อมูล เราจะตั้งค่าเป็น 0 หากมีข้อมูล เราจะเพิ่มทีละหนึ่ง และเขียน วิธีการ หลัก ของเรา :


public static void main(String[] args) {
    ThreadDemo threadDemo = new ThreadDemo();

    Thread t1 = new Thread(threadDemo);
    Thread t2 = new Thread(threadDemo);
    Thread t3 = new Thread(threadDemo);

    t1.start();
    t2.start();
    t3.start();

}

เมื่อเรียกใช้คลาสของเรา เราจะเห็นว่า ตัวแปร ThreadLocalยังคงเหมือนเดิมโดยไม่คำนึงถึงเธรดที่เข้าถึงตัวแปรนั้น แต่จำนวนของเธรดจะเพิ่มขึ้น

ตัวนับ: 1
ตัวนับ: 2
ตัวนับ: 3
threadLocalCounter: 0
threadLocalCounter: 0
threadLocalCounter: 0

กระบวนการเสร็จสิ้นด้วยรหัสออก 0

ลบ()

เพื่อทำความเข้าใจว่า เมธอด Removeทำงานอย่างไร เราจะเปลี่ยนโค้ดเล็กน้อยใน คลาส ThreadDemo :


if(threadLocalCounter.get() != null) {
      threadLocalCounter.set(threadLocalCounter.get() + 1);
  } else {
      if (counter % 2 == 0) {
          threadLocalCounter.remove();
      } else {
          threadLocalCounter.set(0);
      }
  }

ในโค้ดนี้ ถ้าตัวนับเธรดเป็นเลขคู่ เราจะเรียกเมธอดremove()บนตัวแปรThreadLocal ของเรา ผลลัพธ์:

ตัวนับ: 3
threadLocalCounter: 0
ตัวนับ: 2
threadLocalCounter: null
ตัวนับ: 1
threadLocalCounter: 0

กระบวนการเสร็จสิ้นด้วยรหัสออก 0

และที่นี่เราเห็นได้ง่ายว่า ตัวแปร ThreadLocalในเธรดที่สองเป็นnull

ThreadLocal.withInitial()

เมธอดนี้สร้างตัวแปรแบบเธรดโลคัล

การใช้งาน คลาส ThreadDemo :


class ThreadDemo implements Runnable {

    int counter;
    ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 1);

    public void run() {
        counter++;
        printCounters();
    }

    public void printCounters(){
        System.out.println("Counter: " + counter);
        System.out.println("threadLocalCounter: " + threadLocalCounter.get());
    }
}

และเราสามารถดูผลลัพธ์ของรหัสของเรา:

ตัวนับ: 1
ตัวนับ: 2
ตัวนับ: 3
threadLocalCounter: 1
threadLocalCounter: 1
threadLocalCounter: 1

กระบวนการเสร็จสิ้นด้วยรหัสออก 0

ทำไมเราจึงควรใช้ตัวแปรดังกล่าว?

ThreadLocal ให้สิ่งที่เป็น นามธรรมเหนือตัวแปรโลคัลที่เกี่ยวข้องกับเธรดของการดำเนินการ java.lang.Thread

ตัวแปร ThreadLocalแตกต่างจากตัวแปรทั่วไปตรงที่แต่ละเธรดมีอินสแตนซ์เริ่มต้นของตัวแปรเป็นของตนเอง ซึ่งเข้าถึงได้ผ่านเมธอด get()และ set()

แต่ละเธรด เช่น ตัวอย่างของ คลาส เธรดมีการแม็พของ ตัวแปร ThreadLocalที่เกี่ยวข้อง คีย์ของแมปคือการอ้างอิงถึง อ็อบเจ็กต์ ThreadLocalและค่าต่างๆ เป็นการอ้างอิงถึงตัวแปรThreadLocal ที่ "ได้มา"

เหตุใดคลาส Random จึงไม่เหมาะสำหรับการสร้างตัวเลขสุ่มในแอปพลิเคชันแบบมัลติเธรด

เราใช้Random class เพื่อรับตัวเลขสุ่ม แต่มันทำงานได้ดีในสภาพแวดล้อมแบบมัลติเธรดหรือไม่? ที่จริงไม่ การสุ่มไม่เหมาะสำหรับสภาพแวดล้อมแบบมัลติเธรด เนื่องจากเมื่อหลายเธรดเข้าถึงคลาสพร้อมกัน ประสิทธิภาพจะลดลง

เพื่อแก้ไขปัญหานี้ JDK 7 ได้แนะนำ คลาส java.util.concurrent.ThreadLocalRandomเพื่อสร้างตัวเลขสุ่มในสภาพแวดล้อมแบบมัลติเธรด ประกอบด้วยสองคลาส: ThreadLocalและRandom

ตัวเลขสุ่มที่ได้รับจากหนึ่งเธรดนั้นไม่ขึ้นอยู่กับเธรดอื่น แต่java.util.Randomจัดเตรียมตัวเลขสุ่มทั่วโลก นอกจากนี้ ไม่เหมือนกับRandomตรงThreadLocalRandomไม่รองรับการเพาะอย่างชัดเจน แต่จะแทนที่เมธอดsetSeed()ที่สืบทอดมาจากRandomเพื่อให้ส่ง UnsupportedOperationException เสมอเมื่อเรียกใช้

มาดูเมธอดของ คลาส ThreadLocalRandom :

วิธี การกระทำ
ThreadLocalRandom ปัจจุบัน () ส่งกลับ ThreadLocalRandom ของเธรดปัจจุบัน
int ถัดไป (int บิต) สร้างหมายเลขสุ่มหลอกถัดไป
double nextDouble(ดับเบิ้ลอย่างน้อย, ดับเบิ้ลผูกพัน) ส่งกลับเลขสุ่มเทียมจากการแจกแจงแบบเดียวกันระหว่างค่าน้อยสุด (รวม) และค่าผูกมัด (เฉพาะ)
int nextInt (int น้อย, int ผูกไว้) ส่งกลับเลขสุ่มเทียมจากการแจกแจงแบบเดียวกันระหว่างค่าน้อยสุด (รวม) และค่าผูกมัด (เฉพาะ)
ยาว ถัดไป ยาว(ยาว n) ส่งกลับเลขสุ่มเทียมจากการแจกแจงแบบสม่ำเสมอระหว่าง 0 (รวม) และค่าที่ระบุ (พิเศษ)
ยาว ถัดไป ยาว (ยาว น้อยที่สุด ยาว ผูกพัน) ส่งกลับเลขสุ่มเทียมจากการแจกแจงแบบเดียวกันระหว่างค่าน้อยสุด (รวม) และค่าผูกมัด (เฉพาะ)
โมฆะ setSeed (เมล็ดยาว) ส่งUnsupportedOperationException _ เครื่องกำเนิดนี้ไม่รองรับการเพาะ

รับตัวเลขสุ่มโดยใช้ ThreadLocalRandom.current()

ThreadLocalRandomเป็นการรวมกันของคลาส ThreadLocalและ Random มันบรรลุประสิทธิภาพที่ดีขึ้นในสภาพแวดล้อมแบบมัลติเธรดโดยหลีกเลี่ยงการเข้าถึงอินสแตนซ์ของคลาสแบบสุ่ม พร้อมกัน

ลองใช้ตัวอย่างที่เกี่ยวข้องกับหลายเธรดและดูแอปพลิเคชันของเรากับ คลาส ThreadLocalRandom :


import java.util.concurrent.ThreadLocalRandom;

class RandomNumbers extends Thread {

    public void run() {
        try {
            int bound = 100;
            int result = ThreadLocalRandom.current().nextInt(bound);
            System.out.println("Thread " + Thread.currentThread().getId() + " generated " + result);
        }
        catch (Exception e) {
            System.out.println("Exception");
        }
    }

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

				for (int i = 0; i < 10; i++) {
            RandomNumbers randomNumbers = new RandomNumbers();
            randomNumbers.start();
        }

        long endTime = System.currentTimeMillis();

        System.out.println("Time taken: " + (endTime - startTime));
    }
}

ผลลัพธ์ของโปรแกรมของเรา:

เวลาที่ใช้: 1
เธรด 17 สร้าง 13
เธรด 18 สร้าง 41
เธรด 16 สร้าง 99
เธรด 19 สร้าง 25
เธรด 23 สร้าง 33
เธรด 24 สร้าง 21
เธรด 15 สร้าง 15
เธรด 21 สร้าง 28
เธรด 22 สร้าง 97
เธรด 20 สร้าง 33

และตอนนี้เรามาเปลี่ยน คลาส RandomNumbers ของเรา และใช้Randomในนั้น:


int result = new Random().nextInt(bound);
เวลาที่ใช้: 5
เธรด 20 สร้าง 48
เธรด 19 สร้าง 57
เธรด 18 สร้าง 90
เธรด 22 สร้าง 43
เธรด 24 สร้าง 7
เธรด 23 สร้าง 63
เธรด 15 สร้าง 2
เธรด 16 สร้าง 40
เธรด 17 สร้าง 29
เธรด 21 สร้าง 12

รับทราบ! ในการทดสอบของเรา บางครั้งผลลัพธ์ก็เหมือนกันและบางครั้งก็แตกต่างกัน แต่ถ้าเราใช้เธรดมากขึ้น (เช่น 100) ผลลัพธ์จะเป็นดังนี้:

สุ่ม — 19-25 มิลลิวินาที
ThreadLocalRandom — 17-19 มิลลิวินาที

ดังนั้น ยิ่งมีเธรดในแอปพลิเคชันของเรามากเท่าใด ประสิทธิภาพการทำงานก็จะยิ่งสูงขึ้นเมื่อใช้ คลาส Randomในสภาพแวดล้อมแบบมัลติเธรด

ในการสรุปและย้ำความแตกต่างระหว่าง คลาส RandomและThreadLocalRandom :

สุ่ม เธรด LocalRandom
หากเธรดที่แตกต่างกันใช้อินสแตนซ์เดียวกันของRandomจะเกิดข้อขัดแย้งและประสิทธิภาพการทำงานจะลดลง ไม่มีข้อขัดแย้งหรือปัญหาใด ๆ เนื่องจากตัวเลขสุ่มที่สร้างขึ้นนั้นเป็นแบบโลคัลสำหรับเธรดปัจจุบัน
ใช้สูตรความสอดคล้องเชิงเส้นเพื่อเปลี่ยนค่าเริ่มต้น ตัวสร้างตัวเลขสุ่มเริ่มต้นโดยใช้เมล็ดที่สร้างขึ้นภายใน
มีประโยชน์ในแอปพลิเคชันที่แต่ละเธรดใช้ชุดวัตถุสุ่ม ของตัวเอง มีประโยชน์ในแอปพลิเคชันที่หลายเธรดใช้ตัวเลขสุ่มแบบขนานในกลุ่มเธรด
นี่คือคลาสผู้ปกครอง นี่คือชั้นเรียนของเด็ก