«Единственный способ творить великие дела – это любить то, что ты делаешь»
Веб Ученик


В Java потоки представлены классом Thread. Создание потоков позволяет выполнять несколько задач одновременно, что ускоряет процесс выполнения программы.

Существует два основных способа создания потоков в Java.

Наследование от класса Thread и Реализация интерфейса Runnable.



1 способ. Наследование от класса Thread

Первый способ создания потоков в Java заключается в наследовании от класса Thread.

Что для этого нужно?

Нужно создать новый класс, который наследует класс Thread, и переопределить метод run().

Затем создать экземпляр нового класса и вызвать метод start().

Java-код. Пример 1.1

public class CreateThread1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();		// Экземпляр класса MyThread. Создание нового потока
        myThread.start();						// start() - запускает поток и вызывает метод run() 
        // myThread.start();        			// ! см. Примечание №1
        MyThread myThread2 = new MyThread();
        myThread2.start();
    }
}

class MyThread extends Thread {		// MyThread наследует класс Thread
    @Override
    public void run() {				// Метод run()
        System.out.println("Привет " + Thread.currentThread().getName());		 // Код, который выполняет поток
    }
}																				 			// -- level_11/

Результат в консоли

Привет Thread-0
Привет Thread-1

Примечание №1. Метод start() не может быть вызван дважды для одного и того же объекта, даже если поток завершён. Иначе будет выброшено исключение IllegalThreadStateException.

Метод run() - не запускает поток, а просто содержит код и это просто хранилище для логики, которая может работать параллельно.

Метод start() - запускает поток и вызывает метод run().

Thread.currentThread().getName() возвращает имя текущего выполняющегося потока.

Это происходит благодаря тому, что метод Thread.currentThread() возвращает объект класса Thread, а затем на этом объекте вызывается метод getName().

Метод sleep - порядок выполнения потоков

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

Привет Thread-1
Привет Thread-0

Если добавить еще несколько потоков, то можно убедиться в этом.

С помощью метода sleep - можно упорядочить последовательность работы потоков.

Java-код. Пример 1.2

public class CreateThread1 {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();		// поток №1
        myThread.start();
        MyThread myThread2 = new MyThread();    // поток №2
        myThread2.start();

        new MyThread().start();     // Можно и так - 2 новых потока
        new MyThread().start();
        
// ----------------- Метод sleep() ----------------------------------------------------        

        Thread.sleep(500);          // Поток main СПИТ 500 мс.
        System.out.println("Привет, я метод sleep() потока " + Thread.currentThread().getName());

        for (int i = 0; i < 3; i++) {
            Thread.sleep(1000);		// Текущий поток СПИС 1 с.
            new MyThread().start();
        }
    }
}	 																		

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Привет " + Thread.currentThread().getName());
    }
}																								// -- level_11/

Результат в консоли

Привет Thread-0
Привет Thread-2
Привет Thread-3
Привет Thread-1
Привет, я метод sleep() потока main
Привет Thread-4
Привет Thread-5
Привет Thread-6

Метод Thread.sleep() используется для приостановки выполнения текущего потока на какое-то время.

Он принимает в качестве параметра число миллисекунд — то время, на которое необходимо «усыпить» поток.

Метод sleep() — статический: он усыпляет текущий поток, то есть тот, который работает в данный момент.

Поток в состоянии сна можно прервать. В таком случае в программе возникнет исключение InterruptedException.

2 способ. Реализация интерфейса Runnable

Второй способ создания потоков в Java заключается в реализации интерфейса Runnable.

Что это значит? Что нужно сделать для создания потока?

Нужно создать класс, который реализует интерфейс Runnable, и переопределить метод run().

Затем создать экземпляр Thread и передать в конструктор свой Runnable (экземпляр класса, реализующий этот интерфейс).

Java-код. Интерфейс Runnable

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

Реализация интерфейса Runnable - это альтернативный способ создания потока. Вместо наследования от класса Thread создаётся класс, который реализует интерфейс Runnable, и переопределяется метод run().

Java-код. Пример 2.1

public class CreateThread2 {

    public static void main(String[] args) {
        ClassRunnable classRunnable = new ClassRunnable();
        Thread thread = new Thread(classRunnable);               // 1. Создание нового потока с передачей в конструктор 
        													     // класса Thread объекта classRunnable
        Thread thread1 = new Thread(new ClassRunnable());        // 2. Аналогичная запись: одна строка объединяет 2 предыдущих

        thread.start();                                          // Запуск потоков
        thread1.start();
    }

}

class ClassRunnable implements Runnable {       // ClassRunnable имплементирует интерфейс Runnable
    @Override
    public void run() {
        System.out.println("Привет " + Thread.currentThread().getName());
    }
}																								// -- level_11/

Результат в консоли

Привет Thread-0
Привет Thread-1

Последовательность выполнения потоков

Добавим цикл в метод run(), чтобы увидеть как работают потоки. Каждый поток выполнит действие 100 раз.

Java-код. Пример 2.2

public class CreateThread2 {

    public static void main(String[] args) {
        ClassRunnable classRunnable = new ClassRunnable();
        Thread thread = new Thread(classRunnable);               
                                                                 
        Thread thread1 = new Thread(new ClassRunnable());       
        
        thread.start();                                        
        thread1.start();
    }

}

class ClassRunnable implements Runnable {       
    @Override
    public void run() {						// Добавляем цикл
        for (int i = 0; i < 100; i++) {
            System.out.println("Привет №" + i + ". Я поток " + Thread.currentThread().getName());
        }
    }
}																								// -- level_11/

Результат в консоли

.
.
Привет №19. Я поток Thread-0
Привет №20. Я поток Thread-0
Привет №21. Я поток Thread-0
Привет №0. Я поток Thread-1
Привет №1. Я поток Thread-1
Привет №2. Я поток Thread-1
.
.
Привет №24. Я поток Thread-0
Привет №10. Я поток Thread-1
Привет №25. Я поток Thread-0
Привет №11. Я поток Thread-1
.
.

Из примера видно, что в работе потоков нет последовательности:
первые 2 итерации цикла "Привет №0" и "Привет №1" - это НЕ первый поток Thread-0, а второй Thread-1, да и вывод этих строк в консоль идет НЕ сначала.

Последовательность выполнения потоков в Java контролировать нельзя. В каком порядке запускать новые потоки, решает планировщик потоков — часть JVM, которая определяет, какой поток должен выполниться в каждый конкретный момент времени и какой поток нужно приостановить.

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

Потоки в цикле. Методы sleep() и join()

Запускаем потоки в цикле. Используем методы sleep() и join().

Java-код. Пример 2.3

public class CreateThread2 {

    public static void main(String[] args) throws InterruptedException {
        ClassRunnable classRunnable = new ClassRunnable();
        Thread thread = new Thread(classRunnable);               // Создание нового потока с передачей в конструктор
                                                                 // класса Thread объекта classRunnable
        Thread thread1 = new Thread(new ClassRunnable());        // Аналогичная запись: одна строка объединяет 2 предыдущих

        thread.start();                                          // Запуск потоков Thread-0 и Thread-1
        thread.join();      // Метод join() используется, чтобы сначала отработал поток Thread-0. Thread-1 при этом ЖДЕТ
        thread1.start();

        Thread.sleep(500);            // Метод main спит 0,5 секунды, чтобы сначала отработали 2 потока
        System.out.println("--------- Метод main проснулся. Первые 2 потока отработали -----------------");

        for (int i = 0; i < 5; i++) {          // Еще 5 потоков. Каждый выводит приветствие 2 раза (цикл for в run())
            Thread.sleep(2000);                // Каждый поток спит 2 секунды
            new Thread(new ClassRunnable()).start();
        }
    }
}

class ClassRunnable implements Runnable {       // ClassRunnable имплементирует интерфейс Runnable
    @Override
    public void run() {
        for (int i = 0; i < 2; i++) {
            System.out.println("Привет №" + i + ". Я поток " + Thread.currentThread().getName());
        }
    }
} 																							// -- level_11/

Результат в консоли

Привет №0. Я поток Thread-0
Привет №1. Я поток Thread-0
Привет №0. Я поток Thread-1
Привет №1. Я поток Thread-1
--------- Метод main проснулся. Первые 2 потока отработали -----------------
Привет №0. Я поток Thread-2
Привет №1. Я поток Thread-2
Привет №0. Я поток Thread-3
Привет №1. Я поток Thread-3
Привет №0. Я поток Thread-4
Привет №1. Я поток Thread-4
Привет №0. Я поток Thread-5
Привет №1. Я поток Thread-5
Привет №0. Я поток Thread-6
Привет №1. Я поток Thread-6
Комментарии к примеру.
  • Метод join() позволяет приостановить выполнение текущего потока до тех пор, пока поток, к которому применён данный метод, не закончит свою работу.

    То есть в нашем случае:

    1. Метод join() thread.join() - приостанавливает выполнение потока Thread-1 пока не выполниться поток Thread-0. Также метод join() приостанавливает поток main.

    2. Если бы main не спал Thread.sleep(500), то, чтобы Thread-1 выполнился первым - до main, нужно было бы вызвать метод join() и у него thread1.join().

    3. Но main спит 0,5 секунды, поэтому Thread-1 успевает за это время сработать (метод join() для него не нужен).

  • Метод sleep(2000) в цикле для потоков позволяет соблюсти очередность их выполнения.

Создание объекта Runnable с использованием анонимного класса

Как было рассмотрено выше, второй метод создания потоков в Java подразумевает использование класса, который имплементирует интерфейс Runnable.

Но есть и другой вариант использования интерфейса Runnable для создания потоков: не создавать класс, который имплементирует интерфейс Runnable, а сразу создать объект Runnable, который затем передать в конструктор класса Thread при создании нового потока.

При этом объект Runnable создается с использованием анонимного класса.  

Java-код. Пример 3.0

public class CreateThread3 {

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {        // создание объекта Runnable и переопределение run()
            @Override
            public void run() {
                System.out.println("Привет. Я поток " + Thread.currentThread().getName());
            }
        };

        Thread thread = new Thread(runnable);       // Создание нового потока с передачей в конструктор
                                                    // класса Thread объекта runnable
        Thread thread1 = new Thread(runnable);
        thread.start();                             // Thread-0
        thread.join();								// Thread-1 запускается после выполнения Thread-0				
        thread1.start();                            // Thread-1

        for (int i = 0; i < 10; i++) {              // Thread-2 . . . Thread-11
            Thread.sleep(500);
            new Thread(runnable).start();
        }
    }
}																									// -- level_11/

Результат в консоли

Привет. Я поток Thread-0
Привет. Я поток Thread-1
Привет. Я поток Thread-2
Привет. Я поток Thread-3
Привет. Я поток Thread-4
Привет. Я поток Thread-5
Привет. Я поток Thread-6
Привет. Я поток Thread-7
Привет. Я поток Thread-8
Привет. Я поток Thread-9
Привет. Я поток Thread-10
Привет. Я поток Thread-11

В примере создаётся объект типа Runnable с помощью анонимного класса, который реализует метод run(). Затем создаётся объект Thread, ему передаётся объект runnable в качестве параметра конструктора, и запускается поток. 

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

В конструктор Thread передаем создание объекта Runnable

В примере выше сначала создавался объект Runnable, затем он передавался в конструктор класса Thread при создании нового потока.

Можно сделать так: сразу при создании потока в конструктор класса Thread передать создание объекта Runnable.

Java-код. Пример 3.1

public class CreateThread3_1 {

    public static void main(String[] args) throws InterruptedException {

        Runnable runnable = new Runnable() {        // 1.1 Отдельно СОЗДАЕМ объект Runnable
            @Override
            public void run() {
                System.out.println("Привет. Я поток " + Thread.currentThread().getName());
            }
        };
        // 1.2 Создание нового потока с передачей в конструктор класса Thread объекта runnable. Ниже это сделано в цикле
        Thread thread = new Thread(runnable);
        thread.start();                             // Thread-0
// --------------------------------------------------------------------------------------------------------------
// 2. СРАЗУ при создании потока в КОНСТРУКТОР класса Thread передаем СОЗДАНИЕ объекта Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Привет. Я поток " + Thread.currentThread().getName() + ". И я создан иначе!");
            }
        }).start();                               // И сразу запускаем поток Thread-1
// --------------------------------------------------------------------------------------------------------------

        for (int i = 0; i < 10; i++) {              // Thread-2 . . . Thread-11
            Thread.sleep(500);
            new Thread(runnable).start();
        }
    }
}																							// -- level_11/

Результат в консоли

Привет. Я поток Thread-0
Привет. Я поток Thread-1. И я создан иначе!
Привет. Я поток Thread-2
Привет. Я поток Thread-3
Привет. Я поток Thread-4
Привет. Я поток Thread-5
Привет. Я поток Thread-6
Привет. Я поток Thread-7
Привет. Я поток Thread-8
Привет. Я поток Thread-9
Привет. Я поток Thread-10
Привет. Я поток Thread-11

Определяем поведение потока с помощью лямбда-выражения

Создание объекта Runnable и переопределение метода run() можно записать короче, используя лямбда-выражение.

Лямбда-выражения в Java - это отдельная тема, однако здесь мы увидим пример того, как создать объект Runnable с помощью лямбда-выражения.

Java-код

public class CreateThread3_3 {
    public static void main(String[] args) throws InterruptedException {

        Runnable t = new Runnable() {           // Создаем экземпляр Runnable как обычно
            @Override
            public void run() {                 // Переопределяем метод run() - код, выполняющийся при запуске потока
                System.out.println("Привет. Я поток 't' " + Thread.currentThread().getName());
            }
        };

        // Аналогичный код с использованием лямбда-выражения
        Runnable l = () -> System.out.println("Привет. Я поток 'l' " + Thread.currentThread().getName());

        Thread thread = new Thread(t); // Создаем поток: в конструктор передаем объект t, реализующий интерфейс Runnable
        thread.start();                // Запускаем поток
        thread.join();                 // Этот поток выполняется первым

        // Создаем и запускаем поток в цикле: в конструктор передаем объект l, реализующий интерфейс Runnable
        for (int i = 0; i < 5; i++) {
            new Thread(l).start();
        }
    }
}

Результат в консоли

Привет. Я поток 't' Thread-0
Привет. Я поток 'l' Thread-1
Привет. Я поток 'l' Thread-2
Привет. Я поток 'l' Thread-4
Привет. Я поток 'l' Thread-5
Привет. Я поток 'l' Thread-3
Комментарии к примеру.
  • Сначала мы создаем экземпляр Runnable как обычно Runnable t = new Runnable(), переопределяем метод run(), создаем и запускаем поток.
  • Затем определяем поведение потока с помощью лямбда-выражения Runnable l = () ->.
    При этом так же создаётся экземпляр Runnable. Метод run() явно не указывается, но также переопределяется. Далее создаётся новый поток (в данном случае в цикле) и запускается как обычно.

Для создания объекта Runnable с помощью лямбда-выражения в Java необходимо:
1. Создать ссылку на интерфейс Runnable и написать лямбда-выражение для метода run().
2. Создать объект класса Thread, передав созданную ссылку на выполняемый интерфейс.
3. Вызвать метод start() для запуска потока.

Создание и запуск потока с использованием лямбда-выражения

В примере выше создавался экземпляр класса Runnable и переопределялся метод run() с помощью лямбда-выражения. Затем этот экземпляр передавался в конструктор класса Thread при создании потока.

В примере ниже лямбда-выражение используется для создания и запуск потока. Естественно, одновременно с этим создается экземпляр класса Runnable и переопределялся метод run(). Все умещается в одну строку.

Java-код. Пример 3.3

public class CreateThread3_3 {

    public static void main(String[] args) throws InterruptedException {

        // СРАЗУ при создании потока в КОНСТРУКТОР класса Thread передаем СОЗДАНИЕ объекта Runnable
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hi " + Thread.currentThread().getName());
            }
        }).start(); // Запуск потока

        Thread.sleep(500);

        // Создание и запуск потока с использованием лямбда-выражения
        new Thread(() -> System.out.println("Hi Hi " + Thread.currentThread().getName())).start();

        // Тот же код в цикле
        for (int i = 0; i < 5; i++) {
            new Thread(() -> System.out.println("Hello " + Thread.currentThread().getName())).start();
        }
    }
}

Результат в консоли

Hi Thread-0
Hi Hi Thread-1
Hello Thread-3
Hello Thread-4
Hello Thread-5
Hello Thread-6
Hello Thread-2

P.S.

Добрый день. Я изучаю JAVA. Некоторые темы мне довольно сложно освоить и усвоить. Поэтому я пишу такие вот статья с примерами. Делаю я это в большей степени для себя. Так мне проще понять и запомнить. Если кому-то пригодится – буду рад.

Этот материал используется исключительно в учебных целях и никакой коммерческой выгоды не несет. Текст частично взят из нейросети Яндекса, частично написан мною – материал берется из разных источников. Код писал сам, но за основу опять же брал какие-то уже существующие данные.

Если я где-то ошибся, что-то не правильно/не так описал – это нормально. Во-первых, я учусь, во-вторых, ни на что не претендую. Просьба это понимать.

Потоки в Java
Способы создания потоков 1 способ. Наследование от класса Thread2 способ. Реализация интерфейса RunnableСоздание объекта Runnable с использованием анонимного классаОпределяем поведение потока с помощью лямбда-выраженияСоздание и запуск потока с использованием лямбда-выражения
Ключевое слово Synchronized СинхронизацияСинхронизация методаСинхронизация блока кодаМьютекс и МониторСемафорСинхронизация в статических методах. Синхронизация по классуДополнительно. Объект МОНИТОРМожет ли поток блокировать мьютекс двух объектов?
Executor и ExecutorService Интерфейс ExecutorServiceExecutorService - реализация пула ПотоковСоздание пула потоков - Класс ExecutorsМетод execute() - выполнение задач в пуле потоковСоздание объекта Runnable с использованием анонимного класса
Интерфейс Callable Интерфейс Callable. Метод call() возвращает результатМетод submit() - задача в очереди на выполнениеFuture в Java - получить результат задачи. Метод get()Метод isDone - завершена ли задача?Методы shutdown() и shutdownNow()
Проблемы многопоточности Первый способ - блокирующая синхронизацияВторой способ: lock free или неблокирующая синхронизацияLock-free программирование. Пакет java.util.concurrent.atomicCompare-and-Set в Java. AtomicТретий способ Wait-free
Пакет java.util.concurrent Атомики в Java. Lock-free и thread-safe. Compare-and-Swap (CAS)Интерфейсы Future и CallableФреймворк ExecutorКласс ExecutorsОчереди в java.util.concurrentПродюсер и консьюмер в JavaКоллекции для работы в многопоточной средеThreadLocal и ThreadLocalRandomСинхронизаторы - синхронизация потоков
Тематические публикации
site.komp36.ru Как начать свое дело? Свой сайт «Укладка плитки» 9 000 руб. Лендинг адаптируется под любой вид отделочных работ, продажу товаров или услуг. Сайт поможет найти новых клиентов
site.komp36.ru Продающий лендинг «Установка и продажа окон» 9 000 руб. Одностраничный продающий сайт. Установка и продажа окон, установка дверей, перегородок. Заявки прямо с сайта
Последние статьи
Популярные статьи
Тематические публикации
site.komp36.ru Как начать свое дело? Свой сайт «Укладка плитки» 9 000 руб. Лендинг адаптируется под любой вид отделочных работ, продажу товаров или услуг. Сайт поможет найти новых клиентов
site.komp36.ru Продающий лендинг «Установка и продажа окон» 9 000 руб. Одностраничный продающий сайт. Установка и продажа окон, установка дверей, перегородок. Заявки прямо с сайта