السلامة في الخيوط في لغة الجافا هي موضوع هام جداً. توفر الجافا دعمًا للبيئة متعددة الخيوط باستخدام خيوط الجافا، نعلم أن الخيوط المتعددة التي تم إنشاؤها من نفس الكائن تشترك في متغيرات الكائن، وهذا يمكن أن يؤدي إلى عدم اتساق البيانات عندما تُستخدم الخيوط لقراءة وتحديث البيانات المشتركة.
سلامة الخيط
السبب في عدم اتساق البيانات هو أن تحديث قيمة أي حقل ليس عملية ذرية، فإنها تتطلب ثلاث خطوات؛ أولاً قراءة القيمة الحالية، وثانياً القيام بالعمليات اللازمة للحصول على القيمة المحدثة، وثالثاً تعيين القيمة المحدثة لمرجع الحقل. دعونا نتحقق من ذلك من خلال برنامج بسيط حيث تقوم الخيوط المتعددة بتحديث البيانات المشتركة.
package com.journaldev.threads;
public class ThreadSafety {
public static void main(String[] args) throws InterruptedException {
ProcessingThread pt = new ProcessingThread();
Thread t1 = new Thread(pt, "t1");
t1.start();
Thread t2 = new Thread(pt, "t2");
t2.start();
// انتظر حتى تنتهي الخيوط من المعالجة
t1.join();
t2.join();
System.out.println("Processing count="+pt.getCount());
}
}
class ProcessingThread implements Runnable{
private int count;
@Override
public void run() {
for(int i=1; i < 5; i++){
processSomething(i);
count++;
}
}
public int getCount() {
return this.count;
}
private void processSomething(int i) {
// معالجة بعض المهام
try {
Thread.sleep(i*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
في البرنامج أعلاه، يتم زيادة قيمة العداد بمقدار 1 أربع مرات، ونظرًا لأن لدينا خيطين، يجب أن تكون قيمته 8 بعد انتهاء تنفيذ كل من الخيوط. ولكن عند تشغيل البرنامج أكثر من مرة، ستلاحظ أن قيمة العداد تتغير بين 6 و 7 و 8. يحدث هذا لأنه حتى إذا كان count++ يبدو عملية ذرية، إلا أنها ليست كذلك وتسبب في فساد البيانات.
سلامة الخيوط في جافا
سلامة الخيوط في جافا هي العملية التي تجعل برنامجنا آمنًا للاستخدام في بيئة متعددة الخيوط، وهناك طرق مختلفة يمكننا من خلالها جعل برنامجنا آمنًا من حيث الخيوط.
- التزامن هو أداة الأسهل والأكثر استخدامًا لسلامة الخيوط في جافا.
- استخدام فئات Atomic Wrapper من حزمة java.util.concurrent.atomic. على سبيل المثال AtomicInteger
- استخدام الأقفال من حزمة java.util.concurrent.locks.
- استخدام فئات مجموعات آمنة للخيوط، تحقق من هذه المشاركة لاستخدام ConcurrentHashMap لسلامة الخيوط.
- استخدام الكلمة المفتاحية volatile مع المتغيرات لجعل كل خيط يقرأ البيانات من الذاكرة، وليس من ذاكرة الخيط.
جافا synchronized
التزامن هو الأداة التي من خلالها يمكننا تحقيق سلامة الخيوط، تضمن الآلة الظاهرية في جافا أن الشيفرة المتزامنة ستُنفذ بواسطة خيط واحد فقط في كل مرة. يتم استخدام الكلمة المفتاحية synchronized في جافا لإنشاء شيفرة متزامنة وفي الداخل تستخدم الأقفال على الكائن أو الفئة للتأكد من أن خيطًا واحدًا فقط ينفذ الشيفرة المتزامنة.
- تعمل مزامنة جافا على قفل وفتح المورد قبل أن يدخل أي خيط في الشيفرة المتزامنة، يجب عليها الحصول على قفل على الكائن عند دخول الشيفرة المتزامنة، وعند انتهاء تنفيذ الشيفرة، يقوم بإلغاء قفل المورد الذي يمكن أن يكون قد تم قفله من قبل خيوط أخرى. في هذه الأثناء، تكون الخيوط الأخرى في حالة انتظار لقفل المورد المتزامن.
- يمكننا استخدام كلمة “متزامن” بطريقتين، الأولى هي جعل الطريقة بأكملها متزامنة والطريقة الأخرى هي إنشاء كتلة متزامنة.
- عندما تكون الطريقة متزامنة، تقوم بقفل الكائن، إذا كانت الطريقة ثابتة، تقوم بقفل الفئة، لذا دائمًا من الأفضل استخدامكتلة متزامنة لقفل الأقسام فقط من الطريقة التي تحتاج إلى تزامن.
- عند إنشاء كتلة متزامنة، يجب علينا توفير المورد الذي سيتم الحصول على قفل عليه، يمكن أن يكون XYZ.class أو أي حقل كائن من الفئة.
synchronized(this)
سيقوم بقفل الكائن قبل دخول الكود المتزامن.- يجب عليك استخدامأدنى مستوى من القفل، على سبيل المثال، إذا كانت هناك عدة كتل متزامنة في الفئة وإحداها تقوم بقفل الكائن، فإن الكتل المتزامنة الأخرى لن تكون متاحة أيضًا للتنفيذ بواسطة الخيوط الأخرى. عندما نقوم بقفل كائن، يتم الحصول على قفل على جميع حقول الكائن.
- توفر مزامنة جافا سلامة البيانات على حساب الأداء، لذا يجب استخدامها فقط عندما يكون ذلك ضروريًا.
- تعمل مزامنة جافا فقط في نفس بيئة التشغيل (JVM)، لذا إذا كنت بحاجة إلى قفل بعض المورد في بيئة تشغيل متعددة (JVM)، فلن تعمل وقد تحتاج إلى النظر في آلية قفل عالمية.
- تزامن جافا يمكن أن يؤدي إلى الانغلاقات، تحقق من هذا المنشور حول الـ انغلاق في جافا وكيفية تجنبها.
- لا يمكن استخدام الكلمة المفتاحية synchronized في جافا للبنائين والمتغيرات.
- من الأفضل إنشاء كائن خيالي خاص لاستخدامه في الكتلة المتزامنة بحيث لا يمكن تغيير مرجعه بواسطة أي كود آخر. على سبيل المثال، إذا كان لديك طريقة setter للكائن الذي تقوم بالتزامن عليه، يمكن تغيير مرجعه بواسطة بعض الشفرات الأخرى مما يؤدي إلى تنفيذ الكتلة المتزامنة بشكل متواز،
- لا ينبغي استخدام أي كائن يتم الاحتفاظ به في مجموعة ثابتة، على سبيل المثال، يجب عدم استخدام السلسلة للتزامن لأنه إذا كان هناك أي كود آخر يقوم بالقفل على نفس السلسلة، فسيحاول الحصول على قفل على نفس كائن المرجع من مجموعة السلاسل وعلى الرغم من عدم ارتباط الشفرتين، سيتم قفل بعضهما البعض.
إليك التغييرات التي نحتاج إلى القيام بها في الشيفرة أعلاه لجعلها آمنة للتعامل مع الخيوط.
//متغير كائن وهمي للتزامن
private Object mutex=new Object();
...
//استخدام كتلة متزامنة لقراءة، زيادة وتحديث قيمة العد بشكل متزامن
synchronized (mutex) {
count++;
}
لنرى بعض أمثلة التزامن وما يمكننا أن نتعلمه منها.
public class MyObject {
// قفل على رصد الكائن
public synchronized void doSomething() {
// ...
}
}
// شفرة الهاكر
MyObject myObject = new MyObject();
synchronized (myObject) {
while (true) {
// تأخير لا نهائي لـ myObject
Thread.sleep(Integer.MAX_VALUE);
}
}
تنويه: يلاحظ أن كود الهاكر يحاول قفل مثيل myObject وبمجرد أن يحصل على القفل، فإنه لن يقوم بإطلاقه مما يتسبب في تعليق طريقة doSomething() في انتظار القفل، وهذا سيتسبب في دخول النظام في حالة انتظار وتسبب في إنكار الخدمة (DoS).
public class MyObject {
public Object lock = new Object();
public void doSomething() {
synchronized (lock) {
// ...
}
}
}
//الكود غير الموثوق به
MyObject myObject = new MyObject();
//تغيير مرجع كائن القفل
myObject.lock = new Object();
يرجى ملاحظة أن كائن القفل هو عام ومن خلال تغيير مرجعه، يمكننا تنفيذ كتلة متزامنة بشكل متوازٍ في عدة خيوط. الحالة مشابهة إذا كان لديك كائن خاص ولكن لديك طريقة معدة لتغيير مرجعه.
public class MyObject {
//تقفيل على مراقب كائن الصف
public static synchronized void doSomething() {
// ...
}
}
//كود الهاكر
synchronized (MyObject.class) {
while (true) {
Thread.sleep(Integer.MAX_VALUE); // Indefinitely delay MyObject
}
}
يرجى ملاحظة أن كود الهاكر يحصل على قفل على مراقب الصف ولا يقوم بإطلاقه، وهذا سيتسبب في حدوث انتظار متبادل و DoS في النظام. فيما يلي مثال آخر حيث تعمل عدة خيوط على نفس مصفوفة السلاسل وبمجرد معالجتها، يتم إلحاق اسم الخيط بقيمة المصفوفة.
package com.journaldev.threads;
import java.util.Arrays;
public class SyncronizedMethod {
public static void main(String[] args) throws InterruptedException {
String[] arr = {"1","2","3","4","5","6"};
HashMapProcessor hmp = new HashMapProcessor(arr);
Thread t1=new Thread(hmp, "t1");
Thread t2=new Thread(hmp, "t2");
Thread t3=new Thread(hmp, "t3");
long start = System.currentTimeMillis();
//بدء جميع الخيوط
t1.start();t2.start();t3.start();
//انتظر حتى تنتهي الخيوط
t1.join();t2.join();t3.join();
System.out.println("Time taken= "+(System.currentTimeMillis()-start));
//تحقق من قيمة المتغير المشترك الآن
System.out.println(Arrays.asList(hmp.getMap()));
}
}
class HashMapProcessor implements Runnable{
private String[] strArr = null;
public HashMapProcessor(String[] m){
this.strArr=m;
}
public String[] getMap() {
return strArr;
}
@Override
public void run() {
processArr(Thread.currentThread().getName());
}
private void processArr(String name) {
for(int i=0; i < strArr.length; i++){
//معالجة البيانات وإلحاق اسم الخيط
processSomething(i);
addThreadName(i, name);
}
}
private void addThreadName(int i, String name) {
strArr[i] = strArr[i] +":"+name;
}
private void processSomething(int index) {
//معالجة بعض العمل
try {
Thread.sleep(index*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
فيما يلي الإخراج عند تشغيل البرنامج أعلاه.
Time taken= 15005
[1:t2:t3, 2:t1, 3:t3, 4:t1:t3, 5:t2:t1, 6:t3]
قيم مصفوفة السلاسل تالفة بسبب البيانات المشتركة وعدم التزامن. فيما يلي كيف يمكننا تغيير طريقة addThreadName() لجعل برنامجنا آمنًا للخيوط.
private Object lock = new Object();
private void addThreadName(int i, String name) {
synchronized(lock){
strArr[i] = strArr[i] +":"+name;
}
}
بعد هذا التغيير، يعمل برنامجنا بشكل جيد وفيما يلي الإخراج الصحيح للبرنامج.
Time taken= 15004
[1:t1:t2:t3, 2:t2:t1:t3, 3:t2:t3:t1, 4:t3:t2:t1, 5:t2:t1:t3, 6:t2:t1:t3]
ذلك كل شيء بالنسبة لسلامة الموضوع في جافا، آمل أن تعلمت عن البرمجة الآمنة للموضوع واستخدام الكلمة المتزامنة.
Source:
https://www.digitalocean.com/community/tutorials/thread-safety-in-java