Процесс — экземпляр программы во время выполнения, независимый объект, которому выделены системные ресурсы (например, процессорное время и память). Каждый процесс выполняется в отдельном адресном пространстве: один процесс не может получить доступ к переменным и структурам данных другого. Если процесс хочет получить доступ к чужим ресурсам, необходимо использовать межпроцессное взаимодействие.
Поток использует то же самое пространства стека, что и процесс, а множество потоков совместно используют данные своих состояний. Каждый поток может работать (читать и писать) с одной и той же областью памяти, в отличие от процессов, которые не могут просто так получить доступ к памяти другого процесса. У каждого потока есть собственные регистры и собственный стек, но другие потоки могут их использовать.
В Java есть два пути разрабоки многопоточных приложений:
Реализовать интерфейс Runnable
// Создание потока
Thread t = new Thread(new Runnable() {
public void run() {
System.out.println("Hello world!");
}
});
// Запуск потока
t.start();
Наследовать класс Thread
// Создание потока
Thread t = new Thread() {
public void run() {
System.out.println("Hello world!");
}
};
// Запуск потока
t.start();
Runnable
Thread
Thread thread = Thread.currentThread()
Некоторые методы:
long getId()
String getName()
void setName(String name)
int getPriority()
void setPriority(int priority)
static void sleep(long ms)
void interrupt()
static boolean interrupted()
void join()
static void yield()
thread.interrupt(); //прервать поток thread
try{
Thread.sleep(5000);
}
catch(InterruptedException e){ //нас прервали
return;
}
for(int i = 0; i< inputs[i];i++){
heavyTask(inputs[i]);
if(Thread.interrupted()){ //нас прервали
return;
}
}
public class UnsafeCounter implements Counter{
private int counter = 0;
@Override
public int get(){
return counter;
}
@Override
public void increment(){
counter++;
}
@Override
public void reset(){
counter = 0;
}
}
public class ExperimentRunner{
private int threadsNumber;
private int experimentsNumber;
private int repeatsNumber;
public ExperimentRunner(int threadsNumber, int experimentsNumber, int repeatsNumber){
this.threadsNumber = threadsNumber;
this.experimentsNumber = experimentsNumber;
this.repeatsNumber = repeatsNumber;
}
public void runExperiments(Counter counter){
long totalTime = 0;
System.out.format("Counter '%s':\n", counter.getClass().getName());
for (int i = 0; i < experimentsNumber; i++){
long startTime = System.currentTimeMillis();
runExperiment(counter);
long elapsed = (System.currentTimeMillis() - startTime);
totalTime += elapsed;
System.out.format(" Experiment [%d/%d]:\tvalue = %d time = %d ms\n", i + 1, experimentsNumber,
counter.get(), elapsed);
counter.reset();
}
System.out.format(" Average time:\t%d ms\n", totalTime / experimentsNumber);
}
private void runExperiment(Counter counter){
Runnable experiment = () -> {
for (int i = 0; i < (repeatsNumber / threadsNumber); i++){
counter.increment();
}
};
List<Thread> threads = Stream.generate(() -> new Thread(experiment)).limit(threadsNumber)
.collect(Collectors.toList());
threads.forEach(t -> t.start());
threads.forEach(t -> {
try{
t.join();
}
catch (InterruptedException e){
e.printStackTrace();
}
});
}
}
private void runExperiment(Counter counter){
Runnable experiment = () -> {
for (int i = 0; i < (repeatsNumber / threadsNumber); i++){
counter.increment();
}
};
List<Thread> threads = Stream.generate(() -> new Thread(experiment))
.limit(threadsNumber).collect(Collectors.toList());
threads.forEach(t -> t.start());
threads.forEach(t -> {
try{
t.join();
}
catch (InterruptedException e){
e.printStackTrace();
}
});
}
public static void main(String[] args)
{
Counter counter = new UnsafeCounter();
ExperimentRunner runner = new ExperimentRunner(4, 5, 100_000_000);
runner.runExperiments(counter));
}
Counter 'ru.naumen.counter.impls.UnsafeCounter': Experiment [1/5]: value = 74618414 time = 25 ms Experiment [2/5]: value = 66158308 time = 3 ms Experiment [3/5]: value = 54063137 time = 3 ms Experiment [4/5]: value = 76346929 time = 4 ms Experiment [5/5]: value = 44644001 time = 4 ms Average time: 7 ms
Быстро, но неправильно.
Участок исполняемого кода программы, в котором производится доступ к общему ресурсу (данным или устройству), который не должен быть одновременно использован более чем одним потоком исполнения
Объект, ограничивающий количество потоков, которые могут войти в заданный участок кода. (Э. Дейкстра)
Возможные операции над семафором:
init(n): //Инициализация семафора (задать начальное значение счётчика) счётчик := n enter()://Захват семафора (ждать пока счётчик станет больше 0, после этого уменьшить счётчик на единицу) счётчик := счётчик - 1 leave()://Освобождение семафора (увеличить счётчик на единицу) счётчик := счётчик + 1
это простейшие двоичные семафоры, которые могут находиться в одном из двух состояний — отмеченном или неотмеченном.
Высокоуровневая кострукция, которая состоит из mutex-а и массива ожидающих очереди потоков.
У монитора должен быть механизм остановки потока и сигнализации о доступности продолжения работы.
Это механизм синхронизации, позволяющий обеспечить исключительный доступ к разделяемому ресурсу между несколькими потоками
Мягкая блокировка — каждый поток пытается получить блокировку перед доступом к соответсвующему разделяемому ресурсу.
Обязательная блокировка — попытка несанкционированного доступа к заблокированному ресурсу будет прервана, через создание исключения.
Синхронизация методов на текущем объекте
public class SynchronizedCounter implements Counter{
private int counter = 0;
@Override
public synchronized int get(){
return counter;
}
@Override
public synchronized void increment(){
counter++;
}
@Override
public synchronized void reset(){
counter = 0;
}
}
Синхронизация блока на произвольном объекте
Object lock = new Object();
synchronized(lock){
//do something
}
Синхронизация статического метода
public class TestStaticSync{
public static synchronized void doSomething(){
//do something
}
}
Синхронизация происходит на объекте TestStaticSync.class
public class Singleton{
private static Singleton instance;
public static synchronized Singleton getInstance(){
if (instance == null){
instance = new Singleton();
}
return instance;
}
}
public class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
Singleton localInstance = instance;
if (localInstance == null) {
synchronized (Singleton.class) {
localInstance = instance;
if (localInstance == null) {
instance = localInstance = new Singleton();
}
}
}
return localInstance;
}
}
volatile int i = 0; // означает, то что переменную i
//нужно всегда брать из общей памяти
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
Singleton localInstance = instance;
if (localInstance == null) {
synchronized (Singleton.class) {
localInstance = instance;
if (localInstance == null) {
instance = localInstance = new Singleton();
}
}
}
return localInstance;
}
}
public class JobQueue{
List<Runnable> jobs = new ArrayList<>();
public synchronized void put(Runnable job){
jobs.add(job);
this.notifyAll();
}
public synchronized Runnable getJob(){
while (jobs.size()==0){
this.wait();
}
return jobs.remove(0);
}
}
Lock l = ...;
l.lock();
try
{
//действия над ресурсом, защищенным данной блокировкой
}
finally
{
l.unlock() //гарантия того, что блокировка будет отпущена
}
Широко используются две основные реализации Lock:
public class ReentrantLockCounter implements Counter{
ReentrantLock lock;
private int counter = 0;
public ReentrantLockCounter(boolean fair){
lock = new ReentrantLock(fair);
}
...
@Override
public void increment(){
lock.lock();
try{
counter++;
}
finally{
lock.unlock();
}
}
...
}
void await() throws InterruptedException;
void signal();
void signalAll();
Lock lock = new ReentrantLock();
Condition blockingPoolA = lock.newCondition();
Condition blockingPoolB = lock.newCondition();
Condition blockingPoolC = lock.newCondition();
public class JobQueueWithLock{
List<Runnable> jobs = new ArrayList<>();
Lock lock = new ReentrantLock();
Condition cond = lock.newCondition();
public void put(Runnable job){
lock.lock();
try {
jobs.add(job);
cond.signalAll();
}
finally {lock.unlock();}
}
public Runnable getJob(){
lock.lock();
try{
while (jobs.size() == 0)
cond.await();
return jobs.remove(0);
}
finally{
lock.unlock();
}
}
}
public class RWLockCounter implements Counter{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private int counter = 0;
public RWLockCounter(boolean fair){
lock = new ReentrantReadWriteLock(fair);
}
...
@Override
public void increment(){
lock.writeLock().lock();
counter++;
lock.writeLock().unlock();
}
...
}
public class StampedLockCounter implements Counter
{
StampedLock lock;
private int counter = 0;
public StampedLockCounter()
{
lock = new StampedLock();
}
...
@Override
public void increment()
{
long stamp = lock.writeLock();
counter++;
lock.unlockWrite(stamp);
}
...
}
Атомарные операции выполняются целиком, их выполнение не может быть прервано планировщиком потоков.
Аппаратная поддержка — compare-and-swap
Классы для выполнения атомарных операций находятся в java.util.concurrent.atomic:
public class SimulatedCAS
{
private int value;
public synchronized int getValue() { return value; }
public synchronized int compareAndSwap(int expectedValue, int newValue)
{
int oldValue = value;
if (value == expectedValue)
{
value = newValue;
}
return oldValue;
}
}
public class AtomicCounter implements Counter
{
private AtomicInteger counter = new AtomicInteger(0);
@Override
public int get()
{
return counter.intValue();
}
@Override
public void increment()
{
counter.incrementAndGet();
}
@Override
public void reset()
{
counter.set(0);
}
}
Метод синхронизации | Среднее время выполнения (мс) |
AtomicInteger | 1701 |
StampedLock | 3164 |
ReentrantReadWriteLock | 3544 |
ReentrantLock | 3654 |
synchronized | 6076 |
public class ShoppingCard{
ThreadLocal<ArrayList<Item>> myThreadLocal = ThreadLocal.withInitial(ArrayList::new);
public void add(Item item){
myThreadLocal.get().add(item);
}
public String getReceipt(){
StringBuilder sb = new StringBuilder();
myThreadLocal.get().forEach(item -> sb.append(String.format("%s - %d$\n", item.getName(), item.getPrice())));
return sb.toString();
}
public void remove(Item item){
myThreadLocal.get().add(item);
}
}
ShoppingCard shoppingCard = new ShoppingCard();
Thread customer1 = new Thread(() -> {
shoppingCard.add(new Item("Banana", 1));
shoppingCard.add(new Item("Beer", 5));
System.out.format("Receipt for customer1 :\n%s\n", shoppingCard.getReceipt());
});
Thread customer2 = new Thread(() -> {
shoppingCard.add(new Item("Cola", 2));
shoppingCard.add(new Item("Bread", 1));
System.out.format("Receipt for customer2 :\n%s\n", shoppingCard.getReceipt());
});
customer1.start();
customer2.start();
customer1.join();
customer2.join();
Цель применения: отделить работу, выполняемую внутри потока, от логики создания потоков.
Создание:
Executors.newCachedThreadPool(); //Создаёт новые потоки при необходимости,
//повторно использует освободившиеся потоки
Executors.newFixedThreadPool(12); //С ограничением количества потоков
Executors.newSingleThreadExecutor(); //Ровно один поток
Executors.newScheduledThreadPool(); //Можно настроить задержку запуска / повторный запуск
ExecutorService workers = Executors.newFixedThreadPool(5);
ExecutorService clients = Executors.newFixedThreadPool(10);
for (int i = 3; i < 20; i++){
final int idx = i;
Callable<Long> task = (i % 2 == 0)? (()->calculateFibonacci(idx)):
(()->calculateFactorial(idx));
Future<Long> future = workers.submit(task);
clients.execute(() -> {
try{
Long result = future.get();
System.out.format("%s for %d = %d\n",
idx % 2 == 0 ? "Fibonacci" : "Factorial",
idx, result);
}
catch (InterruptedException | ExecutionException e){
e.printStackTrace();
}
});
}
workers.shutdown();
clients.shutdown();
Пример RecursiveTask
private static class StandardTask extends RecursiveTask<Long>{
private final Problem problem;
private final int l;
private final int r;
public StandardTask(Problem p, int l, int r) {
this.problem = p;
this.l = l;
this.r = r;
}
@Override
protected Long compute() {
if (r - l <= THRESHOLD){
return problem.solve(l, r);
}
int mid = (l + r) >>> 1;
ForkJoinTask<Long> t1 = new StandardTask(problem, l, mid);
ForkJoinTask<Long> t2 = new StandardTask(problem, mid, r);
t1.fork();
t2.fork();
long res = 0;
res += t2.join();
res += t1.join();
return res;
}
}
Пример RecursiveTask
ForkJoinTask<Long> t1 = new StandardTask(problem, l, mid);
ForkJoinTask<Long> t2 = new StandardTask(problem, mid, r);
t1.fork();
t2.fork();
long res = 0;
res += t2.join();
res += t1.join();
return res;
double average = persons
.parallelStream()
.filter(p -> p.getGender() == Person.Sex.MALE)
.mapToInt(Person::getAge)
.average()
.getAsDouble();
CompletableFuture<Integer> answer = CompletableFuture.completedFuture(42);
CompletableFuture<Integer> answer = CompletableFuture.supplyAsync(() -> someOperation());
CompletableFuture<Integer> answer = CompletableFuture.supplyAsync(() -> someOperation(),
executorService);
Integer result = answer.get(); //блокирует
CompletableFuture<Integer> answer = CompletableFuture.supplyAsync(() -> someOperation());
answer.thenAccept(a -> System.out.format("Answer: %d", a));
CompletableFuture<Integer> wordsCount = CompletableFuture.supplyAsync(() -> getText())
.thenApply(text -> text.split(" "))
.thenApply(words -> words.length);
System.out.format("Words count: %d", wordsCount.get());
CompletableFuture<String> text1 = CompletableFuture.supplyAsync(() -> getText(1));
CompletableFuture<String> text2 = CompletableFuture.supplyAsync(() -> getText(1));
CompletableFuture<Integer> both = text1.
thenCombine(text2, (String textOne, String textTwo) ->
textOne.length() + textTwo.length()
);
both.thenAccept(length -> System.out.format("Total length: %d", length));
CompletableFuture<String> text1 = CompletableFuture.supplyAsync(() -> getText(1));
CompletableFuture<String> text2 = CompletableFuture.supplyAsync(() -> getText(2));
CompletableFuture<String> text3 = CompletableFuture.supplyAsync(() -> getText(3));
CompletableFuture<String> text4 = CompletableFuture.supplyAsync(() -> getText(4));
CompletableFuture<Void> allCompleted = CompletableFuture.allOf(text1, text2, text3, text4);
allCompleted.thenRun(() -> {
try {
System.out.format("Loaded: %d", text1.get());
System.out.format("Loaded: %d", text2.get());
System.out.format("Loaded: %d", text3.get());
System.out.format("Loaded: %d", text4.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
});
);
CompletableFuture<String> text1 = CompletableFuture.supplyAsync(() -> getText(1));
CompletableFuture<String> recovered = text1.handle((result, throwable) -> {
if (throwable != null) {
return "Not text here! Exception: " + throwable;
} else {
return result.toUpperCase();
}
});
CompletableFuture<String> text2 = CompletableFuture.supplyAsync(() -> getText(2));
CompletableFuture<String> recovered = text1.exceptionally(throwable -> "Sorry, try again later");