Елена Булыгина предлагает Вам запомнить сайт «Ленусик»
Вы хотите запомнить сайт «Ленусик»?
Да Нет
×
Прогноз погоды

Основная статья: Java developer

Быстрый старт с Java: пишем «крестики-нолики»

Перед прочтением данной статьи рекомендую ознакомиться с предыдущей, «Быстрый старт с Java: начало», поскольку ожидается, что читатель владеет материалом, изложенным в ней — знает о переменных, условиях, циклах и импорте классов. Сегодня мы углублим знания о Java, создавая игру «Крестики-нолики», которая работает в командной строке (консоли). В процессе будет рассмотрена работа с массивами, а также некоторые аспекты объектно-ориентированного программирования (нестатические методы, нестатические поля, конструктор).

Массивы

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

В Java массивы являются объектами, их создают с помощью директивы new. При создании указываем количество элементов массива или инициализируем его набором значений. Приведенный код иллюстрирует оба варианта:

class Arrays {
    public static void main(String[] args) {
        int[] arr = new int[5];
        int[] arrInit = {1, 2, 3, 4, 5};
        for (int i = 0; i < arr.length; i++) {
            arr[i] = i * 2 + arrInit[i];
        }
        for (int a : arr) {
            System.out.println(a);
        }
    }
}

С элементами массива можно работать как с обычными переменными, присваивая им результат выражения и читая хранимые значения. При этом в квадратных скобках указывается индекс элемента массива. Индексация в Java идёт с 0 (с нуля). Первый цикл инициализирует элементы массива arr при помощи значений из массива arrInit. Каждый массив имеет поле length, содержащее количество его элементов. Второй цикл выводит элементы массива в консоль, используя второй вариант for  без счётчика цикла.

Методы

Кроме main() класс может содержать и другие методы. Рассмотрим в качестве примера класс с методом add(), который вычисляет и возвращает сумму двух значений, переданных как параметры. Обратите внимание на тип int, который стоит перед именем метода — это тип возвращаемого значения. Две переменные в скобках — параметры. Совокупность имени и параметров называют сигнатурой метода. Вызов метода происходит по имени, в скобках указывают передаваемые значения. В методе они попадают в параметры-переменные. Команда return возвращает результат сложения этих двух переменных и обеспечивает выход из метода.

class MethodStatic {
    public static void main(String[] args) {
        int c = add(5, 6);
        System.out.println("5 + 6 = " + c);
    }
 
    static int add(int a, int b) {
        return a + b;
    }
}

Слово static означает, что метод статический. Если мы обращается к какому-либо методу из статического метода, то вызываемый тоже должен быть статическим. Вот почему add() статический — он вызывается из статического main(). Использование статических методов — скорее исключение, чем правило, поэтому давайте посмотрим как сделать add() нестатическим.

Решение одно — создать объект на основании класса. И затем вызывать метод через точку после имени объекта. В этом случае метод может быть нестатическим. Представленный ниже код это иллюстрирует.

class MethodNotStatic {
    public static void main(String[] args) {
        MethodNotStatic method = new MethodNotStatic();
        int c = method.add(5, 6);
        System.out.println("5 + 6 = " + c);
    }
 
    int add(int a, int b) {
        return a + b;
    }
}

Поля класса

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

class FieldExample {
    int a;
 
    public static void main(String[] args) {
        FieldExample field = new FieldExample();
        field.a = 12;
        System.out.println("a = " + field.a);
        System.out.println(field.getA());
        field.printA();
    }
 
    int getA() {
        return a;
    }
 
    void printA() {
        System.out.println(a);
    }
}

Приведённый выше код иллюстрирует работу с нестатическим полем int a. Описание полей принято размещать первыми в коде класса, затем идут описания методов. Возможность обращаться к полю (запись, чтение) мы получаем только после создания объекта. Также видно, что это поле доступно во всех нестатических методах объекта, а в статическом main() — через точку после имени объекта.

Крестики-нолики. Шаблон класса

Приступим к написанию кода игры. Начнём с шаблона класса и определения нужных полей. Именно это содержит приведённый ниже код. Первые две строки — импорт классов. Первыми в теле класса идут описания полей, затем методов. Метод main() используется для создания объекта (так как поля и методы нестатические) и вызова метода game() с игровой логикой.

import java.util.Random;
import java.util.Scanner;
 
class TicTacToe {
    final char SIGN_X = 'x';
    final char SIGN_O = 'o';
    final char SIGN_EMPTY = '.';
    char[][] table;
    Random random;
    Scanner scanner;
 
    public static void main(String[] args) {
        new TicTacToe().game();
    }
 
    TicTacToe() {
        // конструктор: инициализация полей
    }
 
    void game() {
        // игровая логика
    }
 
    // дополнительные методы
}

В качестве полей используем три символьные константы: SIGN_X, SIGN_O и SIGN_EMPTY. Их значения нельзя изменять, об этом говорит модификатор final. Двумерный символьный массив table будет нашим игровым полем. Потребуется также объект random для генерации ходов компьютера и scanner для ввода данных от пользователя.

Имена методов принято писать с маленькой буквы. Однако в коде мы видим метод TicTacToe() — есть ли тут нарушение? Нет, поскольку этот метод особенный и в объектно-ориентированном программировании называется конструктор. Конструктор вызывается сразу после того, как объект создан. Его имя, как видим, должно совпадать с именем класса. Мы используем конструктор для инициализации полей.

TicTacToe() {
    random = new Random();
    scanner = new Scanner(System.in);
    table = new char[3][3];
}

Игровая логика

Игровая логика располагается в методе game() и базируется на бесконечном цикле while. Ниже в фрагменте кода последовательность действий описана через комментарии:

// инициализация таблицы
while (true) {
    // ход человека
    // проверка: если победа человека или ничья:
    //    сообщить и выйти из цикла
    // ход компьютера
    // проверка: если победа компьютера или ничья:
    //    сообщить и выйти из цикла
}

При написании рабочего кода, каждое действие — например, «ход человека», «ход компьютера», «проверка» — мы заменим на вызов соответствующего метода. При возникновении выигрышной или ничейной ситуации (все клетки таблицы заполнены), выходим из цикла с помощью break, завершая игру.

void game() {
    initTable();
    while (true) {
        turnHuman();
        if (checkWin(SIGN_X)) {
            System.out.println("YOU WIN!");
            break;
        }
        if (isTableFull()) {
            System.out.println("Sorry, DRAW!");
            break;
        }
        turnAI();
        printTable();
        if (checkWin(SIGN_O)) {
            System.out.println("AI WIN!");
            break;
        }
        if (isTableFull()) {
            System.out.println("Sorry, DRAW!");
            break;
        }
    }
    System.out.println("GAME OVER.");
    printTable();
}

Реализация вспомогательных методов

Пришло время написать код методов, вызываемых в game(). Самый первый, initTable(), обеспечивает начальную инициализацию игровой таблицы, заполняя её ячейки «пустыми» символами. Внешний цикл, со счетчиком int row, выбирает строки, а внутренний, со счётчиком int col, перебирает ячейки в каждой строке.

 
void initTable() {
    for (int row = 0; row < 3; row++)
        for (int col = 0; col < 3; col++)
            table[row][col] = SIGN_EMPTY;
}

Также потребуется метод, отображающий текущее состояние игровой таблицы printTable().

void printTable() {
    for (int row = 0; row < 3; row++) {
        for (int col = 0; col < 3; col++)
            System.out.print(table[row][col] + " ");
        System.out.println();
    }
}

В методе turnHuman(), который позволяет пользователю сделать ход, мы используем метод nextInt() объекта scanner, чтобы прочитать два целых числа (координаты ячейки) с консоли. Обратите внимание как используется цикл do-while: запрос координат повторяется в случае, если пользователь укажет координаты невалидной ячейки (ячейка таблицы занята или не существует). Если с ячейкой всё в порядке, туда заносится символ SIGN_X — «крестик».

void turnHuman() {
    int x, y;
    do {
        System.out.println("Enter X and Y (1..3):");
        x = scanner.nextInt() - 1;
        y = scanner.nextInt() - 1;
    } while (!isCellValid(x, y));
    table[y][x] = SIGN_X;
}

Валидность ячейки определяет метод isCellValid(). Он возвращает логическое значение: true — если ячейка свободна и существует, false — если ячейка занята или указаны ошибочные координаты.

boolean isCellValid(int x, int y) {
    if (x < 0 || y < 0 || x >= 3|| y >= 3)
        return false;
    return table[y][x] == SIGN_EMPTY;
}

Метод turnAI() похож на метод turnHuman() использованием цикла do-while. Только координат ячейки не считываются с консоли, а генерируются случайно, при помощи метода nextInt(3) объекта random. Число 3, передающееся как параметр, является ограничителем. Таким образом, генерируются случайные целые числа от 0 до 2 (в рамках индексов массива игровой таблицы). И метод isCellValid() снова позволяет нам выбрать только свободные ячейки для занесения в них знака SIGN_O — «нолика».

void turnAI() {
    int x, y;
    do {
        x = random.nextInt(3);
        y = random.nextInt(3);
    } while (!isCellValid(x, y));
    table[y][x] = SIGN_O;
}

Осталось дописать два последних метода — проверка победы и проверка на ничью. Метод checkWin() проверяет игровую таблицу на «победную тройку» — три одинаковых знака подряд, по вертикали или горизонтали (в цикле), а также по двум диагоналям. Проверяемый знак указан как параметр char dot, за счёт чего метод универсален - можно проверять победу и по «крестикам» и по «ноликам». В случае победы возвращается булевское значение true, в противном случае — false.

boolean checkWin(char dot) {
    for (int i = 0; i < 3; i++)
        if ((table[i][0] == dot && table[i][1] == dot &&
                         table[i][2] == dot) ||
                (table[0][i] == dot && table[1][i] == dot &&
                                  table[2][i] == dot))
            return true;
        if ((table[0][0] == dot && table[1][1] == dot &&
                  table[2][2] == dot) ||
                    (table[2][0] == dot && table[1][1] == dot &&
                      table[0][2] == dot))
            return true;
    return false;
}

Метод isTableFull() во вложенном двойном цикле проходит по всем ячейкам игровой таблицы и, если они все заняты, возвращает true. Если хотя бы одна ячейка ещё свободна, возвращается false.

boolean isTableFull() {
    for (int row = 0; row < 3; row++)
        for (int col = 0; col < 3; col++)
            if (table[row][col] == SIGN_EMPTY)
                return false;
    return true;
}

Теперь осталось собрать все эти методы внутри TicTacToe. Последовательность их расположения в теле класса не важна. А после этого можно попробовать сыграть с компьютером в крестики-нолики.

Заключение

На всякий случай прилагаю мой telegram — @biblelamp. Если вас заинтересовала тема, рекомендую почитать «Java-программирование для начинающих» Майка МакГрата и «Изучаем Java» Кэти Сьерра и Берт Бейтс. Также напоминаю ссылку на мою предыдущую статью, где мы начали знакомство с Java.

Если язык Java вас заинтересовал — приглашаем на факультет Java-разработки. Если ещё не совсем уверены — посмотрите истории успеха наших Java-выпускников:

Пройти обучение

28 ноя 19, 11:13
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0

Быстрый старт с Java: от знакомства до вашей первой игры 

Эта статья рассчитана на новичков в программировании. Её цель — объяснить азы языка Java (классы, методы, компиляция, байт-код, JVM, переменные, условия, циклы, импорт). Этот материал поможет:

  • научиться писать код, компилировать и запускать байт-код;
  • познакомиться с переменными;
  • узнать об условных операторах и циклах;
  • освоить использование классов из стандартных пакетов;
  • написать простую консольную игру «Угадай число».

Java Development Kit (JDK)

Для работы с Java необходимо скачать, установить и настроить Java Development Kit (JDK) — пакет программ и утилит для разработчика. Он бесплатно предоставляется компанией Oracle.
В строку поиска Google вводим запрос “jdk 8 download” и в первой же строке поисковой выдачи видим ссылку на страницу загрузки на oracle.com. Листаем вниз, щёлкаем Accept License Agreement — соглашаемся с лицензией. Выбираем пакет в колонке Download, который соответствует вашей операционной системе и её разрядности (32 или 64 бита). Для скачивания нужно будет зарегистрироваться на сайте. Скачиваем и устанавливаем JDK. При выборе пакетов и папки JDK рекомендую отказаться от Public JRE и сократить имя папки до jdk1.8.

Счастливым обладателям Windows потребуется также настройка системных переменных. Как их найти: Панель управления → Система → Дополнительные параметры системы → Вкладка «Дополнительно» → кнопка «Переменные среды», второе окошко сверху. В нем надо создать переменную JAVA_HOME и изменить (дополнить) Path (для этого используются кнопки «Создать» и «Изменить». Значение для переменной JAVA_HOME содержит путь к папке JDK. Переменную Path дополняем значением %JAVA_HOME%\bin. Обратите внимание, что список путей разделяется точкой с запятой (;). Инструкция есть также на сайте Oracle.

JDK содержит много утилит, но нам потребуется только компилятор (javac) и виртуальная машина Java — JVM (java).

Проверить правильность установки и настройки можно в командной строке (на Windows: Win + R → cmd) или в терминале (Linux, Mac), набрав следующие команды:

javac -version
java -version

В качестве альтернативы можно использовать онлайн-JDK вместе с редактором, например на сайте Repl.it.

Написание кода

Писать исходный код можно в любом текстовом редакторе. А компиляцию и запуск проводить в командной строке (терминале). Но удобнее будет работать в редакторе с подсветкой синтаксиса и плагином, автоматизирующим компиляцию и запуск.

Для Windows хорошим выбором будет Notepad++ с плагином NppExec. Подробно о его настройке можно прочитать в статье Виктора Темпалова  «Компилируем и запускаем Java-файлы из Notepad++». Для Linux и Мас есть другие текстовые редакторы, например Atom или Sublime Text.

Hello, world!

При изучении нового языка писать программу, выводящую «Hello, world!», — это традиция. Она возникла в 1978 году, с выходом в свет книги «Язык программирования С» Брайана Кернигана и Денниса Ричи.

class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

Код начинается с ключевого слова class. Java-программы состоят из классов — это «кирпичики» приложения. В реальных приложениях их могут быть десятки, сотни и тысячи. Затем пишется имя класса — HelloWorld. Имена классов принято писать в стиле CamelCase (все слова слитно, каждое с заглавной буквы). Классам, как и другим сущностям — методам, полям и переменным — рекомендуется давать мнемонические, «говорящие» имена.

После имени в фигурных скобках записывается тело класса. Оно может содержать поля, методы и другие классы. Поля хранят данные. Они подобны переменным, однако в Java это несколько иная сущность. Методы содержат набор команд или код. Имена методов (как и полей, переменных) принято писать в стиле camelCase, со строчной буквы.

В нашем классе только один метод — main(). Он особенный, его часто называют стартовым. Когда виртуальная машина Java получает класс для исполнения, она ищет в нём метод main. Если находит — выполняет код, который содержит метод. Если нет — выдаёт сообщение об ошибке. Важно, чтобы метод main() имел строго определённую сигнатуру: был публичным (public), статическим (static), не возвращающим никакого значения (void), с параметром в виде массива строк (String[] args). В противном случае виртуальная машина его просто не найдёт.

Наш метод main() содержит лишь одну строку, которая выводит в консоль текст "Hello, world!".

Перед компиляцией сохраняем написанный код в файл. Имя файла должно соответствовать имени класса с точностью до регистра символов (Java — регистрозависимый язык). Затем открываем командную строку (или терминал) и вызываем сначала компилятор (javac), а потом виртуальную машину JVM (java).

C:\Users\User>javac HelloWorld.java
C:\Users\User>java HelloWorld
Hello, world!

После компиляции возникнет бинарный файл HelloWorld.class, содержащий байт-код. Имя этого файла (без расширения) передаётся как параметр при вызове виртуальной машины. В результате видим строку «Hello, world!» — это результат выполнения программы.

Переменные

Переменные хранят данные (числа, строки, логические значения и другие). Они похожи на коробочки: надписи на них — это имя и тип переменной, а внутри находится значение. При этом возможны две операции:

  • запись значения в переменную (или присвоение значения);
  • чтение значения из переменной. 

В Java перед использованием переменную нужно обязательно объявить, указав тип и имя, поскольку Java — язык со строгой и статической типизацией. Попытка использовать переменную без объявления приведёт к ошибке компиляции. По типу переменные могут быть простыми (числа, символы, логические значения) или ссылочными (массивы, строки, любые другие объекты). В Java есть восемь простых типов (byte, int, short, long, float, double, boolean, char) и бесконечное количество ссылочных.

В примере ниже мы видим объявление двух переменных целого типа (int). Причём переменная second инициируется при объявлении. Затем переменной first присваивается результат выражения, в котором присутствует переменная second. В результате выполнения программы в консоль будут выведены значения обеих переменных: 35 и 10.

class Variables {
    public static void main(String[] args) {
        int first;
        int second = 10;
        first = second * 3 + 5;
        System.out.println(first);
        System.out.println(second);
    }
}

Имя переменной может содержать буквы и цифры, знак подчёркивания и $. Имя не должно начинаться с цифры. В профессиональной разработке в именах переменных используют только буквы. Имя должно явно указывать на назначение переменной. Например: countString — счётчик строк, errorSign — признак ошибки. Рекомендуется избегать однобуквенных имён — за исключением временных переменных (существующих в рамках цикла или небольшого кодового блока).

Проверка условий

Любой язык программирования высокого уровня позволяет изменять порядок выполнения программы в зависимости от заданного условия. В Java для этого используется конструкция if. После этого ключевого слова в скобках записывается логическое выражение, затем в фигурных скобках — группа команд. Если результат логического выражения — true (истина), то эта группа команд выполняется, если false (ложь) — нет.

class ComparisonShort {
    public static void main(String[] args) {
        int i = 10;
        if (i == 10) {
            System.out.println("i = 10");
        }
    }
}

Результат этого примера — строка “i = 10” в консоли, так как результат логического выражения (проверка на равенство) будет true.

В качестве знаков сравнения допустимы: <, <=, >, >=, ==, !=. Добавив ключевое слово else, получаем полную форму оператора сравнения.

class ComparisonFull {
    public static void main(String[] args) {
        int i = 10 + 2;
        if (i == 10) {
            System.out.println("i = 10");
        } else {
            System.out.println("i != 10");
        }
    }
}

Выполнив эту программу, мы увидим в консоли сообщение “i != 10”, так как результат сравнения значения переменной i и цифры 10 будет false. Поэтому выполнится набор команд в фигурных скобках после слова else.

Циклы

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

  • с определённым количеством итераций: for;
  • без определенного количества итераций: while и do-while.
class CycleFor {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
        }
    }
}

Запустив этот пример, мы увидим в консоли колонку чисел от 0 до 4. Это результат работы классического цикла for со счётчиком. В круглых скобках три параметра цикла, разделённые точкой с запятой: объявление и инициализация счётчика, проверка условия продолжения цикла, действие после каждой итерации. В фигурных скобках — тело цикла. В нашем случае это команда, выводящая значение счётчика i в консоль.

Когда будем писать игру, используем циклы while и do-while — там и увидим их работу на практике. Пока достаточно сказать, что их синтаксис проще for, а тело цикла повторяется до тех пор, пока логическое выражение в круглых скобках после while возвращает true.

Пример ниже иллюстрирует вторую форму цикла for — без счётчика. Такой вариант удобен при переборе элементов массива или списка.

class CycleForeach {
    public static void main(String[] args) {
        int[] arr = {5, 4, 3, 2, 1};
        for (int a : arr) {
            System.out.println(a);
        }
    }
}

Импорт и использование классов

Один из важных принципов программирования — DRY: Don’t Repeat Youself (не повторяйся). Разработчик должен уметь использовать то, уже создано, отлажено и работает.

Java поставляется с набором packages (пакетов). Они содержат скомпилированные классы, сгруппированные по тематике. На неё указывает имя пакета: java.io, java.util, java.net и так далее.

В игре нам потребуется случайное число от 0 до 9. Писать генератор случайных чисел незачем, так как в пакете java.util есть готовый класс Random. Чтобы использовать класс из пакета, его нужно импортировать. Строки с директивами импорта располагаются в самом начале файла класса.

import java.util.Random;

Создадим объект на основании класса, чтобы можно было вызвать нужный метод. Слева от знака присваивания (=) объявляем ссылочную переменную (объект) random типа Random. Справа создаём объект с помощью директивы new.

Random random = new Random();

Вызываем метод через точку после имени объекта: random.nextInt(10). Цифра 10 ограничивает диапазон генерируемых чисел (от 0 до 9). В результате получим строку из 30 случайных чисел. Запустите программу несколько раз и обратите внимание, что последовательность чисел не повторяется.

import java.util.Random;
 
class RandomInt {
    public static void main(String[] args) {
        Random random = new Random();
        for (int i = 0; i < 30; i++) {
            System.out.print(random.nextInt(10) + " ");
        }
    }
}

Игра «Угадай число»

Мы узнали достаточно, чтобы написать игру «Угадай число». В ней компьютер «загадывает» целое число от 0 до 9, а человек должен угадать его — есть три попытки.

Создадим класс GuessTheNumber с методом main(), импортируем нужные нам классы и добавим соответствующие объекты. Класс Scanner обеспечит чтение чисел с консоли (System.in — консоль).

import java.util.Scanner;
import java.util.Random;
 
class GuessTheNumber {
    public static void main(String[] args) {
        Random random = new Random();
        Scanner sc = new Scanner(System.in);
    }
}

Объявим и инициируем три переменные: счётчик попыток count, для чтения числа с консоли — guess, для хранения загаданного числа — number.

int count = 0;
int guess = -1; 
int number = random.nextInt(10);

Опишем основной игровой цикл, используя while. В цикле запрашиваем число с консоли, сравниваем с загаданным. В случае несовпадения выводим подсказку и увеличиваем счётчик попыток на 1.

while (count < 3 && guess != number) {
    System.out.print("Guess the number (0..9): ");
    guess = sc.nextInt();
    if (number != guess) {
        System.out.println("Your number is " +
            ((guess > number)? "greater" : "less"));
        count++;
    }
}

Тело цикла while повторяется, пока логическое значение в скобках возвращает true. Таким образом наш цикл будет повторяться, пока значение счётчика попыток меньше 3 и число не отгадано. Обратите внимание на конструкцию, которая позволяет выбрать одно из двух значений согласно условию в скобках. Это тернарный оператор. Если условие в скобках true, то возвращается значение слева от двоеточия, если false — справа.

(guess > number)? "greater" : "less"

Остаётся добавить строку с выводом результата игры (победа или проигрыш) после цикла — тут снова используется тернарный оператор. Текст работающей программы выглядит так:

import java.util.Scanner;
import java.util.Random;
 
class GuessTheNumber {
    public static void main(String[] args) {
        Random random = new Random();
        Scanner sc = new Scanner(System.in);
        int count = 0;
        int guess = -1;
        int number = random.nextInt(10);
        while (count < 3 && guess != number) {
            System.out.print("Guess the number (0..9): ");
            guess = sc.nextInt();
            if (number != guess) {
                System.out.println("Your number is " +
                    ((guess > number)? "greater" : "less"));
                count++;
            }
        }
        System.out.println("You " +
            ((guess == number)? "WIN!" : "Lose: " + number));
    }
}

Сохраняем программу в файл GuessTheNumber.java, компилируем (javac), вызываем JVM (java) — и пробуем выиграть у компьютера.

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

import java.util.Random;
import java.util.Scanner;
 
class GuessTheNumber {
    public static void main(String[] args) {
        Random random = new Random();
        Scanner sc = new Scanner(System.in);
        do {
            //
            // Вставьте фрагмент, который должен повторяться
            //
            System.out.println("Repeat game? Yes - 1, No - 0");
        } while (sc.nextInt() == 1);
    }
}

Обратите внимание, что тело цикла do-while повторяется на тех же условиях, что и тело цикла while: логическое выражение в скобках должно быть true.

Заключение

На всякий случай прилагаю мой telegram — @biblelamp. Если вас заинтересовала тема, рекомендую почитать «Java-программирование для начинающих» Майка МакГрата и «Изучаем Java» Кэти Сьерра и Берт Бейтс. В следующих статьях мы продолжим начальное изучение Java на примере создания других игр. Следите за блогом!

Если язык Java вас заинтересовал — приглашаем на факультет Java-разработки. Если ещё не совсем уверены — посмотрите истории успеха наших Java-выпускников:

Пройти обучение

15 ноя 19, 16:33
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0

Наставничество GeekBrains

Кто такие наставники

Чтобы ученики могли лучше прорабатывать материал и справляться со сложностями в обучении, мы придумали наставничество. Наставники помогают студентам GeekBrains осваивать программу, выполнять домашние задания и делать учебные проекты. 

Так выглядит раздел наставничества. Во вкладке «Обучение» описаны ключевые моменты — что это такое, как стать наставником и какая от этого польза

В GeekBrains можно стать наставником по программированию, маркетингу и дизайну. Помогать учащимся других потоков могут студенты, которые прошли курс, тестирование и сдали шесть из восьми домашних заданий на «хорошо» и «отлично».

Как стать наставником

Если вы выполнили эти условия, в разделе «Обучение» откроется страница «Наставничество». Внутри — вводная информация и списки доступных потоков. Выберите из них приглянувшийся и нажмите кнопку «Стать наставником». Вы увидите pop-up с описанием задания для потенциального наставника: записать короткое видео о том, почему вас интересует эта миссия и понравилось ли вам учиться в GeekBrains.

В доступных потоках перечислены курсы, к которым студент подходит по критериям: прошел курс, сдал шесть из восьми заданий на 4 и 5, успешно справился с тестом и прикрепил ролик

Простое задание для тех, кто хочет стать частью команды наставников GeekBrains

Наставниками смогут стать не все — претендентов рассматривает команда GeekBrains и выбирает тех, кто вписывается в комьюнити. Не волнуйтесь, что вашу заявку пропустят — их внимательно модерируют вручную, потому что наставники — это часть команды.

Когда заявка пройдет модерацию, вы официально станете наставником GeekBrains. Студенты будут обращаться к вам, когда преподаватель не отвечает на срочный вопрос, нужна помощь с домашним заданием или вопросом по теме, которую они не до конца понимают.

Недавно мы ввели систему оценок наставникам и отзывов о них

Почему быть наставником круто

Сейчас в GeekBrains 663 наставника, некоторые провели уже больше десяти потоков. Их профит в том, что благодаря вопросам от студентов они упражняются в сфере, которую недавно изучили, и прокачивают профессиональные навыки. Особо любознательные ученики задают сложные вопросы, на которые наставник не может ответить сразу — тогда они вместе разбираются и находят ответ. Так наставник углубляет свои знания и развивается как преподаватель — вникает в вопрос и учится понятно объяснять.

Что говорят сами наставники

Павел Пимкин, 43 года, Москва

«Я окончил курс «Frontend-разработчик». Стал наставником в феврале и провел уже 14 потоков. Эта практика помогает мне лучше изучить и закрепить тонкости языка, создать сеть людей, которые работают в IT. Но самое важное — когда помогаешь другим, чувствуешь себя нужным.

Бывали и необычные случаи. Я был наставником на курсах Java-1 и Java-2, и после их окончания одна из студенток попросила меня помогать ученикам на курсе по React. Я согласился, но в GeekBrains нет возможности стать наставником этого курса. Тогда я предложил студентке написать письмо в техподдержку с просьбой назначить меня. Через несколько дней это случилось — и было очень приятно, что я стал одним из немногих наставников на курсе React».

 

 

Артем Еремин, 29 лет, Москва

«Я учусь на факультете Java-разработки, был наставником на пяти потоках. Зачем мне это нужно? Приятно помогать людям, и полезно для меня — когда отвечаешь на вопросы студентов, глубже погружаешься в тему, которую уже проходил, освежаешь ее в памяти. Надеюсь, наставничество приносит пользу и студентам.

 

Фрагмент моего наставничества — помогаю студенту разобраться с домашним заданием

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

Дмитрий Бронских, 22 года, Новоуральск

«Закончил две GeekBrains-профессии: «Программист PHP» и «Программист Android» — и сейчас учусь на факультете веб-разработки. Наставничество сразу заинтересовало. Ознакомился с описанием программы и решил, что будет интересно. Выполнил условия задания, стал наставником и сразу взялся курировать первый поток. На сегодняшний день я провел 24 потока. И уже заявился на несколько еще не стартовавших.

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

В итоге я сам лучше разбираюсь в материале, получаю новый опыт, нетипичный взгляд на решение задачи и благодарность от студентов — это всегда приятно. С некоторыми поддерживаем связь и вне обучения».

Валерий Туров, 28 лет, Балашиха

«Я окончил профессию «Программист Java» и после двух курсов устроился на работу по специальности. Наставником решил стать из «корыстных» целей — слышал, что человек, который объясняет тему, начинает сам лучше разбираться в ней. Было интересно углубить знания по Java, и наставничество рассматривал больше как пинок лично мне. Пинок, который поможет преодолеть лень, занятость и нехватку времени, чтобы узнать новое о языке и программировании.

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

Мне нравится помогать людям. Студент приходит со словами: «Слушай, у меня не получается, не могу понять» — и мы вместе разбираемся. Тогда я вижу, как у него появляется понимание темы. Для меня это заряд бодрости и уверенности в себе — нравится это чувство.

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

На странице с отзывами обо мне как наставнике оставили комментарий о том, как я рассказываю о терминах программирования. Парень написал, что я захожу сильно издалека, но в итоге студент все понимает. Действительно: данные, переменные, функции оператора могу объяснить на тортах и автобусах. Студент первого курса ожидает услышать заумные определения, а ты говоришь: «Представь автобус, а в нем злого кондуктора». У человека подвисание происходит: «Мы сейчас точно о программировании говорим?»

«Осознал, что в объяснении терминов захожу издалека, только после этого отзыва»

Такой подход дает результат — студенты действительно понимают, если объяснять на аналогиях и нестандартных примерах. Неподготовленным людям, которые решили стать программистами, сложно оперировать абстрактными понятиями. Студенту проще, если сравнивать термины с тем, что он видит каждый день и может потрогать: со ступеньками лестницы, деревьями. Мы живем в физическом мире и привыкли видеть ручку, бумагу, клавиатуру, мышку.

Когда мы работаем с компьютером, на экране выводится результат действий. В программировании мы пишем код — инструкцию, которую выполняет компьютер. Как он это делает, как преобразует данные? Сложно сходу проделать путь от инструкции к действию вместе с машиной. Если я написал десять инструкций, то должен понимать, что компьютер выдаст в конце. Некоторым людям сложно мыслить такими абстракциями, поэтому примеры «на кошках» помогают.

Однажды я был наставником на курсе Java-2. Со студентами сложились приятельские отношения, но после курса общение в чате прекратилось. Чуть позже мне написал один из ребят: «Валера, ты был наставником у нас на курсе. Может еще побудешь?» Оказалось, они хотят сделать собственный проект — построить приложение под iOS и Android. Говорят: «После курса и стажировки начали делать, но не доделали, а забрасывать не хотим. Чувствуем, что нужна твердая рука. Ты уже работаешь программистом, давай с нами». Я согласился, начали вместе пилить приложение.

Потрясающее было время — для меня это был курс IT-менеджера, тимлидера, скрам-мастера и product owner-а в одном флаконе. Приложение не довели до конца, но остался его скелет на iOS и Android. Надеюсь, что знания у ребят не пропали даром, а исходный код сохранился — может быть, когда-то они закончат».

 

Пройти обучение

19 окт 18, 13:24
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0

Работа за рубежом

Своими впечатлениями о работе, зарплате и развитии IT-рынка в Болгарии делится программист Олег Иванов.

- Олег, было бы здорово сначала узнать немного о Вашем пути до переезда. Кем начинали работать, с какими языками?

- Я начинал как Junior в небольшой конторе в Воронеже. Там требовался своего рода эникейщик, который поддерживал бы сайт и писал Perl-скрипты для обработки данных. Мне повезло: время от времени приходили новые проекты с другими требованиями к технологиям. Довелось писать и на Python, и на C++, и на Java. В итоге стек Java-технологий стал моим основным — и остается таким до сих пор.

Вид на Софию из окна офиса. Фото: Олег Иванов (с)

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

Никогда раньше я не проходил собеседований по всем правилам, со смешными вопросами вроде «Кем вы видите себя через пять лет?» и «Чем абстрактный класс отличается от интерфейса?». Однако пришлось.

- Как вы начали искать работу в иностранных компаниях?

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

Но так получилось, что в то время одна финская компания искала Java-разработчиков, и мне позвонила HR из рекрутингового агентства. Она нашла мое резюме на HeadHunter и предложила собеседование. Я согласился, потому что мне было очень интересно, как это выглядит. Собеседование проходило на английском. И хотя работу я тогда не получил, результат собеседования был для меня скорее положительным. Я осознал, что это не так страшно. Подумал, чем черт не шутит, и разместил резюме на Xing, плюс заполнил профиль в LinkedIn. Xing.com — это немецкий аналог LinkedIn, а я в первую очередь думал о Германии как о наиболее вероятном направлении.

Мое резюме находили — в основном на HeadHunter — российские и украинские HR. Мне предлагали релокацию в США, Нидерланды, Германию и Польшу. Были варианты с Прибалтикой. От продолжения некоторых собеседований я отказался в процессе из-за откровенно грабительских условий.

- Какого плана были «грабительские условия»?

- Нужно понимать, что в США и Западной Европе нет большого дефицита IT-специалистов, что бы там ни писали «в интернетах». Если американцы готовы нанять специалиста из России/Украины/Беларуси, они точно не намерены ему платить столько же, сколько американцу. Им это невыгодно — тем более когда есть индийцы, готовые трудиться за гораздо меньшие деньги, чем разработчики из бывшего СССР.

Но это не самое главное. Часто работодатель выдвигает условие, что ты не имеешь права перейти куда-то, скажем, в течение двух лет. Иначе ты будешь вынужден платить компенсацию. И тому подобные вещи мелким шрифтом. Особенно этим славятся работодатели из США. Я не отрицаю, что при наличии знакомств с прошлой работы там можно неплохо устроиться. Но если начинать с нуля, придется смириться, что уровень жизни может стать даже ниже, чем был в России.

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

- Как сложилось, что выбрали Болгарию?

Меня нашли на Xing рекрутеры, которые искали специалистов для компании в Болгарии. Я не выбирал эту страну раз и навсегда. Я останавливался на ней несколько раз, в том числе после переезда.

Если говорить о первом решении, на него повлияло несколько факторов. Во-первых, климат. Недаром на туристических сайтах Болгарию всегда наделяют эпитетом «солнечная». Это правда. Тут много солнца и значительно теплее, чем в Центральной России.

Во-вторых, Болгария является членом ЕС, что автоматически означает возможность трудоустройства по Blue Card — «Синей карте», которая подтверждает ваше право жить и работать в Евросоюзе.

В-третьих, в Болгарии говорят не на очередном языке германской группы, а на славянском, и пишут кириллицей. Это упрощает адаптацию. Немало людей здесь понимают и говорят по-русски.

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

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

- Какое ПО вы пишете? Пришлось ли осваивать что-то, связанное с местной спецификой?

- Cначала я работал в аутсорсинговой компании. Там успел поучаствовать в двух проектах из сферы «кровавого энтерпрайза». Первый — из области e-commerce, второй — оптовых продаж обуви. «Местной» специфики в Болгарии нет, мне хватило общих знаний о Java-стеке: Spring Framework и тому подобном.

Сейчас я работаю в трейдинговой компании. Здесь тоже никаких специфических знаний не требуется. На мой взгляд, для программиста гораздо важнее уметь разбираться в предметной области, чем во множестве технологий, большинство из которых устареют еще до конца года.

- Организация рабочего процесса отличается от российской?

- Перестраиваться не приходилось. Практически везде, как и у нас, используется Agile в разных ипостасях.

- Как в Болгарии с уровнем зарплат? Кстати, зарплаты «белые»?

- По моим наблюдениям, хороший разработчик (Java или front-end) может рассчитывать на 5 000 лева (2 500 евро) в месяц «чистыми». Если имеет дар убеждения — то и на большее. Это для Болгарии весьма неплохо. В Москве наверняка можно заработать лучше, но и потратишь значительно больше.

В Софии, насколько мне известно, все работают «в белую». Налоги не такие уж большие. Подоходный тут — 10%. Плюс иностранцам вряд ли выгодно зарплату в конверте получать.

- А как с конкуренцией? И в каком состоянии, на Ваш взгляд, сейчас IT-отрасль в Болгарии?

- Дефицит IT-специалистов здесь — не меньше 20 000 человек. Это из новостей за 2016 год, но я думаю, с тех пор ситуация не сильно изменилась. Болгары частенько стремятся сбежать в Западную Европу, и нехватку кадров тут восполнить сложнее, чем в той же Германии.

При этом Болгария старается всячески развивать IT-сектор: многих привлекают низкие налоги, поэтому здесь работают филиалы практически всех крупных аутсорсеров. Такие известные продуктовые компании, как VMWare, тоже имеют филиалы в Софии.

- По каким сайтам лучше ориентироваться, если хочешь прикинуть уровень зарплат для своего направления? Где искать вакансии?

- Есть сервис «БГ Работодател» — аналог Glassdoor для Болгарии. Там можно посмотреть зарплаты и почитать отрицательные отзывы о компаниях (Гугл-переводчик в помощь) — бывает полезно.

Что касается вакансий — есть, например, Rabota.bg. Но считаю, лучше не искать работу на сайтах, а обращаться со своим CV в компанию напрямую. Как минимум, у аутсорсеров всегда есть вакансии. Сам я в Болгарии работу через сайты не искал — меня HR находили.

- Вы говорите, что выбирали Болгарию несколько раз. Это связано со сменой работы уже после переезда? Или появлялись новые варианты в РФ и других странах?

- Новые варианты появляются регулярно: мне пишут на LinkedIn болгарские и европейские работодатели. Сейчас новое место не ищу, но всегда добавляю HR в свою сеть и отвечаю им, пусть даже отказом. Из РФ мне практически не пишут. Возможно, потому что LinkedIn сейчас в России заблокирован.

После переезда, конечно, были сомнения, правильный ли я сделал выбор. Но чем дольше я здесь живу, тем больше убеждаюсь, что это место мне по душе. Дело ведь не только в работе — жизнь не состоит из одного программирования. По крайней мере, для меня.

Все зависит от ожиданий. У меня были коллеги, которые ждали от Болгарии некой «европейскости». Но Балканы — это все же другое. Здесь нет лоска и ровных дорог, но есть красивая природа, спокойная обстановка и корректное отношение. София — довольно провинциальный город. Как и вся Болгария. И в этом огромные плюсы для меня. Хожу пешком на работу через парк каждый день. Из окна офиса неплохой вид :) И я точно зарабатываю не меньше болгар.

- Путешествуете по другим балканским странам?

- Да. Уже побывал в Сербии и Македонии. Вы, наверное, знаете, что Болгария до сих пор не в Шенгене. Поэтому для поездки в его страны нужна виза.

Здесь ее получить можно, но я бы рекомендовал тем, кто собирается переезжать, получить Шенген заранее. Потому что в России его для некоторых стран можно получить на два или три года. А в Болгарии его выдают на тот же срок, что и разрешение на пребывание. Большинство консульств здесь предпочитают давать визу на срок поездки.

Без Шенгена нельзя будет въехать в ту же Грецию, например. А до Эгейского моря от Софии расстояние примерно такое же, как до Черного.

Пройти обучение

26 июл 18, 10:29
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0

Как избежать тупиковых блокировок в Java

В прошлой статье мы обсуждали многопоточность. В этот раз поговорим о главной проблеме многопоточных приложений — тупиковых взаимных блокировках, известных как deadlocks. Такие блокировки возникают, когда минимум два потока одновременно пытаются работать с общими ресурсами и ограничить доступ к этим ресурсам друг для друга. При этом часто создаётся ситуация, когда ни один поток не может ни получить нужный ресурс, ни освободить занимаемый. Для блокировки «конкурентов» поток может использовать mutex, критическую секцию или семафор.

Как происходит блокировка

Два потока работают с общими ресурсами.

Поток 1 захватывает Ресурс 1 и начинает операции с ним.

Поток 2 последовательно захватывает Ресурс 2 и Ресурс 1.

Поток 2 не получает доступа к Ресурсу 1 и в ступоре ждёт, когда тот освободится.

Поток 1 не завершил работу с Ресурсом 1, но пытается захватить Ресурс 2 и тоже впадает в ступор.

Как это выглядит в коде:

public class DeadlockTest {  
 public static void main(String[] args) {  
   final String res1 = "my sample text";  
   final String res2 = "some other text";  

   // Пусть поток P1 навесит замок на ресурс res1, а затем на res2
   Thread P1 = new Thread() {  
     public void run() {  
         synchronized (res1) {  
          System.out.println("Поток 1 навесил замок на Ресурс 1");  
          try { Thread.sleep(100);} catch (Exception e) {}  

          synchronized (res2) {  
           System.out.println("Поток 1 навесил замок на Ресурс 2");  
          }
        }
     }
   }

   // Поток P2 последовательно пытается запереть доступ к res2 и res1
   Thread P2 = new Thread() {  
     public void run() {  
       synchronized (res2) {  
         System.out.println("Поток 2 навесил замок на Ресурс 2");  
         try { Thread.sleep(100);} catch (Exception e) {}  
         synchronized (res1) {  
           System.out.println("Поток 2 навесил замок на Ресурс 1");  
         }
       }
     }
   }

   P1.start();  
   P2.start();  

 }
}  

Видимо-невидимо

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

Есть несколько способов уберечь Java-приложение от «падений» и «зависаний», связанных с противоречиями в работе потоков. Это механизмы synchronized и volatile, алгоритмы, реализованные в классах Java Concurrent, но главное  — забота о структуре вашего приложения.

Ключевое слово volatile в Java

Модификатор volatile используют, когда нужно:

  • обеспечить видимость данных  — убедиться, что при обращении к переменной любой поток получит её последнее записанное значение;
  • исключить кэширование значений переменной и хранить их только в основной памяти.

Как только один поток записал что-то в volatile-переменную, значение идёт прямо в общую память и тут же доступно остальным потокам:

class CarSharingBase
{
  static volatile int your_car_ID = 3222233;
}

Но учтите, что модификатор volatile никак не ограничивает одновременный доступ к данным. А значит, в работу одного потока с полем может вмешаться другой поток. Вот что будет, если два потока одновременно получат доступ к операции увеличения на единицу (i++):

int i = 0;

Поток 1: читает переменную (0)

Поток 1: прибавляет единицу

Поток 2: читает переменную (0)

Поток 1: записывает значение (1)

Поток 2: прибавляет единицу

Поток 2: записывает значение (1)

Если бы два потока не мешали друг другу, а работали последовательно, мы получили бы на выходе значение «2», но вместо этого  видим единицу. Чтобы такого не происходило, нужно обеспечить атомарность операции. Атомарными называют операции, которые могут быть выполнены только полностью. Если они не выполняются полностью, они не выполняются вообще, но прервать их невозможно.

В примере с увеличением на единицу мы видим сразу три действия:  чтение, сложение, запись. Чтение и запись — операции атомарные, но между ними могут вклиниться действия другого потока. Поэтому составная операция инкремента (i++) полностью атомарной не является.

Обратите внимание: с volatile-переменной возможны как атомарные, так и неатомарные операции. Ключевое слово volatile позволяет сделать так, чтобы все потоки читали одно и то же из основной памяти, но не более того.

Простейший способ гарантировать атомарность  — выстроить потоки в очередь за ресурсами с помощью механизма synchronized. Представьте, что на электронный счёт одновременно переводят деньги два клиента. Уж лучше попросить одного из них немного подождать, чем допустить ошибки в денежных расчетах.

Ключевое слово synchronized

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

Используйте synchronized, чтобы:

  • обеспечить доступ только одного потока к методу или блоку единовременно;
  • обеспечить каждому работающему с ресурсами потоку видимость изменений, внесённых предыдущим потоком;
  • гарантировать, что операции внутри блока или метода будут выполнены полностью, либо не выполнены вовсе.

Метод может быть статическим или нет  — без разницы. Но синхронизация влияет на видимость данных в памяти. В прошлой статье мы говорили о взаимном исключении (mutex’e). С его помощью synchronized ограничивает доступ к данным. Образно говоря, это замок, с помощью которого поток запирается наедине с объектом, чтобы никто не мешал работать. Обратите внимание: замок запирают до начала работы. То есть проверка, не заняты ли ресурсы кем-то другим, происходит на входе в synchronized-блок или метод.

public class SynchronizeThis {
    private int syn_result;
    public synchronized int methodGet() {
        return syn_result;
    }
}

Разблокировка же ресурсов происходит на выходе. Поэтому атомарность операций гарантирована.

Данные, которые изменились внутри метода или блока sychronized, находятся в кэше поверх основной памяти и видны следующему потоку, к которому перешёл мьютекс.

Подсказки по блокирующей синхронизации

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

Никогда не используйте synchronized в конструкторе  — получите ошибку компиляции. А ещё остерегайтесь «матрёшек», когда синхронизированные методы одного класса вызывают внутри себя синхронизированные методы других классов.

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

Атомарность с помощью Java Concurrent

Вернёмся к составной операции «чтение-изменение-запись». Когда нам нужно развести потоки по углам, но без мьютекса, можно использовать инструкцию «сравнение с обменом»  — compare and swap (CAS).

Для этого сначала заводят переменную, по значению которой можно понять, заняты ли ресурсы и, если да,  — кем. Например, пока ресурсы свободны, переменная хранит «-1», а если заняты — номер процессора, который с ними работает (0,1 и т.д.).

Поток приходит за свободными ресурсами, видит «-1»,  перезаписывает значение на номер процессора, на котором сам работает, выполняет действия. После завершения всех операций переменной возвращается значение «-1». Если  же поток на входе видит номер какого-то процессора, он получает отказ и не может выполнить намеченную операцию. Это простейший случай сравнения с обменом. Важно понимать, что поток, который получил отказ, не блокируется. Он может сообщить программе, что у него проблемы, и перейти к запасному плану действий.

Можно сказать, что это более интеллигентная форма взаимодействия между потоками. Они уже не бодаются за ресурс и не закрывают дверь перед носом оппонента, а обмениваются сообщениями в духе: «Хотелось бы поработать вот с этим»  — «Извините, оно пока занято. Не желаете ли кофе?».

На этом принципе построен целый ряд алгоритмов синхронизации, которые называют неблокирующими (non-blocking). Создание таких алгоритмов  — задача не для новичка. Но, к статью, в Java «из коробки» есть несколько готовых неблокирующих решений. Они собраны в пакете java.util.concurrent.

ConcurrentLinkedQueue

На русский название класса переводится как «параллельная очередь». Работает такая очередь по принципу First In First Out («Первым зашёл  — первым выйдешь»). Алгоритм основан на CAS, быстр и оптимизирован под работу со сборщиком мусора.

Если вы хотите создать очередь из объектов, а затем добавлять и удалять их в нужный момент, не нужно писать и синхронизировать методы вручную. Достаточно создать классы для потоков, производящих и потребляющих данные, а затем поставить эти потоки в очередь ConcurrentLinkedQueue.

В прошлой статье мы говорили, что в Java поток можно создать как экземпляр класса Thread или как отдельный класс с интерфейсом Runnable. Сейчас мы используем второй подход. Единственный метод интерфейса Runnable —  run(). Чтобы задать нужное поведение для потребителя и производителя, мы будем переопределять этот метод в каждом случае по-своему.

Поток-производитель:

public class ProducerThread implements Runnable {
    @Override
    public void run() {
        System.out.println("Генерируем сообщения в очередь");
        try {
            for (int i = 1; i <= 10; i++) {
                QueueTest.enqueueTask("Задача номер " + i);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Поток-потребитель:

public class ConsumerThread implements Runnable {
    @Override
    public void run() {
        String task;
        System.out.println("Ждём задачи \n");
   //Пока есть задачи в очереди:
        while (QueueTest.isTaskHasBeenSet() || QueueTest.getQueue().size() > 0) {
            if ((task = QueueTest.getQueue().poll()) != null)
                System.out.println("Выполняю задачу : " + task);
            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

Обратите внимание, если очередь пуста, метод poll() вернёт значение null. Поэтому нам нужно было убедиться, что он возвращает что-то другое.

Чтобы узнавать, сколько всего элементов в очереди, у класса ConcurrentLinkedQueue есть метод size(). Он работает медленно, поэтому злоупотреблять им не стоит. При необходимости можно вывести весь список элементов очереди методом toArray(), но сейчас нам это не нужно.

Очередь, в которой будут работать потоки:

import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

public class QueueTest {
    private static Queue<String> queue = null;
    private static boolean taskHasBeenSet = false;

    public static void main(String[] args) {
        queue = new ConcurrentLinkedQueue<String>();
// Создаём и запускаем потребителя и производителя
        Thread producer = new Thread(new ProducerThread());
        Thread consumer = new Thread(new ConsumerThread());

        producer.start();
        consumer.start();

        while (consumer.isAlive()) {
            try {
    // Оставим время на ожидание постановки задач
                Thread.sleep(1000); 
            } catch (InterruptedException e) {
                 // Выводим трейс вместе с текстом исключения
                e.printStackTrace();
            }
        }
   // Всё выполнено нормально, выходим.
        System.exit(0);
    }

    public static Queue<String> getQueue() {
        return queue;
    }

    // Добавляем задачи в очередь
    public static void enqueueTask(String task) {
        try {
            queue.add(task);
            System.out.println("Добавлена задача : " + task);
            Thread.sleep(200);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public static boolean isTaskHasBeenSet() {
        return taskHasBeenSet;
    }
    
    public static void setTaskHasBeenSet(boolean taskHasBeenSet) {
        QueueTest.taskHasBeenSet = taskHasBeenSet;
    }
}

Запустите и посмотрите, как добавляются и выполняются задачи.

Атомарные классы

Пакет java.util.concurrent.atomic включает в себя классы для работы с:

  • примитивами  — AtomicBoolean, AtomicInteger, AtomicLong;
  • ссылочными типами   — AtomicReference;
  • массивами  — AtomicBooleanArray, AtomicIntegerArray, AtomicReferenceArray и др.;
  • аккумуляторами  — DoubleAccumulator, LongAccumulator;
  • обновлениями («апдейтерами»)  — AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater.

Давайте посмотрим, как два потока могут работать с переменной AtomicInteger без синхронизации. Для этого создадим класс myThread, в котором будет атомарный счетчик:

import java.util.concurrent.atomic.AtomicInteger;

class myThread extends Thread{
    public volatile AtomicInteger counter;
/* Для наглядности тестирования мы сделали поле волатильным.
   Данные не попадут в кэш и работу двух потоков будет легче отследить */

    myThread(AtomicInteger counter){
        this.counter = counter;
    }

    @Override
    public void run() {
        for(int i = 0; i < 1000; i++){
            counter.updateAndGet(n -> n + 2);
        }
// После работы счётчика в каждом потоке будем выводить значение:
        System.out.println(counter);
    }
}

Для операций над числами мы использовали метод updateAndGet() класса AtomicInteger. Этот метод увеличивает число на основе аргумента  — лямбда-выражения

Теперь посмотрим на всё это в действии. Создадим и запустим два потока myThread:

public class MultiCounter {
    public static void main(String[] args) {
        AtomicInteger myAtomicCounter = new AtomicInteger(0);

        // Создаём потоки
        myThread t1 = new myThread(myAtomicCounter);
        myThread t2 = new myThread(myAtomicCounter);

        t1.start();
        t2.start();
    }
}

В результате работы кода получим два значения: первое из них будет случайным числом в диапазоне от 2000 до 4000, а второе  — всегда 4000. Например, при первом запуске я получила результат:

2670

4000

Запустите код ещё раз и убедитесь, что меняется только первое значение. Второе число предсказуемо именно благодаря работе атомарного счётчика. Если бы мы использовали для счётчика обычный (неатомарный) Integer, второе число было бы случайным.

Блокирующие и неблокирующие алгоритмы в Java

Блокирующие алгоритмы парализуют работу потока  — навсегда или до момента, пока другой поток не выполнит нужное условие. Представьте, что поток A заблокирован и ждёт, когда поток B завершит операцию. Но тот не завершает, потому что заблокирован кем-то ещё. Случайно создать такую западню в приложении очень легко, а вот просчитать, когда она сработает  — трудно. Если без синхронизации можно обойтись, лучше обойдитесь — ради экономии нервов и ресурсов.

Неблокирующие алгоритмы тоже могут вести к проблемам, например, к live-lock. Это ситуация, когда несколько потоков буксуют:  продолжают работать, но без реального результата. Причиной может быть загруженность потоков сообщениями друг от друга. Чем больше сообщение, тем больше оно грузит память. Другая возможная причина  — неудачная реализация алгоритма, при которой в очереди возникает конфликт.  Поэтому начинающим лучше не мастерить велосипед, а сначала разобраться с чужими наработками.

Важно понимать, что панацеи не существует. Использовать блокирующие или неблокирующие алгоритмы, либо их комбинацию — придётся решать в каждом конкретном случае. Всё будет зависеть от структуры вашего приложения и от того, какие ресурсы вы делаете общими.

Пройти обучение

12 апр 18, 12:14
0 0
Статистика 1
Показы: 1 Охват: 0 Прочтений: 0
Показаны все темы: 5

Последние комментарии

нет комментариев
Читать

Поиск по блогу

Люди

7 пользователям нравится сайт lena2018.mirtesen.ru