Безопасность потоков в Java
Безопасность потоков в 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();
}
}
}
В приведенной выше программе for цикл count увеличивается на 1 четыре раза, и, поскольку у нас есть два потока, его значение должно быть равно 8 после завершения выполнения обоих потоков. Но когда вы запустите приведенную выше программу несколько раз, вы заметите, что значение счетчика варьируется между 6,7,8. Это происходит потому, что даже если count++ кажется атомарной операцией, это НЕ вызывает повреждение данных.
Безопасность потоков в Java
Потокобезопасность в java — это процесс, позволяющий сделать нашу программу безопасной для использования в многопоточной среде. Существуют разные способы сделать нашу программу поточно-безопасной.
- Синхронизация — самый простой и наиболее широко используемый инструмент для обеспечения безопасности потоков в Java.
- Использование классов Atomic Wrapper из пакета java.util.concurrent.atomic. Например, AtomicInteger
- Использование блокировок из пакета java.util.concurrent.locks.
- Используя потокобезопасные классы коллекций, проверьте этот пост, чтобы узнать об использовании ConcurrentHashMap для обеспечения безопасности потоков.
- Использование ключевого слова volatile с переменными, чтобы каждый поток считывал данные из памяти, а не из кэша потока.
Java синхронизирована
Синхронизация — это инструмент, с помощью которого мы можем добиться потокобезопасности. JVM гарантирует, что синхронизированный код будет выполняться только одним потоком за раз. Ключевое слово java synchronized используется для создания синхронизированного кода, а внутри оно использует блокировки объекта или класса, чтобы убедиться, что только один поток выполняет синхронизированный код.
- Синхронизация Java работает над блокировкой и разблокировкой ресурса до того, как какой-либо поток войдет в синхронизируемый код, он должен заблокировать объект, а когда выполнение кода завершится, он разблокирует ресурс, который может быть заблокирован другими потоками. Тем временем другие потоки находятся в состоянии ожидания, чтобы заблокировать синхронизированный ресурс.
- Мы можем использовать ключевое слово synchronized двумя способами: один – сделать синхронизированным весь метод, а другой – создать синхронизированный блок.
- Когда метод синхронизирован, он блокирует объект, если метод статичен, он блокирует класс, поэтому всегда рекомендуется использовать синхронизированный блок для блокировки только тех разделов метода, которым требуется синхронизация.
- При создании синхронизированного блока нам необходимо предоставить ресурс, для которого будет получена блокировка, это может быть XYZ.class или любое поле объекта класса.
synchronized(this)
заблокирует объект перед входом в синхронизированный блок.- Следует использовать самый низкий уровень блокировки, например, если в классе есть несколько синхронизированных блоков, и один из них блокирует объект, тогда другие синхронизированные блоки также будут недоступны для выполнение другими потоками. Когда мы блокируем объект, он блокирует все поля объекта.
- Синхронизация Java обеспечивает целостность данных за счет снижения производительности, поэтому ее следует использовать только в случае крайней необходимости.
- Синхронизация Java работает только в одной и той же JVM, поэтому, если вам нужно заблокировать какой-либо ресурс в среде с несколькими JVM, она не будет работать, и вам, возможно, придется позаботиться о каком-то глобальном механизме блокировки.
- Синхронизация Java может привести к взаимоблокировкам. Прочтите этот пост о взаимоблокировках в Java и о том, как их избежать.
- Ключевое слово JavaScript synchronized нельзя использовать для конструкторов и переменных.
- Предпочтительнее создать фиктивный частный объект для использования в синхронизированном блоке, чтобы его ссылка не могла быть изменена каким-либо другим кодом. Например, если у вас есть метод установки для объекта, для которого вы выполняете синхронизацию, его ссылка может быть изменена каким-либо другим кодом, что приведет к параллельному выполнению синхронизируемого блока.
- Мы не должны использовать какие-либо объекты, которые хранятся в постоянном пуле, например, String не следует использовать для синхронизации, потому что, если какой-либо другой код также блокирует ту же строку, он попытается получить блокировку того же ссылочного объекта из Пул строк, и хотя оба кода не связаны между собой, они будут блокировать друг друга.
Вот изменения кода, которые нам нужно сделать в приведенной выше программе, чтобы сделать ее потокобезопасной.
//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
}
}
Обратите внимание, что хакерский код блокирует монитор класса и не освобождает его, это вызовет взаимоблокировку и отказ в обслуживании в системе. Вот еще один пример, когда несколько потоков работают с одним и тем же массивом строк и после обработки добавляют имя потока к значению массива.
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]
Значения массива String повреждены из-за общих данных и отсутствия синхронизации. Вот как мы можем изменить метод 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]
Это все, что касается безопасности потоков в Java. Надеюсь, вы узнали о безопасном программировании потоков и использовании ключевого слова synchronized.