首页 文章

使用同步方法而不是同步块是否有优势?

提问于
浏览
377

任何人都可以告诉我同步方法优于同步块的优势吗?

22 回答

  • 4

    主要区别在于,如果使用同步块,则可以锁定除此之外的对象,这样可以更灵活 .

    假设您有一个消息队列和多个消息生成者和使用者 . 我们不希望 生产环境 者互相干扰,但消费者应该能够在不必等待 生产环境 者的情况下检索消息 . 所以我们只创建一个对象

    Object writeLock = new Object();
    

    从现在开始,每当制作人想要添加新消息时,我们就会锁定:

    synchronized(writeLock){
      // do something
    }
    

    所以消费者仍然可以阅读, 生产环境 者将被锁定 .

  • 1

    来自Java规范摘要:http://www.cs.cornell.edu/andru/javaspec/17.doc.html

    synchronized语句(第14.17节)计算对象的引用;然后它尝试对该对象执行锁定操作,并且在锁定操作成功完成之前不会继续进行 . ...同步方法(第8.4.3.5节)在调用时自动执行锁定操作;在锁定操作成功完成之前,它的主体不会执行 . 如果该方法是实例方法,则它锁定与调用它的实例关联的锁(即,在执行方法主体期间将被称为this的对象) . 如果方法是静态的,它将锁定与Class对象关联的锁,该Class对象表示定义方法的类 . ...

    根据这些描述,我会说大多数以前的答案是正确的,并且同步方法可能对静态方法特别有用,否则你必须弄清楚如何获得“表示方法所在的类的类对象”定义“ .

    编辑:我原本以为这些是实际Java规范的引用 . 澄清此页面只是规范的摘要/解释

  • 2

    有人可以通过示例告诉我同步方法在synchronized块上的优势吗?谢谢 .

    在块上使用同步方法没有明显的优势 .

    也许是唯一一个(但我不需要包含对象引用 this .

    方法:

    public synchronized void method() { // blocks "this" from here.... 
        ...
        ...
        ...
    } // to here
    

    块:

    public void method() { 
        synchronized( this ) { // blocks "this" from here .... 
            ....
            ....
            ....
        }  // to here...
    }
    

    看到?没有任何优势 .

    do 优于方法,但主要是灵活性,因为你可以使用另一个对象作为锁,而同步方法会锁定整个对象 .

    相比:

    // locks the whole object
    ... 
    private synchronized void someInputRelatedWork() {
        ... 
    }
    private synchronized void someOutputRelatedWork() {
        ... 
    }
    

    // Using specific locks
    Object inputLock = new Object();
    Object outputLock = new Object();
    
    private void someInputRelatedWork() {
        synchronized(inputLock) { 
            ... 
        } 
    }
    private void someOutputRelatedWork() {
        synchronized(outputLock) { 
            ... 
        }
    }
    

    此外,如果方法增长,您仍然可以保持同步部分分开:

    private void method() {
         ... code here
         ... code here
         ... code here
        synchronized( lock ) { 
            ... very few lines of code here
        }
         ... code here
         ... code here
         ... code here
         ... code here
    }
    
  • 2

    Synchronized method

    同步方法有两种效果 .
    首先,当一个线程正在为对象执行同步方法时,所有其他线程调用同一对象的同步方法(暂停执行)直到第一个线程完成对象 .

    其次,当同步方法退出时,它会自动与同一对象的同步方法的任何后续调用 Build 先发生关系 . 这可以保证对所有线程都可以看到对象状态的更改 .

    请注意,构造函数无法同步 - 将synchronized关键字与构造函数一起使用是语法错误 . 同步构造函数没有意义,因为只有创建对象的线程在构造时才能访问它 .

    Synchronized Statement

    与synchronized方法不同,synchronized语句必须指定提供内部锁的对象:我经常使用它来同步对列表或映射的访问,但我不想阻止访问对象的所有方法 .

    问:内部锁定和同步同步是围绕称为内部锁定或监视器锁定的内部实体构建的 . (API规范通常将此实体简称为“监视器” . )内部锁在同步的两个方面都发挥作用:强制对对象状态进行独占访问,并 Build 对可见性至关重要的先发生关系 .

    每个对象都有一个与之关联的内在锁 . 按照惯例,需要对对象字段进行独占和一致访问的线程必须在访问对象之前获取对象的内部锁,然后在完成它们时释放内部锁 . 一个线程被称为在获得锁定和释放锁定之间拥有内在锁定 . 只要一个线程拥有一个内部锁,没有其他线程可以获得相同的锁 . 另一个线程在尝试获取锁时将阻塞 .

    package test;
    
    public class SynchTest implements Runnable {  
        private int c = 0;
    
        public static void main(String[] args) {
            new SynchTest().test();
        }
    
        public void test() {
            // Create the object with the run() method
            Runnable runnable = new SynchTest();
            Runnable runnable2 = new SynchTest();
            // Create the thread supplying it with the runnable object
            Thread thread = new Thread(runnable,"thread-1");
            Thread thread2 = new Thread(runnable,"thread-2");
    //      Here the key point is passing same object, if you pass runnable2 for thread2,
    //      then its not applicable for synchronization test and that wont give expected
    //      output Synchronization method means "it is not possible for two invocations
    //      of synchronized methods on the same object to interleave"
    
            // Start the thread
            thread.start();
            thread2.start();
        }
    
        public synchronized  void increment() {
            System.out.println("Begin thread " + Thread.currentThread().getName());
            System.out.println(this.hashCode() + "Value of C = " + c);
    //      If we uncomment this for synchronized block, then the result would be different
    //      synchronized(this) {
                for (int i = 0; i < 9999999; i++) {
                    c += i;
                }
    //      }
            System.out.println("End thread " + Thread.currentThread().getName());
        }
    
    //    public synchronized void decrement() {
    //        System.out.println("Decrement " + Thread.currentThread().getName());
    //    }
    
        public int value() {
            return c;
        }
    
        @Override
        public void run() {
            this.increment();
        }
    }
    

    使用同步方法,阻止和不同步交叉检查不同的输出 .

  • 2

    同步方法用于锁定所有对象 . 同步块用于锁定特定对象

  • -3

    可以使用反射API检查同步方法 . 这对于测试某些 Contract 非常有用,例如模型中的所有方法都是同步的 .

    以下代码段打印了Hashtable的所有同步方法:

    for (Method m : Hashtable.class.getMethods()) {
            if (Modifier.isSynchronized(m.getModifiers())) {
                System.out.println(m);
            }
    }
    
  • 17

    唯一的区别: synchronized blocks allows granular locking unlike synchronized method

    基本上, synchronized 块或方法已被用于通过避免内存不一致错误来编写线程安全代码 .

    这个问题非常陈旧,在过去的7年里,很多事情都发生了变化 . 为线程安全引入了新的编程结构 .

    您可以使用高级并发API而不是 synchronied 块来实现线程安全 . 本文档page提供了良好的编程构造以实现线程安全 .

    Lock Objects支持锁定习惯用法,简化了许多并发应用程序 .

    Executors定义了用于启动和管理线程的高级API . java.util.concurrent提供的执行程序实现提供了适用于大规模应用程序的线程池管理 .

    Concurrent Collections使管理大量数据更容易,并且可以大大减少同步的需要 .

    Atomic Variables具有最小化同步和帮助避免内存一致性错误的功能 .

    ThreadLocalRandom (在JDK 7中)提供了从多个线程有效生成伪随机数 .

    更好地替换synchronized是ReentrantLock,它使用 Lock API

    可重入互斥锁具有与使用同步方法和语句访问的隐式监视器锁相同的基本行为和语义,但具有扩展功能 .

    锁示例:

    class X {
       private final ReentrantLock lock = new ReentrantLock();
       // ...
    
       public void m() {
         lock.lock();  // block until condition holds
         try {
           // ... method body
         } finally {
           lock.unlock()
         }
       }
     }
    

    对于其他编程结构,也请参阅java.util.concurrentjava.util.concurrent.atomic包 .

    请参阅此相关问题:

    Synchronization vs Lock

  • 6

    同步方法

    优点:

    • 您的IDE可以指示同步的方法 .

    • 语法更紧凑 .

    • 强制将同步块拆分为单独的方法 .

    缺点:

    • 与此同步,因此外人也可以同步到它 .

    • 在同步块之外移动代码更加困难 .

    同步块

    优点:

    • 允许使用私有变量进行锁定,从而强制锁定保留在类中 .

    • 可以通过搜索对变量的引用来找到同步块 .

    缺点:

    • 语法更复杂,因此使代码更难阅读 .

    就个人而言,我更喜欢使用同步方法,而类只关注需要同步的东西 . 这样的类应该尽可能小,因此应该很容易查看同步 . 其他人不应该关心同步 .

  • 4

    唯一真正的区别是同步块可以选择同步的对象 . synchronized方法只能使用 'this' (或同步类方法的相应Class实例) . 例如,这些在语义上是等价的:

    synchronized void foo() {
      ...
    }
    
    void foo() {
        synchronized (this) {
          ...
        }
    }
    

    后者更灵活,因为它可以竞争任何对象的关联锁,通常是成员变量 . 它也更精细,因为您可以在块之前和之后执行并发代码,但仍然在方法内 . 当然,您可以通过将并发代码重构为单独的非同步方法来轻松使用同步方法 . 使用哪个使代码更易于理解 .

  • 404

    与线程同步 . 1)永远不要在线程中使用synchronized(this)它不起作用 . 与(this)同步使用当前线程作为锁定线程对象 . 由于每个线程独立于其他线程,因此不存在同步协调 . 2)代码测试表明,在Mac上的Java 1.6中,方法同步不起作用 . 3)synchronized(lockObj)其中lockObj是所有线程的公共共享对象,同步它将起作用 . 4)ReenterantLock.lock()和.unlock()工作 . 请参阅Java教程 .

    以下代码显示了这些要点 . 它还包含将替换ArrayList的线程安全Vector,以显示添加到Vector的许多线程不会丢失任何信息,而与ArrayList相同的线程可能会丢失信息 . 0)当前代码显示由于竞争条件导致的信息丢失A)注释当前标记的A行,并取消注释其上方的A行,然后运行,方法丢失数据但不应该丢失 . B)反向步骤A,取消注释B和//结束块} . 然后运行以查看结果没有数据丢失C)注释掉B,取消注释C.运行,看到同步(这)丢失数据,如预期的那样 . 没有时间完成所有变化,希望这会有所帮助 . 如果同步(this)或方法同步有效,请说明您测试的Java和OS的版本 . 谢谢 .

    import java.util.*;
    
    /** RaceCondition - Shows that when multiple threads compete for resources 
         thread one may grab the resource expecting to update a particular 
         area but is removed from the CPU before finishing.  Thread one still 
         points to that resource.  Then thread two grabs that resource and 
         completes the update.  Then thread one gets to complete the update, 
         which over writes thread two's work.
         DEMO:  1) Run as is - see missing counts from race condition, Run severa times, values change  
                2) Uncomment "synchronized(countLock){ }" - see counts work
                Synchronized creates a lock on that block of code, no other threads can 
                execute code within a block that another thread has a lock.
            3) Comment ArrayList, unComment Vector - See no loss in collection
                Vectors work like ArrayList, but Vectors are "Thread Safe"
             May use this code as long as attribution to the author remains intact.
         /mf
    */ 
    
    public class RaceCondition {
        private ArrayList<Integer> raceList = new ArrayList<Integer>(); // simple add(#)
    //  private Vector<Integer> raceList = new Vector<Integer>(); // simple add(#)
    
        private String countLock="lock";    // Object use for locking the raceCount
        private int raceCount = 0;        // simple add 1 to this counter
        private int MAX = 10000;        // Do this 10,000 times
        private int NUM_THREADS = 100;    // Create 100 threads
    
        public static void main(String [] args) {
        new RaceCondition();
        }
    
        public RaceCondition() {
        ArrayList<Thread> arT = new ArrayList<Thread>();
    
        // Create thread objects, add them to an array list
        for( int i=0; i<NUM_THREADS; i++){
            Thread rt = new RaceThread( ); // i );
            arT.add( rt );
        }
    
        // Start all object at once.
        for( Thread rt : arT ){
            rt.start();
        }
    
        // Wait for all threads to finish before we can print totals created by threads
        for( int i=0; i<NUM_THREADS; i++){
            try { arT.get(i).join(); }
            catch( InterruptedException ie ) { System.out.println("Interrupted thread "+i); }
        }
    
        // All threads finished, print the summary information.
        // (Try to print this informaiton without the join loop above)
        System.out.printf("\nRace condition, should have %,d. Really have %,d in array, and count of %,d.\n",
                    MAX*NUM_THREADS, raceList.size(), raceCount );
        System.out.printf("Array lost %,d. Count lost %,d\n",
                 MAX*NUM_THREADS-raceList.size(), MAX*NUM_THREADS-raceCount );
        }   // end RaceCondition constructor
    
    
    
        class RaceThread extends Thread {
        public void run() {
            for ( int i=0; i<MAX; i++){
            try {
                update( i );        
            }    // These  catches show when one thread steps on another's values
            catch( ArrayIndexOutOfBoundsException ai ){ System.out.print("A"); }
            catch( OutOfMemoryError oome ) { System.out.print("O"); }
            }
        }
    
        // so we don't lose counts, need to synchronize on some object, not primitive
        // Created "countLock" to show how this can work.
        // Comment out the synchronized and ending {, see that we lose counts.
    
    //    public synchronized void update(int i){   // use A
        public void update(int i){                  // remove this when adding A
    //      synchronized(countLock){            // or B
    //      synchronized(this){             // or C
            raceCount = raceCount + 1;
            raceList.add( i );      // use Vector  
    //          }           // end block for B or C
        }   // end update
    
        }   // end RaceThread inner class
    
    
    } // end RaceCondition outter class
    
  • 75

    使用同步块,您可以拥有多个同步器,以便可以同时进行多个同时但不冲突的事物 .

  • 35

    正如已经说过的,synchronized块可以使用用户定义的变量作为锁定对象,当同步函数只使用“this”时 . 当然,您可以使用应该同步的函数区域进行操作 . 但是每个人都说同步函数和块之间没有区别,它使用“this”作为锁定对象来覆盖整个函数 . 事实并非如此,区别在于将在两种情况下生成的字节代码 . 在同步块使用的情况下,应该分配局部变量,该变量保持对“this”的引用 . 因此,我们将有一个更大的功能大小(如果您只有很少的功能,则不相关) .

    您可以找到的差异的更详细的解释这里:http://www.artima.com/insidejvm/ed2/threadsynchP.html

  • 1

    TLDR; 既不使用 synchronized 修饰符也不使用 synchronized(this){...} 表达式,而 synchronized(myLock){...} 其中 myLock 是包含私有对象的最终实例字段 .


    在方法声明中使用 synchronized 修饰符和在方法体中使用 synchronized(..){ } 表达式之间的区别是:

    • 方法签名上指定的 synchronized 修饰符

    • 在生成的JavaDoc中可见,
      在测试Modifier.SYNCHRONIZED的方法修改器时,

    • 可通过reflection以编程方式确定,
      synchronized(this) { .... } 相比,

    • 需要更少的输入和缩进

    • (取决于您的IDE)在类大纲和代码完成中可见,

    • 在非静态方法上声明时使用 this 对象作为锁,或者在静态方法上声明时使用封闭类 .

    • synchronized(...){...} 表达式允许您

    • 只是同步方法体的各个部分的执行,

    • 将在构造函数或(static)初始化块中使用,

    • 选择控制同步访问的锁定对象 .

    但是,使用 synchronized 修饰符或 synchronized(...) {...}this 作为锁定对象(如 synchronized(this) {...} 中),具有相同的缺点 . 两者都使用它自己的实例作为同步的锁对象 . 这很危险,因为不仅对象本身而且持有对该对象的引用的其他外部对象/代码也可以将其用作具有潜在严重副作用的同步锁(性能下降和deadlocks) .

    因此,最佳做法是既不将 synchronized 修饰符也不将 synchronized(...) 表达式与 this 一起用作锁定对象,而是将锁定对象专用于此对象 . 例如:

    public class MyService {
        private final lock = new Object();
    
        public void doThis() {
           synchronized(lock) {
              // do code that requires synchronous execution
            }
        }
    
        public void doThat() {
           synchronized(lock) {
              // do code that requires synchronous execution
            }
        }
    }
    

    您还可以使用多个锁定对象,但需要特别注意确保在嵌套时不会导致死锁 .

    public class MyService {
        private final lock1 = new Object();
        private final lock2 = new Object();
    
        public void doThis() {
           synchronized(lock1) {
              synchronized(lock2) {
                  // code here is guaranteed not to be executes at the same time
                  // as the synchronized code in doThat() and doMore().
              }
        }
    
        public void doThat() {
           synchronized(lock1) {
                  // code here is guaranteed not to be executes at the same time
                  // as the synchronized code in doThis().
                  // doMore() may execute concurrently
            }
        }
    
        public void doMore() {
           synchronized(lock2) {
                  // code here is guaranteed not to be executes at the same time
                  // as the synchronized code in doThis().
                  // doThat() may execute concurrently
            }
        }
    }
    
  • 6

    注意: static synchronized方法和块在Class对象上工作 .

    public class MyClass {
       // locks MyClass.class
       public static synchronized void foo() {
    // do something
       }
    
       // similar
       public static void foo() {
          synchronized(MyClass.class) {
    // do something
          }
       }
    }
    
  • 2

    经常在方法级别上使用锁定太粗鲁了 . 为什么通过锁定整个方法来锁定一段不访问任何共享资源的代码 . 由于每个对象都有一个锁,因此您可以创建虚拟对象来实现块级同步 . The block level is more efficient because it does not lock the whole method.

    这里有一些例子

    Method Level

    class MethodLevel {
    
      //shared among threads
    SharedResource x, y ;
    
    public void synchronized method1() {
       //multiple threads can't access
    }
    public void synchronized method2() {
      //multiple threads can't access
    }
    
     public void method3() {
      //not synchronized
      //multiple threads can access
     }
    }
    

    Block Level

    class BlockLevel {
      //shared among threads
      SharedResource x, y ;
    
      //dummy objects for locking
      Object xLock = new Object();
      Object yLock = new Object();
    
        public void method1() {
         synchronized(xLock){
        //access x here. thread safe
        }
    
        //do something here but don't use SharedResource x, y
        // because will not be thread-safe
         synchronized(xLock) {
           synchronized(yLock) {
          //access x,y here. thread safe
          }
         }
    
         //do something here but don't use SharedResource x, y
         //because will not be thread-safe
        }//end of method1
     }
    

    [Edit]

    对于 Collection ,如 VectorHashtable ,当 ArrayListHashMap 不是时,它们会同步,您需要设置synchronized关键字或调用Collections synchronized方法:

    Map myMap = Collections.synchronizedMap (myMap); // single lock for the entire map
    List myList = Collections.synchronizedList (myList); // single lock for the entire list
    
  • 5

    我知道这是一个老问题,但我快速阅读了这里的回复,我并没有真正看到有人提到有时 synchronized 方法可能是 wrong 锁 .
    来自Java Concurrency In Practice(第72页):

    public class ListHelper<E> {
      public List<E> list = Collections.syncrhonizedList(new ArrayList<>());
    ...
    
    public syncrhonized boolean putIfAbsent(E x) {
     boolean absent = !list.contains(x);
    if(absent) {
     list.add(x);
    }
    return absent;
    }
    

    上面的代码具有线程安全的 appearance . 然而,实际上并非如此 . 在这种情况下,锁定是在类的实例上获得的 . 但是, list 可能会被另一个不使用该方法的线程修改 . 正确的方法是使用

    public boolean putIfAbsent(E x) {
     synchronized(list) {
      boolean absent = !list.contains(x);
      if(absent) {
        list.add(x);
      }
      return absent;
    }
    }
    

    以上代码将阻止 all threads 尝试修改 list 以修改列表,直到同步块完成 .

  • 27

    实际上,同步方法优于同步块的优点是它们更具抗骚扰性;因为您无法选择要锁定的任意对象,所以您不能滥用synchronized方法语法来执行愚蠢的操作,例如锁定字符串文字或锁定从线程下更改的可变字段的内容 .

    另一方面,使用synchronized方法,您无法保护锁被任何可以获取对象引用的线程获取 .

    因此,在方法上使用synchronized作为修饰符可以更好地保护您的牛群免受伤害,同时将synchronized块与私有最终锁定对象结合使用可以更好地保护您自己的代码免受牛犊的攻击 .

  • 29

    使用synchronized块的重要注意事项:小心你用作锁定对象的东西!

    上面的user2277816的代码片段说明了这一点,因为对字符串文字的引用被用作锁定对象 . 意识到字符串文字在Java中自动实现,你应该开始看到问题:在文字“锁定”上同步的每一段代码,分享同样的锁!这很容易导致完全不相关的代码片段死锁 .

    您需要注意的不仅仅是String对象 . 盒装基元也是一种危险,因为自动装箱和valueOf方法可以重复使用相同的对象,具体取决于值 .

    有关更多信息,请参阅:https://www.securecoding.cert.org/confluence/display/java/LCK01-J.+Do+not+synchronize+on+objects+that+may+be+reused

  • 5

    一般来说,除了明确使用对象的监视器与隐含的此对象之外,这些大致相同 . 我认为有时被忽略的同步方法的一个缺点是,在使用“this”引用进行同步时,您将保留外部对象锁定在同一对象上的可能性 . 如果碰到它,这可能是一个非常微妙的错误 . 在内部显式对象或其他现有字段上进行同步可以避免此问题,从而完全封装同步 .

  • 12

    大多数情况下,我使用它来同步对列表或映射的访问,但我不想阻止访问该对象的所有方法 .

    在以下代码中,修改列表的一个线程不会阻止等待正在修改映射的线程 . 如果方法在对象上同步,则每个方法都必须等待,即使它们所做的修改不会发生冲突 .

    private List<Foo> myList = new ArrayList<Foo>();
    private Map<String,Bar) myMap = new HashMap<String,Bar>();
    
    public void put( String s, Bar b ) {
      synchronized( myMap ) {
        myMap.put( s,b );
        // then some thing that may take a while like a database access or RPC or notifying listeners
      }
    }
    
    public void hasKey( String s, ) {
      synchronized( myMap ) {
        myMap.hasKey( s );
      }
    }
    
    public void add( Foo f ) {
      synchronized( myList ) {
        myList.add( f );
    // then some thing that may take a while like a database access or RPC or notifying listeners
      }
    }
    
    public Thing getMedianFoo() {
      Foo med = null;
      synchronized( myList ) {
        Collections.sort(myList);
        med = myList.get(myList.size()/2); 
      }
      return med;
    }
    
  • 136

    当java编译器将源代码转换为字节代码时,它会以非常不同的方式处理同步方法和同步块 .

    当JVM执行synchronized方法时,执行线程识别方法的method_info结构设置了ACC_SYNCHRONIZED标志,然后它自动获取对象的锁,调用方法并释放锁 . 如果发生异常,则线程会自动释放锁 .

    另一方面,同步方法块会绕过JVM对获取对象锁定和异常处理的内置支持,并要求以字节代码显式写入功能 . 如果您读取具有同步块的方法的字节代码,您将看到十几个额外的操作来管理此功能 .

    这显示了生成同步方法和同步块的调用:

    public class SynchronizationExample {
        private int i;
    
        public synchronized int synchronizedMethodGet() {
            return i;
        }
    
        public int synchronizedBlockGet() {
            synchronized( this ) {
                return i;
            }
        }
    }
    

    synchronizedMethodGet() 方法生成以下字节代码:

    0:  aload_0
    1:  getfield
    2:  nop
    3:  iconst_m1
    4:  ireturn
    

    这是 synchronizedBlockGet() 方法的字节代码:

    0:  aload_0
    1:  dup
    2:  astore_1
    3:  monitorenter
    4:  aload_0
    5:  getfield
    6:  nop
    7:  iconst_m1
    8:  aload_1
    9:  monitorexit
    10: ireturn
    11: astore_2
    12: aload_1
    13: monitorexit
    14: aload_2
    15: athrow
    

    同步方法和块之间的一个显着区别是,同步块通常会减小锁定范围 . 由于锁定范围与性能成反比,因此最好只锁定关键的代码段 . 使用synchronized块的最好例子之一是double checked locking in Singleton pattern,而不是锁定整个 getInstance() 方法,我们只锁定用于创建Singleton实例的关键代码段 . 这大大提高了性能,因为锁定只需要一到两次 .

    使用同步方法时,如果混合静态同步和非静态同步方法,则需要格外小心 .

  • 3

    在同步方法的情况下,将在对象上获取锁定 . 但是,如果使用synchronized块,则可以选择指定要获取锁定的对象 .

    示例:

    Class Example {
        String test = "abc";
        // lock will be acquired on String  test object.
        synchronized (test) {
            // do something
        }
    
       lock will be acquired on Example Object
       public synchronized void testMethod() {
         // do some thing
       } 
    
       }
    

相关问题