Одновременное использование объекта только одним потоком

#java #multithreading #concurrency

#java #многопоточность #параллелизм

Вопрос:

У меня есть коллекция ( Map<Строка, MyObject> ). В многопоточной среде мне нужно, чтобы экземпляр MyObject в map использовался только одним потоком одновременно. Рассмотрим приведенный ниже пример.

 public class MyObject{

     String name;
     public MyObject(String objName){
        this.name = name;
     }
     public void doSomeTimeConsumingAction(){
        Thread.sleep(10000);
     }
     public void doSomeOther(){
        //doSomething
     }
     public void doMany(){
        //doSomething
     }
}

public class ObjectUtil {

      public static Map<String, MyObject> map = new HashMap<>();
      static {
         map.put("a", new MyObject("a"));
         map.put("b", new MyObject("b"));
         map.put("c", new MyObject("c"));
      }
      
      public static getObject(String key){
           return map.get(key);
      }

}

public static void main(String[] args){

    Thread t1 = new Thread(new Runnable(){
      @Override
      public void run(){
         System.out.println("t1 starts");
         MyObject obj = ObjectUtil.getObject("a");
         obj.doSomeTimeConsumingAction();
         System.out.println("t1 ends");
      });
      Thread t2 = new Thread(new Runnable(){
      @Override
      public void run(){
         System.out.println("t2 starts");
         MyObject obj = ObjectUtil.getObject("a");
         obj.doSomeTimeConsumingAction();
         System.out.println("t2 ends");
      });
      Thread t3 = new Thread(new Runnable(){
      @Override
      public void run(){
         System.out.println("t3 starts");
         MyObject obj = ObjectUtil.getObject("b");
         obj.doSomeTimeConsumingAction();
         System.out.println("t3 ends");
      });

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

Мой ожидаемый результат

 t1 starts
t2 starts
t3 starts
/* wait 10 sec */
t1 ends
t3 ends
/* wait 10 sec */
t2 ends
  

Объяснение -> В приведенном выше примере потоки t1 и t2 оба пытаются получить доступ к одному и тому же экземпляру с карты с помощью ключа «a», в то время как t3 обращается к другому экземпляру с помощью ключа «b».Итак, t1, t2, t3 запускаются одновременно . в то время как t1, t3 (или t2, t3) заканчиваются первыми, затем заканчивается другой.

Я не могу использовать метод synchronized on map или GetObject (), потому что это не заблокировало бы используемый экземпляр объекта.

Проще говоря, как я могу узнать, используется ли экземпляр объекта потоком? и, как предотвратить доступ другого потока к тому же экземпляру? Возможно ли это?

Редактировать: Обновил код, я также не могу синхронизировать метод doSomeTimeConsumingAction в объекте, потому что тогда поток мог бы получить доступ к другим методам.Речь идет не о методах доступа, доступ ко всему экземпляру должен осуществляться только одним потоком одновременно. Простите меня, если я прошу слишком многого.

Комментарии:

1. Вы можете синхронизировать doSomeTimeConsumingOperation .

2. @BurakSerdar Я не могу этого сделать, потому что мне нужно, чтобы весь объект использовался один раз. Обновлен код . Надеюсь, это могло бы помочь. Спасибо!

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

4. Вы можете использовать synchronized(obj) блоки каждый раз, когда обращаетесь к экземпляру MyObject.

Ответ №1:

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

Это легко достигается за счет doSomeTimeConsumingAction синхронизации:

 public class MyObject{

     String name;
     public MyObject(String objName){
        this.name = name;
     }
     public synchronized void doSomeTimeConsumingAction(){
        Thread.sleep(10000);
     }
}
  

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

   public void run(){
     System.out.println("t2 starts");
     MyObject obj = ObjectUtil.getObject("a");
     synchronized (obj) {
         obj.doSomeTimeConsumingAction();
         // call other methods of obj if you want
     }
     System.out.println("t2 ends");
  });
  

Комментарии:

1. Обновил мой вопрос. Пожалуйста, помогите мне.

2. «Дело не в методах доступа, доступ ко всему экземпляру должен осуществляться только одним потоком одновременно» — затем синхронизируйте все методы или используйте второй подход (см. Обновление)

3. Здесь возникает сомнение: если я сделаю все методы синхронизированными, не сделает ли это два разных метода одного и того же экземпляра доступными в двух потоках одновременно? Тем самым делая один и тот же экземпляр доступным для двух потоков? Я не уверен в том, как synchronized в методе работает на уровне экземпляра.

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

Ответ №2:

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

Если вы не доверяете (всем) своим вызывающим пользователям следовать этому правилу — вот простой способ «блокировки» a MyObject .

     public static Map<String, MyObject> map = new ConcurrentHashMap<>();

    public static void handle(String key, Consumer<MyObject> handle) {
      map.computeIfPresent(key, o -> {
        handler.apply(o); // Only one thread can be here at a time for any given "o"
        return o;
      });
    }
    ...
    ObjectUtil.handle("a", o -> o.doSomeTimeConsumingAction());
  

Несмотря на краткость и безопасность, это не самый высокий показатель производительности потоков (CHM также может блокировать доступ к другим MyObject потокам).

Ответ №3:

Это хрестоматийный пример использования synchronized блоков. Если вы хотите иметь взаимное исключение для объекта, вы можете удобно объявить doSomeTimeConsumingAction() метод как synchronized .

 public synchronized void doSomeTimeConsumingAction() throws InterruptedException {
    Thread.sleep(10000);
}
  

Даже если вы пометили метод как synchronized , фактическая блокировка применяется к объекту, а не к методу. Блокировки могут применяться только к объектам реального времени, таким как objects, а методы — это просто логические блоки.

Java разработана таким образом, что позволяет вам контролировать потокобезопасность методов, а не блокировать весь объект на уровне класса. Итак, вам решать, какие методы должны быть synchronized , а какие нет.

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