• 高并发编程之synchronized


    一、什么是线程? 

      线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。
      线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
     
    二、synchronized关键字
      首先看一段代码
     1 /**
     2  * synchronized关键字,对某个对象加锁
     3  * @author Wuyouxin
     4  *
     5  */
     6 public class T {
     7 
     8     private int count = 10;
     9     private Object obj = new Object();
    10     
    11     public void  m (){
    12         synchronized(obj){//任何线程想要执行下面代码就必须先要拿到obj的锁。
    13             count--;
    14             System.out.println(Thread.currentThread().getName() + " count:" + count);
    15         }
    16     }
    17 }

      如果想要执行synchronized里面的这段代码,就必须获取到obj这个对象的锁,如果多和线程同时执行时,A线程获取到了obj对象并且加锁,B对象来执行这段代码获取到obj对象 ,但是发现obj对象已经被锁定,所以就会等待A线程执行完这段代码并释放锁,等A对象释放之后B对象再锁定obj对象再执行。这就是简单的互斥锁。上面代码中我们锁定的是obj对象但是在正常开发中不会去专门new一个对象来锁定,所以我们一般是是锁定自身,在使用这个方法前先new本身的对象再去执行。而且synchronized锁定的是对象,而不是代码块。

    1 public void  m (){
    2     synchronized(this){//任何线程想要执行下面代码就必须先要拿到T类的对象的锁。
    3         count--;
    4         System.out.println(Thread.currentThread().getName() + " count:" + count);
    5     }
    6 }

      如果一个方法执行,需要锁定到本身的对象还有一种写法。

    1     public synchronized void  m (){ //等同于在方法块中synchronized(this){}
    2         count--;
    3         System.out.println(Thread.currentThread().getName() + " count:" + count);
    4     }

      当一个synchronized修饰一个静态方法时,静态方法不需要new对象出来就可以直接方法,这时锁定的是T.class对象

     1 public class T {
     2     private static int count = 10;
     3     
     4     public synchronized static void m1 (){
     5         count--;
     6     }
     7     
     8     public static void m2 (){
     9         synchronized(T.class){ //这个方法跟上面方法等价
    10             count--;
    11         }
    12     }
    13 }

      我们看下面代码:

     1 public class T implements Runnable{
     2 
     3     private int count = 10;
     4     @Override
     5     public /*synchronized*/ void run() {
     6         count--;
     7         System.out.println(Thread.currentThread().getName() + " count:" + count);
     8     }
     9 
    10     public static void main(String[] args) {
    11         T t = new T();
    12         for (int i=0; i<5 ; i++){
    13             new Thread(t, "thread" +i).start();
    14         }
    15     }
    16 }

      这里我们将synchronized注释起来,去运行这段代码。

      我们发现thread0和thread1输出了同样的结果,这是怎么回事呢?这里其实发生了线程重入的问题,当thread0执行完成count--之后还没来得及打印,线程thread1进来打断了thread0发现count是9,然后减一后输出8,然后thread0再输出,但是这时count已经变成8了所以也输出了8。如果我们加上synchronized,则每一个线程执行时都要获取到T的锁并且锁定后才会执行,这样几不会出现线程重入的情况。

    二、在现在中synchronized方法与普通方法可以同时执行么?

      我们看下面代码:

     1 /**
     2  * synchronized方法与非synchronized方法可以同时运行么?
     3  * @author Wuyouxin
     4  *
     5  */
     6 public class T {
     7 
     8     public synchronized void m1(){
     9         System.out.println(Thread.currentThread().getName() + "m1 start");
    10         try {
    11             Thread.sleep(10000);
    12         } catch (InterruptedException e) {
    13             e.printStackTrace();
    14         }
    15         System.out.println(Thread.currentThread().getName() + "m1 end");
    16     }
    17     
    18     public void m2 (){
    19         System.out.println(Thread.currentThread().getName() + "m2 start");
    20         try {
    21             Thread.sleep(5000);
    22         } catch (InterruptedException e) {
    23             e.printStackTrace();
    24         }
    25         System.out.println(Thread.currentThread().getName() + "m2 end");
    26     }
    27 
    28     public static void main(String[] args) {
    29         final T t = new T();
    30         
    31         Runnable run1 = new Runnable() {
    32             @Override
    33             public void run() {
    34                 t.m1();
    35             }
    36         };
    37         new Thread(run1, "thread1").start();
    38         
    39         Runnable run2 = new Runnable() {
    40             @Override
    41             public void run() {
    42                 t.m2();
    43             }
    44         };
    45         new Thread(run2, "thread2").start();
    46     }
    47 }

      答案是当然可以,因为只有synchronized方法才会去获取对象锁,而非synchronized不需要获取对象锁,所以可以一起执行,并不会互相影响。

    三、脏读问题

      对写业务方法加锁,对读业务不加锁,容易产生脏读。我们看下面代码:

     1 /**
     2  * 对写业务加锁,对读业务不加锁,容易产生脏读问题
     3  * @author Wuyouxin
     4  *
     5  */
     6 public class Account {
     7     private String name;
     8     private double balance;
     9     
    10     /**
    11      * 设置余额
    12      * @param name
    13      * @param balance
    14      */
    15     public synchronized void set (String name, double balance){
    16         this.name = name;
    17         try {
    18             Thread.sleep(2000);
    19         } catch (Exception e) {
    20         }
    21         this.balance = balance;
    22     }
    23     
    24     /**
    25      * 根据名字获取余额
    26      * @param name
    27      * @return
    28      */
    29     public double get (String name){
    30         return this.balance;
    31     }
    32     
    33     public static void main(String[] args) {
    34         final Account account = new Account();
    35         
    36         new Thread(new Runnable() {
    37             @Override
    38             public void run() {
    39                 account.set("张三", 100);
    40             }
    41         }).start();
    42         
    43         try {
    44             TimeUnit.SECONDS.sleep(1);
    45         } catch (InterruptedException e) {
    46             e.printStackTrace();
    47         }
    48         
    49         System.out.println(account.get("张三"));
    50         
    51         try {
    52             TimeUnit.SECONDS.sleep(2);
    53         } catch (InterruptedException e) {
    54             e.printStackTrace();
    55         }
    56         
    57         System.out.println(account.get("张三"));
    58         
    59     }
    60 }

      在上面这段代码中,我们对写业务进行加锁,而读没有加锁,结果2次输出第一次为0.0第二次为100.0。我们发现2次读到的数据不一致,这就是脏读,而在实际的开发中我们应当去根据业务逻辑去判断如果读的方法不要求数据完全一致,则不需要加锁,这样可以提高效率。如果读的方法完全要求读取数据一致则必须加锁,否则会出现脏读的情况。

    四、锁重入

      一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,再次申请时任然会获得该对象的锁,也就是说synchronized的锁是可以重入的。

     1 /**
     2  * 一个同步方法可以调用另一个同步方法,一个线程已经拥有某个对象的锁,
     3  * 再次申请时任然会获得该对象的锁,也就是说synchronized的锁是可以重入的。
     4  * @author Wuyouxin
     5  *
     6  */
     7 public class T {
     8 
     9     synchronized void m1 (){
    10         System.out.println("m1 start");
    11         try {
    12             Thread.sleep(1000);
    13         } catch (InterruptedException e) {
    14             e.printStackTrace();
    15         }
    16         m2();
    17     }
    18     
    19     synchronized void m2 (){
    20         try {
    21             Thread.sleep(2000);
    22         } catch (InterruptedException e) {
    23             e.printStackTrace();
    24         }
    25         System.out.println("m2");
    26     }
    27 }

      锁重入还有一种情况就是子类调用父类的同步方法。

     1 /**
     2  * 继承中,子类调用父类的同步方法。
     3  * @author Wuyouxin
     4  *
     5  */
     6 public class T1 {
     7 
     8     synchronized void m(){
     9         System.out.println("m start");
    10         try {
    11             Thread.sleep(1000);
    12         } catch (InterruptedException e) {
    13             e.printStackTrace();
    14         }
    15         System.out.println("m end");
    16     }
    17     
    18     public static void main(String[] args) {
    19         new T2().m();
    20     }
    21 }
    22 
    23 class T2 extends T1 {
    24     @Override
    25     synchronized void m (){
    26         System.out.println("m2 stard");
    27         super.m();
    28         System.out.println("m2 end");
    29     }
    30 }

     

    五、遇到异常释放锁

       程序在执行过程中,如果遇到异常,默认情况所会被释放,所以,在并发编程中,有异常要多加小心,不然可能会发生不一致的情况。

      比如,在一个web app处理过程中,多个servlet线程共同访问同一个资源,这时如果异常处理不合适,在第一个线程中抛出异常,其他线程就会进入同步代码区,有可能会访问到异常产生时的数据。因此要非常小心的处理同步业务逻辑中的异常。

      我们看下面代码:

     1 /**
     2  * 程序在执行过程中,如果遇到异常,默认情况所会被释放,
     3  * 所以,在并发编程中,有异常要多加小心,不然可能会发生不一致的情况。
     4  * @author Wuyouxin
     5  *
     6  */
     7 public class T {
     8     int count = 0;
     9     synchronized void m(){
    10         System.out.println(Thread.currentThread().getName()+" start");
    11         while (true){
    12             count ++;
    13             System.out.println(Thread.currentThread().getName()+ " count=" + count);
    14             try {
    15                 TimeUnit.SECONDS.sleep(1);
    16             } catch (InterruptedException e) {
    17                 e.printStackTrace();
    18             }
    19             
    20             if (count == 5){
    21                 int i = 1/0;//这里会产生异常,导致锁释放,如果不想让锁释放需要捕获异常
    22             }
    23         }
    24     }
    25     
    26     public static void main(String[] args) {
    27         final T t = new T();
    28         new Thread(new Runnable() {
    29             @Override
    30             public void run() {
    31                 t.m();
    32             }
    33         }, "T1").start();
    34         
    35         new Thread(new Runnable() {
    36             @Override
    37             public void run() {
    38                 t.m();
    39             }
    40         }, "T2").start();
    41     }
    42 }

      上面代码在执行时首先T1线程进入m方法,并且将t对象锁定,如果不出异常T1线程将一直执行m()方法的循环,T2线程无法进入,但是在count=5时出现异常,T1线程释放锁,T2线程进入继续执行,但是拿到的count数据时T1线程执行之后的数据,这里在实际开发中如果不做异常处理则有可能拿到错误的数据继续执行。所以在多线程开发中要特别注意异常的处理。

    六、volatile 关键字

       volatile关键字,使一个变量在多个线程之间可见,A,B线程都用到一个变量,java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。使用vilatile关键字,会让所有线程都会读取到变量的修改值。

      在下面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,会把running值从内存中读取到t1线程的工作区中,在运行过程中之间使用这个copy,并不会每次都去读取堆内存,这样,当主线程修改running的值之后,t1线程感知不到,所以不会停止运行。

     1 /**
     2  * volatile关键字,使一个变量在多个线程之间可见,A,B线程都用到一个变量,
     3  * java默认是A线程中保留一份copy,这样如果B线程修改了该变量,则A线程未必知道。
     4  * 使用vilatile关键字,会让所有线程都会读取到变量的修改值。
     5  * 在下面的代码中,running是存在于堆内存的t对象中,当线程t1开始运行的时候,
     6  * 会把running值从内存中读取到t1线程的工作区中,在运行过程中之间使用这个copy,
     7  * 并不会每次都去读取堆内存,这样,当主线程修改running的值之后,
     8  * t1线程感知不到,所以不会停止运行。
     9  * @author Wuyouxin
    10  *
    11  */
    12 public class T {
    13 
    14     /*volatile*/ boolean running = true;
    15     
    16     void m (){
    17         System.out.println("m.start");
    18         while (running){
    19             20         }
    21         System.out.println("m.end");
    22     }
    23     
    24     public static void main(String[] args) {
    25         final T t = new T();
    26         new Thread(new Runnable() {
    27             @Override
    28             public void run() {
    29                 t.m();
    30             }
    31         }, "t1").start();
    32         
    33         try {
    34             TimeUnit.SECONDS.sleep(1);
    35         } catch (InterruptedException e) {
    36             e.printStackTrace();
    37         }
    38         t.running = false;
    39     }
    40 }

      使用volatile后当堆内存中running发生改变之后会强制所有线程都去堆内存中读取running的值。

      volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,也就是说volatile不能替代synchronized

     

     1 /**
     2  * volatile并不能保证多个线程共同修改running变量时所带来的不一致问题,
     3  * 也就是说volatile不能替代synchronized
     4  * @author Wuyouxin
     5  *
     6  */
     7 public class T {
     8     volatile int count = 0;
     9     /*synchronized*/ void m(){
    10         for (int i = 0; i < 10000; i++) {
    11             count ++;
    12         }
    13     }
    14     
    15     public static void main(String[] args) {
    16         List<Thread> threads = new ArrayList<Thread>();
    17         final T t = new T();
    18         for (int i = 0; i < 10; i++) {
    19             threads.add(new Thread(new Runnable() {
    20                 @Override
    21                 public void run() {
    22                     t.m();
    23                 }
    24             }, "Thread-" + i));
    25         }
    26         
    27         for (Thread thread : threads) {
    28             thread.start();
    29         }
    30         
    31         for (Thread thread : threads) {
    32             try {
    33                 thread.join();//让主线程等待子线程运行结束之后再执行
    34             } catch (InterruptedException e) {
    35                 e.printStackTrace();
    36             }
    37         }
    38         
    39         System.out.println(t.count);
    40     }
    41 }

      这段代码执行之后的结果我们理想状态应该是100000但是实际执行确实一个比100000小的一个随机数,这是什么原因呢?因为volatile只保证可见性而不能像synchronized一样既可以保证可见性也可以保证原子性。在线程1在读到count的值++之后往内存中写入的过程中,线程2开始执行,发现内存中的值还是线程1没有写入的值拿到后再++再写入内存,这样导致两个线程写入了同样的值,所有最后的结果要小于100000。

    七、AtomXXX类

      如果在操作中仅仅是数字的++,--等操作,java提高了一些原子操作的类AtomXXX类,AtomXXX类本身方法都是原子性的,但是不能保证多个方法连续调用是原则性的。

      我们看下面代码:

     1 /**
     2  * 如果再操作中仅仅是数字的++,--等操作,java提高了一些原子操作的类AtomXXX类
     3  * 
     4  * @author Wuyouxin
     5  *
     6  */
     7 public class T {
     8     AtomicInteger count = new AtomicInteger(0);
     9     
    10     //这里可以不用synchronized,AtomicInteger方法内部使用了更底层的方式保证原子性
    11     /*synchronized*/ void m (){
    12         for (int i = 0; i < 10000; i++) {
    13             count.incrementAndGet();//相当于count++,但是还保证原子性
    14         }
    15     }
    16     
    17     public static void main(String[] args) {
    18         final T t = new T();
    19         List<Thread> threads = new ArrayList<Thread>();
    20         for (int i = 0; i < 10; i++) {
    21             threads.add(new Thread(new Runnable() {
    22                 @Override
    23                 public void run() {
    24                     t.m();
    25                 }
    26             }, "thread-" + i));
    27         }
    28         
    29         for (Thread thread : threads) {
    30             thread.start();
    31         }
    32         
    33         for (Thread thread : threads) {
    34             try {
    35                 
    36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } 39 } 40 41 System.out.println(t.count); 42 } 43 }

      AtomXXX类本身方法都是原子性的,但是不能保证多个方法连续调用是原子性的。

      我们看下面代码:

    1  /*synchronized*/ void m (){
    2      for (int i = 0; i < 10000; i++) {
    3         if (count.get()<1000){ //count.get()也具有原子性
    4              count.incrementAndGet();
    5          }
    6      }
    7  }

      我们把上面的m方法修改成这个样子,加入一个if判断,而且这个判断的count.gat()方法也具有原子性,但是在这两个方法之间不具有原子性,也有可能会被其他线程打断。当线程1达到999的时候还没+1,但是被线程2打断进入+1,而线程1又+1就变成1001了。

    八、synchronized优化

       synchronized中的代码越少效率越高。我们来比较下面2个方法:

     1 public class T {
     2 
     3     int count = 0;
     4     synchronized void m1(){
     5         try {
     6             TimeUnit.SECONDS.sleep(1);
     7         } catch (InterruptedException e) {
     8             e.printStackTrace();
     9         }
    10         //业务逻辑中只有下面代码需要同步,这时不应该给所有方法上锁
    11         count ++;
    12         
    13         try {
    14             TimeUnit.SECONDS.sleep(2);
    15         } catch (InterruptedException e) {
    16             e.printStackTrace();
    17         }
    18     }
    19     
    20     void m2(){
    21         try {
    22             TimeUnit.SECONDS.sleep(1);
    23         } catch (InterruptedException e) {
    24             e.printStackTrace();
    25         }
    26         //采用细粒度的锁可以使线程争用时间变短,提高效率
    27         synchronized (this) {
    28             count ++;
    29         }
    30         
    31         try {
    32             TimeUnit.SECONDS.sleep(2);
    33         } catch (InterruptedException e) {
    34             e.printStackTrace();
    35         }
    36     }
    37 }

      上面m1方法给整个方法都上了锁,而m2只给count++上了锁,m2的锁粒度更细,所以执行效率更高。

    九、避免被锁定的对象引用发生改变

       当一个对象被锁定时t1对象锁定对象的引用发生改变时,t2线程就会发现新的对象没有被锁定,则会进入方法锁定新的对象,所以应当避免被锁定的对象的引用发生改变。

      我们看下面代码:

     1 /**
     2  * 锁定某对象o,如果o的属性发生改变,不影响锁的使用
     3  * 但是如果o改变成另外一个对象,则锁定的对象发生变化,
     4  * 应该避免将锁定对象的引用变成另外的对象
     5  * @author Wuyouxin
     6  *
     7  */
     8 public class T {
     9     
    10     Object o = new Object();
    11     
    12     void m (){
    13         synchronized(o){
    14             while(true){
    15                 try {
    16                     TimeUnit.SECONDS.sleep(1);
    17                 } catch (InterruptedException e) {
    18                     e.printStackTrace();
    19                 }
    20                 System.out.println(Thread.currentThread().getName());
    21             }
    22         }
    23     }
    24     
    25     public static void main(String[] args) {
    26         final T t = new T();
    27         new Thread(new Runnable() {
    28             
    29             @Override
    30             public void run() {
    31                 t.m();
    32             }
    33         }, "t1").start();
    34         
    35         try {
    36             TimeUnit.SECONDS.sleep(3);
    37         } catch (InterruptedException e) {
    38             e.printStackTrace();
    39         }
    40         
    41         Thread t2 = new Thread(new Runnable() {
    42             
    43             @Override
    44             public void run() {
    45                 t.m();
    46             }
    47         }, "t2");
    48         //锁定的对象发生变化,当执行到这时发现new出来的o没有被锁定则t2开始执行,如果没有这句话
    49         //t2就永远不会执行
    50         t.o = new Object();
    51         t2.start();
    52     }
    53 
    54 }

      上面代码中由于T中的o发生了引用的对象发生了改变,导致t2线程和t1线程同时进入了m方法,这样就会导致出现数据混乱,失去了同步的意义。

      由上述代码也可以证明锁是锁在堆内存中的对象上,而不是栈内存的引用变量上。

    、避免使用字符串常量作为锁

       不要以字符串常量作为锁定对象,在下面的例子中,m1和m2其实锁定的是同一个对象这种情况还会发生比较诡异的现象,比如你用到一个类库,在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中也锁定了“Hello”,这时候就有可能发生比较诡异的死锁阻塞。因为你的线程和你用到的类库不经意间使用了同一把锁。

     1 /**
     2  * 不要以字符串常量作为锁定对象,在下面的例子中,m1和m2其实锁定的是同一个对象
     3  * 这种情况还会发生比较诡异的现象,比如你用到一个类库,
     4  * 在该类库中代码锁定了字符串“Hello”,但是你读不到源码,所以你在自己的代码中
     5  * 也锁定了“Hello”,这时候就有可能发生比较诡异的死锁阻塞。
     6  * 因为你的线程和你用到的类库不经意间使用了同一把锁。
     7  * @author Wuyouxin
     8  *
     9  */
    10 public class T {
    11 
    12     //这两个对象其实是同一个对象,放在字符串池中
    13     String s1 = "Hello";
    14     String s2 = "Hello";
    15     
    16     void m1(){
    17         synchronized (s1) {
    18             
    19         }
    20     }
    21     
    22     void m2(){
    23         synchronized (s2) {
    24             
    25         }
    26     }
    27 }

    十一、淘宝曾经的面试题

       实现一个容器提供两个方法 add和size,写两个线程,线程1添加10个元素到容器中,线程2进行监控,当容器内元素达到5时,线程2给出提示并结束。

       我们看下面方法:

     1 /**
     2  * 实现一个容器提供两个方法 add和size,写两个线程,线程1添加10个元素到容器中,
     3  * 线程2进行监控,当容器内元素达到5时,线程2给出提示并结束。
     4  * @author Wuyouxin
     5  *
     6  */
     7 public class MyContainer1 {
     8     //这里一定要加volatile,否则线程2无法监测到堆内存中 的list
     9     volatile List<Object> list = new ArrayList<Object>();
    10     
    11     public void add (Object o){
    12         list.add(o);
    13     }
    14     
    15     public int size (){
    16         return list.size();
    17     }
    18     
    19     public static void main(String[] args) {
    20         final MyContainer1 c = new MyContainer1();
    21         
    22         new Thread(new Runnable() {
    23             
    24             @Override
    25             public void run() {
    26                 for (int i = 0; i < 10; i++) {
    27                     c.add(new Object());
    28                     System.out.println("add:" + i);
    29                     try {
    30                         TimeUnit.SECONDS.sleep(1);
    31                     } catch (InterruptedException e) {
    32                         e.printStackTrace();
    33                     }
    34                 }
    35                 
    36             }
    37         }, "t1").start();
    38         
    39         new Thread(new Runnable() {
    40             
    41             @Override
    42             public void run() {
    43                 while (true){
    44                     if(c.size() == 5)
    45                         break;
    46                 }
    47                 System.out.println("结束");
    48             }
    49         }, "t2").start();
    50     }
    51     
    52 }

      上面代码我们需要注意的是在list之前一定要加volatile关键字,否则线程2无法获得list的变化。还存在一个问题,这里方法没有加锁,如果再加入几个线程,就会导致list的数据不精确,这里就需要使用到锁。但是,t2线程的死循环很浪费cpu,如果不使用死循环,该怎么做。

      我们看下面代码:

     1 /**
     2  * 这里就要使用wait和notify做到,
     3  * wait会让t2对象进入等待状态并且释放锁,而t1在执行到
     4  * c.size()==5时使用notify唤醒等待的线程。
     5  * 需要注意的是,这种方法必须要让t2先执行,也就是先让t2监听才可以
     6  * @author Wuyouxin
     7  *
     8  */
     9 public class MyContainer2 {
    10 
    11     //这里一定要加volatile,否则线程2无法监测到堆内存中 的list
    12     volatile List<Object> list = new ArrayList<Object>();
    13     
    14     public void add (Object o){
    15         list.add(o);
    16     }
    17     
    18     public int size (){
    19         return list.size();
    20     }
    21     
    22     public static void main(String[] args) {
    23         final MyContainer2 c = new MyContainer2();
    24         
    25         final Object lock = new Object();
    26         
    27         new Thread(new Runnable() {
    28             @Override
    29             public void run() {
    30                 synchronized (lock) {
    31                     System.out.println("t2启动 ");
    32                     if(c.size() != 5){
    33                         try {
    34                             lock.wait();
    35                         } catch (InterruptedException e) {
    36                             e.printStackTrace();
    37                         }
    38                     }
    39                     System.out.println("结束");
    40                 }
    41             }
    42         }, "t2").start();
    43         
    44         try {
    45             TimeUnit.SECONDS.sleep(1);
    46         } catch (Exception e) {
    47         }
    48         
    49         new Thread(new Runnable() {
    50             
    51             @Override
    52             public void run() {
    53                 System.out.println("t1启动");
    54                 synchronized (lock) {
    55                     for (int i = 0; i < 10; i++) {
    56                         c.add(new Object());
    57                         System.out.println("add:" + i);
    58                         if (c.size() == 5){
    59                             lock.notify();
    60                         }
    61                         try {
    62                             TimeUnit.SECONDS.sleep(1);
    63                         } catch (InterruptedException e) {
    64                             e.printStackTrace();
    65                         }
    66                     }
    67                 }
    68             }
    69         }, "t1").start();
    70     }
    71 }

      这里就要使用wait和notify做到,wait会让t2对象进入等待状态并且释放锁,而t1在执行到c.size()==5时使用notify唤醒等待的线程。需要注意的是,这种方法必须要让t2先执行,也就是先让t2监听才可以。但是上面代码也存在一个问题,t2是在t1执行结束之后才结束,而不是c.size()==5时立刻结束。这是因为wait等待会释放锁,但是notify唤醒不会释放锁,所以才会导致t2要在t1之后才结束。这里我们就需要让t1线程运行notify唤醒其他线程之后再调用wait等待并释放锁,而t2线程需要结束之后再使用notify去唤醒其他线程。

      所以应当是下面这样:

     1 /**
     2  * 但是上面代码也存在一个问题,t2是在t1执行结束之后才结束,
     3  * 而不是c.size()==5时立刻结束。这是因为wait等待会释放锁,
     4  * 但是notify唤醒不会释放锁,所以才会导致t2要在t1之后才结束。
     5  * 这里我们就需要让t1线程运行notify唤醒其他线程之后再调用wait等待并释放锁,
     6  * 而t2线程需要结束之后再使用notify去唤醒其他线程
     7  * @author Wuyouxin
     8  *
     9  */
    10 public class MyContainer2 {
    11 
    12     //这里一定要加volatile,否则线程2无法监测到堆内存中 的list
    13     volatile List<Object> list = new ArrayList<Object>();
    14     
    15     public void add (Object o){
    16         list.add(o);
    17     }
    18     
    19     public int size (){
    20         return list.size();
    21     }
    22     
    23     public static void main(String[] args) {
    24         final MyContainer2 c = new MyContainer2();
    25         
    26         final Object lock = new Object();
    27         
    28         new Thread(new Runnable() {
    29             @Override
    30             public void run() {
    31                 synchronized (lock) {
    32                     System.out.println("t2启动 ");
    33                     if(c.size() != 5){
    34                         try {
    35                             lock.wait();
    36                         } catch (InterruptedException e) {
    37                             e.printStackTrace();
    38                         }
    39                     }
    40                     System.out.println("结束");
    41                     lock.notify();
    42                 }
    43             }
    44         }, "t2").start();
    45         
    46         try {
    47             TimeUnit.SECONDS.sleep(1);
    48         } catch (Exception e) {
    49         }
    50         
    51         new Thread(new Runnable() {
    52             
    53             @Override
    54             public void run() {
    55                 System.out.println("t1启动");
    56                 synchronized (lock) {
    57                     for (int i = 0; i < 10; i++) {
    58                         c.add(new Object());
    59                         System.out.println("add:" + i);
    60                         if (c.size() == 5){
    61                             lock.notify();
    62                             try {
    63                                 lock.wait();
    64                             } catch (InterruptedException e) {
    65                                 e.printStackTrace();
    66                             }
    67                         }
    68                         try {
    69                             TimeUnit.SECONDS.sleep(1);
    70                         } catch (InterruptedException e) {
    71                             e.printStackTrace();
    72                         }
    73                     }
    74                 }
    75             }
    76         }, "t1").start();
    77     }
    78 }

      但是上面的通讯太复杂了,如果只涉及到通讯而不涉及到同步时 synchronized wait/notify就会显得太重了,这时就应该考虑使用countdownlatch/cyclicbarrier/semaphore,使用Latch(门闩)代替wait notify来进行通知,好处是通信方式简单,同时还可以指定等待时间,使用await和countdown方法代替wait和notify,CountDownLatch不涉及锁定,当count的值为零时当前线程继续运行。

     1 /**
     2  * 但是上面的通讯太复杂了,如果只涉及到通讯而不涉及到同步时 synchronized wait/notify
     3  * 就会显得太重了,这时就应该考虑使用countdownlatch/cyclicbarrier/semaphore,
     4  * 使用Latch(门闩)代替wait notify来进行通知,好处是通信方式简单,同时还可以指定等待时间,
     5  * 使用await和countdown方法代替wait和notify,CountDownLatch不涉及锁定,
     6  * 当count的值为零时当前线程继续运行。
     7  * @author Wuyouxin
     8  *
     9  */
    10 public class MyContainer3 {
    11 
    12     volatile List<Object> list = new ArrayList<Object>();
    13     
    14     public void add (Object o){
    15         list.add(o);
    16     }
    17     
    18     public int size (){
    19         return list.size();
    20     }
    21     
    22     public static void main(String[] args) {
    23         final MyContainer3 c = new MyContainer3();
    24         
    25         final CountDownLatch lock = new CountDownLatch(1);
    26         
    27         new Thread(new Runnable() {
    28             @Override
    29             public void run() {
    30                 System.out.println("t2启动 ");
    31                 if(c.size() != 5){
    32                     try {
    33                         lock.await();
    34                     } catch (InterruptedException e) {
    35                         e.printStackTrace();
    36                     }
    37                 }
    38                 System.out.println("结束");
    39             }
    40         }, "t2").start();
    41         
    42         try {
    43             TimeUnit.SECONDS.sleep(1);
    44         } catch (Exception e) {
    45         }
    46         
    47         new Thread(new Runnable() {
    48             
    49             @Override
    50             public void run() {
    51                 System.out.println("t1启动");
    52                 for (int i = 0; i < 10; i++) {
    53                     c.add(new Object());
    54                     System.out.println("add:" + i);
    55                     if (c.size() == 5){
    56                         lock.countDown();
    57                     }
    58                     try {
    59                         TimeUnit.SECONDS.sleep(1);
    60                     } catch (InterruptedException e) {
    61                         e.printStackTrace();
    62                     }
    63                 }
    64             }
    65         }, "t1").start();
    66     }
    67 }

      

    -------------------- END ---------------------

     

    
    

     

    最后附上作者的微信公众号地址和博客地址 

     

    
    

     

    公众号:wuyouxin_gzh

     

    
    

     

     

    
    

     

     

     

    
    

     

    Herrt灬凌夜:https://www.cnblogs.com/wuyx/

  • 相关阅读:
    深入AXI4总线- [一]握手机制
    《数字集成电路静态时序分析基础》笔记⑩
    搭建自己的gitlab
    idea连接github问题
    itguang
    [转]springboot+idea热部署(自动刷新)
    25匹马,5条赛道,一匹马一个赛道,求决胜1,2,3名至少多少场
    [转]PostgreSQL主从流复制部署
    [转]centos7 安装jdk11 并设置默认java版本
    [转]centos7 修改yum源为阿里源
  • 原文地址:https://www.cnblogs.com/wuyx/p/8710825.html
Copyright © 2020-2023  润新知