Синхронизация потоков. Оператор synchronized


При работе потоки нередко обращаются к каким-то общим ресурсам, которые определены вне потока, например, обращение к какому-то файлу. Если одновременно несколько потоков обратятся к общему ресурсу, то результаты выполнения программы могут быть неожиданными и даже непредсказуемыми. Например, определим следующий код:

 

public class ThreadsApp {

 

public static void main(String[] args) {

 

CommonResource commonResource= new CommonResource();

for (int i = 1; i < 6; i++){

 

Thread t = new Thread(new CountThread(commonResource));

t.setName("Поток "+ i);

t.start();

}

}

}

 

class CommonResource{

 

int x=0;

}

 

class CountThread implements Runnable{

 

CommonResource res;

CountThread(CommonResource res){

this.res=res;

}

public void run(){

res.x=1;

for (int i = 1; i < 5; i++){

System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x);

res.x++;

try{

Thread.sleep(100);

}

catch(InterruptedException e){}

}

}

}

 

Здесь определен класс CommonResource, который представляет общий ресурс и в котором определено одно целочисленное поле x.

Этот ресурс используется классом потока CountThread. Этот класс просто увеличивает в цикле значение x на единицу. Причем при входе в поток значение x=1:

res.x=1;

 

То есть в итоге мы ожидаем, что после выполнения цикла res.x будет равно 4.

В главном классе программы запускается пять потоков. То есть мы ожидаем, что каждый поток будет увеличивать res.x с 1 до 4 и так пять раз. Но если мы посмотрим на результат работы программы, то он будет иным:

Поток 1 1

Поток 2 1

Поток 3 1

Поток 5 1

Поток 4 1

Поток 5 6

Поток 2 6

Поток 1 6

Поток 3 6

Поток 4 6

Поток 4 11

Поток 2 11

Поток 5 11

Поток 3 11

Поток 1 11

Поток 4 16

Поток 1 16

Поток 3 16

Поток 5 16

Поток 2 16

То есть пока один поток не окончил работу с полем res.x, с ним начинает работать другой поток.

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

 

class CountThread implements Runnable{

 

CommonResource res;

CountThread(CommonResource res){

this.res=res;

}

public void run(){

synchronized(res){

res.x=1;

for (int i = 1; i < 5; i++){

System.out.printf("%s %d \n", Thread.currentThread().getName(), res.x);

res.x++;

try{

Thread.sleep(100);

}

catch(InterruptedException e){}

}

}

}

}

 

При создании синхронизированного блока кода после оператора synchronized идет объект-заглушка: synchronized(res). Причем в качестве объекта может использоваться только объект какого-нибудь класса, но не примитивного типа.

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

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

При применении оператора synchronized к методу пока этот метод не завершит выполнение, монопольный доступ имеет только один поток - первый, который начал его выполнение. Для применения synchronized к методу, изменим классы программы:

 

public class ThreadsApp {

 

public static void main(String[] args) {

 

CommonResource commonResource= new CommonResource();

for (int i = 1; i < 6; i++){

 

Thread t = new Thread(new CountThread(commonResource));

t.setName("Поток "+ i);

t.start();

}

}

}

 

class CommonResource{

 

int x;

synchronized void increment(){

x=1;

for (int i = 1; i < 5; i++){

System.out.printf("%s %d \n", Thread.currentThread().getName(), x);

x++;

try{

Thread.sleep(100);

}

catch(InterruptedException e){}

}

}

}

 

class CountThread implements Runnable{

 

CommonResource res;

CountThread(CommonResource res){

this.res=res;

}

 

public void run(){

res.increment();

}

}

 

Результат работы в данном случае будет аналогичен примеру выше с блоком synchronized. Здесь опять в дело вступает монитор объекта CommonResource - общего объекта для всех потоков. Поэтому синхронизированным объявляется не метод run() в классе CountThread, а метод increment класса CommonResource. Когда первый поток начинает выполнение метода increment, он захватывает монитор объекта CommonResource. А все потоки также продолжают ожидать его освобождения.

 

Взаимодействие потоков. Задача "Producer-Consumer"

 

Нередко работа одного потока зависит от другого. Например, есть классическая задача "Производители и потребители" ("Producer-Consumer"), в которой производители производят товар и доставляют его в магазин, а потребители покупают этот товар. При этом потребители могут купить товар только тогда, когда производители произвели этот товар. То есть в данном случае есть потоки производителей и потоки потребителей, и они должны как-то взаимодействовать. Есть еще ряд подобных задач, например, "Писатели и читатели" - одновременно в библиотеке может быть либо писатели, которые пишут книгу, либо читатели, которые читают книгу.

Рассмотрим одну из модификаций задачи "Producer-Consumer", суть которой будет заключаться в следующем: есть склад, на который производители добавляют товары, а потребители берут товары. Потребитель может взять товар только при его наличии на складе, то есть когда производитель его добавил. Здесь как раз могут пригодиться синхронизированные методы:

 

public class ProducerConsumerApp {

 

public static void main(String[] args) {

 

Store store = new Store();

new Producer(store).start();

new Consumer(store).start();

}

}

// создаем объект склада, с которого будут брать товары покупатели

// и куда будут вносить товары производители

class Store {

int counter = 0; // счетчик товаров

final int N = 5; // максимально допустимое число

 

// синхронизированный метод для производителей

synchronized int put() {

if(counter<=N) //если товаров меньше

{

counter++; // кладем товар

System.out.println ("склад имеет " + counter + " товар(ов)");

return 1; // в случае удачного выполнения возвращаем 1

}

return 0;// в случае неудачного выполнения возвращаем 0

}

// метод для покупателей

synchronized int get() {

if(counter>0) //если хоть один товар присутствует

{

counter--; //берем товар

System.out.println ("склад имеет " + counter + " товар(ов)");

return 1;// в случае удачного выполнения возвращаем 1

}

return 0;// в случае неудачного выполнения возвращаем 0

}

}

// поток производителя

class Producer extends Thread {

Store store; //объект склада, куда кладем товар

int product=5; // количество товаров, которые надо добавить

 

Producer(Store store)

{

this.store=store;

}

 

public void run() {

try

{

while(product>0){ //пока у производителя имеются товары

product=product-store.put(); //кладем один товар на склад

System.out.println ("производителю осталось произвести " + product + " товар(ов)");

sleep(100); // время простоя

}

}

catch(InterruptedException e)

{

System.out.println ("поток производителя прерван");

}

}

}

// поток покупателя

class Consumer extends Thread {

Store store; //объект склада, с которого покупатель будет брать товар

int product=0; //текущее количество товаров со склада

 

Consumer(Store store)

{

this.store=store;

}

 

public void run() {

try

{

while(product<5){// пока количество товаров не будет равно 5

 

product=product+store.get(); //берем по одному товару со склада

System.out.println ("Потребитель купил " + product + " товар(ов)");

sleep(100);

}

}

catch(InterruptedException e)

{

System.out.println ("поток потребителя прерван");

}

}

}

 

 

Производитель и покупатель по очереди захватывают монитор объекта Store, выполняя его метод put() или get()

Результат работы программы:

склад имеет 1 товар(ов)

производителю осталось произвести 4 товар(ов)

склад имеет 0 товар(ов)

Потребитель купил 1 товар(ов)

склад имеет 1 товар(ов)

производителю осталось произвести 3 товар(ов)

склад имеет 0 товар(ов)

Потребитель купил 2 товар(ов)

склад имеет 1 товар(ов)

производителю осталось произвести 2 товар(ов)

склад имеет 0 товар(ов)

Потребитель купил 3 товар(ов)

склад имеет 1 товар(ов)

производителю осталось произвести 1 товар(ов)

склад имеет 0 товар(ов)

Потребитель купил 4 товар(ов)

склад имеет 1 товар(ов)

производителю осталось произвести 0 товар(ов)

склад имеет 0 товар(ов)

Потребитель купил 5 товар(ов)

 



Дата добавления: 2017-01-26; просмотров: 1297;


Поиск по сайту:

Воспользовавшись поиском можно найти нужную информацию на сайте.

Поделитесь с друзьями:

Считаете данную информацию полезной, тогда расскажите друзьям в соц. сетях.
Poznayka.org - Познайка.Орг - 2016-2024 год. Материал предоставляется для ознакомительных и учебных целей.
Генерация страницы за: 0.025 сек.