0Pricing
Java Academy · บทเรียน

ความปลอดภัยของเธรดและผลข้างเคียง

หลีกเลี่ยงสถานะที่เปลี่ยนแปลงร่วมกัน

ความปลอดภัยของเธรดและผลข้างเคียง เป็นบทเรียน Java Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Java Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

การทำงานแบบ parallel หมายถึงการเข้าถึงพร้อมกัน

ในสตรีมแบบ parallel หลายเธรดจะเรียกใช้แลมบ์ดาของคุณพร้อมกัน สถานะที่เปลี่ยนแปลงได้ร่วมกันซึ่งแลมบ์ดาเหล่านั้นเข้าถึงจะกลายเป็นสภาวะแข่งขัน

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int safe = IntStream.rangeClosed(1, 1000).parallel().sum();
        System.out.println(safe);
    }
}

อันตราย: การเปลี่ยนแปลงตัวแปรร่วมกัน

ตัวนับ int ธรรมดาที่หลายเธรดเข้าถึงจะทำให้การอัปเดตบางส่วนสูญหาย อย่าสะสมค่าลงในสถานะที่เปลี่ยนแปลงได้ร่วมกันภายใน forEach

import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicInteger counter = new AtomicInteger();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(n -> counter.incrementAndGet());
        System.out.println(counter.get());
    }
}

เหตุใดชุดข้อมูลทั่วไปจึงทำงานผิดพลาด

การเพิ่มข้อมูลลงใน ArrayList ที่ไม่ปลอดภัยต่อการทำงานหลายเธรดจาก forEach แบบ parallel อาจทำให้ข้อมูลเสียหายหรือองค์ประกอบสูญหาย วิธีแก้ไม่ใช่การใช้ข้อมูลร่วมกันแล้วแก้ไข แต่คือไม่ใช้แนวทางดังกล่าวตั้งแต่แรก

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> result = IntStream.rangeClosed(1, 10).parallel()
            .boxed()
            .collect(Collectors.toList());
        System.out.println(result.size());
    }
}

เครื่องมือที่เหมาะสม: collect

ใช้ collect แทน forEach ที่ก่อผลข้างเคียง ระบบตัวรวบรวมจะสะสมโครงสร้างเก็บข้อมูลบางส่วนแยกกันในแต่ละเธรด แล้วรวมเข้าด้วยกันอย่างปลอดภัย

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        List<Integer> squares = IntStream.rangeClosed(1, 6).parallel()
            .map(n -> n * n)
            .boxed()
            .collect(Collectors.toList());
        System.out.println(squares);
    }
}

เครื่องมือที่เหมาะสม: reduce

reduce ปลอดภัยต่อการทำงานหลายเธรดโดยธรรมชาติเมื่อการดำเนินการมีสมบัติการจัดหมู่และไม่มีสถานะ เพราะจะรวมผลลัพธ์บางส่วนโดยไม่แก้ไขข้อมูลร่วมกัน

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 1000).parallel()
            .reduce(0, Integer::sum);
        System.out.println(sum);
    }
}

แลมบ์ดาไร้สถานะ

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

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        long count = IntStream.rangeClosed(1, 1000).parallel()
            .filter(n -> n % 2 == 0)
            .count();
        System.out.println(count);
    }
}

ชนิดอะตอมิกเป็นทางออกสำรอง

หากจำเป็นต้องใช้ตัวนับร่วมกันจริง ๆ ให้ใช้คลาสแบบอะตอมิก เช่น AtomicLong คลาสเหล่านี้จัดลำดับการอัปเดตได้อย่างถูกต้อง แต่จะเพิ่มการแย่งใช้ทรัพยากร

import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        AtomicLong sum = new AtomicLong();
        IntStream.rangeClosed(1, 1000).parallel()
            .forEach(sum::addAndGet);
        System.out.println(sum.get());
    }
}

ชุดข้อมูลที่ทำงานพร้อมกัน

เมื่อต้องใช้โครงสร้างแมปร่วมกันระหว่างเธรด ConcurrentHashMap จะมีความปลอดภัย อย่างไรก็ตาม ควรเลือกใช้ Collectors.groupingByConcurrent เมื่อทำได้

import java.util.Map;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> words = List.of("a", "bb", "cc", "ddd");
        Map<Integer, List<String>> byLen = words.parallelStream()
            .collect(Collectors.groupingByConcurrent(String::length));
        System.out.println(byLen);
    }
}

หลีกเลี่ยงตรรกะที่ขึ้นอยู่กับลำดับ

อย่าพึ่งพาลำดับที่พบภายในแลมบ์ดาแบบ parallel การดำเนินการที่สมมติว่าประมวลผลตามลำดับจะให้ผลลัพธ์ผิดเมื่อทำงานแบบ parallel

import java.util.stream.IntStream;

public class Main {
    public static void main(String[] args) {
        int sum = IntStream.rangeClosed(1, 100).parallel()
            .map(n -> n * 2)
            .sum();
        System.out.println(sum);
    }
}

ฟังก์ชันบริสุทธิ์ทำงานแบบ parallel ได้อย่างปลอดภัย

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

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<String> in = List.of("red", "green", "blue");
        String out = in.parallelStream()
            .map(String::toUpperCase)
            .sorted()
            .collect(Collectors.joining(","));
        System.out.println(out);
    }
}

ไม่รบกวนข้อมูล

อย่าแก้ไขชุดข้อมูลต้นทางขณะที่สตรีมกำลังทำงานอยู่ การทำเช่นนี้ทำให้เกิด ConcurrentModificationException หรือพฤติกรรมที่ไม่กำหนด ไม่ว่าจะทำงานแบบลำดับเดียวหรือแบบ parallel

import java.util.List;
import java.util.stream.Collectors;

public class Main {
    public static void main(String[] args) {
        List<Integer> src = List.of(1, 2, 3, 4);
        List<Integer> doubled = src.parallelStream()
            .map(n -> n * 2)
            .collect(Collectors.toList());
        System.out.println(doubled);
    }
}

ตรวจสอบความเข้าใจ

วิธีที่แนะนำในการสะสมผลลัพธ์จากสตรีมแบบ parallel คืออะไร?

สรุปทบทวน

คุณทำให้สตรีมแบบ parallel ทำงานได้อย่างถูกต้อง:

  • สถานะที่เปลี่ยนแปลงได้ร่วมกันในแลมบ์ดาแบบ parallel ทำให้เกิดสภาวะแข่งขัน
  • ควรใช้ collect และ reduce แทน forEach ที่ก่อผลข้างเคียง
  • ทำให้แลมบ์ดาไร้สถานะ บริสุทธิ์ และไม่รบกวนข้อมูล
  • หากจำเป็นต้องใช้ข้อมูลร่วมกัน ให้ใช้ชนิดอะตอมิกหรือชุดข้อมูลที่ทำงานพร้อมกัน แต่ควรเป็นทางเลือกสุดท้าย

คำถามที่พบบ่อย

บทเรียน “ความปลอดภัยของเธรดและผลข้างเคียง” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “ความปลอดภัยของเธรดและผลข้างเคียง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Java Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Java Academy มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “ความปลอดภัยของเธรดและผลข้างเคียง”

หลีกเลี่ยงสถานะที่เปลี่ยนแปลงร่วมกัน คุณปฏิบัติ Java Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Java Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Java Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน

บทเรียน “ความปลอดภัยของเธรดและผลข้างเคียง” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Java Academy นี้ได้ไหม

ได้ บทเรียน Java Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การสร้างสตรีมแบบขนาน
  2. เมื่อการประมวลผลแบบขนานช่วยได้
  3. ความปลอดภัยของเธรดและผลข้างเคียง
  4. ข้อผิดพลาดที่พบบ่อย
← กลับไปที่ Java Academy