Проблема Runnable

Ти вже знаєш про інтерфейс Runnable та клас Thread. який його реалізує. Давай згадаємо, як виглядає цей інтерфейс:


public interface Runnable {
	public abstract void run();
}

Звернімо увагу на те, що в методу run значення, що повертається, — void. А що як нам потрібно отримати якийсь результат роботи потоку у вигляді числа, рядка чи будь-якого іншого об'єкта? Тоді доведеться щось вигадувати. Наприклад, таке:


public class Fibonacci implements Runnable {
 
 
 
	private final int index;
 
	private int result;
 
 
 
	public Fibonacci(int index) {
 
    		this.index = index;
 
	}
 
 
 
	@Override
 
	public void run() {
 
    		int first = 0;
 
    		int second = 1;
 
    		if (index == 1) {
 
        			result = first;
 
    		} else if (index == 2) {
 
        			result = second;
 
    		} else {
 
        			for (int i = 0; i < index - 2; i++) {
 
            				int temp = second;
 
            				second += first;
 
            				first = temp;
 
        			}
 
 
 
            			result = second;
 
    		}
 
	}
 
 
 
	public static void printByIndex(int index) throws InterruptedException {
 
    		Fibonacci fibonacci = new Fibonacci(index);
 
    		Thread thread = new Thread(fibonacci);
 
    		thread.start();
 
    		thread.join();
 
    		System.out.println(index + "-е число Фібоначчі: " + fibonacci.result);
 
	}
 
}

Запустимо такий метод main:


	public static void main(String[] args) throws Exception {
    		Fibonacci.printByIndex(10);
	}

У консолі виведеться:

10-е число Фібоначчі: 34

У цього коду є низка недоліків. Наприклад, головний потік буде заблоковано на час виклику методу printByIndex, оскільки він містить виклик методу join.

Інтерфейс Callable

Тепер давай розглянемо інтерфейс, який Java надає нам “з коробки” і який може виступати альтернативою Runnable. Це інтерфейс Callable:


public interface Callable<V> {
 
	V call() throws Exception;
 
}

Як бачиш, у нього так само, як і в Runnable, лише один метод. Призначення в цього методу таке ж, як і в методу run, — він буде містити код, який буде виконуватися в паралельному потоці. Із відмінностей можеш побачити значення, що повертається. Тепер це може бути будь-який тип, який ти вкажеш при реалізації інтерфейсу:


public class CurrentDate implements Callable<Long> {
 
	@Override
 
	public Long call() {
 
    		return new Date().getTime();
 
	}
 
}

Ще приклад:


Callable<String> task = () -> {
 
	Thread.sleep(100);
 
	return "Done";
 
};

І ще трохи корисного: метод call може прокидати Exception, тому на відміну від методу run можна не стежити за винятками, що перевіряються, які виникають всередині методу:


public class Sleep implements Runnable {

	@Override

	public void run() {

    	    try {

        	        Thread.sleep(1000);

    	    } catch (InterruptedException ignored) {

    	    }

	}

}

public class Sleep implements Callable {

	@Override

	public Object call() throws InterruptedException {

    	    Thread.sleep(1000);

    	    return null;

	}

}

Інтерфейс Future

Ще один інтерфейс, який буде щільно працювати з Callable, — це Future. Future надає результат асинхронних (паралельних) обчислень (те саме значення, яке повертає метод call). Він дозволяє перевірити, чи закінчилися обчислення, почекати на їхнє завершення, отримати їхній результат тощо.

Методи Future

  • boolean isDone() — метод повертає true, якщо це завдання (обчислення) завершено. Завершеними вважаються ті завдання, які завершено штатним способом, винятком, або виконання яких скасовано.

  • V get() — метод в разі потреби блокує потік, який його викликав, і після завершення обчислень повертає їхній результат.

  • V get(long timeout, TimeUnit unit) — як і попередній метод, блокує потік, що його викликав, в очікуванні на результат, але лише на час, який встановлено параметрами методу.

  • boolean cancel(boolean mayInterruptIfRunning) — метод намагається зупинити виконання завдання. Якщо завдання ще не почало виконуватися, воно вже ніколи не запуститься. Якщо завдання було в процесі виконання, то параметр mayInterruptIfRunning визначає, чи буде спроба перервати потік, який виконує завдання. Після виклику методу cancel метод isDone завжди буде повертати true.

  • boolean isCancelled() — метод повертає true, якщо завдання скасовано до того, як воно завершиться штатним способом. Метод завжди буде повертати true, якщо перед цим викликано метод cancel, який повернув true.

Приклад коду на Callable та Future


import java.util.HashMap;
 
import java.util.Map;
 
import java.util.concurrent.*;
 
 
 
public class Fibonacci implements Callable<Integer> {
 
 
 
	private final int index;
 
 
 
	public Fibonacci(int index) {
 
    		this.index = index;
 
	}
 
 
 
	@Override
 
	public Integer call() {
 
    		int first = 0;
 
    		int second = 1;
 
    		if (index == 1) {
 
        			return first;
 
    		} else if (index == 2) {
 
        			return second;
 
    		} else {
 
        		for (int i = 0; i < index - 2; i++) {
 
            			int temp = second;
 
            			second += first;
 
            			first = temp;
 
        		}
 
 
 
        			return second;
 
    		}
 
	}
 
 
 
	public static Future<Integer> calculateAsync(int index) throws Exception {
 
    		Fibonacci fibonacci = new Fibonacci(index);
 
 
 
    		// об'єкт future буде представляти результат обчислень завдання fibonacci
 
    		FutureTask<Integer> future = new FutureTask<>(fibonacci);
 
 
 
    		// оскільки клас FutureTask реалізує й інтерфейс Future, й інтерфейс Runnable,
 
	 	// можна передавати його екземпляри до конструктора Thread
 
    		Thread thread = new Thread(future);
 
    		thread.start();
 
 
 
    		return future;
 
	}
 
}

Запустимо такий метод main:


	public static void main(String[] args) throws Exception {
    		Map<Integer, Future<Integer>> tasks = new HashMap<>();
    		for (int i = 10; i < 20; i++) {
        			tasks.put(i, Fibonacci.calculateAsync(i));
    		}
 
    		for (Map.Entry<Integer, Future<Integer>> entry : tasks.entrySet()) {
        			Future<Integer> task = entry.getValue();
        			int index = entry.getKey();
        			int result;
        			// перевіряємо, чи завершено завдання
        			if (task.isDone()) {
            				// отримуємо результат обчислень
            				result = task.get();
        			} else {
            				try {
                				// очікуємо на результат обчислень ще 100 мілісекунд
                				result = task.get(100, TimeUnit.MILLISECONDS);
            				} catch (TimeoutException e) {
                				// перериваємо виконання завдання
                				task.cancel(true);
                				System.out.println("Обчислення " + index + "-го числа Фібоначчі не вклалося у відведений час.");
                				return;
            				}
        			}
        			System.out.println(index + "-е число Фібоначчі: " + result);
    		}
	}

В консолі буде виведено:

16-е число Фібоначчі: 610
17-те число Фібоначчі: 987
18-те число Фібоначчі: 1597
19-те число Фібоначчі: 2584
10-те число Фібоначчі: 34
11-те число Фібоначчі: 55
12-те число Фібоначчі: 89
13-те число Фібоначчі: 144
14-те число Фібоначчі: 233
15-те число Фібоначчі: 377