Помощничек
Главная | Обратная связь


Археология
Архитектура
Астрономия
Аудит
Биология
Ботаника
Бухгалтерский учёт
Войное дело
Генетика
География
Геология
Дизайн
Искусство
История
Кино
Кулинария
Культура
Литература
Математика
Медицина
Металлургия
Мифология
Музыка
Психология
Религия
Спорт
Строительство
Техника
Транспорт
Туризм
Усадьба
Физика
Фотография
Химия
Экология
Электричество
Электроника
Энергетика

ЛАБОРАТОРНАЯ РАБОТА №8



 

Тема: Потоки в Java.

Цель: Ознакомится с потоками в Java.

Теоретические ведомости

Многопоточное программирование.

Рассмотренные вопросы:

7.1. Использование потоков в Java.

7.2. Реализация потоков, методы класса Thread.

7.3. Приоритеты и группы потоков.

7.4. Синхронизация потоков.

 

Многопоточное программирование.

7.1. Использование потоков в Java.

В отличие от многих других языков программирования, java предлагает встроенную поддержку многопоточного программирования. Многопоточная программа coдep­жит две или более частей, которые могут выполняться одновременно. Каждая часть такой программы называется потоком (thгead), и каждый поток задает отдельный путь выполнения. То есть, многопоточность ­ это специализированная форма многозадачности.

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

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

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

Многозадачные потоки требуют меньше накладных расходов, чем многозадачные процессы. Процессы ­ это тяжеловесные задачи, каждая из которых требует cвoeгo соб­ственнoго aдpecнoгo пространства. Межпроцессные коммуникации дорогостоящи и oг­раничены. Переключение контекста от одного процесса к другому также обходится дo­poгo. С другой стороны, потоки являются облегченными. Они разделяют одно и тоже адресное пространство и совместно используют один и тот же тяжеловесный процесс.

­Коммуникации между потоками являются экономными, а переключения контекста меж­ду потоками характеризуется низкой стоимостью. Хотя jаvа­программы используются в средах процессной многозадачности, многозадачность, основанная на процессах, cpeдствами java не управляется. А вот многопоточная многозадачность средствами java управ­ляется.

Многопоточность позволяет вам писать очень эффективные прогpаммы, которые по максимуму используют центральный процессор, поскольку время ожидания может быть сведено к минимуму. Это особенно важно для интерактивных сетевых сред, в которых работает java, так как в них наличие ожидания и простоев ­ обычное явление. Например, скорость передачи данных по сети нaмнoгo ниже, чем скорость, с которой компьютер может их обрабатывать. Даже ресурсы локальной файловой системы читаются и пишутся намного медленнее, чем темп их обработки в процессоре. И, конечно, ввод пользователя намного медленнее, чем компьютер. В однопоточных средах ваша программа вынуждена ожидать окончания таких задач, прежде чем переходить к следующей, ­ даже если центральный процессор большую часть времени простаивает. Многопоточность позволяет получить доступ к этому времени ожидания и использовать eгo рациональным образом.

Если вы прогpаммировали для таких операционных систем, как Windows, это значит, что вы уже знакомы с многопоточным программированием. Однако тот факт, что java управляет потоками, делает многопоточность особенно удобной, поскольку многие детали подконтрольны вам как программисту.

7.2. Реализация потоков, методы класса Thread.

В наиболее общем смысле вы создаете поток, реализуя объект класса Thread. В java определены два способа, какими это можно сделать.

1. Реализуя интерфейс Runnable.

2. Расширяя класс Thread.

­Реализация Runnable

­Самый простой способ создания потока ­ это объявление класса, реализующего ин­терфейс Runnable. Runnable абстрагирует единицу исполняемого кода. Вы можете конструировать поток из любого объекта, реализующего интерфейс Runnable. Чтобы реализовать Runnable, класс должен объявить единственный метод run ( ) :

public void run ()

Внутри run () вы определяете код, который, собственно, составляет новый поток. Важно понимать, что run () может вызывать другие методы, использовать другие классы, объявлять переменные ­ точно так же, как это делает главный поток. Единственным отличием является то, что run () устанавливает точку входа для другого, параллельного по­тока внутри вашей программы. Этот поток завершится, когда run () вернет управление.

После того как будет объявлен класс, реализующий интерфейс Runnable, вы создади­те объект типа Thread из этого класса. В Thread определено несколько конструкторов. Тот, который должен использоваться в данном случае, выглядит следующим образом:

Thread(Runnable объект­ потока, Striпg имя ­потока)

В этом конструкторе объект потока ­ это экземпляр класса, реализующего интер­фейс Runnable . Он определяет, те начнется выполнение потока. Имя нового потока передается в имя ­ потока.

После того, как новый поток будет создан, он не запускается до тех пор, пока вы не вызовете метод start (), объявленный в классе Thread. По сути, start () выполняет вызов run (). Метод start () показан ниже:

void start ()

Рассмотрим пример, создающий новый поток и запускающий ero выполнение:

// Создание второго потока.

class NewThread implements Runnable (

Thread t;

NewThread() {

// Создать новый, второй поток

t = ­ new Thread (thi s, "Демонстрационный поток") ;

System. out .println ("Дочерний поток создан: " + t) ;

t.start(); // Запустить поток

­)

// Точка входа BToporo потока.

pиblic void run() {

try (

for(int i ­= 5; i > 0; i­­) {

System.out.println ("Дочерний поток: " + i);

Thread.sleep(500);

­}

catch (InterrиptedException е) {

System.out.println ("Дочерний поток прерван");

}

System.out.println ("Дочерний поток завершен");

­}

­}
­class ThreadDemo {

public static void main(String args[]) (

new NewThread(); // создать. новый поток

try (

for(int i ­ = 5; i > 0; i­­) (

System.out.println ("Главный поток: " + i);

Thread.sleep(1000);

­}

} catch (InterruptedException е) {

System.out.println ("Главный поток прерван.");

­System.out.println ("Главный поток завершен.");

}

}

­Внутри конструктора NewThread в следующем операторе создается новый объект Thread:

­t = ­ new Thread(this, "Демонстрационный поток");

­Передача this в первом аргументе означает, что вы хотите, чтобы новый поток вызвал run () метод объекта this. Далее вызывается start (), чем запускается выполнение потока, начиная с метода run () . Это запускает цикл for дочернего потока. После вызова start () конструктор NewThread возвращает управление main () . Коrда главный поток продолжает свою работу, он входит в свой цикл for. После этого оба потока выполняются параллельно, разделяя ресурсы центрального процессора, вплоть до завершения своих циклов. Вывод, генерируемый этой программой, показан ниже (ваш вывод может варьи­роваться, в зависимости от скорости процессора и загрузки).

­дочерний поток: Тhrеаd[Демонстрационный поток,5,mаin]

главный поток: 5

Дочерний поток: 5

Дочерний поток: 4

главный поток: 4

Дочерний поток: 3

дочерний поток: 2

главный поток: 3

Дочерний поток: 1

Дочерний поток завершен.

главный поток: 2

главный поток: 1

главный поток завершен.

­Как уже упоминалось ранее, в многопоточной программе часто главный поток должен завершать выполнение последним. Фактически, для некоторых старых виртуальных машин java (JVM), если главный поток завершается до завершения дочерних потоков, то исполняющая система java может "зависнуть". Предыдущая программа гарантирует, что главный поток завершится последним, поскольку главный поток "спит" 1000 миллисекунд между итерациями цикла, а дочерний поток "спит" только 500 миллисекунд. Это заставляет дочерний поток завершиться раньше главного. Но далее вы узнаете лучший способ ожидания завершения потоков.

Расширение Thread .

­Второй способ создания потока ­ это объявить класс, расширяющий Thread, а за­ тем создать экземпляр этого класса. Расширяющий класс обязан переопределить метод run (), который является точкой входа для нового потока. Он также должен вызвать start () для запуска выполнения нового потока. Ниже приведен пример предыдущей программы, переписанной с использованием расширения Thread.

// Создание второго потока расширением Thread

class NewThread extends Thread {

NewThread { ) {

// Создать новый второй поток

suреr("Демонстрационный поток");

Sуstеm.оut.println("Дочерний поток: " + this);

Start(); // Запустить поток

­// Точка входа второго потока.

public void run() {

try {

for(int i ­= 5; i > 0; i­­) {

Sуstеm.оut.println {"Дочерний поток: " + i);

Thread.sleep{500);

­}

catch (InterruptedException е) {

Sуstеm.оut.println ("Дочерний поток прерван.");

}

Sуstеm.оut.println ("Дочерний поток завершен.");

­class ExtendThread {

public static void main(String args[]) {

new NewThread{); // Создать новый поток

try {

for(int i =­ 5; i > 0; i­­) {

System.out.println ("главный поток: " + i);

Thread.sleep(1000);

­}

} catch (InterruptedException е) {

Sуstеm.оut.println ("главный поток прерван.");

}

Sуstеm.оut.println ("главный поток завершен.");

}

}

­Эта программа генерирует точно такой же вывод, что и предыдущая версия. Как вы можете видеть, дочерний поток создается при конструировании объекта NewThread, который наследуется от Thread.

Обратите внимание на super () внутри NewThread. Он вызывает следующую форму конструктора Thread:

­public Thread(String имя_­потока)

Здесь имя_потока указывает имя потока.

­7.3. Приоритеты и группы потоков.

Приоритеты потоков используются планировщиком потоков для принятия реше­ний о том, когда каждому из потоков будет разрешено работать. Теоретически высоко приоритетные потоки получают больше времени процессора, чем низкоприоритетные.

Практически объем времени процессора, который получает поток, часто зависит от нескольких факторов помимо eгo приоритета. (Например, то, как операционная систе­ма реализует многозадачность, может влиять на относительную доступность времени процессора). Высокоприоритетный поток может также выгpужать низкоприоритетный. Например, когда низкоприоритетный поток работает, а высокоприоритетный собирается продолжить свою прерванную работу (в связи с приостановкой или ожиданием заверше­нии операции ввода-вывода), то последний выгpужает низкоприоритетный поток.

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

Чтобы установить приоритет потока, используйте метод setPriority (), который яв­ляется членом класса Thread. Так выглядит eгo общая форма:

final void setPriority(int уровень)

Здесь уровень специфицирует новый уровень приоритета для вызывающего потока.

Значение уровень должно быть в пределах диапазона от MIN ­ PRIORITY до МАХ ­ PRIORIТY.

В настоящее время эти значения равны соответственно 1 и 10. Чтобы вернуть потоку приоритет по умолчанию, укажите NORМ­PRIORITY, который в настоящее время равен 5.

Эти приоритеты определены как статические финальные (static final) переменные в классе Thread.

Вы можете получить текущее значение приоритета потока, вызвав метод

getPriori ty () класса Thread, как показано ниже:

final int getPriority()

Реализации java могут иметь принципиально разное поведение в том, что касается планирования потоков. Версия для Windows ХР /98jNT /2000 работает более­ менее ожидаемым образом. Однако другие версии могут работать несколько иначе. Большинство несовпадений возникают, когда вы полагаетесь на вытесняющую многозадачность вместо совместного использования времени процессора. Наиболее безопасный способ получить предсказуемое межплатформенное поведение java ­ это использовать потоки, которые принудительно осуществляют управление центральным процессором.

В следующем примере демонстрируются два потока с разными приоритетами, которые выполняются на платформе без вытесняющей многозадачности иначе, чем на платформе с упомянутой многозадачностью. Один поток получает приоритет на два уровня выше нормального, как определено Thread.NORМ­PRIORITY, а другой ­ на два уровня ниже нормального. Потоки стартуют и готовы к выполнению в течение 10 секунд. Каждый поток выполняет цикл, подсчитывающий количество итераций. Через 10 секунд главный поток останавливает оба потока. Затем количество итераций цикла, которое успел выполнить каждый поток, отображается.

// Демонстрация приоритетов потоков.

class clicker implements Runnable {

long click = 0;

Thread t;

private volatile boolean running = true;

­public c1icker(int р) {

t ­ new Thread(this);

t.setPriority(p);

­}

public void run() {

while (running) {

click++ ;

}

}

public void stop ()

running = fa1se;

}

public void start()

t. start () ;

­}

}

c1ass HiLoPri {

public static void main(String args[]) {

Thread.currentThread() . setpriority (Thread.МAX PRIORITY);

c1icker hi ­ new c1icker(Thread.NORМ­PRIORITY + 2);

c1icker lo = new c1icker(Thread.NORМ­PRIORITY ­ 2);

lo. start () ;

hi. start () ;

try {

Thread.s1eep(10000);

catch (InterruptedException е) {

Sуstеm.оut.println("rлавный поток прерван.");

­}

lo.stop();

hi.stop () ;

// Ожидание 10 секунд до прерывания.

try {

hi.t.join () ;

lo.t.join ();

} catch (InterruptedException е) {

Sуstеm.оut.println ("Перехвачено исключение InterruptedException");

}

Sуstеm.оut.println ("Низкоприоритетный поток: " + lo.c1ick);

Sуstеm.оut.println ("Высокоприоритетный поток: " + hi. c1ick) ;

}

}

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

­Низкоприоритетный поток: ­4408112

Высокоприоритетный поток: 589626904

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

Еще одно замечание относительно предыдущей программы. Обратите внимание, что переменной running предшествует слово volatile. volatile используется здесь, чтобы гарантировать, что значение running будет проверяться на каждом шаге итераций цикла:

while(running)

click++;

Без указания volatile Java имеет возможность оптимизировать цикл таким образом, что будет создана локальная копия running. Применение vo1ati1e предотвращает эту оптимизацию, сообщая jаvа, что running может изменяться неявным для кода образом.

7.4. Синхронизация потоков.

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

Ключом к синхронизации является концепция монитора (также называемого семафором). Монитор - это объект, который используется, как взаимное исключение (mutually exclusive lock ­ mutex), или мьютекс. Только один поток одновременно может владеть монитором. Когда поток запрашивает блокировку, говорят, что он входит в монитор. Все другие потоки, которые пытаются войти в заблокированный монитор, будут приостанов­лены до тех пор, пока первый поток не выйдет из монитора. Обо всех этих прочих по­токах говорят, что они ожидают монитора. Поток, который владеет монитором, может повторно войти в него, если пожелает.

Если вы имели дело с синхронизацией в других языках, таких как С или С++, то знаете, что использовать ее не просто. Это потому, что эти языки сами по себе не поддерживают синхронизацию. Вместо этого, чтобы синхронизировать потоки, ваша программа должна использовать примитивы операционной системы. К счастью, поскольку java рeaлизует синхронизацию через языковые элементы, большая часть сложности, ассоцииро­ванная с синхронизацией, исчезает.

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

­Использование синхронизированных методов.

Синхронизация в java проста, поскольку объекты имеют собственные, ассоциирован­ные с ними неявные мониторы. Чтобы войти в монитор объекта, следует просто вызвать метод, модифицированный ключевым словом synchronized. Когда поток находится внутри синхронизированного метода, все другие потоки, которые пытаются вызвать eгo (или любые другие синхронизированные методы) на том же экземпляре, должны ожидать.

Чтобы выйти из монитора и передать управление объектом другому ожидающему потоку, владелец монитора просто возвращает управление из синхронизированного метода. Чтобы понять необходимость синхронизации, давайте начнем с простого примера, который не использует ее, хотя и должен. Следующая прогpамма содержит три простых класса. Первый из них, Callme, имеет единственный метод ­ ca11 (). Этот метод при­нимает параметр типа String по имени msg. Этот метод пытается напечатать строку msg внутри квадратных скобок. Интересно отметить, что после того, как ca11() печатает открывающую скобку и строку msg, он вызывает Thread. sleep (1000), который приоста­навливает текущий поток на одну секунду.

­Конструктор следующего класса, Ca11er, принимает ссылку на экземпляр класса Callme и String, которые сохраняются соответственно в target и msg. Конструктор также создает новый поток, который вызовет метод run () объекта. Поток стартует немедленно. Метод run () класса Ca11er вызывает метод ca1l () на экземпляре target класса Callme, передавая ему строку msg. Наконец, класс Synch начинает с создания единст­венного экземпляра Ca11me и трех экземпляров Ca1ler, каждый с уникальной строкой сообщения. Один экземпляр Ca1lme передается каждому Ca11er.

­// Эта проrрамма не синхронизирована.

class Callme (

void call(String msg) (

System.out.println("[" + msg);

try (

Thread.sleep(1000);

catch(InterruptedException е) (

System.out.println {"прервано");

­}

System.out.println {"]");

}

}

class Caller implements Runnable (

String msg;

Callme target;

Thread t;

public Caller(Callme targ, String s) (

target ­ targ;

msg ­ s;

t ­= new Thread(this);

t.start();

}

public void run() (

target.call(msg);

­class Synch (

pиblic static void main(String args[])

Callme target ­ new Callme();

Caller оb1= ­ new Caller(target, "Добро пожаловать");

Caller оb2= ­ new Caller (target, "в синхронизированный") ;

Caller оb3= ­ new Caller (target, "мир!");

// wait for threads to end

try (

оb1. t . join () ;

оb2 . t . join () ;

оb3 . t . join () ;

} catch(InterruptedException е) (

System.out.println ("Прервано");

}

}

}

­Вот вывод, сгенерированный этой программой:

­Добро пожаловать [в синхронизированный[мир!]

]

]

­Как видите, вызывая sleep (), метод cal1 () позволяет переключиться на выполне­ние другого потока. Это приводит к смешанному выводу трех строк сообщений. В этой программе нет ничего, что предотвращает вызов потоками одного и того же метода на одном и том же объекте в одно и то же время. Это называется состоянием гонок, посколь­ку три потока соревнуются друг с другом в окончании выполнения метода. Этот пример использует sleep(), чтобы сделать эффект повторяемым и наглядным. В большинстве ситуаций этот эффект более неуловим и менее предсказуем, поскольку вы не можете предвидеть, когда произойдет переключение контекста. Это может привести к тому, что программа один раз отработает правильно, а другой раз ­ нет. Чтобы исправить эту программу, вы должны сериализировать доступ к cal1 (). То есть вы должны разрешить доступ к этому методу одновременно только одному потоку.

Чтобы сделать это, вам нужно просто предварить объявление cal1 () ключевым словом synchronized, как показано ниже:

class Callme {

synchronized void call(String msg) {

­Это предотвратит доступ другим потокам к ca11 (), когда один из них уже использует его. После того как слово synchronized добавлено к ca11 (), результат работы программы будет выглядеть следующим образом:

[Добро пожаловать]

[в синхронизированный]

[мир! ]

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

­Оператор synchronized

­Хотя создание synchronized методов в ваших классах ­ простой и эффективный способ достижения синхронизации, все же он работает не во всех случаях. Чтобы понять, почему, рассмотрим следующее. Предположим, что вы хотите синхронизировать доступ к объектам классов, которые не были предназначены для многопоточного доступа. То есть класс не использует методов synchronized. Более того, класс был написан не вами, а независимым разработчиком, и у вас нет доступа к его исходному коду. Значит, вы не можете добавить слово synchronized к объявлению соответствующих методов класса. Как может быть синхронизирован доступ к объектам такого класса? К счастью, существует довольно простое решение этой проблемы: вы просто заключаете вызовы методов этого класса в блок synchronized.

Вот общая форма оператора synchronized:

synchronized (объект) {

// операторы, подлежащие синхронизации

}

­Здесь объект ­- это ссылка на синхронизируемый объект. Блок synchronized гарантирует, что вызов метода ­члена объекта произойдет только тогда, когда текущий поток успешно войдет в монитор объекта.

­

 




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

©2015-2020 studopedya.ru Все права принадлежат авторам размещенных материалов.