Синхронизированное ключевое слово в Java

Введение Это вторая статья из серии статей о параллелизме в Java. В предыдущей статье [/ concurrency-in-java-the-executor-framework /] мы узнали о пуле исполнителей и различных категориях исполнителей в Java. В этой статье мы узнаем, что такое ключевое слово synchronized и как его можно использовать в многопоточной среде. Что такое синхронизация? В многопоточной среде возможно, что несколько потоков могут попытаться получить доступ к одному и тому же ресурсу.

Вступление

Это вторая статья в серии статей о параллелизме в Java. В предыдущей статье мы узнали о Executor и различных категориях Executors в Java.

В этой статье мы узнаем, что такое synchronized и как его можно использовать в многопоточной среде.

Что такое синхронизация?

В многопоточной среде возможно, что несколько потоков могут попытаться получить доступ к одному и тому же ресурсу. Например, два потока пытаются записать в один и тот же текстовый файл. В отсутствие какой-либо синхронизации между ними возможно, что данные, записанные в файл, будут повреждены, когда два или более потоков имеют доступ на запись к одному и тому же файлу.

Кроме того, в JVM каждый поток хранит локальную копию переменных в своем стеке. Фактическое значение этих переменных может быть изменено другим потоком. Но это значение не может быть обновлено в локальной копии другого потока. Это может привести к неправильному выполнению программ и недетерминированному поведению.

Чтобы избежать таких проблем, Java предоставляет нам synchronized , которое действует как блокировка для определенного ресурса. Это помогает достичь связи между потоками, так что только один поток получает доступ к синхронизированному ресурсу, а другие потоки ждут, пока ресурс освободится.

synchronized может использоваться несколькими способами, например, для синхронизированного блока :

 synchronized (someObject) { 
 // Thread-safe code here 
 } 

Его также можно использовать с таким методом:

 public synchronized void somemMethod() { 
 // Thread-safe code here 
 } 

Как работает синхронизация в JVM

Когда поток пытается войти в синхронизированный блок или метод, он должен получить блокировку синхронизируемого объекта. Один и только один поток может получить эту блокировку за раз и выполнить код в этом блоке.

Если другой поток пытается получить доступ к синхронизированному блоку до того, как текущий поток завершит выполнение блока, он должен подождать. Когда текущий поток выходит из блока, блокировка автоматически снимается, и любой ожидающий поток может получить эту блокировку и войти в синхронизированный блок:

  • Для synchronized блока блокировка приобретается на объекте, указанном в скобках после ключевого слова synchronized
  • Для synchronized static метода блокировка приобретается на объекте .class
  • Для synchronized экземпляра блокировка приобретается на текущем экземпляре этого класса, то есть на this экземпляре.

Синхронизированные методы

Определить synchronized методы так же просто, как просто включить ключевое слово перед типом возвращаемого значения. Давайте определим метод, который последовательно выводит числа от 1 до 5.

Два потока попытаются получить доступ к этому методу, поэтому давайте сначала посмотрим, как это закончится без их синхронизации, а затем мы заблокируем общий объект и посмотрим, что произойдет:

 public class NonSynchronizedMethod { 
 
 public void printNumbers() { 
 System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); 
 
 for (int i = 0; i < 5; i++) { 
 System.out.println(Thread.currentThread().getName() + " " + i); 
 } 
 
 System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); 
 } 
 } 

Теперь давайте реализуем два настраиваемых потока, которые обращаются к этому объекту и хотят запустить метод printNumbers() :

 class ThreadOne extends Thread { 
 
 NonSynchronizedMethod nonSynchronizedMethod; 
 
 public ThreadOne(NonSynchronizedMethod nonSynchronizedMethod) { 
 this.nonSynchronizedMethod = nonSynchronizedMethod; 
 } 
 
 @Override 
 public void run() { 
 nonSynchronizedMethod.printNumbers(); 
 } 
 } 
 
 class ThreadTwo extends Thread { 
 
 NonSynchronizedMethod nonSynchronizedMethod; 
 
 public ThreadTwo(NonSynchronizedMethod nonSynchronizedMethod) { 
 this.nonSynchronizedMethod = nonSynchronizedMethod; 
 } 
 
 @Override 
 public void run() { 
 nonSynchronizedMethod.printNumbers(); 
 } 
 } 

Эти потоки совместно используют общий объект NonSynchronizedMethod и они одновременно будут пытаться вызвать несинхронизированный метод printNumbers() для этого объекта.

Чтобы проверить это поведение, напишем основной класс:

 public class TestSynchronization { 
 public static void main(String[] args) { 
 
 NonSynchronizedMethod nonSynchronizedMethod = new NonSynchronizedMethod(); 
 
 ThreadOne threadOne = new ThreadOne(nonSynchronizedMethod); 
 threadOne.setName("ThreadOne"); 
 
 ThreadTwo threadTwo = new ThreadTwo(nonSynchronizedMethod); 
 threadTwo.setName("ThreadTwo"); 
 
 threadOne.start(); 
 threadTwo.start(); 
 
 } 
 } 

Выполнение кода даст нам что-то вроде:

 Starting to print Numbers for ThreadOne 
 Starting to print Numbers for ThreadTwo 
 ThreadTwo 0 
 ThreadTwo 1 
 ThreadTwo 2 
 ThreadTwo 3 
 ThreadTwo 4 
 Completed printing Numbers for ThreadTwo 
 ThreadOne 0 
 ThreadOne 1 
 ThreadOne 2 
 ThreadOne 3 
 ThreadOne 4 
 Completed printing Numbers for ThreadOne 

ThreadOne запустился первым, хотя ThreadTwo завершился первым.

И запуск его снова встречает нас еще одним нежелательным результатом:

 Starting to print Numbers for ThreadOne 
 Starting to print Numbers for ThreadTwo 
 ThreadOne 0 
 ThreadTwo 0 
 ThreadOne 1 
 ThreadTwo 1 
 ThreadOne 2 
 ThreadTwo 2 
 ThreadOne 3 
 ThreadOne 4 
 ThreadTwo 3 
 Completed printing Numbers for ThreadOne 
 ThreadTwo 4 
 Completed printing Numbers for ThreadTwo 

Эти результаты полностью случайны и совершенно непредсказуемы. Каждый прогон даст разные результаты. Примите во внимание тот факт, что потоков может быть намного больше , и у нас могут возникнуть проблемы. В реальных сценариях это особенно важно учитывать при доступе к какому-либо общему ресурсу, например файлу или другому типу ввода-вывода, в отличие от простой печати на консоли.

Теперь давайте адекватно synchronize наш метод:

 public synchronized void printNumbers() { 
 System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); 
 
 for (int i = 0; i < 5; i++) { 
 System.out.println(Thread.currentThread().getName() + " " + i); 
 } 
 
 System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); 
 } 

Абсолютно ничего не изменилось, кроме ключевого слова synchronized Теперь, когда мы запускаем код:

 Starting to print Numbers for ThreadOne 
 ThreadOne 0 
 ThreadOne 1 
 ThreadOne 2 
 ThreadOne 3 
 ThreadOne 4 
 Completed printing Numbers for ThreadOne 
 Starting to print Numbers for ThreadTwo 
 ThreadTwo 0 
 ThreadTwo 1 
 ThreadTwo 2 
 ThreadTwo 3 
 ThreadTwo 4 
 Completed printing Numbers for ThreadTwo 

Это выглядит примерно правильно.

Здесь мы видим, что даже несмотря на то, что два потока выполняются одновременно, только один из потоков входит в синхронизированный метод за раз, которым в данном случае является ThreadOne .

После завершения выполнения ThreadTwo может начать выполнение метода printNumbers() .

Синхронизированные блоки

Основная цель многопоточности - выполнять как можно больше задач параллельно. Однако синхронизация ограничивает параллелизм для потоков, которые должны выполнять синхронизированный метод или блок.

Это снижает пропускную способность и возможности параллельного выполнения приложения. Этого недостатка нельзя полностью избежать из-за общих ресурсов.

Однако мы можем попытаться уменьшить объем кода, который должен выполняться синхронно, сохраняя как можно меньше кода в области synchronized . Может быть много сценариев, в которых вместо синхронизации всего метода можно просто синхронизировать несколько строк кода в методе.

Мы можем использовать synchronized блок, чтобы заключить только эту часть кода, а не весь метод.

Поскольку внутри синхронизированного блока выполняется меньше кода, блокировка снимается каждым из потоков быстрее. В результате другие потоки тратят меньше времени на ожидание блокировки, и пропускная способность кода значительно возрастает.

Давайте изменим предыдущий пример, чтобы синхронизировать только for печатающий последовательность чисел, поскольку реально это единственная часть кода, которая должна быть синхронизирована в нашем примере:

 public class SynchronizedBlockExample { 
 
 public void printNumbers() { 
 
 System.out.println("Starting to print Numbers for " + Thread.currentThread().getName()); 
 
 synchronized (this) { 
 for (int i = 0; i < 5; i++) { 
 System.out.println(Thread.currentThread().getName() + " " + i); 
 } 
 } 
 
 System.out.println("Completed printing Numbers for " + Thread.currentThread().getName()); 
 } 
 } 

Давайте теперь проверим вывод:

 Starting to print Numbers for ThreadOne 
 Starting to print Numbers for ThreadTwo 
 ThreadOne 0 
 ThreadOne 1 
 ThreadOne 2 
 ThreadOne 3 
 ThreadOne 4 
 Completed printing Numbers for ThreadOne 
 ThreadTwo 0 
 ThreadTwo 1 
 ThreadTwo 2 
 ThreadTwo 3 
 ThreadTwo 4 
 Completed printing Numbers for ThreadTwo 

Хотя может показаться тревожным, что ThreadTwo «начал» печать чисел до того, как ThreadOne завершил свою задачу, это происходит только потому, что мы позволили потоку пройти мимо System.out.println(Starting to print Numbers for ThreadTwo) перед остановкой ThreadTwo с помощью замок.

Это нормально, потому что мы просто хотели синхронизировать последовательность чисел в каждом потоке. Мы можем ясно видеть, что два потока печатают числа в правильной последовательности, просто синхронизируя цикл for

Заключение

В этом примере мы увидели, как мы можем использовать ключевое слово synchronized в Java для достижения синхронизации между несколькими потоками. Мы также узнали, когда можно использовать синхронизированный метод и блоки с примерами.

Как всегда, вы можете найти код, использованный в этом примере, здесь .

comments powered by Disqus