Как использовать синхронизированные блоки между классами?
Я хочу знать, как использовать синхронизированные блоки между классами. Я имею в виду, что я хочу иметь синхронизированные блоки в более чем одном классе, но все они синхронизируются на одном объекте. Единственный способ, которым я думал о том, как это сделать, это так:
//class 1
public static Object obj = new Object();
someMethod(){
synchronized(obj){
//code
}
}
//class 2
someMethod(){
synchronized(firstClass.obj){
//code
}
}
В этом примере я создал произвольный объект для синхронизации в первом классе, а во втором также синхронизировал его, статически ссылаясь на него. Тем не менее, это кажется мне плохим кодированием. Есть ли лучший способ добиться этого?
7 ответов
Наличие статического объекта, который используется в качестве блокировки, как правило, нежелательно, потому что только один поток за раз во всем приложении может прогрессировать. Когда у вас есть несколько классов, все с одной и той же блокировкой, что еще хуже, вы можете получить программу, в которой практически нет параллелизма.
Причина, по которой Java имеет встроенные блокировки для каждого объекта, заключается в том, что объекты могут использовать синхронизацию для защиты своих собственных данных. Потоки вызывают методы объекта, если объект должен быть защищен от одновременных изменений, вы можете добавить синхронизированное ключевое слово в методы объекта, чтобы каждый вызывающий поток должен был получить блокировку для этого объекта, прежде чем он сможет выполнить метод для него. Таким образом, вызовы несвязанных объектов не требуют одинаковой блокировки, и у вас больше шансов на то, что код будет фактически выполняться одновременно.
Блокировка не обязательно должна быть вашей первой методикой параллелизма. На самом деле есть ряд методов, которые вы можете использовать. В порядке убывания предпочтения:
1) по возможности устранять изменяемое состояние; неизменяемые объекты и функции без сохранения состояния идеальны, потому что нет состояния для защиты и не требуется блокировка.
2) использовать поток-заключение, где вы можете; если вы можете ограничить состояние одним потоком, то сможете избежать скачек данных и проблем с видимостью памяти, а также минимизировать количество блокировок.
3) использовать библиотеки параллелизма и фреймворки, а не блокировать собственные объекты с блокировкой. Познакомьтесь с классами в java.util.concurrent. Они написаны намного лучше, чем все, что разработчик приложений может собрать вместе.
Как только вы сделали столько, сколько можете, используя 1, 2 и 3 выше, вы можете подумать об использовании блокировки (где блокировка включает в себя такие параметры, как ReentrantLock, а также внутреннюю блокировку). Связывание блокировки с защищаемым объектом минимизирует объем блокировки, так что поток не удерживает блокировку дольше, чем это необходимо.
Кроме того, если блокировки не на блокируемых данных, то если в какой-то момент вы решите использовать разные блокировки вместо того, чтобы все блокировались на одной и той же вещи, то избежать взаимных блокировок может быть проблемой. Блокировка структур данных, которые требуют защиты, упрощает анализ поведения блокировки.
Советы, чтобы избежать внутренних замков в целом, могут быть преждевременной оптимизацией. Сначала убедитесь, что вы фиксируете правильные вещи не больше, чем необходимо.
ОПЦИЯ 1:
Более простым способом было бы создать отдельный объект (singleton), используя enum или статический внутренний класс. Затем используйте его для блокировки обоих классов, это выглядит элегантно:
// use any singleton object, at it's simplest can use any unique string in double quotes
public enum LockObj {
INSTANCE;
}
public class Class1 {
public void someMethod() {
synchronized (LockObj.INSTANCE) {
// some code
}
}
}
public class Class2 {
public void someMethod() {
synchronized (LockObj.INSTANCE) {
// some code
}
}
}
ВАРИАНТ:2
Вы можете использовать любую строку, так как JVM гарантирует, что она присутствует только один раз для каждой JVM. Уникальность заключается в том, чтобы убедиться, что в этой строке нет других блокировок. Не используйте эту опцию вообще, это просто для прояснения концепции.
public class Class1 {
public void someMethod() {
synchronized ("MyUniqueString") {
// some code
}
}
}
public class Class2 {
public void someMethod() {
synchronized ("MyUniqueString") {
// some code
}
}
}
Ваш код кажется мне действительным, даже если он не выглядит так хорошо. Но, пожалуйста, сделайте свой объект, который вы синхронизируете в финале. Однако могут быть некоторые соображения в зависимости от вашего фактического контекста.
В любом случае следует четко указать в Javadocs, что вы хотите архивировать.
Другой подход заключается в синхронизации на FirstClass
например
synchronized (FirstClass.class) {
// do what you have to do
}
Однако каждый synchronized
метод в FirstClass
идентичен синхронизированному блоку выше. Другими словами, они также synchronized
на том же объекте. - В зависимости от контекста это может быть лучше.
При других обстоятельствах, возможно, вы бы предпочли BlockingQueue
реализация, если это сводится к тому, что вы хотите синхронизировать по доступу БД или аналогичным.
Я думаю, что вы хотите сделать это. У вас есть два рабочих класса, которые выполняют некоторые операции над одним и тем же контекстным объектом. Затем вы хотите заблокировать оба рабочих класса в объекте контекста. Тогда следующий код будет работать для вас.
public class Worker1 {
private final Context context;
public Worker1(Context context) {
this.context = context;
}
public void someMethod(){
synchronized (this.context){
// do your work here
}
}
}
public class Worker2 {
private final Context context;
public Worker2(Context context) {
this.context = context;
}
public void someMethod(){
synchronized (this.context){
// do your work here
}
}
}
public class Context {
public static void main(String[] args) {
Context context = new Context();
Worker1 worker1 = new Worker1(context);
Worker2 worker2 = new Worker2(context);
worker1.someMethod();
worker2.someMethod();
}
}
Для вашего сценария я могу предложить вам написать класс Helper, который возвращает объект монитора через определенный метод. Само имя метода определяет логическое имя объекта блокировки, который помогает читабельности вашего кода.
public class LockingSupport {
private static final LockingSupport INSTANCE = new LockingSupport();
private Object printLock = new Object();
// you may have different lock
private Object galaxyLock = new Object();
public static LockingSupport get() {
return INSTANCE;
}
public Object getPrintLock() {
return printLock;
}
public Object getGalaxyLock() {
return galaxyLock;
}
}
В ваших методах, где вы хотите применить синхронизацию, вы можете попросить службу поддержки вернуть соответствующий объект блокировки, как показано ниже.
public static void unsafeOperation() {
Object lock = LockingSupport.get().getPrintLock();
synchronized (lock) {
// perform your operation
}
}
public void unsafeOperation2() { //notice static modifier does not matter
Object lock = LockingSupport.get().getPrintLock();
synchronized (lock) {
// perform your operation
}
}
Ниже приведены несколько преимуществ:
- Имея такой подход, вы можете использовать ссылки на методы, чтобы найти все места, где используется общая блокировка.
- Вы можете написать расширенную логику для возврата другого объекта блокировки (например, на основе пакета классов вызывающего, чтобы вернуть один и тот же объект блокировки для всех классов одного пакета, но другой объект блокировки для классов другого пакета и т. Д.)
- Вы можете постепенно обновить реализацию Lock для использования
java.util.concurrent.locks.Lock
API-интерфейсы. как показано ниже
например (изменение типа объекта блокировки не нарушит существующий код, хотя было бы нецелесообразно использовать объект блокировки как синхронизированный (блокировка))
public static void unsafeOperation2() {
Lock lock = LockingSupport.get().getGalaxyLock();
lock.lock();
try {
// perform your operation
} finally {
lock.unlock();
}
}
Надеется, что это помогает.
Я думаю, что вы идете неправильным путем, используя синхронизированные блоки вообще. Начиная с Java 1.5 есть пакет java.util.concurrent
что дает вам высокий уровень контроля над проблемами синхронизации.
Есть например Semaphore
Класс, который обеспечивает некоторую базовую работу, где вам нужна только простая синхронизация:
Semaphore s = new Semaphore(1);
s.acquire();
try {
// critical section
} finally {
s.release();
}
даже этот простой класс дает вам гораздо больше, чем синхронизированный, например, возможность tryAcquire()
который сразу же вернет, была ли получена блокировка или нет, и предоставит вам возможность выполнять некритическую работу, пока блокировка не станет доступной.
Использование этих классов также проясняет, какие цели имеют ваши объекты. Хотя универсальный объект монитора может быть неправильно понят, Semaphore
по умолчанию что-то связанное с многопоточностью.
Если вы загляните дальше в concurrent-пакет, вы найдете более конкретные классы синхронизации, такие как ReentrantReadWriteLock
что позволяет определить, что может быть много одновременных операций чтения, в то время как только операции записи фактически синхронизируются с другими операциями чтения / записи. Вы найдете Phaser
который позволяет синхронизировать потоки так, что конкретные задачи будут выполняться синхронно (в противоположность synchornized
), а также множество структур данных, которые могут сделать синхронизацию ненужной вообще в определенных ситуациях.
В целом: не используйте простой synchronized
вообще, если вы не знаете точно, почему или вы застряли с Java 1.4. Трудно читать и понимать, и, скорее всего, вы реализуете хотя бы части более высоких функций Semaphore
или же Lock
,
Прежде всего, вот проблемы с вашим текущим подходом:
- Объект блокировки не вызывается
lock
или похожие. (Да... придирка) - Переменная не
final
, Если что-то случайно (или намеренно) изменитсяobj
, ваша синхронизация сломается. - Переменная
public
, Это означает, что другой код может вызвать проблемы при получении блокировки.
Я полагаю, что некоторые из этих эффектов лежат в основе вашей критики: "мне это кажется плохим кодированием".
На мой взгляд, здесь есть две фундаментальные проблемы:
У тебя дырявая абстракция. Публикация объекта блокировки вне "класса 1" любым способом (в виде общедоступной или закрытой переменной пакета ИЛИ через геттер) представляет механизм блокировки. Этого следует избегать.
Использование единой "глобальной" блокировки означает, что у вас есть узкое место параллелизма.
Первая проблема может быть решена путем абстрагирования от блокировки. Например:
someMethod() {
Class1.doWithLock(() -> { /* code */ });
}
где doWithLock()
статический метод, который принимает Runnable
или же Callable
или аналогичный, а затем запускает его с соответствующей блокировкой. Реализация doWithLock()
может использовать свой собственный private static final Object lock
... или какой-либо другой механизм блокировки в соответствии с его спецификацией.
Вторая проблема сложнее. Чтобы избавиться от "глобальной блокировки", обычно требуется либо переосмыслить архитектуру приложения, либо перейти на другие структуры данных, не требующие внешней блокировки.