ความปลอดภัยของเธรดและผลข้างเคียง
หลีกเลี่ยงสถานะที่เปลี่ยนแปลงร่วมกัน
ความปลอดภัยของเธรดและผลข้างเคียง เป็นบทเรียน 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การสร้างสตรีมแบบขนาน
- เมื่อการประมวลผลแบบขนานช่วยได้
- ความปลอดภัยของเธรดและผลข้างเคียง
- ข้อผิดพลาดที่พบบ่อย