• Java 多线程 之 线程的同步机制


    一、线程的安全问题

      1、问题的发现

        当有多个线程同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,这就是线程安全的。

        下面通过一个案例来演示线程的安全问题。

        模拟电影票买票的过程,其中,一共有100张票。下面来模拟电影票的售票窗口,实现多个窗口同时卖票,采用线程对象来模拟,通过实现 Runnable 接口子类来模拟。

        示例:

     1 public class WindowTest1 {
     2     public static void main(String[] args) {
     3         Window w = new Window();
     4 
     5         Thread t1 = new Thread(w, "窗口1");
     6         Thread t2 = new Thread(w, "窗口2");
     7         Thread t3 = new Thread(w, "窗口3");
     8 
     9         //同时卖票
    10         t1.start();
    11         t2.start();
    12         t3.start();
    13     }
    14 }
    15 
    16 class Window implements Runnable {
    17 
    18     private int ticket = 100;
    19 
    20     @Override
    21     public void run() {
    22         
    23         while (true) {
    24             //有票,可以出售
    25             if (ticket > 0) {
    26 
    27                 //出票操作,使用 sleep 模拟一下出票时间
    28                 try {
    29                     Thread.sleep(100);
    30                 } catch (InterruptedException e) {
    31                     e.printStackTrace();
    32                 }
    33                 //获取当前线程对象的名字
    34                 System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
    35 
    36 
    37                 ticket--;
    38             } else {
    39                 break;
    40             }
    41         }
    42     }
    43 }

      运行结果,发现会有这样的现象发生:

      (1)错票

        

      (2)重票

        

         在运行结果中可以看到会有两个问题发生:

          ① 相同的票数,比如100这张票被卖了两次;

          ② 不存在的票,比如 0 和 -1 票,是不存在的;

      2、分析问题

        针对于上面的售票现象,为什么会出现这样的情况呢?

        当只有一个窗口售票或多个窗口分别出售自己的票是没有问题的。但是当三个窗口,同时访问共享的资源,就会导致线程不同步,这种问题称为 线程不安全

      线程安全问题的产生的原理:

          可以发现多个线程执行的不确定性引起执行结果的不稳定;

          多个线程对公共的数据处理,会造成操作的不完整性,会破坏数据。

        注意:线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

      

      3、问题的总结

        问题出现的原因当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

        如何解决对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

             当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

    二、同步机制

      Java 对于多线程的安全问题提供了专业的解决方式:同步机制

      针对上面的售票案例简单描述一下同步机制:

    当窗口1线程进入操作的时候,窗口2和窗口3线程只能在外面等着,窗口1操作结束,窗口1、窗口2和窗口3才有机会进入代码去执行。
    也就是说某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

      同步方式的分析:

        1、同步的方式,解决了线程的安全问题(好处)

        2、操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。(局限性)

      同步的注意项:

        (1)操作共享数据的代码,即为需要被同步的代码加锁;

            不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

        (2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

        (3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

            要求:多个线程必须要共用同一把锁。

      

    三、方式一:同步代码块

      1、语法格式

    synchronized (对象){
        // 需要被同步的代码 / 可能会出现线程安全问题的代码(访问共享数据的代码)
    }
    

      

      2、实现方式使用同步代码块

        代码示例:

     1 public class WindowTest1 {
     2     public static void main(String[] args) {
     3         Window w = new Window();
     4 
     5         Thread t1 = new Thread(w, "窗口1");
     6         Thread t2 = new Thread(w, "窗口2");
     7         Thread t3 = new Thread(w, "窗口3");
     8 
     9         //同时卖票
    10         t1.start();
    11         t2.start();
    12         t3.start();
    13     }
    14 }
    15 
    16 class Window implements Runnable {
    17 
    18     private int ticket = 100;
    19     //创建一个 同步监视器,锁对象
    20     private Object obj = new Object();
    21 
    22     @Override
    23     public void run() {
    24         
    25         while (true) {
    26             synchronized (obj) {  //使用 synchronized 给操作共享数据的地方加锁
    27                 //有票,可以出售
    28                 if (ticket > 0) {
    29 
    30                     //出票操作,使用 sleep 模拟一下出票时间
    31                     try {
    32                         Thread.sleep(100);
    33                     } catch (InterruptedException e) {
    34                         e.printStackTrace();
    35                     }
    36                     //获取当前线程对象的名字
    37                     System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
    38 
    39 
    40                     ticket--;
    41                 } else {
    42                     break;
    43                 }
    44             }
    45         }
    46     }
    47 }

      如果加锁都需要创建一个对象,我们可以改进一下:

     1 public class WindowTest1 {
     2     public static void main(String[] args) {
     3         Window w = new Window();
     4 
     5         Thread t1 = new Thread(w, "窗口1");
     6         Thread t2 = new Thread(w, "窗口2");
     7         Thread t3 = new Thread(w, "窗口3");
     8 
     9         //同时卖票
    10         t1.start();
    11         t2.start();
    12         t3.start();
    13     }
    14 }
    15 
    16 class Window implements Runnable {
    17 
    18     private int ticket = 100;
    19 
    20     @Override
    21     public void run() {
    22         
    23         while (true) {
    24             synchronized (this) {  //方式二:此时的this:唯一的Window1的对象
    25                 //有票,可以出售
    26                 if (ticket > 0) {
    27 
    28                     //出票操作,使用 sleep 模拟一下出票时间
    29                     try {
    30                         Thread.sleep(100);
    31                     } catch (InterruptedException e) {
    32                         e.printStackTrace();
    33                     }
    34                     //获取当前线程对象的名字
    35                     System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
    36 
    37 
    38                     ticket--;
    39                 } else {
    40                     break;
    41                 }
    42             }
    43         }
    44     }
    45 }

      注意:这里的 this 指的就是当前的 Window 对象,因为在main方法中多个 Thread 共用了同一个 Window 对象,所以这里的 this 是公共的锁。

      3、继承方式使用同步代码块

     1 public class WindowTest2 {
     2     public static void main(String[] args) {
     3         Window2 t1 = new Window2();
     4         Window2 t2 = new Window2();
     5         Window2 t3 = new Window2();
     6 
     7         t1.setName("窗口1");
     8         t2.setName("窗口2");
     9         t3.setName("窗口3");
    10 
    11         t1.start();
    12         t2.start();
    13         t3.start();
    14     }
    15 }
    16 
    17 class Window2 extends Thread {
    18     private static int ticket = 100;
    19     private static Object obj = new Object();
    20 
    21     @Override
    22     public void run() {
    23 
    24         while (true) {
    25             synchronized (obj) {
    26                 if (ticket > 0) {
    27 
    28                     try {
    29                         Thread.sleep(100);
    30                     } catch (InterruptedException e) {
    31                         e.printStackTrace();
    32                     }
    33 
    34                     System.out.println(getName() + ":卖票,票号为:" + ticket);
    35                     ticket--;
    36                 } else {
    37                     break;
    38                 }
    39             }
    40         }
    41     }
    42 }

      对于继承的方式,如果我们也想避免创建的繁琐,可以这样写:

     1 public class WindowTest2 {
     2     public static void main(String[] args) {
     3         Window2 t1 = new Window2();
     4         Window2 t2 = new Window2();
     5         Window2 t3 = new Window2();
     6 
     7         t1.setName("窗口1");
     8         t2.setName("窗口2");
     9         t3.setName("窗口3");
    10 
    11         t1.start();
    12         t2.start();
    13         t3.start();
    14     }
    15 }
    16 
    17 class Window2 extends Thread {
    18     private static int ticket = 100;
    19 
    20     @Override
    21     public void run() {
    22 
    23         while (true) {
    24             synchronized (Window2.class) {  //方式2
    25                 if (ticket > 0) {
    26 
    27                     try {
    28                         Thread.sleep(100);
    29                     } catch (InterruptedException e) {
    30                         e.printStackTrace();
    31                     }
    32 
    33                     System.out.println(getName() + ":卖票,票号为:" + ticket);
    34                     ticket--;
    35                 } else {
    36                     break;
    37                 }
    38             }
    39         }
    40     }
    41 }

      注意这里是不能使用 this 的,因为在 main 中创建了多个 Window2对象,它们各不一样。但是我们可以使用当前类的对象,全局唯一的类对象来充当锁。因为类对象只会加载一次,是全局唯一的。

      4、小结

        (1)操作共享数据的代码,即为需要被同步的代码加锁;

            不能包含的代码少了(同步将不起作用),也不能包含多了(可能造成死锁或与逻辑混乱)

        (2)共享数据:多个线程共同操作的变量。例如:ticket就是共享数据。

        (3)同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。

            要求:多个线程必须要共用同一把锁。

          注意:(具体情况还要具体分析!

          ① 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。

          ② 在继承Thread类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器

        

    四、方式二:同步方法

      1、语法格式

    public static synchronized void show (String name){
       可能会产生线程安全问题的代码 / 可能会出现线程安全问题的代码(访问了共享数据的代码)
    }
    

        synchronized还可以放在方法声明中,表示整个方法为同步方法

       如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

      2、实现方式使用同步方法

        代码示例:

     1 public class WindowTest3 {
     2     public static void main(String[] args) {
     3         Window3 w = new Window3();
     4 
     5         Thread t1 = new Thread(w);
     6         Thread t2 = new Thread(w);
     7         Thread t3 = new Thread(w);
     8 
     9         t1.setName("窗口1");
    10         t2.setName("窗口2");
    11         t3.setName("窗口3");
    12 
    13         t1.start();
    14         t2.start();
    15         t3.start();
    16     }
    17 }
    18 
    19 class Window3 implements Runnable {
    20     private int ticket = 100;
    21 
    22 
    23     @Override
    24     public void run() {
    25         while (true) {
    26             show();
    27         }
    28     }
    29 
    30     private synchronized void show() {  //同步监视器:this
    31         if (ticket > 0) {
    32 
    33             try {
    34                 Thread.sleep(100);
    35             } catch (InterruptedException e) {
    36                 e.printStackTrace();
    37             }
    38 
    39             System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
    40 
    41             ticket--;
    42         }
    43     }
    44 }

        注意:这里的锁对象是 this,因为在 main 方法中还是共用了同一个 Window3 对象,这里的 this 就是此对象,可以公共的锁对象。如果在 main 中创建了多个 Window3 对象并传递给 Thread来启动,这样并不可以保证同步哦!

      3、继承方式使用同步方法

        代码示例:

     1 public class WindowTest4 {
     2     public static void main(String[] args) {
     3         Window4 t1 = new Window4();
     4         Window4 t2 = new Window4();
     5         Window4 t3 = new Window4();
     6 
     7 
     8         t1.setName("窗口1");
     9         t2.setName("窗口2");
    10         t3.setName("窗口3");
    11 
    12         t1.start();
    13         t2.start();
    14         t3.start();
    15 
    16     }
    17 }
    18 
    19 class Window4 extends Thread {
    20     private static int ticket = 100;
    21 
    22     @Override
    23     public void run() {
    24         while (true) {
    25             show();
    26         }
    27     }
    28 
    29     private static synchronized void show() {  //同步监视器:Window4.class
    30         if (ticket > 0) {
    31 
    32             try {
    33                 Thread.sleep(100);
    34             } catch (InterruptedException e) {
    35                 e.printStackTrace();
    36             }
    37 
    38             System.out.println(Thread.currentThread().getName() + ":卖票,票号为:" + ticket);
    39             ticket--;
    40         }
    41     }
    42 }

        注意:在继承中这样使用的锁对象就是 Window4.class,当前的类对象。

        切记不能写成这样

    //private synchronized void show(){ //同步监视器:t1,t2,t3。此种解决方式是错误的
    

         这样他们的锁对象就不再是类对象,而是以每个实例对象为锁的,并不是公共的锁,不能保证同步。

      4、小结

        (1)同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。

        (2)非静态的同步方法,同步监视器是实现类对象:this;

            静态的同步方法,同步监视器是当前类对象:当前类本身

      

    五、同步的总结

      1、分析同步原理

        

      2、同步机制中的锁

        (1)同步锁机制

        (2)synchronized 的锁是什么?

          ① 任意对象都可以作为同步锁,所有对象都自动含有单一的锁(监视器)。

          ② 同步方法的锁:静态方法(类名.class)、非静态方法(this)

          ③ 同步代码块:自己指定,很多时候也是指定为 this 或 类名.class

        (3)注意

          ① 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则无法保证共享资源的安全;

          ② 一个线程类中的所有静态方法共用一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

      3、同步的范围

        (1)如何找问题,即代码是否存在线程安全?(重要)

          ① 明确哪些代码是多线程运行的代码;

          ② 明确多个线程是否有共享数据;

          ③ 明确多线程运行代码中是否有多条语句操作共享数据;

        (2)如果解决?(重要)

          ① 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行;

          ② 即所有操作共享数据的这些语句都要放在同步范围中;

        (3)切记

          ① 范围太小:没锁住所有有安全问题的代码

          ② 范围太大:没发挥多线程的功能;

      4、释放锁的操作

        (1)当前线程的同步方法、同步代码块执行结束;

        (2)当前线程在同步代码块、同步方法中遇到 break、return 终止了该代码块、该方法的继续执行;

        (3)当前线程在同步代码块、同步方法中出现了未处理的 Error 或 Exception,导致异常结束;

        (4)当前线程在同步代码块、同步方法中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁。

      5、不会释放锁的操作

        (1)线程执行同步代码块或同步方式时,程序调用 Thread.sleep()、Thread.yield() 方法暂停当前线程的执行;

        (2)线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起,该线程不会释放锁(不同监视器);

           应尽量避免使用 suspend() 和 resume() 来控制线程;

    六、Lock 锁——JDK5.0 新增锁

      1、Lock 锁

        (1)从 JDK5.0 开始,Java 提供了更强大的线程同步机制——通过显示定义同步锁对象来实现同步,同步锁使用 Lock 对象充当;

        (2)java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具。

            锁提供了对共享资源的独占访问,每次只能有一个线程对 Lock 对象加锁,线程开始访问共享资源之前应先获得 Lock 对象。

        (3)ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock,可以显式加锁、释放锁。

      2、语法

    class A{
        private final ReentrantLock lock = new ReenTrantLock();
            public void m(){
                lock.lock();
                try{
                    //保证线程安全的代码;
                }
                finally{
                    lock.unlock();
                }
            }
    }        
    

        注意:如果同步代码有异常,要将 unlock( )写入finally语句块

      3、实现方式使用 Lock 锁

        使用步骤

          ① 在成员位置创建一个ReentrantLock对象(Lock接口的一个实现类)

          ② 在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁

          ③ 在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁

        示例:

     1 public class WindowTest5 {
     2     public static void main(String[] args) {
     3         Window5 w = new Window5();
     4 
     5         Thread t1 = new Thread(w);
     6         Thread t2 = new Thread(w);
     7         Thread t3 = new Thread(w);
     8 
     9         t1.setName("窗口1");
    10         t2.setName("窗口2");
    11         t3.setName("窗口3");
    12 
    13         t1.start();
    14         t2.start();
    15         t3.start();
    16     }
    17 }
    18 
    19 class Window5 implements Runnable {
    20 
    21     private int ticket = 100;
    22     //1.实例化ReentrantLock
    23     private ReentrantLock lock = new ReentrantLock();
    24 
    25     @Override
    26     public void run() {
    27         while (true) {
    28             try {
    29                 //2.调用锁定方法 lock()
    30                 lock.lock();
    31 
    32                 if(ticket > 0){
    33 
    34                     try {
    35                         Thread.sleep(100);
    36                     } catch (InterruptedException e) {
    37                         e.printStackTrace();
    38                     }
    39 
    40                     System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
    41                     ticket--;
    42                 }else{
    43                     break;
    44                 }
    45             } finally {
    46                 //3.调用解锁方法:unlock(),无论程序是否异常,都会把锁释放
    47                 lock.unlock();
    48             }
    49         }
    50     }
    51 }

      4、继承方式使用 Lock 锁

        代码示例:

     1 public class WindowTest6 {
     2     public static void main(String[] args) {
     3         Window6 w1 = new Window6();
     4         Window6 w2 = new Window6();
     5         Window6 w3 = new Window6();
     6 
     7         w1.setName("窗口一");
     8         w2.setName("窗口二");
     9         w3.setName("窗口三");
    10 
    11         w1.start();
    12         w2.start();
    13         w3.start();
    14     }
    15 }
    16 
    17 class Window6 extends Thread {
    18 
    19     private static int ticket = 100;
    20     //1.实例化ReentrantLock
    21     private static ReentrantLock lock = new ReentrantLock();
    22 
    23     @Override
    24     public void run() {
    25         while (true) {
    26             try {
    27                 //2.调用锁定方法 lock()
    28                 lock.lock();
    29 
    30                 if(ticket > 0){
    31 
    32                     try {
    33                         Thread.sleep(100);
    34                     } catch (InterruptedException e) {
    35                         e.printStackTrace();
    36                     }
    37 
    38                     System.out.println(Thread.currentThread().getName() + ":售票,票号为:" + ticket);
    39                     ticket--;
    40                 }else{
    41                     break;
    42                 }
    43             } finally {
    44                 //3.调用解锁方法:unlock()
    45                 lock.unlock();
    46             }
    47         }
    48     }
    49 }

      5、synchronized 与 Lock 的对比

        (1)Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized 是隐式锁,出了作用自动释放;

        (2)Lock 只有代码块锁,synchronized 有代码块锁和方法所;

        (3)使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)

        优先使用顺序:

    Lock ——>同步代码块(已经进入了方法体,分配了相应资源)——>同步方法(在方法体之外)

    七、线程的死锁问题

      1、死锁

        (1)不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁;

        (2)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续;

        (3)死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。

      2、死锁的必要条件

        死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件

        1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
        2)请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
        3)不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
        4)环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

      3、解决方法

        (1)专门的算法、原则;

        (2)尽量减少同步资源的定义;

        (3)尽量避免嵌套同步

      4、示例

        示例一:

     1 //死锁的演示
     2 class A {
     3     public synchronized void foo(B b) { //同步监视器:A类的对象:a
     4         System.out.println("当前线程名: " + Thread.currentThread().getName()
     5                 + " 进入了A实例的foo方法"); //
     6         try {
     7             Thread.sleep(200);
     8         } catch (InterruptedException ex) {
     9             ex.printStackTrace();
    10         }
    11         System.out.println("当前线程名: " + Thread.currentThread().getName()
    12                 + " 企图调用B实例的last方法"); //
    13         b.last();
    14     }
    15 
    16     public synchronized void last() {//同步监视器:A类的对象:a
    17         System.out.println("进入了A类的last方法内部");
    18     }
    19 }
    20 
    21 class B {
    22     public synchronized void bar(A a) {//同步监视器:b
    23         System.out.println("当前线程名: " + Thread.currentThread().getName()
    24                 + " 进入了B实例的bar方法"); //
    25         try {
    26             Thread.sleep(200);
    27         } catch (InterruptedException ex) {
    28             ex.printStackTrace();
    29         }
    30         System.out.println("当前线程名: " + Thread.currentThread().getName()
    31                 + " 企图调用A实例的last方法"); //
    32         a.last();
    33     }
    34 
    35     public synchronized void last() {//同步监视器:b
    36         System.out.println("进入了B类的last方法内部");
    37     }
    38 }
    39 
    40 public class DeadLock1 implements Runnable {
    41     A a = new A();
    42     B b = new B();
    43 
    44     public void init() {
    45         Thread.currentThread().setName("主线程");
    46         // 调用a对象的foo方法
    47         a.foo(b);
    48         System.out.println("进入了主线程之后");
    49     }
    50 
    51     public void run() {
    52         Thread.currentThread().setName("副线程");
    53         // 调用b对象的bar方法
    54         b.bar(a);
    55         System.out.println("进入了副线程之后");
    56     }
    57 
    58     public static void main(String[] args) {
    59         DeadLock1 dl = new DeadLock1();
    60         new Thread(dl).start();
    61 
    62 
    63         dl.init();
    64     }
    65 }

        示例二:

     1 public class DeadLock2 {
     2     public static void main(String[] args) {
     3 
     4         StringBuffer s1 = new StringBuffer();
     5         StringBuffer s2 = new StringBuffer();
     6 
     7 
     8         new Thread() {
     9             @Override
    10             public void run() {
    11 
    12                 synchronized (s1) {
    13 
    14                     s1.append("a");
    15                     s2.append("1");
    16 
    17                     try {
    18                         Thread.sleep(100);
    19                     } catch (InterruptedException e) {
    20                         e.printStackTrace();
    21                     }
    22 
    23 
    24                     synchronized (s2) {
    25                         s1.append("b");
    26                         s2.append("2");
    27 
    28                         System.out.println(s1);
    29                         System.out.println(s2);
    30                     }
    31                 }
    32             }
    33         }.start();
    34 
    35 
    36         new Thread(new Runnable() {
    37             @Override
    38             public void run() {
    39                 synchronized (s2) {
    40 
    41                     s1.append("c");
    42                     s2.append("3");
    43 
    44                     try {
    45                         Thread.sleep(100);
    46                     } catch (InterruptedException e) {
    47                         e.printStackTrace();
    48                     }
    49 
    50                     synchronized (s1) {
    51                         s1.append("d");
    52                         s2.append("4");
    53 
    54                         System.out.println(s1);
    55                         System.out.println(s2);
    56                     }
    57                 }
    58             }
    59         }).start();
    60     }
    61 
    62 }
  • 相关阅读:
    Scrum敏捷开发中的各种会议到底怎么开?
    敏捷的方法与误区
    原创:分享封装好的面向JAVA的memcached客户端操作类
    ReactRouter路由配置
    脚手架创建react项目
    React基础知识
    Fetch请求
    redux学习笔记
    npm install常用用法
    python3+selenium3自动化8——当click事件失效,如何解决 测试
  • 原文地址:https://www.cnblogs.com/niujifei/p/14404178.html
Copyright © 2020-2023  润新知