Инструменты пользователя

Инструменты сайта


java_thread

Назад

Введение

Класс Thread

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

  • Первый – наследование от класса java.lang.Thread и переопределение его метода run.
  • Второй – реализация интерфейса java.lang.Runnable и создание потока на основе этой реализации.

В принципе это методы эквивалентны, разница в деталях. Наследование от java.lang.Thread делает его единственным родителем класса, что не всегда удобно. Я лично предпочитаю реализовывать java.lang.Runnable.

public class SampleThread extends Thread
{
    public SampleThread()
    {
        super();
    }
    public void run()
    {
        System.out.println("Hello, threads world!");
    }
    public static void main(String[] args)
    {
        Thread t = new SampleThread();
        t.start();
    }
}
public class SampleRunnable implements Runnable
{
 
    public SampleRunnable()
    {
        super();
    }
 
    public void run()
    {
        System.out.println("Hello, threads world!");
    }
 
    public static void main(String[] args)
    {
        Runnable r = new SampleRunnable();
        Thread t = new Thread(r);
        t.start();
    }
}
  1. У потоков могут быть различные приоритеты. Существует несколько констант – Thread.MIN_PRIORITY == 1, Thread.NORM_PRIORITY == 5 и Thread.MAX_PRIORITY == 10.
  2. Потоки могу быть демонами. (Потоки-демоны удобно использовать для фоновых задач).
  3. Поток можно приостановить на определенный промежуток времени. Изнутри. Делается это через статический метод Thread.sleep() с параметром – количеством миллисекунд, на которое приостанавливается поток.

Запускаем несколько потоков

В данном пример видно как происходит работа с потоками

public class ThreadTest implements Runnable 
{
    public void run()
    {
        double calc;
        for (int i = 0; i < 50000; i++) 
        {
            calc = Math.sin(i * i);
            if (i % 10000 == 0) {
                System.out.println(getName()+ " Количество " + i / 10000);
            }
        }
    }
 
    public String getName() 
    {
        return Thread.currentThread().getName();
    }
 
    public static void main(String s[]) 
    {
        // Инициализирую поток
        Thread t[] = new Thread[3];
        for (int i = 0; i < t.length; i++) 
        {
            t[i] = new Thread(new ThreadTest(),"Поток " + i);
        }
 
        // Запуск потоков
        for (int i = 0; i < t.length; i++) 
        {
            t[i].start();
            System.out.println(t[i].getName()+ " Стратует");
        }
    }
}

Результат:

Поток 0 Стратует
Поток 0 Количество 0
Поток 1 Стратует
Поток 1 Количество 0
Поток 2 Стратует
Поток 2 Количество 0
Поток 0 Количество 1
Поток 2 Количество 1
Поток 1 Количество 1
Поток 0 Количество 2
Поток 2 Количество 2
Поток 1 Количество 2
Поток 0 Количество 3
Поток 1 Количество 3
Поток 2 Количество 3
Поток 0 Количество 4
Поток 1 Количество 4
Поток 2 Количество 4

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

Приоритеты

Формула вычисления приоритетов позволяет равномерно распределить все допустимые значения для всех запускаемых потоков. На самом деле, константа минимального приоритета имеет значение 1, максимального 10, нормального 5. Так что в простых программах можно явно пользоваться этими величинами и указывать в качестве, например, пониженного приоритета значение 3.

public class ThreadTest implements Runnable 
{
    public void run()
    {
        double calc;
        for (int i = 0; i < 50000; i++) 
        {
            calc = Math.sin(i * i);
            if (i % 10000 == 0) {
                System.out.println(getName()+ " Количество " + i / 10000);
            }
        }
    }
 
    public String getName() 
    {
        return Thread.currentThread().getName();
    }
 
    public static void main(String s[]) 
    {
        // Подготовка потоков
        Thread t[] = new Thread[3];
        for (int i=0; i<t.length; i++) 
        {
            t[i]=new Thread(new ThreadTest(),"Поток "+i);
            if(i==0) t[i].setPriority(Thread.MIN_PRIORITY);
            if(i==1) t[i].setPriority(Thread.NORM_PRIORITY);
            if(i==2) t[i].setPriority(Thread.MAX_PRIORITY);
        }
        // Запуск потоков
        for (int i=0; i<t.length; i++) 
        {
            t[i].start();
            System.out.println(t[i].getName()+" Старт");
        }
    }
}

Результат:

Поток 0 Старт
Поток 1 Старт
Поток 2 Старт
Поток 1 Количество 0
Поток 2 Количество 0
Поток 0 Количество 0
Поток 2 Количество 1
Поток 1 Количество 1
Поток 0 Количество 1
Поток 2 Количество 2
Поток 1 Количество 2
Поток 2 Количество 3
Поток 1 Количество 3
Поток 2 Количество 4
Поток 1 Количество 4
Поток 0 Количество 2
Поток 0 Количество 3
Поток 0 Количество 4

Демон-потоки

Демон -потоки позволяют описывать фоновые процессы, которые нужны только для обслуживания основных потоков выполнения и не могут существовать без них. Для работы с этим свойством существуют методы setDaemon() и isDaemon().

public class ThreadTest implements Runnable 
{
 
    // Отдельная группа, в которой будут
    // находиться все потоки ThreadTest
    public final static ThreadGroup GROUP = new ThreadGroup("Daemon demo");
    // Стартовое значение, указывается при создании объекта
    private int start;
 
    public ThreadTest(int s) 
    {
        start = (s % 2 == 0) ? s : s + 1;
        new Thread(GROUP, this, "Thread " + start).start();
    }
 
    public void run() 
    {
        // Начинаем обратный отсчет
        for (int i = start; i > 0; i--)
        {
            try 
            {
                Thread.sleep(300);
            } catch (InterruptedException e) 
            {
            }
            // По достижении середины порождаем
            // новый поток с половинным начальным значением
            if (start > 2 && i == start / 2) 
            {
                new ThreadTest(i);
            }
        }
    }
 
    public static void main(String s[]) 
    {
        //Анонимный вызов класса
        new ThreadTest(16);
        new DaemonDemo();
    }
}
 
//-------------------------//
public class DaemonDemo extends Thread 
{
 
    public DaemonDemo() 
    {
        super("Daemon demo thread");
        setDaemon(true);
        start();
    }
 
    public void run() 
    {
        Thread threads[] = new Thread[10];
        while (true) 
        {
            // Получаем набор всех потоков из  тестовой группы
            int count = ThreadTest.GROUP.activeCount();
            //Если нам не хватит 10 потоков пересоздаем потоки
            if (threads.length < count) 
            {
                threads = new Thread[count + 10];
            }
            //Записуем все потоки в threads и получаем их количество
            count = ThreadTest.GROUP.enumerate(threads);
 
            // Распечатываем имя каждого потока
            for (int i = 0; i < count; i++) 
            {
                System.out.println(threads[i].getName() + ", ");
            }
 
            try 
            {
                Thread.sleep(300);
            } catch (InterruptedException e) 
            {
            }
        }
    }
}

В этом примере происходит следующее. Потоки ThreadTest имеют некоторое стартовое значение, передаваемое им при создании. В методе run() это значение последовательно уменьшается. При достижении половины от начальной величины порождается новый поток с вдвое меньшим начальным значением. По исчерпании счетчика поток останавливается. Метод main() порождает первый поток со стартовым значением 16. В ходе программы будут дополнительно порождены потоки со значениями 8, 4, 2.

За этим процессом наблюдает демон -поток DaemonDemo. Этот поток регулярно получает список всех существующих потоков ThreadTest и распечатывает их имена для удобства наблюдения.

Несмотря на то, что демон -поток никогда не выходит из метода run(), виртуальная машина прекращает работу, как только все не- демон -потоки завершаются.

Синхронизация

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

Пример работы не синхронизируемого потока

public class ThreadTest 
{
    private int a = 1, b = 2;
 
    public void one() 
    {
        a = b;
    }
 
    public void two() 
    {
        b = a;
    }
 
    public static void main(String s[]) 
    {
        int a11 = 0, a22 = 0, a12 = 0;
        for (int i = 0; i < 1000; i++) {
            final ThreadTest o = new ThreadTest();
 
            // Запускаем первый поток, который
            // вызывает один метод
            new Thread() 
            {
 
                public void run() 
                {
                    o.one();
                }
            }.start();
 
            // Запускаем второй поток, который
            // вызывает второй метод
            new Thread() 
            {
 
                public void run() 
                {
                    o.two();
                }
            }.start();
 
            // даем потокам время отработать 
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) 
            {
            }
 
            // анализируем финальные значения
            if (o.a == 1 && o.b == 1) 
            {
                a11++;
            }
            if (o.a == 2 && o.b == 2) 
            {
                a22++;
            }
            if (o.a != o.b) 
            {
                a12++;
            }
        }
        System.out.println(a11 + " " + a22 + " " + a12);
    }
}
//Результат: 135 864 1

В этом примере два потока исполнения одновременно обращаются к одному и тому же объекту, вызывая у него два разных метода, one() и two(). Эти методы пытаются приравнять два поля класса a и b друг другу, но в разном порядке. Учитывая, что исходные значения полей равны 1 и 2, соответственно, можно было ожидать, что после того, как потоки завершат свою работу, поля будут иметь одинаковое значение. Однако понять, какое из двух возможных значений они примут, уже невозможно. Посмотрим на результат программы:

Блокировки

В основном хранилище для каждого объекта поддерживается блокировка ( lock ), над которой можно произвести два действия – установить ( lock ) и снять ( unlock ). Только один поток в один момент времени может установить блокировку на некоторый объект. Если до того, как этот поток выполнит операцию unlock, другой поток попытается установить блокировку, его выполнение будет приостановлено до тех пор, пока первый поток не отпустит ее.

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

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

В Java-программе для того, чтобы воспользоваться механизмом блокировок, существует ключевое слово <color red>synchronized</color>. Оно может быть применено в двух вариантах – для объявления synchronized -блока и как модификатор метода. В обоих случаях действие его примерно одинаковое.

synchronized (ref) 
{
   ...
}

Прежде, чем начать выполнять действия, описанные в этом блоке, поток обязан установить блокировку на объект, на который ссылается переменная ref (поэтому она не может быть null ). Если другой поток уже установил блокировку на этот объект, то выполнение первого потока приостанавливается до тех пор, пока не удастся выполнить операцию lock.

После этого блок выполняется. При завершении исполнения (как успешном, так и в случае ошибок) производится операция unlock, чтобы освободить объект для других потоков.

public class ThreadTest implements Runnable
{
    private static ThreadTest shared = new ThreadTest();
 
    public void process()
    {
        for (int i = 0; i < 3; i++)
        {
            System.out.println(Thread.currentThread().getName() + " " + i);
            Thread.yield();
        }
    }
 
    public void run()
    {
        synchronized(shared)
        {
            shared.process();
        }
 
    }
 
    public static void main(String s[])
    {
        for (int i = 0; i < 3; i++)
        {
            new Thread(new ThreadTest(),"Thread-" + i).start();
        }
    }
}

Теперь результат будет строго упорядочен:

Thread-0 0
Thread-0 1
Thread-0 2
Thread-1 0
Thread-1 1
Thread-1 2
Thread-2 0
Thread-2 1
Thread-2 2

Synchronized -методы работают аналогичным образом. Прежде, чем начать выполнять их, поток пытается заблокировать объект, у которого вызывается метод. После выполнения блокировка снимается. В предыдущем примере аналогичной упорядоченности можно было добиться, если использовать не synchronized -блок, а объявить метод process() синхронизированным.

Методы wait(), notify(), notifyAll()

Каждый объект в Java имеет не только блокировку для synchronized блоков и методов, но и так называемый wait-set, набор потоков исполнения. Любой поток может вызвать метод wait() любого объекта и таким образом попасть в его wait-set. При этом выполнение такого потока приостанавливается до тех пор, пока другой поток не вызовет у этого же объекта метод notifyAll(), который пробуждает все потоки из wait-set. Метод notify() пробуждает один случайно выбранный поток из данного набора.

public class ThreadTest implements Runnable
{
 
    final static private Object shared = new Object();
    private int type;
 
    public ThreadTest(int i)
    {
        type = i;
    }
 
    public void run()
    {
        if (type == 1 || type == 2)
        {
            synchronized (shared)
            {
                try
                {
                    shared.wait();
                } catch (InterruptedException e)
                {
                }
                System.out.println("Thread " + type + " after wait()");
            }
        } else
        {
            synchronized (shared)
            {
                shared.notifyAll();
                System.out.println("Thread " + type + " after notifyAll()");
            }
        }
    }
 
    public static void main(String s[])
    {
        ThreadTest w1 = new ThreadTest(1);
        new Thread(w1).start();
        try
        {
            Thread.sleep(100);
        } catch (InterruptedException e)
        {
        }
        ThreadTest w2 = new ThreadTest(2);
        new Thread(w2).start();
        try
        {
            Thread.sleep(100);
        } catch (InterruptedException e)
        {
        }
        ThreadTest w3 = new ThreadTest(3);
        new Thread(w3).start();
    }
}

Результатом программы будет:

Thread 3 after notifyAll()
Thread 1 after wait()
Thread 2 after wait()

Рассмотрим, что произошло. Во-первых, был запущен поток 1, который тут же вызвал метод wait() и приостановил свое выполнение. Затем то же самое произошло с потоком 2. Далее начинает выполняться поток 3. Сразу обращает на себя внимание следующий факт. Еще поток 1 вошел в synchronized -блок, а стало быть, установил блокировку на объект shared. Но, судя по результатам, это не помешало и потоку 2 зайти в synchronized -блок, а затем и потоку 3. Причем, для последнего это просто необходимо, иначе как можно «разбудить» потоки 1 и 2?

Можно сделать вывод, что потоки, прежде чем приостановить выполнение после вызова метода wait(), отпускают все занятые блокировки. Итак, вызывается метод notifyAll(). Как уже было сказано, все потоки из wait-set возобновляют свою работу. Однако чтобы корректно продолжить исполнение, необходимо вернуть блокировку на объект, ведь следующая команда также находится внутри synchronized -блока!

Функции потока

ИмяОписание
getName()Это имя потока, которое используется только для упрощения его идентификации.
currentThread()Этот статический метод позволяет в любом месте кода получить ссылку на объект класса Thread, представляющий текущий поток исполнения.