【问题标题】:Using an Object by only one thread at a time一次仅由一个线程使用对象
【发布时间】:2026-01-01 18:40:02
【问题描述】:

我有一个集合( Map )。在多线程环境中,我需要 map 中的 myObject 实例一次只能由一个线程使用。考虑下面的例子。

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)先结束,然后另一个结束。

我不能在 map 或 getObject() 方法上使用同步,因为它不会锁定要使用的对象实例。

简单地说,我如何知道一个对象的实例是否被线程使用?以及,如何防止其他线程访问同一个实例?有可能吗?

编辑:更新了代码,我也无法同步对象中的方法doSomeTimeConsumingAction,因为线程可以访问其他方法。这与访问方法无关,整个实例只能通过以下方式访问一次一个线程。如果问的太多,请见谅。

【问题讨论】:

  • 可以同步doSomeTimeConsumingOperation
  • @BurakSerdar 我不能这样做,因为我需要使用整个对象一次。更新了代码。希望它可以帮助。谢谢!
  • 好吧,你可以让所有的方法同步。也许您应该通过解释您的实际用例来扩展问题,因为这样呈现时看起来很简单。
  • 每次访问 MyObject 实例时都可以使用 synchronized(obj) 块。

标签: java multithreading concurrency


【解决方案1】:

这是使用synchronized 块的教科书示例。如果您想对一个对象进行互斥,您可以轻松地将doSomeTimeConsumingAction() 方法声明为synchronized

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

即使您将方法标记为synchronized,实际的锁定也会应用于对象而不是方法。锁只能应用于对象等实时实体,方法只是逻辑块。

Java 的设计方式是让您可以控制以使方法成为线程安全的,而不是在类级别锁定整个对象。所以,由你决定哪些方法应该是synchronized,哪些不应该。

我了解您在使用同步块时的困境,我建议您阅读更多内容并使用它来获得更舒适的体验。

【讨论】:

    【解决方案2】:

    同步所有方法是不行的,除非你知道每个调用者只会调用一个方法。否则调用可能会交错,这通常是个坏主意。如果有疑问,调用者应该在处理之前同步整个实例。

    如果您不信任(所有)您的来电者遵守该规则 - 这是“锁定”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 也可能会阻止对 other MyObjects 的访问)。

    【讨论】:

      【解决方案3】:

      你想和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");
        });
      

      【讨论】:

      • 更新了我的问题。请帮帮我。
      • “这与访问的方法无关,整个实例一次只能由一个线程访问” - 然后使所有方法同步,或者使用第二种方法(见更新)
      • 这里有一个疑问,如果我让所有方法同步,它不会使同一实例的两个不同方法同时在两个线程中可用吗?从而使相同的实例可用于两个线程?我不确定方法中的同步在实例级别如何工作。
      • 不,synchronized 获得对象锁定,而不是方法锁定。当另一个线程在同一个对象上运行另一个同步方法时,一个线程无法进入同步方法。