Общее·количество·просмотров·страницы

Java Dev Notes - разработка на Java (а также на JavaScript/Python/Flex и др), факты, события из АйТи

Показаны сообщения с ярлыком Java. Показать все сообщения
Показаны сообщения с ярлыком Java. Показать все сообщения

суббота, 15 марта 2014 г.

Семафоры в Java: реализация простого семафора

Семафор используется для обмена сигналами между потоками, или же для охраны критической секции. Их также можно использовать и вместо локов. Несмотря на то, что в JDK уже реализован семафор (java.util.concurrent.Semaphore), полезно будет самим реализовать этот объект.

Итак, у нашего семафора будет всего лишь два метода: take, release. Соответственно, простейшая реализация будет такой:

public class SimpleSemaphore {
    boolean taken = false;

    public synchronized void take() {
        this.taken = true;
        this.notify();
    }

    public synchronized void release() throws InterruptedException {
        while (!this.taken) wait();
        this.taken = false;
    }
}

Теперь давайте рассмотрим программу, которая использует семафор для обмена сигналами. У нас будет два потока: SignalSender, SignalReceiver, которые будут посылать друг другу сигналы. Вот код:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        SimpleSemaphore semaphore = new SimpleSemaphore();
        new Thread(new SignalSender(semaphore)).start();
        Thread.currentThread().sleep(2000);
        new Thread(new SignalReceiver(semaphore)).start();
    }


    static class SignalSender implements Runnable {
        private final SimpleSemaphore semaphore;

        public SignalSender(SimpleSemaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            System.out.println("[SignalSender] run");
            while (true) {
                try {
                    doSomeWork();
                    semaphore.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

        private void doSomeWork() throws InterruptedException {
            System.out.println("[SignalSender] do some work");
            Thread.sleep(500);
        }
    }

    static class SignalReceiver implements Runnable {
        private final SimpleSemaphore semaphore;

        public SignalReceiver(SimpleSemaphore semaphore) {
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
            System.out.println("[SignalReceiver] run");
            while (true) {
                try {
                    semaphore.release();
                    doSomeWork();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }

        private void doSomeWork() throws InterruptedException {
            System.out.println("[SignalReceiver] do some work");
            Thread.sleep(700);
        }
    }
}

Посмотрим на вывод консоли:

[SignalSender] run
[SignalSender] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalReceiver] run
[SignalReceiver] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalReceiver] do some work
[SignalSender] do some work
[SignalSender] do some work
[SignalReceiver] do some work
Мы видим, как потоки обмениваются сигналами, при этом они ждут друг друга, и начинают работу, только получив соответствующий сигнал от семафора.

пятница, 14 марта 2014 г.

Элементарная реализация BlockingQueue на Java

BlockingQueue (блокирующая очередь) - это очередь, которая блокирует поток, в двух случаях:
  • поток пытается получить элементы из пустой очереди
  • поток пытается положить элементы в полную очередь

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

В Java есть интерфейс BlockingQueue, а также множество реализаций этого интерфейса:

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

public class BlockingQueue {

    private List queue = new LinkedList();
    private int  limit = 10;

    public BlockingQueue(int limit){
        this.limit = limit;
    }


    public synchronized void put(Object item) throws InterruptedException  {
        while (this.queue.size() == this.limit) {
            wait();
        }
        if (this.queue.size() == 0) {
            notifyAll();
        }
        this.queue.add(item);
    }


    public synchronized Object take() throws InterruptedException{
        while (this.queue.size() == 0){
            wait();
        }
        if (this.queue.size() == this.limit){
            notifyAll();
        }

        return this.queue.remove(0);
    }
}

Рассмотрим метод put:

public synchronized void put(Object item) throws InterruptedException  {
    while (this.queue.size() == this.limit) {
        wait();
    }
    if (this.queue.size() == 0) {
        notifyAll();
    }
    this.queue.add(item);
}
Если очередь заполнена, то поток ставится в ожидание. Если же очередь пустая, то вызывается метод notifyAll, который пробуждает потоки, которые поставлены в ожидание при получении элементов из очереди. Аналогично работает метод take.

Обратите внимание, что в нашей простейшей реализации методы put, take являются synchronized в отличие от реализаций в JDK. Это сделано из двух соображений:

  • простота
  • желание обойтись только элементарными примитивами синхронизации в JDK, а именно методами wait, notifyAll

Давайте промоделируем работу очереди размеров в 5 элементов. Пусть у нас есть один поток (Producer), который, скажем, каждые 2 миллисекунды создает и кладет новый объект в очередь (пока что не подключаем поток, который забирает элементы из очереди). Очевидно, что через 10 миллисекунд, после того как очередь вся будет заполнена, при попытке положить шестой по счету элемент в очередь, этот поток будет поставлен в ожидание вот этим кусом кода из метода put:

while (this.queue.size() == this.limit) {
    wait();
}
Теперь подключим второй поток, который будет забирать элементы из очереди (Consumer). Он определит, что очередь полная и пошлет notification всем ожидающим потокам. Это произойдет в следующем куске кода метода take:
if (this.queue.size() == this.limit){
    notifyAll();
}
После этого поток заберет первый элемент из списка, и вернет его в методе take, выйдя таким образом из этого (synchronized!!!) метода и освободив монитор, ассоциированный c очередью. Затем монитор захватит поток Producer, пробудившийся от ожидания, и продолжит класть элементы в очередь.

Теперь приведем полный код демо-примера:

import java.util.LinkedList;
import java.util.List;

public class BlockingQueue {

    private List queue = new LinkedList();
    private int  limit = 10;

    public BlockingQueue(int limit){
        this.limit = limit;
    }


    public synchronized void put(T item) throws InterruptedException  {
        System.out.println("[BlockingQueue] try to put: " + item );
        while (this.queue.size() == this.limit) {
            System.out.println("[BlockingQueue] queue is full, waiting until space is free");
            wait();
        }
        if (this.queue.size() == 0) {
            System.out.println("[BlockingQueue] queue is empty, notify");
            notifyAll();
        }
        this.queue.add(item);
        System.out.println("[BlockingQueue] put ok: " + item );
    }


    public synchronized T take() throws InterruptedException{
        System.out.println("[BlockingQueue] try to take");
        while (this.queue.size() == 0){
            System.out.println("[BlockingQueue] queue is empty, waiting until smth is put");
            wait();
        }
        if (this.queue.size() == this.limit){
            System.out.println("[BlockingQueue] queue is full, notify");
            notifyAll();
        }

        T item = this.queue.remove(0);
        System.out.println("[BlockingQueue] take ok: " + item );
        return item;
    }
}
import java.util.Random;

public class Main {

    public static void main(String[] args) throws InterruptedException {
        BlockingQueue queue = new BlockingQueue(5);
        new Thread(new Producer(queue)).start();
        Thread.currentThread().sleep(1000);
        new Thread(new Consumer(queue)).start();
    }

    static class Producer implements Runnable {
        private final BlockingQueue queue;

        public Producer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            System.out.println("[Producer] run");
            while (true) {
                try {
                    queue.put(produce());
                    Thread.currentThread().sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private Integer produce() {
            Integer i = new Random().nextInt(100);
            System.out.println("[Producer] produce: " + i);
            return i;
        }
    }

    static class Consumer implements Runnable {
        private final BlockingQueue queue;

        public Consumer(BlockingQueue queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            System.out.println("[Consumer] run");
            while (true) {
                try {
                    consume();
                    Thread.currentThread().sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void consume() throws InterruptedException {
            Integer i = queue.take();
            System.out.println("[Consumer] consumed: " + i);
        }
    }
}

Вывод консоли будет такой (первая часть, где Producer кладет первые пять элементов в очередь):
[Producer] run
[Producer] produce: 86
[BlockingQueue] try to put: 86
[BlockingQueue] queue is empty, notify
[BlockingQueue] put ok: 86
[Producer] produce: 73
[BlockingQueue] try to put: 73
[BlockingQueue] put ok: 73
[Producer] produce: 20
[BlockingQueue] try to put: 20
[BlockingQueue] put ok: 20
[Producer] produce: 84
[BlockingQueue] try to put: 84
[BlockingQueue] put ok: 84
[Producer] produce: 73
[BlockingQueue] try to put: 73
[BlockingQueue] put ok: 73
[Producer] produce: 67
[BlockingQueue] try to put: 67
[BlockingQueue] queue is full, waiting until space is free
Затем вступает в работу Consumer:
[Consumer] run
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 86
[Consumer] consumed: 86
[BlockingQueue] put ok: 67
[Producer] produce: 63
[BlockingQueue] try to put: 63
[BlockingQueue] queue is full, waiting until space is free
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 73
[Consumer] consumed: 73
[BlockingQueue] put ok: 63
[Producer] produce: 90
[BlockingQueue] try to put: 90
[BlockingQueue] queue is full, waiting until space is free
[BlockingQueue] try to take
[BlockingQueue] queue is full, notify
[BlockingQueue] take ok: 20
[Consumer] consumed: 20
[BlockingQueue] put ok: 90
[Producer] produce: 57
[BlockingQueue] try to put: 57
[BlockingQueue] queue is full, waiting until space is free
На этом куске вывода хорошо видно, как Consumer и Producer попеременно используют очередь для обмена данными.

пятница, 7 марта 2014 г.

Демо-проект: Apache CXF, Jetty, MySQL

Решил выложить на Github небольшой демо-проект: клиент-серверное приложение, управляющее аккаунтами (счетами).

Начальная задача: сделать сервер, который позволяет читать состояние счета по его ID, а также добавлять/снимать средства со счета. Состояния счетов хранятся с БД. Сервер должен работать в нагруженное среде, поэтому нужно добавить кеширование. Транспортный протокол может быть любой. Я выбрал веб-сервисы, реализация веб-сервисов была сделана с помощью Apache CXF. После запуска сервера WSDL можно посмотреть по адресу: http://localhost:9000/accountService?wsdl

Вторая часть задачки: сделать клиента, который в несколько потоков читает/пишет значения счетов. Кол-во читателей/писателей, а также множество идентификаторов счетов задается параметрами клиента (например, из командной строки).

Третья часть задачки: сервер должен статистику работы скидывать в файл. Статистика должна быть такой: общее кол-во обработанных запросов, а также удельное кол-во запросов в ед. времени (т.е. rps - requests per second). Нужно предусмотреть возможность сброса статсы в ноль. Сброс статсы в ноль я сделал с помощью вызова HTTP-хендлера, который и обрабатывается Jetty. Сброс статсы осуществляется HTTP GET-ом урлы http://localhost:9001/zero. По урлу http://localhost:9001/getStats получаем текущее значение rps.

Несколько моментов про реализацию. Итак, рассмотрим реализацию AccountServiceImpl:

@WebService(endpointInterface = "net.iryndin.clientserverdemo1.commons.api.AccountService",serviceName = "AccountService")
public class AccountServiceImpl implements AccountService {
    private Map cache = new ConcurrentHashMap<>();

    @Override
    public Long getAmount(Integer id) {
        AtomicLong balance = cache.get(id);
        statsHelper.incrQueries();
        if (balance == null) {
            return 0L;
        } else {
            return balance.get();
        }
    }

    @Override
    public void addAmount(final Integer id, Long value) throws Exception {
        AtomicLong balance = cache.get(id);
        if (balance == null) {
            balance = new AtomicLong(0);
            cache.put(id, balance);
        }
        balance.addAndGet(value);
        final AtomicLong finalBalance = balance;
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    accountDao.save(id, finalBalance.get());
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        });
        statsHelper.incrQueries();
    }
}  
Видно, что чтение и запись идет прямиком к кеш, т.е. скорость чтения и записи определяется скоростью работы кеша. Запись в базу запускается в отдельном потоке. Если бы она запускалась из этого же потока, то и производительность записи была бы совсем другой. На самом деле, такое решение, конечно, не всегда приемлемо, но поскольку это тестовый проект, я сделал именно так.

Теперь рассмотрим моменты реализации клиента. См. файл MainClient:

    private static void runReaders(int count) {
        if (count <=0) {
            System.out.println("No readers run");
            return;
        }
        ExecutorService executorService = Executors.newFixedThreadPool(count);
        System.out.println("Run readers: " + count);
        while (count > 0) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    while (running) {
                        accountService.getAmount(idsHelper.getRandomId());
                    }
                }
            });
            count--;
        }
    }

    private static void runWriters(int count) {
        if (count <=0) {
            System.out.println("No writers run");
            return;
        }
        ExecutorService executorService = Executors.newFixedThreadPool(count);
        System.out.println("Run writers: " + count);
        while (count > 0) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    while (running) {
                        boolean positive = new Random().nextBoolean();
                        int balance = new Random().nextInt(100000);
                        balance = positive ? balance : -balance;
                        try {
                            accountService.addAmount(idsHelper.getRandomId(), (long) balance);
                        } catch (Exception e) {
                            //
                        }
                    }
                }
            });
            count--;
        }
    }
Мы запускаем потоки по числу rCount, wCount и крутимся там в бесконечном цикле (в бесконечном, так как переменную running в коде никто не меняет). Все этот создает неплохую нагрузку на сервер.

Последнее, рассмотрим скрипт на Python 3 (measure.py), который запускает клиент с различными значениями параметров rCount, wCount и измеряет производительность (просто считывает данные с урла http://localhost:9001/getStats):

import subprocess
import time
import urllib.request

def main():
    readers = [0, 20, 40, 60, 80, 100]
    writers = [0, 20, 40, 60, 80, 100]

    output = open('rps.txt','w', 1)
    
    for r in readers:
        for w in writers:
            run_client(r,w,output)
            print('Done reader=%d, writer=%d' % (r,w))
            
    output.close()

def run_client(readers_qty, writers_qty, output):        
    if readers_qty==0 and writers_qty==0:
        return
    proc = subprocess.Popen(['java', '-jar', '-Xms2G', '-Xmx8G', 'client.jar', str(readers_qty), str(writers_qty)], shell=False)
    time.sleep(1)
    f = urllib.request.urlopen('http://localhost:9001/zero')
    time.sleep(30)
    f = urllib.request.urlopen('http://localhost:9001/getStats')
    s = f.read()
    rps = int(s)
    print('rps=%d' % rps)
    output.write('(%d,%d): %d\n' % (readers_qty,writers_qty,rps))
    
    proc.terminate()
    time.sleep(5)
     
if __name__ == "__main__":
    main()

В результате, мы получаем следующую табличку (по горизонтали: число wCount, по вертикали: rCount, значения в ячейках: requests-per-second):

rCount/wCount 0 20 40 60 80 100
0 - 12197 12319 12865 12648 13614
20 13324 13240 13049 12657 12499 12858
40 13198 12249 12676 13112 12882 12442
60 12420 12250 12348 12928 12534 13013
80 12531 12934 12304 12428 13133 12978
100 12972 13204 11869 11541 12781 12496
Т.е. сервер держит где-то 12-13К реквестов. Это число полносью определяется производительностью ConcurrentHashMap.

понедельник, 27 февраля 2012 г.

OpenMQ database persistence in Glassfish

JMS-провайдер в сервере Glassfish является OpenMQ - сановская (а теперь оракловая) разработка с открытым исходным кодом.

По-умолчанию, OpenMQ записывает данные в файлы. Это не всегда хорошо, я столкнулся с тем, что при вырубании Глассфиша по kill -9 PID файловое хранилище OpenMQ портится и с восстановлением сообщений в очередях и топиках возникают проблемы. Поэтому имеет смысл рассмотреть переход на хранение сообщений в СУБД. Мы рассмотрим MySQL.

Пусть директория, в которую установлен Глассфиш обозначается как GLASSFISH_HOME. Тогда нужно отредактировать файл с настройками OpenMQ: GLASSFISH_HOME/glassfish/domains/domain1/imq/instances/imqbroker/props/config.properties. Добавим в него следующие строки:

imq.brokerid=imqbroker
imq.persist.store=jdbc
imq.persist.jdbc.dbVendor=mysql
imq.persist.jdbc.mysql.property.url=jdbc:mysql://localhost:3306/mqdb
imq.persist.jdbc.mysql.createdburl=jdbc:mysql://localhost:3306/mqdb
imq.persist.jdbc.mysql.needpassword=true
imq.persist.jdbc.mysql.user=USERNAME
imq.persist.jdbc.mysql.password=PASSWORD

Таким образом, мы настроили сохранение JMS-данных в БД. Нужно еще положить JAR-файл с драйвером БД (mysql-connector-java-5.1.17-bin.jar) в каталог GLASSFISH_HOME/mq/lib/ext.

Теперь создадим БД. Для этого воспользуемся утилитой imqdbmgr, которая находится в каталоге GLASSFISH_HOME/mq/bin. Пишем команду:
./imqdbmgr create all -b imqbroker
и получаем следующие таблицы в БД:

MQBKR41Simqbroker       
MQCON41Simqbroker       
MQCONSTATE41Simqbroker  
MQCREC41Simqbroker      
MQDST41Simqbroker       
MQJMSBG41Simqbroker     
MQMSG41Simqbroker       
MQPROP41Simqbroker      
MQSES41Simqbroker       
MQTMLRJMSBG41Simqbroker 
MQTXN41Simqbroker       
MQVER41Simqbroker
Теперь можно запускать сам Глассфиш - сохранение JMS-данных пойдет в БД, а не в файлы.

Когда создавалась БД, я столкнулся с одной проблемой. Дело в том, что у меня в MySQL кодировкой по умолчанию является UTF-8. Это проблема для OpenMQ, т.к. при создании таблицы MQTMLRJMSBG41Simqbroker появляются ошибки. Но эта проблема решается очень просто - для этого для данной Бд нужно объявить кодировку latin1:

ALTER DATABASE mqdb DEFAULT CHARACTER SET latin1 COLLATE latin1_general_ci;

Тогда все пройдет без ошибок.

Полезные ссылки:

OpenMQ, the Open source Message Queuing, for beginners and professionals (OpenMQ from A to Z)

Browse OpenMQ source code

Oracle GlassFish Message Queue 4.4.2 Technical Overview

Oracle GlassFish Message Queue 4.4.2 Administration Guide

среда, 31 августа 2011 г.

java.text.SimpleDateFormat и потоки

Использование класса java.text.SimpleDateFormat, не является потокобезопасным.

Например, вот такой код:


import java.text.*;

class MiscUtils {
public final static DateFormat fmt = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

public static String formatDate(Date date) {
return fmt.format(date);
}
}

//
// usage:
//
void anyMethod() {
Date date = getDate();
String s = MiscUtils.formatDate(date);
// now we have formatted date here!
}


может вызвать проблемы, если метод MiscUtils.formatDate будет одновременно вызван из нескольких потоков.

Вот выдержка со страницы Java Docs SimpleDateFormat:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.


Решение 1.
Всякий раз, когда нужно отформатировать (или пропарсить) дату, создавать новый экземпляр java.text.SimpleDateFormat

Решение 2.
Создать свою, потокобезопасную реализацию форматтера с блэкджеком и шлюхами с синхронизацией:

public class ThreadSafeSimpleDateFormat {

private DateFormat df;

public ThreadSafeSimpleDateFormat(String format) {
this.df = new SimpleDateFormat(format);
}

public synchronized String format(Date date) {
return df.format(date);
}

public synchronized Date parse(String string) throws ParseException {
return df.parse(string);
}
}


Решение 3.
Заюзать какую-нибудь стороннюю бибилиотечку наподобие JodaTime или класс FastDateFormat из Apache Commons.

пятница, 19 августа 2011 г.

Эффективное копирование файлов в NIO

Эффективное копирование файлов в NIO:

 // Getting file channels

FileChannel in = new FileInputStream(source).getChannel();
FileChannel out = new FileOutputStream(target).getChannel();
 
// JavaVM does its best to do this as native I/O operations.
in.transferTo (0, in.size(), out);
 
// Closing file channels will close corresponding stream objects as well.
out.close();
in.close();

воскресенье, 7 августа 2011 г.

HashMap, LinkedHashMap и TreeMap в Java

Вкратце, чем различаются между собой HashMap, LinkedHashMap и TreeMap из пакета java.util?

Все три класса имплементируют интерфейс java.util.Map. Но в каждой реализации есть всякие "плюшечки":


  • HashMap - не дает никаких гарантий по поводу порядка итерации по элементам. Скорость доступа: O(1).

  • LinkedHashMap - итерация по элементам в том же порядке, в котором они были вставлены в коллекцию, т.е. в insertion order. Скорость доступа: O(1).

  • TreeMap - держит коллекцию отсортированной по ключам (используется либо метод compareTo() либо предоставленный компаратор), поэтому итерация по элементам будет в порядке сортировки по ключам - natural ordering. Скорость доступа: O(log(n)).

пятница, 20 мая 2011 г.

Устройство java.util.HashMap. Часть 4.

Рассмотрим, как объекты кладутся в хеш-таблицу и получаются из нее. Для начала рассмотрим функцию хеширования:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

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

Теперь рассмотрим функцию, которая получает индекс в массиве бакетов:

static int indexFor(int h, int length) {
return h & (length-1);
}

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

Теперь рассмотрим функцию получения объекта по ключу:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}


Здесь все просто: для получения значения по null-ключу мы используем специальный метод: getForNullKey. Затем получаем индекс в массиве бакетов и проходимся по списку Entry в данной ячейке списка.

Рассмотрим метод getForNullKey:
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) return e.value;
}
return null;
}


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

Теперь рассмотрим метод для записи объекта в хеш-таблицу:
public V put(K key, V value) {
if (key == null) return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
 
modCount++;
addEntry(hash, key, value, i);
return null;
}

Здесь для добавления объекта в null-ключом вызывается специальный метод - putForNullKey. Далее, просматривается список в массиве бакетов по данному хешу. Если в этом списке нет такого ключа, то для добавления объекта в этот список вызывается метод addEntry, заодно увеличивается счетчик modCount - который подсчитывает количество структурных модификаций.

Рассмотрим метод addEntry:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}

Этот метод добавляет новый объект в начало списка, и также увеличивает размер массива бакетов, если достигнуто пороговое значение.

Мы рассмотрели основные аспекты java.util.HashMap - все остальное можно легко посмотреть по исходникам.

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 3.

Здесь мы рассмотрим варианты создания хеш-таблицы: ее конструкторы, включая и неявные конструкторы (т.е. clone).

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
 
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
 
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}


Здесь все довольно просто. Сначала мы проверяем аргументы на валидность в этом блоке кода:
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);


Затем находим ближайшую ступень двойки и записываем ее в capacity. Устанавливаем loadFactor и threshold = (int)(capacity * loadFactor), а также массив бакетов:
table = new Entry[capacity];
Затем вызываем метод init(), который является хуком для классов, наследующих от HashMap. В данной реализации это просто пустой метод.

Теперь рассмотрим конструктор без аргументов:

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}


Здесь все то же самое, за исключением проверки аргументов. Это и логично, т.к. аргументов в конструкторе нет. Они могли бы вызвать конструктор с аргументами вот так:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}


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

Теперь рассмотрим конструктор, который создает хеш-таюлицу по другой хеш-таблице:

public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}

Здесь также все просто - выбираются значения initialCapacity и loadFactor, после этого все элементы копируются из одной таблицы в другую, используя метод putAllForCreate. Этот же метод используется в методе clone.

А вот и метод clone:

public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
result.table = new Entry[table.length];
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
result.putAllForCreate(this);
 
return result;
}


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

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 2.

Здесь мы рассмотрим поля класса java.util.HashMap.

Итак:

static final int DEFAULT_INITIAL_CAPACITY = 16;
Начальная емкость хеш-таблицы

static final int MAXIMUM_CAPACITY = 1 << 30;
Максимальная емкость хеш-таблицы - составляет 2^29

static final float DEFAULT_LOAD_FACTOR = 0.75f;
Фактор загрузки по умолчанию

transient Entry[] table;
Массив бакетов. Его длина всегда должна быть степенью двойки. Этот массив автоматически увеличивается вдвое, если фактор загрузки превышен.

transient int size;
Количество мэппингов, которые содержатся в данной хеш-таблице.

int threshold;
Пороговое значение, следующее значение размера количества мэппингов, по достижении которого размер хеш-таблицы (а точнее, размер массива бакетов) будет увеличен. Рассчитывается как (емкость * фактор_загрузки).

final float loadFactor;
Значение фактора загрузки для данной хеш-таблицы.

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

private transient Set<Map.Entry<K,V>> entrySet = null;
Представление хеш-таблицы в виде множества Entry.

Как видим, полей не очень много.

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 3.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 1.

Одним из важнейших классом в Java Collections Framework является java.util.HashMap. Этой структура данных и ее реализация заслуживает отдельного разговора. В данном посте я хочу разобрать назначение HashMap и некоторые его особенности без погружения в исходный код. В основном, это будет перевод javadocs к классу java.util.HashMap.

java.util.HashMap - это реализация интерфейса java.util.Map на основе хеш-таюлицы. Эта реализация поддерживает также null-значения в качестве значений, а также и в качестве ключей. Не дается никаких гарантий по поводу внутреннего порядка данных. Особенно, не гарантируется, что порядок членов коллекции будет все время один и тот же.

Данная реализация обеспечивает постоянное время выполнения для базовых операций - get и put, т.е. время порядка O(1). При этом делается предположение, что хеш-функция равномерно распределяет элементы по бакетам (buckets). Бакет - элемент хеш-таблицы, который содержит список данных, положенных в хеш-таблицу. Остаток от деления хеш-кода каждого из этих данных на размер хеш-таблицы (точнее, на количество бакетов) один и тот же.

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

Каждый экземпляр класса java.util.HashMap имеет два параметра для настройки производительности - начальная емкость (initial capacity) и фактор загрузки (load factor). Емкость хеш-таблицы - это количество бакетов в ней, а начальная емкость - это то количество бакетов, с которым хеш-таблица создается. Фактор загрузки определяет, насколько должна быть заполнена хеш-таблица перед тем, как автоматически увеличить ее емкость.

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

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

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

Если нет внешнего объекта, по которому синхронизируется такое совместное использование хеш-таблицы, то следует "обернуть" ее следующим методом:
Map m = Collections.synchronizedMap(new HashMap(...));

Итераторы, которые возвращаются в таком случае, имеют поведение fail-fast: если хеш-таблица структурно модифицируется иным образом, кроме как через методы remove или add этого итератора, то итератор выкидывает ConcurrentModificationException.

Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 3.
Устройство java.util.HashMap. Часть 4.

четверг, 19 мая 2011 г.

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

Ключевое слово volatile применяется к полям класса и в общем означает, что данное поле будет использоваться (модифицироваться) несколькими потоками.

Применение этого модификатора к полю класса означает:
  • Значение этого поля никогда не будет кэшироваться потоками в свою локальную область памяти. Все чтения и записи будут идти напрямую в "главную" память, т.е. ту, в которой и размещено поле класса
  • Доступ к переменной действует как будто бы она была заключена в блок synchronized, который синхронизируется по самому полю

Поясним первый пункт. Java позволяет потокам, которые имеют доступ к зашаренным переменным сохранять локальные копии этих переменных в своей области памяти для более эффективной работы. Точнее, это позволяет сделать саму реализацию многопоточности более эффективной. Эти "рабочии копии" должны быть синхронизированы с мастер-копией только лишь в заранее определенных точках, которые обусловлены синхронизацией - а именно, когда объект лочится (locks) и разлочивается (unlocks). Как правило, чтобы убедиться, что разделяемая переменная целостно и надежно (reliably) обновляется, поток должен быть уверенным, что он имеет эксклюзивный доступ к такой переменной. Это делается получением лока на переменную, который и обеспечивает взаимное исключение для доступа к этой переменной.

Для пояснения второго пункта приведем табличку для сравнения volatile и synchronized. Не зря во втором пункте использовано слово "как будто": Доступ к переменной действует как будто бы она была заключена в блок synchronized. Т.к. явно в коде монитор, по которому синхронизируется доступ к переменной, не используется....

Итак, табличка:
Свойствоsynchronizedvolatile
Тип переменнойТолько объектОбъект или примитив
Разрешен ли nullнетда
Может ли быть блокировкаданет
Когда происходит синхронизацияКогда явно происходит вход в блок synchronizedКаждый раз при доступе к переменной


Дополнительно см: The volatile keyword in Java

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

В Java есть ключевое слово transient. Оно применяется к полям класса и означает, что данное поле не входит в персистентное состояние класса. Т.е. при сериализации данное поле не будет записываться. И, соответственно, при де-сериализации оно не будет восстанавливаться из потока байтов.

Применение:
private transient <member-variable>;
или
transient private <member-variable>;

Пример кода:
public class Foo {
private String saveMe;
private transient String dontSaveMe;
private transient String password;
//...
}

среда, 20 октября 2010 г.

Паттерн Строитель (Builder)

Строитель (Builder) - порождающий шаблон проектирования, который выделяет процесс создания сложного объекта так, чтобы различные реализации строителя могли бы создавать различные реализации объекта. UML диаграмма паттерна Строитель представлена ниже (взято из Wikipedia):



Объекты на диаграмме:

  • Builder - абстрактный строитель, определяющий интерфейс

  • Concrete Builder - предоставляет конкретную реализацию строителя. Конкретный строитель создает и собирает части объекта в единое целое.

  • Director - класс Директора отвечает за задание корректной последовательности конструирования объекта. Класс получает Конкретного Строителя, как параметр, и выполняет необходимые операции с ним.

  • Product - конечный продукт. Создается Директором с помощью Коннкретного Строителя


Мотивы применения паттерна:

  • алгоритм создания сложного объекта не должен зависеть от того, из каких частей состоит объект и как они стыкуются между собой

  • процесс конструирования должен обеспечивать различные представления конструируемого объекта


Плюсы применения паттерна Строитель:

  • изолирует код, реализующий конструирование и представление

  • предоставляет полный контроль над процессом создания объекта

  • позволяет независимо изменять ход процесса создания объекта



Пусть имеется код создания некоторого объекта. Со временем, объект меняется, добавляются новые вариации объекта, появляются новые методы. Без данного паттерна пришлось бы переписывать код создания объекта и его последующего использования столько раз, сколько произошло изменений и во всех местах, где этот объект создается. При использовании паттерна Строитель код и порядок создания объекта инкапсулирован в класс Конкретного Строителя. При изменении процесса создания изменить код придется только в одном месте - в этом Конкретном Строителе. При добавлении новой вариации объекта придется лишь добавить еще одного Конкретного Строителя.

Для чего нужен Директор? Опишем это на примере. Пусть подрядчик (Строитель) строит дом. Подрядчик знает, как нужно делать фундамент, возводить стены, делать стропила и класть крышу. Директор (распорядитель) говорит подрядчику, в какой последовательности это делать: сначала нужно заложить фундамент, затем стены, а после этого сделать крышу. В коде это будет выглядеть так:

public class HouseBuilder {
protected House house;
 
public House getHouse() {
return house;
}
 
public void createHouse() {
house = new House();
}
 
public abstract void buildFoundation();
public abstract void buildWalls();
public abstract void buildRoof();
}
 
public class Director {
public House constructHouse(HouseBuilder builder) {
builder.createHouse();
builder.buildFoundation();
builder.buildWalls();
builder.buildRoof();
return builder.getHouse();
}
}


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

public class HouseBuilder {
protected House house;
 
public House getHouse() {
return house;
}
 
public void createHouse() {
house = new House();
buildFoundation();
buildWalls();
buildRoof();
}
 
protected abstract void buildFoundation();
protected abstract void buildWalls();
protected abstract void buildRoof();
}
 
public class Director {
public House constructHouse(HouseBuilder builder) {
builder.createHouse();
return builder.getHouse();
}
}


В заключении можно добавить, что Строитель часто используется вместе с паттерном Компоновщик (Composite).

понедельник, 11 октября 2010 г.

Паттерн Одиночка (Singleton)

Шаблон (паттерн) проектирования "Одиночка" реализует математическую концепцию синглетона. В математике, синглетон - множество с одним элементом. Например, {10} - это синглетон, т.е. множество, состоящее из одного элемента - числа 10.

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

Рассмотрим варианты реализации одиночки на Java. При этом будем обращать внимание на многопоточность и связанные с ней возможные проблемы. Изложенное ниже следует статье в англоязычной Википедии Singleton pattern. Также использовались некоторые другие источники.

Самая простая реализация:
public class Singleton {
 
private static final Singleton INSTANCE = new Singleton();
 
// Private constructor prevents instantiation from other classes
private Singleton() {
}
 
public static Singleton getInstance() {
return INSTANCE;
}
}

Эта реализация потокобезопасна. Объект INSTANCE создается тогда, когда класс инициализируется. Например, это может произойти, когда будет вызван какой-нибудь статический метод класса. Конструктор объявляется private, доступ к экземпляру класса возможен только через getInstance(). Такая реализация, однако, может не подойти, если создание экземпляра класса требует много ресурсов. В этом случае нужно подумать об отложенной инициализации синглетона.

Вариант Уильяма Пу (William Pugh)
Это вариант отложенной инициализации (или еще говорят: ленивой инициализации)
public class Singleton {
 
// Private constructor prevents instantiation from other classes
private Singleton() {
}
 
/**
* SingletonHolder is loaded on the first execution of Singleton.getInstance()
* or the first access to SingletonHolder.INSTANCE, not before.
*/

private static class SingletonHolder {
public static final Singleton INSTANCE = new Singleton();
}
 
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
 
}

Эта реализация "ленива" настолько, насколько это возможно. Здесь используется идиома Initialization on demand holder idiom - т.е. отложенная инициализация с использованием объекта-холдера.

Несмотря на отсутствие synchronized в коде, эта реализация потокобезопасна. Такая реализация Одиночки работает на всех версиях Java. Здесь используются та часть спецификации Java (см. 8.3.1.1 static Fields и 12.4.2 Detailed Initialization Procedure), в которой описывается порядок инициализации класса. При инициализации класса Java-машина сама заботится о синхронизации, т.к. нет нужды подставлять использовать synchronized.

Т.к. на внутренний класс SingletonHolder нет ссылок нигде, кроме метода getInstance, то и инциализирован он будет не ранее, чем метод getInstance будет вызван. При такой инициализации Java-машина сама позаботится о синхронизации. Так что этот вариант - пример отличной потокобезопасной реализации паттерна Одиночка.

Реализация с double-checked locking (DCL)
Рассмотрим еще одну реализацию. Эта реализация использует двойную проверку блокировки. Она будет корректно работать в многопоточной среде только начиная с версии Java 5, т.к. в Java 5 была изменена модель памяти. Ранее существовавшая модель памяти не позволяла корректно выполняться такой реализации в многопоточной среде. Вот код:
public class Singleton {
 
// volatile is needed so that multiple thread can reconcile the instance
// semantics for volatile changed in Java 5.
private volatile static Singleton singleton;
 
private Singleton() {
 
}
 
// synchronized keyword has been removed from here
public static Singleton getSingleton() {
// needed because once there is singleton available no need to acquire
// monitor again & again as it is costly
if(singleton==null) {
synchronized(Singleton.class){
// this is needed if two threads are waiting at the monitor at the
// time when singleton was getting instantiated
if(singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

Однако, лучше не использовать double-checked locking. Это старый прием, который был вызван особенностями ранних JVM. Теперь его иногда даже рассматривают как анти-паттерн.

Подводя итоги, можно рекомендовать релализацию синглетона с помощью с использованием объекта-холдера.

Разница в synchronized(this) и synchronized(SomeClass.class)

Рассмотрим, в чем разница между
synchronized(this) {
//some stuff
}
и
synchronized(SomeClass.class) {
//some stuff
}

В Java каждый объект имеет ассоциированный с ним монитор (или lock - объект блокировки). Когда мы пишем такой код:
synchronized(this) {
//some stuff
}

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

Когда мы пишем такой код:
synchronized(SomeClass.class) {
//some stuff
}
мы получаем блокировку, ассоциированную с объектом SomeClass.class. Такую блокировку имеет смысл использовать, если мы изменяем данные (переменные) уровня класса, т.е. статические переменные.

суббота, 2 октября 2010 г.

Чтение всего файла в массив байтов

Нужно прочитать файл целиком в массив байтов. Например, для текстовых файлов такое нужно, чтобы затем из байтов сделать строку с нужной кодировкой: public String(byte[] bytes, String encoding).

Далее, код функции, которая позволяет считать файл целиком в массив байтов (взято отсюда: Reading a File into a Byte Array):

public static byte[] getBytesFromFile(File file) throws IOException {
InputStream is = new FileInputStream(file);
long length = file.length();
if (length > Integer.MAX_VALUE) {
return null;
}
 
byte[] bytes = new byte[(int)length];
 
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
offset += numRead;
}
 
// Ensure all the bytes have been read in
if (offset < bytes.length) {
throw new IOException("Could not completely read file "+file.getName());
}
 
// Close the input stream and return bytes
is.close();
return bytes;
}

Постоянные читатели