Автор оригинала: Pankaj Kumar.
Безопасность потоков в Java-очень важная тема. Java обеспечивает поддержку многопоточной среды с использованием потоков Java, мы знаем, что несколько потоков, созданных из одного и того же объекта, совместно используют переменные объекта, и это может привести к несоответствию данных , когда потоки используются для чтения и обновления общих данных.
Безопасность Резьбы
Причина несоответствия данных заключается в том, что обновление любого значения поля не является атомарным процессом, для этого требуется три шага: сначала прочитать текущее значение, во-вторых, выполнить необходимые операции для получения обновленного значения и в-третьих, присвоить обновленное значение ссылке на поле.
Давайте проверим это с помощью простой программы, в которой несколько потоков обновляют общие данные.
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(); //wait for threads to finish processing 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) { // processing some job try { Thread.sleep(i*1000); } catch (InterruptedException e) { e.printStackTrace(); } } }
В приведенной выше программе для цикла count увеличивается на 1 четыре раза, и, поскольку у нас есть два потока, его значение должно быть равно 8 после завершения выполнения обоих потоков. Но когда вы запустите вышеуказанную программу несколько раз, вы заметите, что значение счетчика колеблется в пределах 6,7,8. Это происходит потому, что даже если count++ кажется атомной операцией, это НЕ так и приводит к повреждению данных.
Потокобезопасность в Java
Потокобезопасность в java-это процесс, позволяющий сделать нашу программу безопасной для использования в многопоточной среде, существуют различные способы, с помощью которых мы можем сделать нашу программу потокобезопасной.
- Синхронизация-это самый простой и наиболее широко используемый инструмент для обеспечения потокобезопасности в java.
- Использование классов атомарной оболочки из java.util.concurrent.atomic пакета. Например, атомный регистр
- Использование блокировок из java.util.concurrent.замки упаковка.
- Используя потокобезопасные классы коллекции, проверьте этот пост на предмет использования ConcurrentHashMap для обеспечения потокобезопасности.
- Использование ключевого слова volatile с переменными для того, чтобы каждый поток считывал данные из памяти, а не из кэша потоков.
Синхронизация Java
Синхронизация-это инструмент, с помощью которого мы можем добиться потокобезопасности, JVM гарантирует, что синхронизированный код будет выполняться только одним потоком за раз. ключевое слово java synchronized используется для создания синхронизированного кода и внутренне использует блокировки объекта или класса, чтобы убедиться, что синхронизированный код выполняется только одним потоком.
- Синхронизация Java работает над блокировкой и разблокировкой ресурса перед тем, как какой-либо поток войдет в синхронизированный код, он должен получить блокировку объекта, а когда выполнение кода заканчивается, он разблокирует ресурс, который может быть заблокирован другими потоками. Тем временем другие потоки находятся в состоянии ожидания для блокировки синхронизированного ресурса.
- Мы можем использовать ключевое слово synchronized двумя способами: один-синхронизировать полный метод, а другой-создать синхронизированный блок.
- Когда метод синхронизирован, он блокирует Объект , если метод статичен , он блокирует Класс , поэтому всегда рекомендуется использовать синхронизированный блок для блокировки только тех разделов метода, которые нуждаются в синхронизации.
- При создании синхронизированного блока нам необходимо указать ресурс, на котором будет получена блокировка, это может быть XYZ.class или любое поле объекта класса.
синхронизированный(это)
заблокирует объект перед входом в синхронизированный блок.- Вы должны использовать самый низкий уровень блокировки , например, если в классе несколько синхронизированных блоков и один из них блокирует объект, то другие синхронизированные блоки также будут недоступны для выполнения другими потоками. Когда мы блокируем объект, он получает блокировку всех полей Объекта.
- Синхронизация Java обеспечивает целостность данных по стоимости производительности, поэтому ее следует использовать только тогда, когда это абсолютно необходимо.
- Синхронизация Java работает только в одной и той же JVM, поэтому, если вам нужно заблокировать какой-либо ресурс в нескольких средах JVM, это не сработает, и вам, возможно, придется позаботиться о каком-либо глобальном механизме блокировки.
- Синхронизация Java может привести к взаимоблокировкам, проверьте этот пост о взаимоблокировке в java и о том, как их избежать .
- Ключевое слово Java synchronized нельзя использовать для конструкторов и переменных.
- Предпочтительно создать фиктивный частный объект для использования в синхронизированном блоке, чтобы его ссылка не могла быть изменена каким-либо другим кодом. Например, если у вас есть метод настройки для объекта, для которого вы выполняете синхронизацию, его ссылка может быть изменена каким-либо другим кодом, что приведет к параллельному выполнению синхронизированного блока.
- Мы не должны использовать какой-либо объект, который поддерживается в постоянном пуле, например, строка не должна использоваться для синхронизации, потому что, если какой-либо другой код также блокирует ту же строку, он попытается получить блокировку одного и того же ссылочного объекта из пула строк , и даже если оба кода не связаны, они заблокируют друг друга.
Вот изменения кода, которые нам нужно внести в приведенную выше программу, чтобы сделать ее потокобезопасной.
//dummy object variable for synchronization private Object mutex=new Object(); ... //using synchronized block to read, increment and update count value synchronously synchronized (mutex) { count++; }
Давайте рассмотрим некоторые примеры синхронизации и что мы можем из них извлечь.
public class MyObject { // Locks on the object's monitor public synchronized void doSomething() { // ... } } // Hackers code MyObject myObject = new MyObject(); synchronized (myObject) { while (true) { // Indefinitely delay myObject Thread.sleep(Integer.MAX_VALUE); } }
Обратите внимание, что код хакера пытается заблокировать экземпляр MyObject, и как только он получает блокировку, он никогда не освобождает ее, вызывая блокировку метода doSomething() в ожидании блокировки, это приведет к тому, что система зайдет в тупик и вызовет отказ в обслуживании (DoS).
public class MyObject { public Object lock = new Object(); public void doSomething() { synchronized (lock) { // ... } } } //untrusted code MyObject myObject = new MyObject(); //change the lock Object reference myObject.lock = new Object();
Обратите внимание, что объект блокировки является общедоступным, и, изменив его ссылку, мы можем выполнять синхронизированный блок параллельно в нескольких потоках. Аналогичный случай имеет место, если у вас есть частный объект, но есть метод настройки для изменения его ссылки.
public class MyObject { //locks on the class object's monitor public static synchronized void doSomething() { // ... } } // hackers code 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(); //start all the threads t1.start();t2.start();t3.start(); //wait for threads to finish t1.join();t2.join();t3.join(); System.out.println("Time taken= "+(System.currentTimeMillis()-start)); //check the shared variable value now 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++){ //process data and append thread name processSomething(i); addThreadName(i, name); } } private void addThreadName(int i, String name) { strArr[i] = strArr[i] +":"+name; } private void processSomething(int index) { // processing some job 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]
Значения строкового массива повреждены из-за общих данных и отсутствия синхронизации. Вот как мы можем изменить добавить имя потока() метод, чтобы сделать нашу программу потокобезопасной.
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]
Это все для потокобезопасности в java, я надеюсь, вы узнали о потокобезопасном программировании и использовании ключевого слова synchronized.