【问题标题】:What Cases Require Synchronized Method Access in Java?Java中哪些情况需要同步方法访问?
【发布时间】:2010-09-23 12:05:26
【问题描述】:

在什么情况下需要同步对实例成员的访问? 我知道对类的静态成员的访问总是需要同步 - 因为它们在类的所有对象实例之间共享。

我的问题是,如果我不同步实例成员,我什么时候会出错?

例如,如果我的班级是

public class MyClass {
    private int instanceVar = 0;

    public setInstanceVar()
    {
        instanceVar++;
    }

    public getInstanceVar()
    {
        return instanceVar;
    }
}

在什么情况下(使用 MyClass 类)我需要拥有方法: public synchronized setInstanceVar()public synchronized getInstanceVar()?

提前感谢您的回答。

【问题讨论】:

    标签: java concurrency synchronization methods non-static


    【解决方案1】:

    这取决于你是否希望你的类是线程安全的。大多数类不应该是线程安全的(为简单起见),在这种情况下您不需要同步。如果您需要它是线程安全的,您应该同步访问使变量可变。 (它避免了其他线程获取“陈旧”数据。)

    【讨论】:

      【解决方案2】:

      在 Java 中,对 int 的操作是原子的,所以不,在这种情况下,如果您一次只进行 1 次写入和 1 次读取,则不需要同步。

      如果这些是 long 或 double,您确实需要同步,因为可能会更新 long/double 的一部分,然后读取另一个线程,最后更新 long/double 的另一部分。

      【讨论】:

      • 虽然要小心 ++ 和 -- 运算符,但它们不是原子的。为了安全起见,请尝试使用 java.util.concurrent.atomic 中的类。
      • 对整数的操作在 Java 中不是原子的。
      • 您可以从不同的线程修改一个 int,在线程中获得本地化缓存,使 ++ 操作读取和写入不同步。仅仅因为 longs 和 doubles 读取和写入作为两个操作操作(加载高位和加载低位以获得所有 64 位)并不意味着整数突然变得原子,因为它们遭受的非线程安全问题减少了 1 个。
      【解决方案3】:

      如果你想让这个类线程安全,我会将instanceVar 声明为volatile,以确保你总是从内存中获得最新的值,而且我会创建setInstanceVar() synchronized,因为在JVM增量不是原子操作。

      private volatile int instanceVar =0;
      
      public synchronized setInstanceVar() { instanceVar++;
      
      }
      

      【讨论】:

      • 不需要使字段可变,但 setInstanceVar() 确实需要同步。
      • 或者你可以使用 java.util.concurrent.atomic.AtomicInteger。
      • Daniel:如果 getInstanceVar 不同步,那么 volatile 会确保返回的值是新鲜的。但大多数程序员可能会忽略这一点。
      • InverseFalcon: AtomicInteger 如果有大量这些对象可能会导致问题。 AtomicIntegerFieldUpdater 可能更好,但在您必须使用它时往往会提供更差的性能。
      • @Tom,你不希望你的 get 方法也同步吗?
      【解决方案4】:

      。粗略地说,答案是“视情况而定”。在这里同步你的 setter 和 getter 只会有保证多个线程无法读取彼此之间的变量增量操作的预期目的:

       synchronized increment()
       { 
             i++
       }
      
       synchronized get()
       {
         return i;
        }
      

      但这在这里根本行不通,因为要确保您的调用者线程获得与它递增的相同的值,您必须保证您是原子递增然后检索,而您在这里没有这样做- 即你必须做类似的事情

        synchronized int {
          increment
          return get()
        }
      

      基本上,同步对于定义需要保证运行线程安全的操作很有用(换句话说,您不能创建一个单独的线程破坏您的操作并使您的类行为不合逻辑或破坏您期望的状态的情况的数据)。这实际上是一个比这里可以解决的更大的话题。

      这本书 Java Concurrency in Practice 非常好,而且肯定比我可靠得多。

      【讨论】:

        【解决方案5】:

        synchronized 修饰符确实是一个的想法,应该不惜一切代价避免。我认为 Sun 尝试让锁定更容易实现是值得称道的,但 synchronized 只会带来更多的麻烦而不值得。

        问题在于synchronized 方法实际上只是获取this 上的锁定并在方法执行期间持有它的语法糖。因此,public synchronized void setInstanceVar() 将等同于如下内容:

        public void setInstanceVar() {
            synchronized(this) {
                instanceVar++;
            }
        }
        

        这很糟糕有两个原因:

        • 同一类中的所有 synchronized 方法使用完全相同的锁,这会降低吞吐量
        • 任何人都可以访问锁,包括其他类的成员。

        没有什么可以阻止我在另一个班级做这样的事情:

        MyClass c = new MyClass();
        synchronized(c) {
            ...
        }
        

        synchronized 块内,我持有MyClass 中所有synchronized 方法所需的锁。这进一步降低了吞吐量并显着增加了死锁的机会。

        更好的方法是拥有一个专用的lock 对象并直接使用synchronized(...) 块:

        public class MyClass {
            private int instanceVar;
            private final Object lock = new Object();     // must be final!
        
            public void setInstanceVar() {
                synchronized(lock) {
                    instanceVar++;
                }
            }
        }
        

        或者,您可以使用java.util.concurrent.Lock 接口和java.util.concurrent.locks.ReentrantLock 实现来达到基本相同的结果(实际上在Java 6 上也是如此)。

        【讨论】:

        • @Daniel 我非常喜欢你的解释,但它不应该“必须”被声明为“静态”,尤其是拥有线程安全的公共类吗?因为您还想保证所有用于获得锁定的线程都将使用相同的(共享)对象,该对象不会被修改。 private static final Object lock = new Object(); // must be static final
        • lock 不需要为 static,因为它正在同步的数据 (instanceVar) 本身不是静态的。静态锁几乎总是一个非常非常严重的问题的迹象,您应该尽可能避免它们。
        • @Daniel hmmm... 我想我错过了这里的“MyClass”没有扩展 Thread 所以它不会有自己的线程。所以这里使类线程安全的目的是当一些外部线程共享'MyClass'的相同对象(实例)时,它们应该以同步方式访问[更新] instanceVar。换句话说,如果 MyClass 被声明为扩展 Thread,则锁应该被声明为“静态”。对吗?
        • 扩展Thread实际上只是一个实现细节。顺便说一句,这是不好的做法,但仍然可行。如果您这样做,那么您将有至少两个线程针对同一个实例运行。这些线程将使用完全相同的锁定对象。但是,对于针对MyClass 的不同实例运行的线程,锁定同一个对象是没有意义的,因为它们没有访问相同的数据!因此,我们可以将其设为静态,但这样做绝对没有任何好处,而且会带来很多痛苦。
        • 很好的解释,但你能解释一下为什么“必须是最终的!”吗?
        【解决方案6】:

        简单地说,当您有多个线程访问同一实例的同一方法时,您使用同步,这将改变对象/或应用程序的状态。

        它是一种防止线程之间竞争条件的简单方法,实际上您应该只在计划让并发线程访问同一个实例(例如全局对象)时使用它。

        现在,当您使用并发线程读取对象实例的状态时,您可能需要查看 java.util.concurrent.locks.ReentrantReadWriteLock——理论上它允许同时读取多个线程, 但只允许一个线程写入。因此,在每个人似乎都在给出的 getter 和 setting 方法示例中,您可以执行以下操作:

        public class MyClass{
            private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
            private int myValue = 0;
        
            public void setValue(){
                rwl.writeLock().lock();
                myValue++;
               rwl.writeLock().unlock();
            }
        
            public int getValue(){
               rwl.readLock.lock();
               int result = myValue;
               rwl.readLock.unlock();
               return result;
            }
        }
        

        【讨论】:

        • ReadWriteLock 的问题在于它会带来一些额外的开销。这种开销只有在你有 long 读取操作时才值得,否则你最好只使用一个 ReentrantLock。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-05-06
        • 1970-01-01
        • 2023-03-16
        • 2019-08-23
        • 2016-07-02
        • 2015-09-03
        相关资源
        最近更新 更多