【问题标题】:Java synchronized method lock on object, or method?Java同步方法锁定对象还是方法?
【发布时间】:2011-03-04 02:49:44
【问题描述】:

如果我在同一个类中有 2 个同步方法,但每个方法访问不同的变量,2 个线程可以同时访问这 2 个方法吗?锁定是否发生在对象上,或者它是否与同步方法中的变量一样具体?

例子:

class X {

    private int a;
    private int b;

    public synchronized void addA(){
        a++;
    }

    public synchronized void addB(){
        b++;
    }

}

两个线程能否同时访问执行x.addA() 和x.addB() 的X 类的同一个实例?

【问题讨论】:

    标签: java multithreading thread-safety locking synchronized


    【解决方案1】:

    来自synchronized methods 上的“Java™ 教程”:

    首先,对同一对象同步方法的两次调用是不可能交错的。当一个线程正在为一个对象执行同步方法时,所有其他为同一对象调用同步方法的线程都会阻塞(暂停执行),直到第一个线程处理完该对象。

    来自synchronized blocks 上的“Java™ 教程”:

    同步语句对于通过细粒度同步提高并发性也很有用。例如,假设 MsLunch 类有两个从未一起使用的实例字段 c1 和 c2。这些字段的所有更新都必须同步,但没有理由阻止 c1 的更新与 c2 的更新交错 - 这样做会通过创建不必要的阻塞来降低并发性。 我们不使用同步方法或以其他方式使用与此关联的锁,而是创建两个对象来提供锁。

    (强调我的)

    假设您有 2 个非交错变量。因此,您希望同时从不同的线程访问每个线程。您需要定义 lock 不是在对象类本身上,而是在类 Object 上,如下所示(来自第二个 Oracle 链接的示例):

    public class MsLunch {
    
        private long c1 = 0;
        private long c2 = 0;
    
        private Object lock1 = new Object();
        private Object lock2 = new Object();
    
        public void inc1() {
            synchronized(lock1) {
                c1++;
            }
        }
    
        public void inc2() {
            synchronized(lock2) {
                c2++;
            }
        }
    }
    

    【讨论】:

    • 这是可行的,因为线程有自己的堆栈,但共享实例成员,包括任何声明的锁定对象。
    【解决方案2】:

    如果您将方法声明为 已同步(就像您通过键入 public synchronized void addA() 所做的那样),您将在 whole 对象上进行同步,因此两个线程访问不同的变量无论如何,来自同一个对象会相互阻挡。

    如果您想一次只同步一个变量,以便两个线程在访问不同变量时不会相互阻塞,您可以在synchronized () 块中分别同步它们。如果 a 和 b 是您将使用的对象引用:

    public void addA() {
        synchronized( a ) {
            a++;
        }
    }
    
    public void addB() {
        synchronized( b ) {
            b++;
        }
    }
    

    但是因为它们是原始的,所以你不能这样做。

    我建议您改用 AtomicInteger:

    import java.util.concurrent.atomic.AtomicInteger;
    
    class X {
    
        AtomicInteger a;
        AtomicInteger b;
    
        public void addA(){
            a.incrementAndGet();
        }
    
        public void addB(){ 
            b.incrementAndGet();
        }
    }
    

    【讨论】:

    • 如果同步锁定整个对象的方法,那么从同一个对象访问不同变量的两个线程无论如何都会相互阻塞。 这有点误导。在方法上同步在功能上等同于在方法主体周围有一个synchronized (this) 块。对象“this”不会被锁定,而是对象“this”被用作互斥体,并且主体被阻止与其他也在“this”上同步的代码部分同时执行。它对“this”的其他未同步的字段/方法没有影响。
    • 是的,这确实具有误导性。举个实际的例子——看这个——stackoverflow.com/questions/14447095/…——总结:锁定只在同步方法级别,对象的实例变量可以被其他线程访问
    • 第一个例子从根本上被打破了。如果 a 和 b 是对象,例如Integers,在应用 ++ 运算符时,您正在用不同对象替换的实例上进行同步。
    • 修正你的答案并初始化 AtomicInteger:AtomicInteger a = new AtomicInteger(0);
    • 也许这个anwser应该被更新为另一个关于同步对象本身的解释:stackoverflow.com/a/10324280/1099452
    【解决方案3】:

    在java同步中,如果一个线程想要进入同步方法,它将获取该对象的所有同步方法的锁,而不仅仅是该线程正在使用的一个同步方法。 因此,执行 addA() 的线程将在 addA() 和 addB() 上获得锁,因为两者都是同步的。因此具有相同对象的其他线程无法执行 addB()。

    【讨论】:

      【解决方案4】:

      是的,它会阻塞其他方法,因为同步方法适用于所指出的 WHOLE 类对象......但无论如何它会阻塞其他线程执行 ONLY在它输入的任何方法 addA 或 addB 中执行求和时,因为当它完成时......一个线程将 FREE 对象,另一个线程将访问另一个方法,依此类推。

      我的意思是“同步”是为了阻止另一个线程在特定代码执行时访问另一个线程。所以最后这段代码可以正常工作了。

      最后一点,如果有一个 'a' 和 'b' 变量,而不仅仅是一个唯一变量 'a' 或任何其他名称,则无需同步此方法,因为访问其他 var 是完全安全的(其他内存位置)。

      class X {
      
      private int a;
      private int b;
      
      public void addA(){
          a++;
      }
      
      public void addB(){
          b++;
      }}
      

      同样有效

      【讨论】:

        【解决方案5】:

        这个例子(虽然不是很漂亮)可以提供对锁定机制的更多了解。如果 incrementA 同步,而 incrementB 不同步,则 incrementB 将尽快执行,但如果 incrementB 也是同步,那么它必须在 incrementB之前“等待”incrementA 完成> 可以完成它的工作。

        这两种方法都被调用到单个实例 - 对象上,在这个例子中它是:job,“竞争”线程是 aThread 和 main。

        尝试在 incrementB 中使用 'synchronized' 而没有它,您会看到不同的结果。如果 incrementB 是 'synchronized' 然后它必须等待 incrementA() 完成。每个变体运行几次。

        class LockTest implements Runnable {
            int a = 0;
            int b = 0;
        
            public synchronized void incrementA() {
                for (int i = 0; i < 100; i++) {
                    this.a++;
                    System.out.println("Thread: " + Thread.currentThread().getName() + "; a: " + this.a);
                }
            }
        
            // Try with 'synchronized' and without it and you will see different results
            // if incrementB is 'synchronized' as well then it has to wait for incrementA() to finish
        
            // public void incrementB() {
            public synchronized void incrementB() {
                this.b++;
                System.out.println("*************** incrementB ********************");
                System.out.println("Thread: " + Thread.currentThread().getName() + "; b: " + this.b);
                System.out.println("*************** incrementB ********************");
            }
        
            @Override
            public void run() {
                incrementA();
                System.out.println("************ incrementA completed *************");
            }
        }
        
        class LockTestMain {
            public static void main(String[] args) throws InterruptedException {
                LockTest job = new LockTest();
                Thread aThread = new Thread(job);
                aThread.setName("aThread");
                aThread.start();
                Thread.sleep(1);
                System.out.println("*************** 'main' calling metod: incrementB **********************");
                job.incrementB();
            }
        }
        

        【讨论】:

          【解决方案6】:

          方法声明上的同步是这个的语法糖:

           public void addA() {
               synchronized (this) {
                    a++;
               }
            }
          

          在静态方法上,它是语法糖:

           ClassA {
               public static void addA() {
                    synchronized(ClassA.class) {
                        a++;
                    }
           }
          

          我认为,如果 Java 设计者当时知道现在对同步的理解,他们就不会添加语法糖,因为它往往会导致并发的错误实现。

          【讨论】:

          • 不正确。同步方法生成与同步(对象)不同的字节码。虽然功能相同,但它不仅仅是语法糖。
          • 我不认为“语法糖”被严格定义为字节码等价物。关键是它在功能上是等效的。
          • 如果 Java 设计者知道什么是已经了解监视器,他们会/应该做不同的事情,而不是基本上模仿 Unix 的内部结构。 Per Brinch Hansen said 'clearly I have laboured in vain' when he saw the Java concurrency primitives.
          • 这是真的。 OP给出的示例似乎锁定了每个方法,但实际上它们都锁定在同一个对象上。非常具有欺骗性的语法。在使用 Java 10 多年后,我不知道这一点。因此,出于这个原因,我会避免使用同步方法。我一直以为每一个用synchronized定义的方法都会创建一个不可见的对象。
          【解决方案7】:

          来自 oracle 文档link

          使方法同步有两个效果:

          首先,同一对象上的同步方法的两次调用不可能交错。当一个线程正在为一个对象执行同步方法时,所有其他为同一对象调用同步方法的线程都会阻塞(暂停执行),直到第一个线程处理完该对象。

          其次,当一个同步方法退出时,它会自动与任何后续对同一对象的同步方法调用建立起之前的关系。这保证了对象状态的变化对所有线程都是可见的

          查看此文档page 以了解内在锁和锁定行为。

          这将回答您的问题:在同一个对象 x 上,当同步方法之一正在执行时,您不能同时调用 x.addA() 和 x.addB()。

          【讨论】:

            【解决方案8】:

            如果您有一些未同步的方法正在访问和更改实例变量。在您的示例中:

             private int a;
             private int b;
            

            当其他线程处于同一对象的同步方法中时,任意数量的线程都可以同时访问这些非同步方法,并且可以对实例变量进行更改。 例如:-

             public void changeState() {
                  a++;
                  b++;
                }
            

            您需要避免非同步方法访问实例变量并对其进行更改的情况,否则使用同步方法没有意义。

            在以下场景中:-

            class X {
            
                    private int a;
                    private int b;
            
                    public synchronized void addA(){
                        a++;
                    }
            
                    public synchronized void addB(){
                        b++;
                    }
                 public void changeState() {
                      a++;
                      b++;
                    }
                }
            

            只有一个线程可以在 addA 或 addB 方法中,但同时任意数量的线程可以进入 changeState 方法。没有两个线程可以同时进入 addA 和 addB(因为对象级锁定),但同时任意数量的线程可以进入 changeState。

            【讨论】:

              【解决方案9】:

              这可能不起作用,因为从 Integer 到 int 的装箱和自动装箱取决于 JVM,如果两个不同的数字在 -128 和 127 之间,它们很可能会被散列到同一个地址。

              【讨论】:

                【解决方案10】:

                访问的锁在对象上,而不是在方法上。在方法中访问哪些变量是无关紧要的。

                在方法中添加“同步”意味着运行代码的线程必须在继续之前获取对象上的锁。添加“静态同步”意味着运行代码的线程必须在继续之前获取类对象上的锁。或者,您可以将代码包装在这样的块中:

                public void addA() {
                    synchronized(this) {
                        a++;
                    }
                }
                

                这样你就可以指定必须获取锁的对象。

                如果您想避免锁定包含对象,您可以选择:

                【讨论】:

                  【解决方案11】:

                  您可以执行以下操作。在这种情况下,您使用 a 和 b 上的锁来同步,而不是“this”上的锁。我们不能使用 int,因为原始值没有锁,所以我们使用 Integer。

                  class x{
                     private Integer a;
                     private Integer b;
                     public void addA(){
                        synchronized(a) {
                           a++;
                        }
                     }
                     public synchronized void addB(){
                        synchronized(b) {
                           b++;
                        }
                     }
                  }
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 2010-10-01
                    • 2023-04-04
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2018-01-27
                    • 2018-04-13
                    相关资源
                    最近更新 更多