Java
Многопоточность
https://github.com/LeonardoZ/java-concurrency-patterns
Java concurrency API
Базовые классы:
- класс
Thread
- интерфейс
Runnable
- класс
ThreadLocal
- интерфейс
ThreadFactory
Механизмы синхронизации:
- ключевое слово
synchronized
-
интерфейс
Lock
и его реализацииReentrantLock
ReentrantReadWriteLock
StampedLock
- класс
Semaphore
- позволяет ограничивать доступ к общему ресурсу - класс
CountDownLatch
(https://howtodoinjava.com/java/multi-threading/when-to-use-countdownlatch-java-concurrency-example-tutorial/) - позволяет дождаться завершения нескольких операций - класс
CyclicBarrier
- для сихнронизации нескольких потоков в одной точке - класс
Phaser
- для контроля над выполнением таска, состоящего из нескольких фаз. Следующая таска не начинается, пока все потоки не завершили текущую.
Экзекуторы:
- интерфейсы
Executor
иExecutorService
- класс
ThreadPoolExecutor
- тред-пул - класс
ScheduledThreadPoolExecutor
- тред-пул с возможностью отложенного либо периодического выполнения - класс
Executors
- статический класс, упрощающий создание экзекуторов - интерфейс
Callable
- альтернативаRunnable
: таска, возвращающая значение - интерфейс
Future
- интерфейс, позволяющий получить значение, возвращенное изCallable
Конкурентные структуры данных:
ConcurrentLinkedDeque
ConcurrentLinkedQueue
LinkedBlockingDeque
LinkedBlockingQueue
PriorityBlockingQueue
ConcurrentSkipListMap
ConcurrentHashMap
AtomicBoolean
,AtomicInteger
,AtomicLong
,AtomicReference
Паттерны
Сигналирование
Когда использовать: Таска хочет сообщить о событии другой таске.
Проще всего реализовать, используя семафор Semaphore
, мьютекс ReentrantLock
, либо же используя методы wait()
и notify()
класса Object
:
public void task1() {
section1();
commonObject.notify();
}
public void task2() {
commonObject.wait();
section2();
}
Рандеву
Когда использовать: Таска А ждет события от таски Б, а таска Б ждет события от А.
То же самое, что и в сигналировании, только нужно два примитива синхронизации:
public void task1() {
section1_1();
commonObject1.notify();
commonObject2.wait();
section1_2();
}
public void task2() {
section2_1();
commonObject2.notify();
commonObject1.wait();
section2_2();
}
Мьютекс
Когда использовать: когда только один таск одновременно должен иметь доступ к куску кода.
Реализуется, используя synchronized
, ReentrantLock
или Semaphore
.
public void task() {
preCriticalSection();
lockObject.lock() // The critical section begins
criticalSection();
lockObject.unlock(); // The critical section ends
postCriticalSection();
}
Мультиплекс
Когда использовать: когда не больше N тасков должны выполнять кусок кода.
То есть как мьютекс, только не один, а N. Для решения подходит Semaphore
.
public void task() {
preCriticalSection();
semaphoreObject.acquire();
criticalSection();
semaphoreObject.release();
postCriticalSection();
}
Барьер
Когда использовать: когда несколько тасков должны сихнронизироваться в одной точке. Ни один из таском не может продолжать выполнение, пока все таски не достигнут этой строчки.
Используется CyclicBarrier
. Он называется cyclic, потому что может быть переиспользован. Первым аргументом принимает количество таском, а вторым может принимать Runnable
, который будет запущен как только все потоки достигли точки синхронизации, но перед тем, как они пошли дальше.
public void task() {
preSyncPoint();
barrierObject.await();
postSyncPoint();
}
Read-write lock
Когда использовать: когда потоков, которые хотят писать, меньше, чем потоков, которые хотят читать.
Использовать ReentrantReadWriteLock
Thread pool
Использовать классы, реализующие интерфейс ExecutorService
.
Executor Framework
Базовые компоненты:
- интерфейс
Executor
: имеет единственный методexecute
, который принимаетRunnable
-таску. Эта таска будет запущена, а вот когда и как - решит конкретная реализация экзекутора. - интерфейс
ExecutorService
: расширениеExecutor
, наделяющее его дополнительными возможностями. Позволяет прервать выполнеие тасков и получать футуры для получения прогресса/результата выполнения тасков. Содержит методыinvokeAll
,invokeAny
для выполнения всех или любого из переданных тасков. - класс
ThreadPoolExecutor
- реализацияExecutorService
, использующая непосредственно thread pool. Для создания рекомендуется использовать фабричные методыExecutors.newCachedThreadPool()
,Executors.newFixedThreadPool(int)
иExecutors.newSingleThreadExecutor()
. Предоставляет расширяемые методыbeforeExecute
иafterExecute
, посредством которых может быть расширен. - класс
Executors
- предоставляет статические фабричные методы для создания реализацийExecutor
,ExecutorService
,ScheduledExecutorService
,ThreadFactory
, иCallable
.
Запрос сетевого ресурса
URL url = new URL("https://adventofcode.com/2018/day/1/input");
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
InputStream stream;
try {
stream = conn.getInputStream();
}
catch(IOException e)
{
stream = conn.getErrorStream(); // без этого, в случае HTTP-статуса, отличного от 200, мы получим просто исключение и не сможем получить тело ответа
}
InputStreamReader reader = new InputStreamReader(stream);
BufferedReader in = new BufferedReader(reader);
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
Если нужно обратиться к HTTPS, то нужно HttpURLConnection
просто заменить на HttpsURLConnection
.
Если нужно использовать прокси, то делаем так:
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.2.102", 3128)));
Добавить заголовок можно так:
conn.setRequestProperty("cookie", "COOKIE_CONTENTS");
Работа с I/O
В Java NIO основные концепции - каналы и буферы. При чтении данных каналы читают их из источника в буффер. При записи - буфер пишет в канал.
Каналы
Самые важные реализации каналов:
FileChannel
- работает с файламиDatagramChannel
- работает с UDPSocketChannel
- работает с TCPServerSocketChannel
- работает с входящими TCP-соединениями, для каждого входящего соединения создаетсяSocketChannel
.
Пример использования FileChannel
:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();
while(buf.hasRemaining()){
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
aFile.close();
Строка buf.flip()
очень важна - она переводит буфер из режима записи в буфер в режим чтения из буфера.
Буферы
Работа с буфером состоит из 4 шагов:
- Пишем данные в буфер
- Вызываем
buffer.flip()
- Читаем данные из буфера
- Вызываем
buffer.clear()
илиbuffer.compact()
.
Основные реализации буферов:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
Создание буфера
Каждый класс-наследник Buffer
имеет метод allocate()
, который аллоцирует новый буфер:
ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf2 = CharBuffer.allocate(48);
Запись данных в буфер
В буфер можно записать данные 2 способами:
- Через канал
- Вручную
Вот пример записи через канал:
int bytesRead = inChannel.read(buf);
А вот - вручную:
buf.put(127);
У put()
есть много перегрузок, можно писать одно значение, массив значений, и даже значения из другого буфера.
Чтение данных из буфера
Так же как и с записью, есть 2 способа.
Читать из буфера в канал:
int bytesWritten = inChannel.write(buf);
Читать из буфера в переменную:
byte aByte = buf.get();
Очистка буфера
При вызове clear()
внутренний position
выставляется в 0, а limit
- в capacity
. Это означает, что данные не очищаются, но следующая запись будет поверх старых данных.
Чтение файла построчно
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
// process the line.
}
}
List to array
List<Double> exampleList = new ArrayList<Double>();
...
Double[] example = new Double[exampleList.size()];
example = exampleList.toArray(example);
Map array to array
String[] parts = Stream.of(line.split(";")).map(x -> x.trim())
.toArray(String[]::new);