แนวคิดของการล็อคฟรีได้รับการกล่าวถึงในการแนะนำของ [Java ที่เกิดขึ้นพร้อมกันสูง 1] เนื่องจากมีแอพพลิเคชั่นที่ไม่มีล็อคจำนวนมากในซอร์สโค้ด JDK จึงมีการแนะนำการล็อคฟรีที่นี่
1 คำอธิบายโดยละเอียดเกี่ยวกับหลักการของคลาส Lockless
1.1 CAS
กระบวนการของอัลกอริทึม CAS มีดังนี้: มันมี 3 พารามิเตอร์ CAS (V, E, N) V แสดงถึงตัวแปรที่จะอัปเดต E แสดงถึงค่าที่คาดหวังและ n หมายถึงค่าใหม่ เฉพาะในกรณีที่ V
เมื่อค่าเท่ากับค่า E ค่าของ V จะถูกตั้งค่าเป็น N หากค่า V แตกต่างจากค่า E นั่นหมายความว่าเธรดอื่น ๆ ได้ทำการอัปเดตแล้วและเธรดปัจจุบันไม่ได้ทำอะไรเลย ในที่สุด CAS ส่งคืนคุณค่าที่แท้จริงของการดำเนินงาน V. CAS ปัจจุบันนั้นดำเนินการด้วยทัศนคติในแง่ดีและเชื่อเสมอว่าสามารถดำเนินการเสร็จสมบูรณ์ได้ เมื่อหลายเธรดทำงานตัวแปรโดยใช้ CAS ในเวลาเดียวกันมีเพียงหนึ่งเดียวเท่านั้นที่จะชนะและอัปเดตได้สำเร็จและส่วนที่เหลือจะล้มเหลว เธรดที่ล้มเหลวจะไม่ถูกระงับจะได้รับการบอกว่าความล้มเหลวได้รับอนุญาตและได้รับอนุญาตให้ลองอีกครั้งและแน่นอนว่าเธรดที่ล้มเหลวจะอนุญาตให้มีการยกเลิกการดำเนินการ ตามหลักการนี้ CAS
การดำเนินการถูกล็อคทันทีและเธรดอื่น ๆ ยังสามารถตรวจจับสัญญาณรบกวนไปยังเธรดปัจจุบันและจัดการได้อย่างเหมาะสม
เราจะพบว่ามีขั้นตอนมากเกินไปใน CAS เป็นไปได้ไหมว่าหลังจากตัดสินว่า V และ E เหมือนกันเมื่อเรากำลังกำหนดค่าเราเปลี่ยนเธรดและเปลี่ยนค่า อะไรทำให้เกิดความไม่สอดคล้องกันของข้อมูล?
ในความเป็นจริงความกังวลนี้ซ้ำซ้อน กระบวนการดำเนินการทั้งหมดของ CAS เป็นการดำเนินการอะตอมซึ่งเสร็จสมบูรณ์โดยคำสั่ง CPU
1.2 คำแนะนำ CPU
คำสั่ง CPU ของ CAS คือ CMMPXCHG
รหัสคำสั่งมีดังนี้:
/ * accumulator = al, axe หรือ eax ขึ้นอยู่กับว่าการเปรียบเทียบไบต์คำหรือสองคำจะดำเนินการ */ if (accumulator == ปลายทาง) {zf = 1; ปลายทาง = แหล่งที่มา; } else {zf = 0; สะสม = ปลายทาง; - หากค่าเป้าหมายเท่ากับค่าในการลงทะเบียนจะตั้งค่าสถานะ JUMP และตั้งค่าข้อมูลต้นฉบับเป็นเป้าหมาย หากคุณไม่รอคุณจะไม่ตั้งค่าสถานะกระโดด
Java มีคลาสฟรีล็อคมากมายดังนั้นเรามาแนะนำคลาสฟรีล็อคด้านล่าง
2 ไร้ประโยชน์
เรารู้อยู่แล้วว่าการล็อคฟรีมีประสิทธิภาพมากกว่าการบล็อก ลองมาดูกันว่า Java ใช้คลาสที่ปราศจากล็อคเหล่านี้อย่างไร
2.1. Atomicinteger
Atomicinteger เช่นเดียว
Atomicinteger ระดับสาธารณะขยายจำนวนใช้งาน java.io.serializable
มีการดำเนินการ CAS จำนวนมากใน Atomicinteger ซึ่งเป็นเรื่องปกติซึ่งเป็น:
Public Final Boolean เปรียบเทียบ (คาดหวัง int, int update) {
return unsafe.compareandswapint (นี่, valueOffset, คาดหวัง, อัปเดต);
-
ที่นี่เราจะอธิบายวิธีการที่ไม่ปลอดภัย compareandswapint หมายความว่าหากค่าของตัวแปรที่มีการชดเชยในคลาสนี้คือ ValueOffset จะเหมือนกับค่าที่คาดหวังให้ตั้งค่าของตัวแปรนี้เพื่ออัปเดต
อันที่จริงตัวแปรที่มีค่าออฟเซ็ตคือค่า
คงที่ {ลอง {valueOffSet = unsafe.ObjectFieldOffset (Atomicinteger.class.getDeclaredField ("ค่า")); } catch (exception ex) {โยนข้อผิดพลาดใหม่ (ex); -เราได้กล่าวไว้ก่อนหน้านี้ว่า CAS อาจล้มเหลว แต่ค่าใช้จ่ายของความล้มเหลวมีขนาดเล็กมากดังนั้นการดำเนินการทั่วไปจึงอยู่ในลูปที่ไม่มีที่สิ้นสุดจนกว่าจะประสบความสำเร็จ
public int final int getandincrement () {สำหรับ (;;) {int current = get (); int next = current + 1; ถ้า (เปรียบเทียบ (ปัจจุบัน, ถัดไป)) ส่งคืนกระแส; -2.2 ไม่ปลอดภัย
จากชื่อคลาสเราจะเห็นว่าการดำเนินการที่ไม่ปลอดภัยนั้นเป็นการดำเนินการที่ไม่ปลอดภัยเช่น:
ตั้งค่าตามออฟเซ็ต (ฉันได้เห็นฟังก์ชั่นนี้ใน Atomicinteger ที่เพิ่งเปิดตัว)
Park () (หยุดกระทู้นี้จะถูกกล่าวถึงในบล็อกในอนาคต)
การดำเนินการ CAS ที่ไม่ใช่ API ที่ไม่ใช่สาธารณะอาจแตกต่างกันอย่างมากใน JDK รุ่นต่าง ๆ
2.3. อะตอม
Atomicinteger ได้รับการกล่าวถึงก่อนหน้านี้และแน่นอน Atomicboolean, Atomiclong ฯลฯ มีความคล้ายคลึงกันทั้งหมด
สิ่งที่เราต้องการแนะนำที่นี่คือ Atomicreference
Atomicreference เป็นคลาสเทมเพลต
Atomicreference ระดับสาธารณะ <v> ใช้ java.io.serializable
สามารถใช้เพื่อห่อหุ้มข้อมูลทุกประเภท
ตัวอย่างเช่นสตริง
การทดสอบแพ็คเกจ; นำเข้า java.util.concurrent.atomic.atomicreference; การทดสอบระดับสาธารณะ {public final atomicreference <String> atomicString = new AtomicReference <String> ("Hosee"); โมฆะคงที่สาธารณะหลัก (สตริง [] args) {สำหรับ (int i = 0; i <10; i ++) {int final int num = i; เธรดใหม่ () {โมฆะสาธารณะ Run () {ลอง {thread.sleep (math.abs ((int) math.random ()*100)); } catch (exception e) {e.printstacktrace (); } if (AtomicString.CompareAndset ("hosee", "ztk")) {system.out.println (thread.currentthread (). getId () + "เปลี่ยนค่า"); } else {system.out.println (thread.currentthread (). getId () + "ล้มเหลว"); - }.เริ่ม(); -ผลลัพธ์:
10failed
13failed
ค่า 9 การเปลี่ยนแปลง
11failed
12failed
15failed
17failed
14failed
16failed
18failed
คุณจะเห็นว่ามีเธรดเดียวเท่านั้นที่สามารถแก้ไขค่าได้และเธรดที่ตามมาจะไม่สามารถแก้ไขได้อีกต่อไป
2.4.AtomicstampedReference
เราจะพบว่ายังมีปัญหาเกี่ยวกับการดำเนินการ CAS
ตัวอย่างเช่นวิธี Atomicinteger ที่เพิ่มขึ้นก่อนหน้านี้
INT INTINT ขั้นสุดท้ายสาธารณะ () {สำหรับ (;;) {int current = get (); int next = current + 1; ถ้า (เปรียบเทียบ (ปัจจุบัน, ถัดไป)) กลับมาถัดไป; - สมมติว่าค่าปัจจุบัน = 1 เมื่อเธรด int current = get () ดำเนินการสลับไปที่เธรดอื่นเธรดนี้จะเปลี่ยน 1 เป็น 2 จากนั้นเธรดอื่นจะเปลี่ยน 2 เป็น 1 อีกครั้ง ในเวลานี้เปลี่ยนเป็นเธรดเริ่มต้น เนื่องจากค่ายังคงเท่ากับ 1 การดำเนินการ CAS ยังสามารถดำเนินการได้ แน่นอนว่าไม่มีปัญหาในการเพิ่มเติม หากมีบางกรณีกระบวนการดังกล่าวจะไม่ได้รับอนุญาตเมื่อมีความไวต่อสถานะของข้อมูล
ในเวลานี้จำเป็นต้องใช้คลาส AtomicstampedReference
มันใช้คลาสคู่ภายในเพื่อห่อหุ้มค่าและการประทับเวลา
คู่คลาสคงที่ส่วนตัว <T> {การอ้างอิงสุดท้าย t; แสตมป์ int สุดท้าย; คู่ส่วนตัว (การอ้างอิง t, แสตมป์ int) {this.reference = การอ้างอิง; this.stamp = แสตมป์; } คงที่ <T> pair <t> ของ (t การอ้างอิง, แสตมป์ int) {ส่งคืนคู่ใหม่ <t> (อ้างอิง, แสตมป์); -แนวคิดหลักของคลาสนี้คือการเพิ่มการประทับเวลาเพื่อระบุการเปลี่ยนแปลงแต่ละครั้ง
// เปรียบเทียบพารามิเตอร์การตั้งค่าคือ: ค่าที่คาดหวังจะเขียนค่าใหม่ที่คาดว่าจะใช้การประทับเวลาใหม่
Public Boolean Pompereandereandset (V ที่คาดว่าจะได้รับ, v newReference, int ที่คาดหวัง, int newstamp) {pair <v> current = pair; return postionreference == current.reference && คาดหวัง == current.stamp && ((newReference == current.reference && Newstamp == current.stamp) || caspair (current, pair.of (newReference, Newstamp))); - เมื่อค่าที่คาดหวังเท่ากับค่าปัจจุบันและการประทับเวลาที่คาดหวังจะเท่ากับการประทับเวลาปัจจุบันค่าใหม่จะถูกเขียนและการประทับเวลาใหม่จะได้รับการปรับปรุง
นี่คือสถานการณ์ที่ใช้ AtomicstampedReference มันอาจจะไม่เหมาะสม แต่ฉันไม่สามารถจินตนาการถึงสถานการณ์ที่ดีได้
พื้นหลังที่เกิดเหตุคือ บริษัท ชาร์จผู้ใช้ที่มียอดคงเหลือต่ำฟรี แต่ผู้ใช้แต่ละคนสามารถเติมเงินได้เพียงครั้งเดียว
การทดสอบแพ็คเกจ; นำเข้า java.util.concurrent.atomic.atomicstampedReference; การทดสอบระดับสาธารณะ {Static AtomicStampedReference <Integer> เงิน = ใหม่ AtomicStampedReference <Treger> (19, 0); โมฆะคงที่สาธารณะหลัก (สตริง [] args) {สำหรับ (int i = 0; i <3; i ++) {int timestamp สุดท้าย = money.getStamp (); เธรดใหม่ () {โมฆะสาธารณะเรียกใช้ () {ในขณะที่ (จริง) {ในขณะที่ (จริง) {จำนวนเต็ม m = money.getReference (); if (m <20) {ถ้า (money.CompareAndset (M, M + 20, timestamp, timestamp + 1)) {system.out.println ("เติมเงินสำเร็จ, ยอดคงเหลือ:" + money.getReference ()); หยุดพัก; }} else {break; - }.เริ่ม(); } เธรดใหม่ () {โมฆะสาธารณะ Run () {สำหรับ (int i = 0; i <100; i ++) {ในขณะที่ (จริง) {int timestamp = money.getStamp (); จำนวนเต็ม m = money.getReference (); if (M> 10) {ถ้า (money.CompareAndset (M, M - 10, timestamp, timestamp + 1)) {system.out.println ("บริโภค 10 หยวน, ยอดคงเหลือ:" + money.getReference ()); หยุดพัก; }} else {break; }} ลอง {thread.sleep (100); } catch (Exception E) {// todo: จัดการข้อยกเว้น}}}}; }.เริ่ม(); -อธิบายรหัสมี 3 เธรดชาร์จผู้ใช้ 3 เธรด เมื่อยอดคงเหลือของผู้ใช้น้อยกว่า 20 ให้ชาร์จใหม่ผู้ใช้ 20 หยวน มีการบริโภค 100 เธรดแต่ละครั้งใช้จ่าย 10 หยวน ผู้ใช้ในขั้นต้นมี 9 หยวน เมื่อใช้ AtomicstampedReference เพื่อนำไปใช้งานผู้ใช้จะได้รับการชาร์จอีกครั้งเท่านั้นเนื่องจากการดำเนินการแต่ละครั้งจะทำให้การประทับเวลา +1 ผลการทำงาน:
เติมเงินสำเร็จ: 39
การบริโภค 10 หยวนสมดุล: 29
การบริโภค 10 หยวนสมดุล: 19
การบริโภค 10 หยวนสมดุล: 9
หากคุณใช้ Atomicreference <จำนวนเต็ม> หรือจำนวนเต็มอะตอมเพื่อนำไปใช้มันจะทำให้เกิดการชาร์จหลายครั้ง
เติมเงินสำเร็จ: 39
การบริโภค 10 หยวนสมดุล: 29
การบริโภค 10 หยวนสมดุล: 19
เติมเงินสำเร็จ: 39
การบริโภค 10 หยวนสมดุล: 29
การบริโภค 10 หยวนสมดุล: 19
เติมเงินสำเร็จ: 39
การบริโภค 10 หยวนสมดุล: 29
2.5. AtomicintegerArray
เมื่อเทียบกับ Atomicinteger การใช้งานอาร์เรย์เป็นเพียงตัวห้อยพิเศษ
Public Final Boolean เปรียบเทียบ (int i, int คาดหวัง, การอัปเดต int) {
ReturnaleanDsetraw (ตรวจสอบ BYTEOFFSET (i), คาดหวัง, อัปเดต);
-
การตกแต่งภายในเพียงแค่ห่อหุ้มอาร์เรย์ปกติ
INT ครั้งสุดท้ายส่วนตัว [] อาร์เรย์;
สิ่งที่น่าสนใจที่นี่คือศูนย์ชั้นนำของตัวเลขไบนารีถูกใช้เพื่อคำนวณการชดเชยในอาร์เรย์
Shift = 31 - Integer.numberofleadingzeros (สเกล);
ศูนย์ชั้นนำหมายความว่าตัวอย่างเช่น 8 บิตแสดงถึง 12,00001100 จากนั้นศูนย์ชั้นนำคือจำนวน 0 ต่อหน้า 1 ซึ่งคือ 4
วิธีการคำนวณการชดเชยไม่ได้ถูกนำมาใช้ที่นี่
2.6. Atomicintegerfieldupdater
ฟังก์ชั่นหลักของคลาส Atomicintegerfieldupdater คือการอนุญาตให้ตัวแปรธรรมดาเพลิดเพลินกับการทำงานของอะตอมเช่นกัน
ตัวอย่างเช่นเดิมมีตัวแปรที่เป็นประเภท int และตัวแปรนี้ถูกนำไปใช้ในหลาย ๆ ที่ อย่างไรก็ตามในสถานการณ์บางอย่างหากคุณต้องการเปลี่ยนประเภท int เป็น atomicinteger หากคุณเปลี่ยนประเภทโดยตรงคุณต้องเปลี่ยนแอปพลิเคชันในที่อื่น Atomicintegerfieldupdater ได้รับการออกแบบมาเพื่อแก้ปัญหาดังกล่าว
การทดสอบแพ็คเกจ; นำเข้า java.util.concurrent.atomic.atomicinteger; นำเข้า java.util.concurrent.atomic.atomicintegerfieldupdater; คะแนน int ผันผวน; สาธารณะ int getScore () {คะแนนคืน; } โมฆะสาธารณะ setScore (คะแนน int) {this.score = คะแนน; }} สาธารณะสุดท้าย atomicintegerfieldupdater <v> vv = Atomicintegerfieldupdater.newupdater (v.class, "คะแนน"); Public Static Atomicinteger AllScore = New Atomicinteger (0); โมฆะคงที่สาธารณะหลัก (สตริง [] args) พ่น InterruptedException {สุดท้าย v stu = new v (); เธรด [] t = เธรดใหม่ [10,000]; สำหรับ (int i = 0; i <10,000; i ++) {t [i] = เธรดใหม่ () {@Override โมฆะสาธารณะเรียกใช้ () {ถ้า (math.random ()> 0.4) {vv.incrementandget (stu); allscore.incrementandget (); - t [i]. start (); } สำหรับ (int i = 0; i <10,000; i ++) {t [i] .join (); } system.out.println ("score ="+stu.getScore ()); System.out.println ("AllScore ="+AllScore); - รหัสด้านบนเปลี่ยนคะแนนโดยใช้ Atomicintegerfieldupdater เป็น Atomicinteger ตรวจสอบความปลอดภัยของด้าย
AllScore ใช้ที่นี่เพื่อตรวจสอบ หากคะแนนและค่า AllScore เหมือนกันก็หมายความว่ามันเป็นแบบเธรดที่ปลอดภัย
บันทึก: