• ,

Из системного администратора в Java Developer-а

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

Сегодня последний днем моего испытательного срока, хотя еще две недели назад мой teamleader сказал, что можешь не париться, ты остаешься.
Что я имею на сегодняшний день:
1. Работа в международной компании (все без исключений заказчики с запада в основном Шведция, Норвегия). Компания bodyshop.
2, Возможность ротации, customer-ов много, проекты разные, каждый может выбрать себе по душе
2. Приходящие носители языка дважды в неделю
3, Ежегодный выезд на заграничный корпоратив
4. Хорошая кухня, много плюшек
5. Замечательный коллектив, количество умных людей на 1 кв.м просто фантастическое

Но давайте вернемся немного назад, еще полтора года назад ничего и близко похожего у меня не было. Работал в государственной компании сисадмином, получал среднюю по больнице зарплату, английский знал посредственно, и в общем надежды, что жизнь начнет становиться лучше не было.
Случайно в соц. сетях наткнулся на сайт JavaRush.ru, один знакомый описывал, как прикольный способ стать программистом и, что он прошел за пару дней уже 5 уровней. Решил попробовать, т.к. еще в школе увлекался и даже хотел поступать в тогда еще УрГУ на мат.мех, но сбился с пути истинного. В общем чем я собственно хуже?

Дело за малым, не спеша за месяц прошел 10 уровней, и тут акция как раз на скидку в 50%, купил годовую подписку. Через месяц бросил работу админом и уехал зимовать в семьей в Таиланд, с мыслью, что буду учиться каждый день и ничего меня не остановит, к этому моменту усиленно учил английский, возможно тема для другой статьи, но мне больше всего помогли стартануть подкасты на английском языке начинал с VOA, а потом по нарастающей с приходом понимания увеличивал сложность. Если кому то интересно более подробно, спрашивайте в комментах.

План я свой начал реализовывать сразу как только мы приехали в город Ао Нанг провинции Краби и нашли себе домик на сезон. Могу сказать одно, нужно заниматься регулярно и все получиться, у меня было примерно 8-12 часов работы 5 дней в неделю, куда входили как написания кода так и занятия английским языком. Могу сказать одно даже в новый год я начал работать 2го января. Ни каких поблажек себе не давал, хотя конечно были моменты когда падает мотивация и хочется все бросить, очень помогали истории успеха тут на сайте, дал себе слово тогда, что обязательно напишу свою, может быть кому то она поможет в трудную минуту найти в себе силы и победить валидатор (ресторан и архиватор, привет) + 20-ти летний опыт занятий спортом тут очень помог, делал все по расписанию.
В выходные с женой и дочкой путешествовали на мотоцикле по ближайшим провинциям Таиланда, наслаждались природой и погодой. Отдыхал по максимуму, это тоже часть работы, хотя иногда в выходные садился за код или книжку по Java, когда уж прям совсем невтерпеж, но старался придерживаться режима.

Примерно после нового года я был уровне на 25-30, решил тогда, что все я крутой программист(на самом деле нет), пора устраиваться работать, сделал резюме на hh, посыпались заявки, опять же описывать как я писал его не буду, но делал его пару недель, и потом регулярно дополнял, потом еще перевел на английский, оформил все в лучшем виде, хоть и опыта не было в программировании, но оно явно приглянулось hr-ам, действительно довольно часто писали. Естественно первый несколько собеседований вернули меня на землю, понял как много я еще не знаю и то, что знаю, нужно было конкретно структурировать, чем я и занялся.

После этого встал выбор куда идти Android или JavaEE попробовал первое, прошел книжку HeadFirst и в целом примерно 30 мелких проектов написал, но тут пользуясь одним из сайтов по изучению английского увидел, как круто у них сделана верстка под мобильные и как хреново работает мобильное приложение. Призадумался и принял решение учить JavaEE, забегая в перед могу сказать, что идею учить Android не бросил и сейчас в планах освоить его. Тем не менее на тот момент отказался от идеи android. Долго ли коротко, начал учить сопутствующие технологии которые обязательны sql/maven/git/spring/hibernate и наверно отдельно выделю rest архитектуру, про нее часто спрашивают и вообще первые же два моих проекта которые ушли в production, это rest api.
Все это нужно знать, все это спрашивают, но знать можно поверхностно, написать 10-ок другой мелких проектов используя их, к примеру задание на стажировку реально пишется за пол дня, день, без фронта, он нафиг на Java не нужен, можно немного поучить JS/React/Node, если есть желание идти на фронт, но можно вообще только их учить и вообще не учить Java, там работы и заказчиков много больше, но там своя специфика.

В общем, примерно мае я вернулся в Россию, потому что были тут дела и решил, что пора устраиваться в офис, до этого все собеседования проходил по Skype (офер не получил ни разу на тот момент).
Приехал начал переодически ходить по собеседованиям, мне это очень не нравится ибо забирает очень много моральных и физических сил, и сильно мешает процессу учебы и написанию кода в целом(Skype помогает), поэтому ходил пару раз в неделю, где то нужны были более опытные люди, где то мне не понравилось, но в итоге прошел очередное собеседование по Skype, меня хотели отправить сразу же работать onsite в Швецию, естественно опыта моего не достаточно было, но я понравился teamleader-у и он меня посоветовал на внутренний проект, после чего я прошел еще одно собеседование уже в офисе у нас в Екатеринбурге, где задали пару вопросов и парочку на английском, после чего сказали, английский твой цитирую: «бывает и хуже», что конечно задело эго, но опять же дало пинок учить дальше, отпустили «погулять» и через 10 минут сделал мне офер, я на тот момент ожидал сильно меньший. Офер я естественно принял.

Хочу отдельно остановиться на первых месяцах работы, почему то мало кто про них пишет, но для меня они были настоящим кошмаром, я думал меня уволят честно говоря. Было адски сложно, дали задачу сразу же писать свой проект, вернее часть проекта один micro service, но для меня он был скорее огромным монстром, потому что мне нужно было продумывать все начиная от архитектуры и используемых фреймворков до exception handling. Сильно нервничал и думал, что от меня ждут, что это все сделаю за пару недель, что очень мешало сосредоточится на задаче и принимать верные решения. В итоге огромная благодарность моему тимлиду, он много помогал мне, объяснил многие базовые вещи и вообще ни как на меня не давил.

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

Простите за длинный текст, и спасибо всем кто дочитал до конца.

ps: думаю по мере опыта, буду дополнять статью мелкими заметками, если кому то будет интересна эта моя писанина
  • ,

Менторство

Решил попробовать себя в наставничестве. Безвозмездно. Ориентируюсь на людей где-то с 20-го уровня, которые начинают делать веб-приложение.
Варианты взаимодействия.
-Можете задавать вопросы.
-Ревью кода какого-нибудь pet project'a.
-Можно выбрать тему и я по ней что-нибудь расскажу/дам ссылки/может, придумаю какое-нибудь задание, которое потом, может, посмотрю.
В нескольких словах о себе: закончил JavaRush, 1.5 года работаю Java-разработчиком.
Если кого-то заинтересует — добавляйтесь лучше сюда

Что такое дедлок

Перевод грамотного объяснения примера из javadocs со Stackoverflow

public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s" + "  has bowed to me!%n", this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s"
                + " has bowed back to me!%n",
                this.name, bower.getName());            
        }
    }

    public static void main(String[] args) {
        final Friend alphonse =
            new Friend("Alphonse");
        final Friend gaston =
            new Friend("Gaston");
        new Thread(new Runnable() {
            @Override
            public void run() { 
               // System.out.println("Thread 1");
                alphonse.bow(gaston); 
               // System.out.println("Th: gaston bowed to alphonse");
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() { 
              //  System.out.println("Thread 2");
                gaston.bow(alphonse);
              //  System.out.println("2.gaston waiting alph bowed");
            }
        }).start();
    }
}


Здесь нужно понять две важные вещи:
1) Что делает каждая из одновременно выполняющихся нитей?
2) Какие локи используются?

Начнем с конца. Вы создали два объекта класса Friend: alphonse и gaston. У каждого из них есть свой лок. Так что локов два: альфонсов и гастонов. При входе в синхронизированный метод объекта, лок этого объекта запирается, и освобождается — отпирается — когда из метода выходят.

Теперь о нитях. Первая, назовем ее нить Alphonse (с большой буквы, чтобы отличить от объекта alphonse) делает следующее (A — обозначает Alphonse)

A: alphonse.bow(gaston) — получает лок alphonse
A: gaston.bowBack(alphonse) — получает лок gaston
A: возвращается из обоих методов, тем самым освобождая лок

В это самое время нить Gaston…

G: gaston.bow(alphonse) — получает лок gaston
G: alphonse.bowBack(gaston) — получает лок alphonse
G: возвращается из обоих методов, тем самым освобождая лок

Теперь сведем эти данные вместе и получим ответ. Нити могут переплетаться (т. е. их события совершаться) в разных порядках. Дедлок, к примеру, получится, если опрядок будет таким:

A: alphonse.bow(gaston) — получает лок alphonse
G: gaston.bow(alphonse) — получает лок gaston
G: пытается вызвать alphonse.bowBack(gaston), но блокируется, ожидая лока alphonse
A: пытается вызвать gaston.bowBack(alphonse), но блокируется, ожидая лока gaston

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

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

A: alphonse.bow(gaston) — получает лок alphonse
A: gaston.bowBack(alphonse) — получает лок gaston
A: возвращается из обоих методов, открывая оба лока
G: gaston.bow(alphonse) — получает лок gaston
G: alphonse.bowBack(gaston) — получает лок alphonse
G: возвращается из обоих методов, открывая оба лока

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

Когда результат зависит от порядка одновременно происходящих событий (запланированного порядка или скорости выполнения), это называется «race condition» — «состоянием гонки». Не все race condition потенциально производят дедлок, однако, по моему опыту, дедлоки происходят только в race condition.

level39.lesson09.big01 Задание 1

Помогите понять, что не нравится валидатору. Спасибо!

Задание 1

Сегодня мы напишем парсер логов.
Лог файл имеет следующий формат:
ip username date event status
Где:
ip — ip адрес с которого пользователь произвел событие.
user — имя пользователя (одно или несколько слов разделенные пробелами).
date — дата события в формате day.month.year hour:minute:second
event — одно из событий:
LOGIN — пользователь залогинился,
DOWNLOAD_PLUGIN — пользователь скачал плагин,
WRITE_MESSAGE — пользователь отправил сообщение,
SOLVE_TASK — пользователь попытался решить задачу,
DONE_TASK — пользователь решил задачу.
Для событий SOLVE_TASK и DONE_TASK существует дополнительный параметр,
который указывается через пробел, это номер задачи.
status — статус:
OK — событие выполнилось успешно,
FAILED — событие не выполнилось,
ERROR — произошла ошибка.
Пример строки из лог файла:
«146.34.15.5 Eduard Petrovich Morozko 05.01.2021 20:22:55 DONE_TASK 48 FAILED».
Записи внутри лог файла не обязательно упорядочены по дате, события могли
произойти и быть записаны в лог в разной последовательности.

Класс, который будет отвечать за парсинг логов называется LogParser.
1.1. Добавь в класс LogParser конструктор с парметром Path logDir, где logDir — директория с логами (логов может быть несколько, все они должны иметь расширение log).
1.2. Реализуй интерфейс IPQuery у класса LogParser:
1.2.1. Метод getNumberOfUniqueIPs(Date after, Date before) должен возвращать
количество уникальных IP адресов за выбранный период. Здесь и далее,
если в методе есть параметры Date after и Date before, то нужно возвратить
данные касающиеся только данного периода (включая даты after и before).
Если параметр after равен null, то нужно обработать все записи, у которых
дата меньше или равна before.
Если параметр before равен null, то нужно обработать все записи, у которых
дата больше или равна after.
Если и after, и before равны null, то нужно обработать абсолютно все записи
(без фильтрации по дате).
1.2.2. Метод getUniqueIPs() должен возвращать множество, содержащее все
неповторяющиеся IP. Тип в котором будем хранить IP будет String.
1.2.3. Метод getIPsForUser() должен возвращать IP, с которых работал
переданный пользователь.
1.2.4. Метод getIPsForEvent() должен возвращать IP, с которых было произведено
переданное событие.
1.2.5. Метод getIPsForStatus() должен возвращать IP, события с которых
закончилось переданным статусом.

Реализацию метода main() можешь менять по своему усмотрению.

package com.javarush.test.level39.lesson09.big01;

import com.javarush.test.level39.lesson09.big01.query.IPQuery;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

public class LogParser implements IPQuery
{
    private Path logDir;

    public LogParser(Path logDir)
    {
        this.logDir = logDir;
    }

    @Override
    public int getNumberOfUniqueIPs(Date after, Date before)
    {
        return getUniqueIPs(after, before).size();
    }

    @Override
    public Set<String> getUniqueIPs(Date after, Date before)
    {
        Set<String> resultSet = new HashSet<>();
        List<String> list = getLinesBetweenDates(after, before);
        for (String str : list)
            resultSet.add(str.split("\\t")[0]);
        return resultSet;
    }

    @Override
    public Set<String> getIPsForUser(String user, Date after, Date before)
    {
        Set<String> resultSet = new HashSet<>();
        List<String> list = getLinesBetweenDates(after, before);
        for (String str : list)
        {
            String[] values = str.split("\\t");
            String userFromFile = values[1];
            if (userFromFile.equals(user))
                resultSet.add(values[0]);
        }
        return resultSet;
    }

    @Override
    public Set<String> getIPsForEvent(Event event, Date after, Date before)
    {
        Set<String> resultSet = new HashSet<>();
        List<String> list = getLinesBetweenDates(after, before);
        for (String str : list)
        {
            String[] values = str.split("\\t");
            String eventFromFile;
            if (values[3].contains(" "))
                eventFromFile = values[3].split(" ")[0];
            else eventFromFile = values[3];
            if (Event.valueOf(eventFromFile).equals(event))
                resultSet.add(values[0]);
        }
        return resultSet;
    }

    @Override
    public Set<String> getIPsForStatus(Status status, Date after, Date before)
    {
        Set<String> resultSet = new HashSet<>();
        List<String> list = getLinesBetweenDates(after, before);
        for (String str : list)
        {
            str = str.trim();
            String[] values = str.split("\\t");
            if (Status.valueOf(values[values.length - 1]).equals(status))
                resultSet.add(values[0]);
        }
        return resultSet;
    }

    private List<String> getLinesFromPath()
    {
        List<String> resultList = null;
        try
        {
            if (logDir != null)
            {
                File[] files = logDir.toFile().listFiles();
                if (files != null)
                {
                    for (File file : files)
                    {
                        resultList = Files.readAllLines(file.toPath(), Charset.defaultCharset());
                    }
                }
            }
        }
        catch (IOException e)
        {
        }
        return resultList;
    }

    private Date getDateFromString(String source)
    {
        Date date = null;
        SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
        try
        {
            date = dateFormat.parse(source);
        }
        catch (ParseException e)
        {
        }
        return date;
    }

    private List<String> getLinesBetweenDates(Date after, Date before)
    {
        List<String> list = getLinesFromPath();
        List<String> resultList = new ArrayList<>();
        if (list != null)
        {
            if (after == null && before == null)
            {
                for (String line : list)
                    resultList.add(line);
            } else if (after == null)
            {
                for (String line : list)
                {
                    String[] values = line.split("\\t");
                    Date dateFromLine = getDateFromString(values[2]);
                    if (dateFromLine != null && dateFromLine.compareTo(before) <= 0)
                        resultList.add(line);
                }
            } else if (before == null)
            {
                for (String line : list)
                {
                    String[] values = line.split("\\t");
                    Date dateFromLine = getDateFromString(values[2]);
                    if (dateFromLine != null && dateFromLine.compareTo(after) >= 0)
                        resultList.add(line);
                }
            } else
            {
                for (String line : list)
                {
                    String[] values = line.split("\\t");
                    Date dateFromLine = getDateFromString(values[2]);
                    if (dateFromLine != null && dateFromLine.compareTo(after) >= 0 && dateFromLine.compareTo(before) <= 0)
                        resultList.add(line);
                }
            }
        }
        return resultList;
    }


}

Рецензия на книгу Head First HTML



… а чай каркаде выпил, похмелья нет, голова на утро не болит. Испытано на самом себе!
© неизв. коробейник в пригородной электричке

Добрый день, уважаемые джаваршевцы и джаварашатессы!
Как учат старшие товарищи по партии, одного языка Java мало, надо еще о микологии смежных областях иметь представление. И вот я, ведомый такими напутствиями, изучил книгу «Изучаем HTML, XHTML и CSS» Элизабет Фримен и Эрика Фримен (Head First HTML with CSS & XHTML By Eric Freeman, Elisabeth Robson). Книга доступна на русском языке в бумажном и электронном виде.
Собственно говоря, мнение о книге такое же, как и об «Изучаем Java», которую описывал здесь. Кратко подитожу.
Книга подходит: а) для изучения HTML & CSS с нуля и получения общего представления о них; б) в качестве материала, используемого для уяснения (понимания) темы; в) для повторения тем, понимание которых пропало.
Кроме этого. Книга максимально далека по стилю изложения от академических учебников. Задачи на усвоение материала весьма специфичны. Как следствие, по каждой теме необходимо придумывать задачи по написанию соответствующего кода самостоятельно.
Доступность материала для понимания не влечет автоматической легкости его запоминания. Необходимо прикладывать дополнительные значительные усилия как для запоминания изученых концепций, так и для запоминания фактической информации.
Я усилия для запоминания материала не прикладывал. Задачи по написанию кода себе не придумывал. Как следствие, уже многое забылось и сайт так сразу не напишу.

С уважением,
Зеленая лягушка.
  • ,

Создание простого веб-приложения на сервлетах и jsp (часть 1)

Эта статья является продолжением моей статьи Создание простейшего web проекта в intellij idea enterprise edition. Пошагово, с картинками, в которой я показал, как создать работающий шаблон веб-проекта.

В этот раз я покажу как создать довольно простое, но вполне симпатичное веб-приложение, используя технологии Java Servlet API и JavaServer Pages API.

Наше приложение будет иметь главную страницу с двумя ссылками:
  1. на страницу добавления пользователя
  2. на страницу просмотра списка пользователей

Так же, буду использовать Intellij Idea Enterprise Edition, Apache Maven (просто подключим несколько зависимостей) и Apache Tomcat. В конце «украсим» наше приложение используя W3.CSS CSS фреймворк.
  • ,

развертывание веб сервиса на Tomcat в Intellij Idea

Здравствуйте.

пытаюсь второй день развернуть веб сервис по разным туториалам, через Идею.
Прочитал кучу мануалов и документации по этой теме, но результат все так же печален — из среды запускаю свою конфигурацию, но по адресу localhost:8080 у меня видно следующее:



за безупречный пример я взял вот эту статью: devcolibri.com/1043
если делать все в точности как тут описано — все работает.
Однако, структура моего проекта, который я создал на основе всего того, что прочел несколько отличается:


Конфигурация моего проекта выглядит вот так:


уж не знаю все ли я верно сделал.
Может кто подсказать, что я делаю не так? Идея запускает томкат, но в логах ошибок нет, все выглядит так, будто все работает.
Подозреваю, что war файл не кладется куда-то. Но куда и как должен именно в идее я не понимаю…
  • ,

развертывание веб сервиса на Tomcat в Intellij Idea

Здравствуйте.

пытаюсь второй день развернуть веб сервис по разным туториалам, через Идею.
Прочитал кучу мануалов и документации по этой теме, но результат все так же печален — из среды запускаю свою конфигурацию, но по адресу localhost:8080 у меня видно следующее:



за безупречный пример я взял вот эту статью: devcolibri.com/1043
если делать все в точности как тут описано — все работает.
Однако, структура моего проекта, который я создал на основе всего того, что прочел несколько отличается:


Конфигурация моего проекта выглядит вот так:


уж не знаю все ли я верно сделал.
Может кто подсказать, что я делаю не так? Идея запускает томкат, но в логах ошибок нет, все выглядит так, будто все работает.
Подозреваю, что war файл не кладется куда-то. Но куда и как должен именно в идее я не понимаю…

Управление непостоянством (volatility)

Указания по использованию volatile-переменных


Автор Брайан Гётц, 19 июня 2007 года. Оригинал: Managing Volatility

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

У локов (locks) есть две основные черты: взаимное исключение (mutual exclusion, mutex) и видимость. Взаимное исключение означает, что лок может быть захвачен только одной нитью в отдельный момент времени, и это свойство можно использовать для реализации протоколов управления доступом к общедоступным ресурсам, так что только одна нить будет их использовать в отдельный момент времени. Видимость — вопрос более тонкий, ее задача обеспечить, что изменения, сделанные в общедоступных ресурсах до освобождения замка будут видимы следующей нити, захватившей этот замок. Если бы синхронизация не гарантировала видимость, нити могли бы получать устаревшие или неверные значения общедоступных переменных, что привело бы к целому ряду серьезных проблем.

Volatile-переменные

Volatile-переменные обладают свойствами видимости, присущими synchronized, но лишены их атомарности. Это означает, что нити автоматически будут использовать самые актуальные значения volatile-переменных. Их можно использовать для нитебезопасности (thread safety, чаще переводится как потокобезопасности), но в очень ограниченном наборе случаев: тех, что не вводят связи между несколькими переменными или между текущими и будущими значениями переменной. Таким образом, одной volatile недостаточно для реализации счетчика, мьютекса или любого класса, чьи неизменные части связаны с несколькими переменными (например, «start <=end»).

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

Условия правильного использования volatile

Заменить локи на volatile можно в ограниченном числе обстоятельств. Для нитебезопасности необходимо, чтобы выполнялись оба критерия:

  1. То, что записывается в переменную, не зависит от ее текущего значения.
  2. Переменная не участвует в инвариантах с другими переменными.

Проще говоря, эти условия означают, что корректные значения, которые могут быть записаны в volatile-переменную, не зависят от любого другого состояния программы, включая текущее состояние переменной. Первое условие исключает использование volatile-переменных как нитебезопасных счетчиков. Хотя инкремент (x++) выглядит как одна операция, в действительности это целая последовательность операций чтения-изменения-записи, которая должна выполняться атомарно, чего volatile не обеспечивает. Корректная операция требовала бы, чтобы значение x оставалось неизменным в течение всей операции, чего нельзя добиться с помощью volatile. (Однако если вам удастся обеспечить, что значение будет записываться только из одной нити, первое условие можно опустить).

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

@NotThreadSafe 
public class NumberRange {
    private int lower, upper;
 
    public int getLower() { return lower; }
    public int getUpper() { return upper; }
 
    public void setLower(int value) { 
        if (value > upper) 
            throw new IllegalArgumentException(...);
        lower = value;
    }
 
    public void setUpper(int value) { 
        if (value < lower) 
            throw new IllegalArgumentException(...);
        upper = value;
    }
}


Поскольку переменные состояния диапазона ограничены таким образом, будет недостаточным сделать поля lower и upper volatile, чтобы обеспечить нитебезопасность класса; по-прежнему будет нужна синхронизация. Иначе рано или поздно не повезет и две нити, выполнившие setLower() и setUpper() с неподходящими значениями могут привести диапазон в противоречивое состояние.

Например, если начальное значение (0, 5), нить A вызывает setLower(4), и в то же время нить B вызывает setUpper(3), эти перемежающиеся операции приведут к ошибке, хотя обе пройдут проверку, которая должна защищать инвариант. В итоге диапазон будет (4, 3) — неверные значения. Нам нужно сделать setLower() и setUpper() атомарными по отношению к другим операциям над диапазоном — и присвоение полям volatile этого не сделает.

Соображения производительности

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

Чрезвычайно трудно сделать точные всеобъемлющие заявления вида «X всегда быстрее, чем Y,» особенно когда речь идет о внутренних операциях виртуальной машины Java. (Например, JVM может полностью снять блокировку в некоторых ситуациях, что затрудняет абстрактное обсуждение затрат на volatile по отношению к синхронизации). Тем не менее, на большинстве современных процессорных архитектур затраты на чтение volatile мало отличаются от затрат на чтение обычных переменных. Затраты на запись volatile значительно больше, чем на запись обычных переменных, из-за ограждения памяти, необходимого для обеспечения видимости, но в целом дешевле, чем установка локов.

Паттерны для правильного использования volatile
Многие эксперты по параллелизму склонны избегать использования volatile-переменных вообще, потому что их труднее использовать правильно, чем локи. Однако существуют некоторые четко определенные паттерны, которые, если следовать им внимательно, могут безопасно использоваться в самых разных ситуациях.
Всегда учитывайте ограничения volatile — используйте только volatile, которые никак не зависят от всего остального в программе, и это должно не позволить вам залезть с этими паттернами на опасную территорию.
Паттерн №1: флаги состояния
Возможно, каноническое использование изменчивых переменных — это простые булевы флаги состояния, указывающие на то, что произошло важное одноразовое событие жизненного цикла, такое как завершение инициализации или запрос на завершение работы. Многие приложения включают конструкцию управления формы: «Пока мы не готовы выключиться, продолжаем работать», как показано в листинге 2:

volatile boolean shutdownRequested;
 
...
 
public void shutdown() { shutdownRequested = true; }
 
public void doWork() { 
    while (!shutdownRequested) { 
        // do stuff
    }
}


Вероятно, метод shutdown () будет вызываться откуда-то извне цикла — в другой нити — поэтому требуется синхронизация для обеспечения правильной видимости переменной shutdownRequested. (Он может быть вызван из слушателя JMX, слушателя действий в нити событий GUI, через RMI, через веб-службу и т. д.). Однако цикл с синхронизированными блоками будет гораздо громоздким, чем цикл с volatile-флагом состояния как в листинге 2. Поскольку volatile упрощает написание кода, а флаг состояния не зависит от какого-либо другого состояния программы, это пример хорошего использования volatile.

Для таких флагов статуса характерно то, что обычно существует только один переход состояния; флаг shutdownRequested переходит из false в true, а затем программа выключается. Этот паттерн можно расширить до флагов состояния, которые могут изменяться туда и обратно, но только если цикл перехода (от false до true to false) будет происходить без внешних вмешательств. В противном случае необходим какой-то атомарный механизм перехода, такой как атомарные переменные.

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

Ошибки видимости, которые возможны при отсутствии синхронизации, могут стать еще более сложным вопросом, когда происходит запись ссылок на объекты вместо примитивных значений. Без синхронизации можно увидеть актуальное значение для ссылки на объект, которое было записано другой нитью, и по-прежнему видеть устаревшие значения состояния этого объекта. (Эта угроза лежит в корне проблемы с печально известной блокировкой с двойной проверкой, где ссылка на объект считывается без синхронизации, и вы рискуете увидеть актуальную ссылку, но получить через нее частично сконструированный объект.)

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

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;
 
    public void initInBackground() {
        // делаем много всякого
        theFlooble = new Flooble();  // единственная запись в theFlooble
    }
}
 
public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // чё-то там делаем...
            // используем theFolooble, но только если она готова
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}


Если бы ссылка на theFlooble не была volatile, код в doWork() рисковал бы увидеть частично сконструированный Flooble при попытке обратиться по theFlooble.

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

Паттерн № 3: независимые наблюдения

Другой простой пример безопасного применения volatile — ситуация, когда наблюдения периодически “публикуются” для использования в рамках программы. Например, есть датчик окружающей среды, который определяет текущую температуру. Фоновая нить может считывать показания этого датчика с периодом в несколько секунд и обновлять volatile-переменную, содержащую текущую температуру. Затем другие нити могут считывать эту переменную, зная, что значение в ней всегда самое актуальное.

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

public class UserManager {
    public volatile String lastUser;
 
    public boolean authenticate(String user, String password) {
        boolean valid = passwordIsValid(user, password);
        if (valid) {
            User u = new User();
            activeUsers.add(u);
            lastUser = user;
        }
        return valid;
    }
}


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

Паттерн № 4: паттерн «volatile bean»

Паттерн “volatile bean” применим во фреймворках, использующих JavaBeans как “glorified structs”. В паттерне “volatile bean” JavaBean используется как контейнер для группы независимых свойств с геттерами и/или сеттерами. Обоснованием необходимости паттерна “volatile bean” является то, что многие фреймворки предоставляют контейнеры для изменяемых держателей данных (например, HttpSession), но объекты, помещенные в эти контейнеры, должны быть нитебезопасными.

В патттерне volatile bean все элементы данных JavaBean являются volatile, а геттеры и сеттеры должны быть тривиальными — они не должны содержать никакой логики, кроме получения или установки соответствующего свойства. Кроме того, для членов данных, которые являются объектными ссылками, упомянутые объекты должны быть эффективно неизменными. (Это запрещает наличие полей-ссылок на массивы, так как когда ссылка массива объявлена volatile, только эта ссылка, а не сами элементы, имеет свойство volatile.) Как и в любой volatile-переменной, не может быть никаких инвариантов или ограничений, связанных с свойствами JavaBeans. Пример JavaBean, написанного по паттерну “volatile bean”, показан в листинге 5:

@ThreadSafe
public class Person {
    private volatile String firstName;
    private volatile String lastName;
    private volatile int age;
 
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
 
    public void setFirstName(String firstName) { 
        this.firstName = firstName;
    }
 
    public void setLastName(String lastName) { 
        this.lastName = lastName;
    }
 
    public void setAge(int age) { 
        this.age = age;
    }
}


Более сложные volatile-паттерны

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

Более продвинутые паттерны использования volatile могут быть чрезвычайно хрупкими. Крайне важно, чтобы ваши предположения были тщательно задокументированы, а эти паттерны сильно инкапсулироваными, потому что даже мельчайшие изменения могут сломать ваш код! Кроме того, учитывая, что основной причиной для более сложных вариантов использования volatile является производительность, убедитесь, что у вас действительно есть выраженная потребность в предполагаемом усилении производительности, прежде чем применять их. Эти паттерны являются компромиссами, которые жертвуют читабельностью или легкостью поддержки ради возможного повышения производительности — если вам не требуется повышение производительности (или вы не можете доказать, что вам это нужно, с помощью строгой программы измерения), то это, вероятно, плохая сделка, потому что вы отказываетесь от чего-то ценного и получаете что-то меньшее взамен.

Паттерн № 5: дешевый лок чтения-записи

Сейчас вы должны уже хорошо понимать, что volatile слишком слаба для реализации счетчика. Поскольку ++ x по факту сокращение трех операций (чтение, добавление, хранение), при неудачном стечении обстоятельств вы потеряете обновленное значение, если несколько потоков попытаются одновременно увеличить volatile-счетчик.

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

@ThreadSafe
public class CheesyCounter {
    // Employs the cheap read-write lock trick
    // All mutative operations MUST be done with the 'this' lock held
    @GuardedBy("this") private volatile int value;
 
    public int getValue() { return value; }
 
    public synchronized int increment() {
        return value++;
    }
}


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

Резюме

Volatile-переменные — это более простая, но более слабая форма синхронизации, чем блокировка, которая в некоторых случаях обеспечивает лучшую производительность или масштабируемость, чем встроенная блокировка. Если вы соблюдаете условия безопасного использования volatile — переменная действительно независима и от других переменных, и от своих собственных предыдущих значений — иногда вы можете упростить код, заменив synchronized на volatile. Однако код с использованием volatile часто бывает более хрупким, чем код с блокировкой. Предлагаемые здесь паттерны охватывают наиболее распространенные случаи, когда волатильность — разумная альтернатива синхронизации. Следуя этим паттернам — и заботясь о том, чтобы не вытеснять их за их собственные пределы — вы сможете безопасно использовать volatile в тех случаях, когда они дают выигрыш.
  • ,

Популярно о лямбдах в java. С примерами и задачами.

Для кого предназначена эта статья?
  • для тех, кто считает, что уже неплохо знает Java Core, но понятия не имеет о лямбдах в джаве. Ну может слышал там что-то про лямбды, но хз что это за ужасы такие.
  • для тех, кто имеет какое-то понимание о лямбдах, но до сих пор боится их использовать.