【问题标题】:Should you synchronize the run method? Why or why not?你应该同步运行方法吗?为什么或者为什么不?
【发布时间】:2011-11-10 22:27:39
【问题描述】:

我一直认为在实现 Runnable 的 java 类中同步 run 方法是多余的。 我想弄清楚人们为什么这样做:

public class ThreadedClass implements Runnable{
    //other stuff
    public synchronized void run(){
        while(true)
             //do some stuff in a thread
        }
    }
}

这似乎是多余和不必要的,因为他们正在为另一个线程获取对象的锁。或者更确切地说,他们明确表示只有一个线程可以访问 run() 方法。但是既然是run方法,那它本身不就是自己的线程吗?所以只有它自己可以访问,不需要单独的锁机制?

我在网上找到了一个建议,通过同步 run 方法,您可能会创建一个事实上的线程队列,例如这样做:

 public void createThreadQueue(){
    ThreadedClass a = new ThreadedClass();
    new Thread(a, "First one").start();
    new Thread(a, "Second one, waiting on the first one").start();
    new Thread(a, "Third one, waiting on the other two...").start();
 }

我永远不会亲自这样做,但这会引发一个问题,即为什么有人会同步 run 方法。 任何想法为什么或为什么不应该同步 run 方法?

【问题讨论】:

  • 队列有问题(对象监视器不公平,第二个线程可能在第一个线程之前运行),我能想象的唯一原因是确保当一个可运行对象被提交两次时一个执行器/线程它不会创建比赛
  • @irreputable 我的教授举了一个例子。我个人永远不会——除非我在等着看是否有任何尚未被指出的绝妙理由这样做。
  • @ratchet 好点。我猜你只希望同步运行它,如果有一个奇怪的原因导致另一个线程可能在同一个对象上执行。但即便如此,我认为我会以不同的方式解决它。
  • @MHP 一个原子布尔 hasRun 和一个 if(!hasRun.CompareAndSwap(false,true))return; 在运行中更好(因为它不会阻塞线程并确保运行只执行一次)但需要额外的代码和单独的 var
  • 这很奇怪。我有点担心你得到的教导。你显然可以看穿迷雾,但你班上的每个人可能都不是这样。当你必须做作业时,这是一个令人讨厌的情况:你做正常的事情还是你的教授做的事情?

标签: java multithreading synchronization synchronized runnable


【解决方案1】:

同步Runnablerun() 方法是完全没有意义的除非你想在多个线程之间共享Runnable并且你想对这些线程的执行。这基本上是一个矛盾的术语。

理论上还有另一个更复杂的场景,您可能希望同步run() 方法,这同样涉及在多个线程之间共享Runnable,但也使用wait()notify()。我在 21 多年的 Java 中从未遇到过它。

【讨论】:

  • @KenyakornKetsombut 到底错了?你刚才遇到了什么其他的例子?请提供证据,而不仅仅是轶事。除其他外,这就是科学。
  • 如果Runnable 必须读取共享对象怎么办?我刚刚在带有if (sharedObject != null) { threadObject = sharedObject.getField(); } 的块中有一个NPE,因为sharedObject 被两条指令之间的另一个线程取消了。声明我的run 方法同步会很愚蠢吗?这不会违背目的(在后台线程中运行),它只会在线程运行时阻止对共享对象的写入。
  • @BenoitDuffez 然后代码应该在共享对象被访问时同步。 Runnable 在整个持续时间内都没有。
  • @user207421 虽然这是一个有效的评论,但我要补充一点,“代码应该在专用锁对象上同步 - 例如,在 run() 方法旁边声明的私有字段”,作为共享正如@BenoitDuffez 指出的那样,对象可能被另一个进程或任务取消,并且您不能在可能是null 的东西上使用synchronized()
【解决方案2】:

浏览代码 cmets 并取消注释并运行不同的块以清楚地看到差异,注意同步只有在使用相同的可运行实例时才会有差异,如果每个启动的线程都有一个新的可运行实例,则不会产生任何区别。

class Kat{

public static void main(String... args){
  Thread t1;
  // MyUsualRunnable is usual stuff, only this will allow concurrency
  MyUsualRunnable m0 = new MyUsualRunnable();
  for(int i = 0; i < 5; i++){
  t1 = new Thread(m0);//*imp*  here all threads created are passed the same runnable instance
  t1.start();
  }

  // run() method is synchronized , concurrency killed
  // uncomment below block and run to see the difference

  MySynchRunnable1 m1 = new MySynchRunnable1();
  for(int i = 0; i < 5; i++){
  t1 = new Thread(m1);//*imp*  here all threads created are passed the same runnable instance, m1
  // if new insances of runnable above were created for each loop then synchronizing will have no effect

  t1.start();
}

  // run() method has synchronized block which lock on runnable instance , concurrency killed
  // uncomment below block and run to see the difference
  /*
  MySynchRunnable2 m2 = new MySynchRunnable2();
  for(int i = 0; i < 5; i++){
  // if new insances of runnable above were created for each loop then synchronizing will have no effect
  t1 = new Thread(m2);//*imp*  here all threads created are passed the same runnable instance, m2
  t1.start();
}*/

}
}

class MyUsualRunnable implements Runnable{
  @Override
  public void  run(){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
}
}

class MySynchRunnable1 implements Runnable{
  // this is implicit synchronization
  //on the runnable instance as the run()
  // method is synchronized
  @Override
  public synchronized void  run(){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
}
}

class MySynchRunnable2 implements Runnable{
  // this is explicit synchronization
  //on the runnable instance
  //inside the synchronized block
  // MySynchRunnable2 is totally equivalent to MySynchRunnable1
  // usually we never synchronize on this or synchronize the run() method
  @Override
  public void  run(){
    synchronized(this){
    try {Thread.sleep(1000);} catch (InterruptedException e) {}
  }
}
}

【讨论】:

    【解决方案3】:

    其实很容易证明“同步或不同步”的合理性

    如果您的方法调用可以改变对象的内部状态,则“同步”,否则不需要

    简单例子

    public class Counter {
    
      private int count = 0; 
    
      public void incr() {
        count++;
      }
    
      public int getCount() {
        return count;
      }
    }
    

    在上面的例子中,incr()需要同步,因为它会改变count的val,而getCount()不需要同步

    但是还有一个极端情况,如果计数是 java.lang.Long, Double, Object 那么你需要声明为

    private volatile long count = 0;
    

    确保 ref 更新是原子的

    基本上这是你在处理多线程时需要考虑 99% 的时间

    【讨论】:

    • 不回答问题。问题具体是关于 'run()' 方法,它与调用线程有特殊关系。
    【解决方案4】:

    根据我的经验,在 run() 方法中添加“同步”关键字没有用。如果我们需要同步多个线程,或者需要线程安全的队列,我们​​可以使用更合适的组件,比如 ConcurrentLinkedQueue。

    【讨论】:

      【解决方案5】:

      为什么?最低限度的额外安全,我看不出有任何可能的情况会有所作为。

      为什么不呢?这不是标准的。如果您作为团队的一员进行编码,当其他成员看到您同步的 run 时,他可能会浪费 30 分钟试图找出您的 run 或您用于运行的框架有什么特别之处Runnable 的。

      【讨论】:

        【解决方案6】:

        void blah() { synchronized(this) { 相比,使用synchronized void blah() 有一个优势,那就是生成的字节码将缩短1 个字节,因为同步将是方法签名的一部分,而不是单独的操作。这可能会影响 JIT 编译器内联方法的机会。除此之外没有区别。

        最好的选择是使用内部private final Object lock = new Object() 来防止有人可能锁定您的显示器。它实现了相同的结果,而没有外部锁定的不利影响。你确实有那个额外的字节,但它很少会有所作为。

        所以我会说不,不要在签名中使用synchronized 关键字。相反,使用类似的东西

        public class ThreadedClass implements Runnable{
            private final Object lock = new Object();
        
            public void run(){
                synchronized(lock) {
                    while(true)
                         //do some stuff in a thread
                    }
                }
            }
        }
        

        根据评论进行编辑:

        考虑一下同步的作用:它防止其他线程进入同一个代码块。所以想象一下你有一个像下面这样的课程。假设当前大小为 10。有人尝试执行添加,并强制调整后备数组的大小。当他们正在调整数组大小时,有人在不同的线程上调用了makeExactSize(5)。现在突然之间,您尝试访问data[6],它会轰炸您。同步应该防止这种情况发生。在多线程程序中,您只需要 NEED 同步。

        class Stack {
            int[] data = new int[10];
            int pos = 0;
        
            void add(int inc) {
                if(pos == data.length) {
                    int[] tmp = new int[pos*2];
                    for(int i = 0; i < pos; i++) tmp[i] = data[i];
                    data = tmp;
                }
                data[pos++] = inc;
            }
        
            int remove() {
                return data[pos--];
            }
        
            void makeExactSize(int size) {
                int[] tmp = new int[size];
                for(int i = 0; i < size; i++) tmp[i] = data[i];
                data = tmp;
            }
        }
        

        【讨论】:

        • 我喜欢字节码参考的热情,但我的意思是同步方法是否有任何好处。我的教授总是写“synchronized void run()”,而我从来没有这样做过,也不需要这样做。但是那个字节码位很有趣--
        • 答案比评论长。我将在答案中对其进行编辑。
        • 其实signiture中的synchronized是指JIT/JVM在调用多个同步方法时可以一直持有锁(即JVM在下次操作时不会释放锁(并立即重新获取))正在调用下一个同步方法)
        • 好吧,这是真的 - 重新获取一个已经持有的锁应该更便宜 - 但是我怀疑这在大多数情况下是显而易见的,我还没有看到我想要一个同步方法的情况线程的实例(通常有更高阶的数据结构用于通信 imo)
        • @ratchet 我的印象是 JIT 甚至可以对方法中声明的锁和私有锁(与调用对象的监视器相反)进行优化。我可能会误会,因为我无法立即为其提供来源。
        【解决方案7】:

        理论上你可以毫无问题地调用 run 方法本身(毕竟它是公开的)。但这并不意味着人们应该这样做。所以基本上没有理由这样做,除了给调用 run() 的线程增加可以忽略不计的开销。好吧,除非您多次调用 new Thread 来使用该实例 - 尽管我 a) 不确定线程​​ API 是否合法,并且 b) 似乎完全没用。

        您的createThreadQueue 也不起作用。非静态方法上的synchronized 在实例对象上同步(即this),因此所有三个线程将并行运行。

        【讨论】:

        • "...所以所有三个线程将按顺序运行。"这就是我的意思是“事实上的”线程队列......他们只会在前一个完成后运行。还是我误会你了? (就像我说的,如果我需要这样的东西,我永远不会那样编写代码......这只是猜测。)
        • 不,我只是感到困惑。这应该说“并行运行”或类似的东西。我会修复它。基本上,如果 run 方法只包含一个 print() 语句,那么您可以将这三个语句组合在一起。
        • 呃实际上误读了这个例子,所以是的,这是可行的——虽然有点奇怪的结构。只需一个简单的for(int i = 0; i &lt; N; i++) a.run(); 即可获得完全相同的结果,而无需额外开销。
        猜你喜欢
        • 2011-01-27
        • 1970-01-01
        • 2011-08-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多