• 多线程同步的五种方法


    一、引言

    前几天面试,被大师虐残了,好多基础知识必须得重新拿起来啊。闲话不多说,进入正题。

    二、为什么要线程同步

    因为当我们有多个线程要同时访问一个变量或对象时,如果这些线程中既有读又有写操作时,就会导致变量值或对象的状态出现混乱,从而导致程序异常。举个例子,如果一个银行账户同时被两个线程操作,一个取100块,一个存钱100块。假设账户原本有0块,如果取钱线程和存钱线程同时发生,会出现什么结果呢?取钱不成功,账户余额是100.取钱成功了,账户余额是0.那到底是哪个呢?很难说清楚。因此多线程同步就是要解决这个问题。

    三、不同步时的代码

    Bank.Java

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3. /** 
    4.  * @author ww 
    5.  * 
    6.  */  
    7. public class Bank {  
    8.   
    9.     private int count =0;//账户余额  
    10.       
    11.     //存钱  
    12.     public  void addMoney(int money){  
    13.         count +=money;  
    14.         System.out.println(System.currentTimeMillis()+"存进:"+money);  
    15.     }  
    16.       
    17.     //取钱  
    18.     public  void subMoney(int money){  
    19.         if(count-money < 0){  
    20.             System.out.println("余额不足");  
    21.             return;  
    22.         }  
    23.         count -=money;  
    24.         System.out.println(+System.currentTimeMillis()+"取出:"+money);  
    25.     }  
    26.       
    27.     //查询  
    28.     public void lookMoney(){  
    29.         System.out.println("账户余额:"+count);  
    30.     }  
    31. }  

    SyncThreadTest.java

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3.   
    4. public class SyncThreadTest {  
    5.   
    6.     public static void main(String args[]){  
    7.         final Bank bank=new Bank();  
    8.           
    9.         Thread tadd=new Thread(new Runnable() {  
    10.               
    11.             @Override  
    12.             public void run() {  
    13.                 // TODO Auto-generated method stub  
    14.                 while(true){  
    15.                     try {  
    16.                         Thread.sleep(1000);  
    17.                     } catch (InterruptedException e) {  
    18.                         // TODO Auto-generated catch block  
    19.                         e.printStackTrace();  
    20.                     }  
    21.                     bank.addMoney(100);  
    22.                     bank.lookMoney();  
    23.                     System.out.println(" ");  
    24.                       
    25.                 }  
    26.             }  
    27.         });  
    28.           
    29.         Thread tsub = new Thread(new Runnable() {  
    30.               
    31.             @Override  
    32.             public void run() {  
    33.                 // TODO Auto-generated method stub  
    34.                 while(true){  
    35.                     bank.subMoney(100);  
    36.                     bank.lookMoney();  
    37.                     System.out.println(" ");  
    38.                     try {  
    39.                         Thread.sleep(1000);  
    40.                     } catch (InterruptedException e) {  
    41.                         // TODO Auto-generated catch block  
    42.                         e.printStackTrace();  
    43.                     }     
    44.                 }  
    45.             }  
    46.         });  
    47.         tsub.start();  
    48.           
    49.         tadd.start();  
    50.     }  
    51.       
    52.       
    53.   
    54. }  

    代码很简单,我就不解释了,看看运行结果怎样呢?截取了其中的一部分,是不是很乱,有写看不懂。

    [java] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 余额不足  
    6. 账户余额:100  
    7.   
    8.   
    9. 1441790503354存进:100  
    10. 账户余额:100  
    11.   
    12.   
    13. 1441790504354存进:100  
    14. 账户余额:100  
    15.   
    16.   
    17. 1441790504354取出:100  
    18. 账户余额:100  
    19.   
    20.   
    21. 1441790505355存进:100  
    22. 账户余额:100  
    23.   
    24.   
    25. 1441790505355取出:100  
    26. 账户余额:100  

    四、使用同步时的代码

    (1)同步方法:

    即有synchronized关键字修饰的方法。 由于Java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。

    修改后的Bank.java

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3. /** 
    4.  * @author ww 
    5.  * 
    6.  */  
    7. public class Bank {  
    8.   
    9.     private int count =0;//账户余额  
    10.       
    11.     //存钱  
    12.     public  synchronized void addMoney(int money){  
    13.         count +=money;  
    14.         System.out.println(System.currentTimeMillis()+"存进:"+money);  
    15.     }  
    16.       
    17.     //取钱  
    18.     public  synchronized void subMoney(int money){  
    19.         if(count-money < 0){  
    20.             System.out.println("余额不足");  
    21.             return;  
    22.         }  
    23.         count -=money;  
    24.         System.out.println(+System.currentTimeMillis()+"取出:"+money);  
    25.     }  
    26.       
    27.     //查询  
    28.     public void lookMoney(){  
    29.         System.out.println("账户余额:"+count);  
    30.     }  
    31. }  

    再看看运行结果:

    [html] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 余额不足  
    6. 账户余额:0  
    7.   
    8.   
    9. 1441790837380存进:100  
    10. 账户余额:100  
    11.   
    12.   
    13. 1441790838380取出:100  
    14. 账户余额:0  
    15. 1441790838380存进:100  
    16. 账户余额:100  
    17.   
    18.   
    19.   
    20.   
    21. 1441790839381取出:100  
    22. 账户余额:0  

    瞬间感觉可以理解了吧。

    注: synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类

    (2)同步代码块

    即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步

    Bank.java代码如下:

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3. /** 
    4.  * @author ww 
    5.  * 
    6.  */  
    7. public class Bank {  
    8.   
    9.     private int count =0;//账户余额  
    10.       
    11.     //存钱  
    12.     public   void addMoney(int money){  
    13.           
    14.         synchronized (this) {  
    15.             count +=money;  
    16.         }  
    17.         System.out.println(System.currentTimeMillis()+"存进:"+money);  
    18.     }  
    19.       
    20.     //取钱  
    21.     public   void subMoney(int money){  
    22.           
    23.         synchronized (this) {  
    24.             if(count-money < 0){  
    25.                 System.out.println("余额不足");  
    26.                 return;  
    27.             }  
    28.             count -=money;  
    29.         }  
    30.         System.out.println(+System.currentTimeMillis()+"取出:"+money);  
    31.     }  
    32.       
    33.     //查询  
    34.     public void lookMoney(){  
    35.         System.out.println("账户余额:"+count);  
    36.     }  
    37. }  

    运行结果如下:

    [html] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 1441791806699存进:100  
    6. 账户余额:100  
    7.   
    8.   
    9. 1441791806700取出:100  
    10. 账户余额:0  
    11.   
    12.   
    13. 1441791807699存进:100  
    14. 账户余额:100  

    效果和方法一差不多。

    注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

    (3)使用特殊域变量(volatile)实现线程同步

        a.volatile关键字为域变量的访问提供了一种免锁机制 
        b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新 
        c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 
        d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量

    Bank.java代码如下:

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3. /** 
    4.  * @author ww 
    5.  * 
    6.  */  
    7. public class Bank {  
    8.   
    9.     private volatile int count = 0;// 账户余额  
    10.   
    11.     // 存钱  
    12.     public void addMoney(int money) {  
    13.   
    14.         count += money;  
    15.         System.out.println(System.currentTimeMillis() + "存进:" + money);  
    16.     }  
    17.   
    18.     // 取钱  
    19.     public void subMoney(int money) {  
    20.   
    21.         if (count - money < 0) {  
    22.             System.out.println("余额不足");  
    23.             return;  
    24.         }  
    25.         count -= money;  
    26.         System.out.println(+System.currentTimeMillis() + "取出:" + money);  
    27.     }  
    28.   
    29.     // 查询  
    30.     public void lookMoney() {  
    31.         System.out.println("账户余额:" + count);  
    32.     }  
    33. }  

    运行效果怎样呢?

    [html] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 余额不足  
    6. 账户余额:100  
    7.   
    8.   
    9. 1441792010959存进:100  
    10. 账户余额:100  
    11.   
    12.   
    13. 1441792011960取出:100  
    14. 账户余额:0  
    15.   
    16.   
    17. 1441792011961存进:100  
    18. 账户余额:100  


    是不是又看不懂了,又乱了。这是为什么呢?就是因为volatile不能保证原子操作导致的,因此volatile不能代替synchronized。此外volatile会组织编译器对代码优化,因此能不使用它就不适用它吧。它的原理是每次要线程要访问volatile修饰的变量时都是从内存中读取,而不是存缓存当中读取,因此每个线程访问到的变量值都是一样的。这样就保证了同步。

    (4)使用重入锁实现线程同步

        在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
         ReenreantLock类的常用方法有:
             ReentrantLock() : 创建一个ReentrantLock实例 
             lock() : 获得锁 
             unlock() : 释放锁 
        注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 
    Bank.java代码修改如下:

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3. import java.util.concurrent.locks.Lock;  
    4. import java.util.concurrent.locks.ReentrantLock;  
    5.   
    6. /** 
    7.  * @author ww 
    8.  * 
    9.  */  
    10. public class Bank {  
    11.   
    12.     private  int count = 0;// 账户余额  
    13.       
    14.     //需要声明这个锁  
    15.     private Lock lock = new ReentrantLock();  
    16.   
    17.     // 存钱  
    18.     public void addMoney(int money) {  
    19.         lock.lock();//上锁  
    20.         try{  
    21.         count += money;  
    22.         System.out.println(System.currentTimeMillis() + "存进:" + money);  
    23.           
    24.         }finally{  
    25.             lock.unlock();//解锁  
    26.         }  
    27.     }  
    28.   
    29.     // 取钱  
    30.     public void subMoney(int money) {  
    31.         lock.lock();  
    32.         try{  
    33.               
    34.         if (count - money < 0) {  
    35.             System.out.println("余额不足");  
    36.             return;  
    37.         }  
    38.         count -= money;  
    39.         System.out.println(+System.currentTimeMillis() + "取出:" + money);  
    40.         }finally{  
    41.             lock.unlock();  
    42.         }  
    43.     }  
    44.   
    45.     // 查询  
    46.     public void lookMoney() {  
    47.         System.out.println("账户余额:" + count);  
    48.     }  
    49. }  

    运行效果怎么样呢?

    [html] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 余额不足  
    6. 账户余额:0  
    7.   
    8.   
    9. 1441792891934存进:100  
    10. 账户余额:100  
    11.   
    12.   
    13. 1441792892935存进:100  
    14. 账户余额:200  
    15.   
    16.   
    17. 1441792892954取出:100  
    18. 账户余额:100  

    效果和前两种方法差不多。

    如果synchronized关键字能满足用户的需求,就用synchronized,因为它能简化代码 。如果需要更高级的功能,就用ReentrantLock类,此时要注意及时释放锁,否则会出现死锁,通常在finally代码释放锁 

    (5)使用局部变量实现线程同步

    Bank.java代码如下:

    [java] view plain copy
     
     
    1. package threadTest;  
    2.   
    3.   
    4. /** 
    5.  * @author ww 
    6.  * 
    7.  */  
    8. public class Bank {  
    9.   
    10.     private static ThreadLocal<Integer> count = new ThreadLocal<Integer>(){  
    11.   
    12.         @Override  
    13.         protected Integer initialValue() {  
    14.             // TODO Auto-generated method stub  
    15.             return 0;  
    16.         }  
    17.           
    18.     };  
    19.       
    20.   
    21.     // 存钱  
    22.     public void addMoney(int money) {  
    23.         count.set(count.get()+money);  
    24.         System.out.println(System.currentTimeMillis() + "存进:" + money);  
    25.           
    26.     }  
    27.   
    28.     // 取钱  
    29.     public void subMoney(int money) {  
    30.         if (count.get() - money < 0) {  
    31.             System.out.println("余额不足");  
    32.             return;  
    33.         }  
    34.         count.set(count.get()- money);  
    35.         System.out.println(+System.currentTimeMillis() + "取出:" + money);  
    36.     }  
    37.   
    38.     // 查询  
    39.     public void lookMoney() {  
    40.         System.out.println("账户余额:" + count.get());  
    41.     }  
    42. }  


    运行效果:

    [html] view plain copy
     
     
    1. 余额不足  
    2. 账户余额:0  
    3.   
    4.   
    5. 余额不足  
    6. 账户余额:0  
    7.   
    8.   
    9. 1441794247939存进:100  
    10. 账户余额:100  
    11.   
    12.   
    13. 余额不足  
    14. 1441794248940存进:100  
    15. 账户余额:0  
    16.   
    17.   
    18. 账户余额:200  
    19.   
    20.   
    21. 余额不足  
    22. 账户余额:0  
    23.   
    24.   
    25. 1441794249941存进:100  
    26. 账户余额:300  


    看了运行效果,一开始一头雾水,怎么只让存,不让取啊?看看ThreadLocal的原理:

    如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本,副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。现在明白了吧,原来每个线程运行的都是一个副本,也就是说存钱和取钱是两个账户,知识名字相同而已。所以就会发生上面的效果。

    ThreadLocal与同步机制 
    a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
    b.前者采用以"空间换时间"的方法,后者采用以"时间换空间"的方式
     
    原文:http://blog.csdn.net/xqb_756148978/article/details/52585223
  • 相关阅读:
    刻舟求剑,
    录制时间是不准确的,
    HIV T2
    DNA RNA
    洛谷 P1428 小鱼比可爱
    Codevs 1081 线段树练习2
    Codevs 1080 线段树联系
    Tarjan算法
    Codevs 2611 观光旅游
    洛谷 1865 A%B问题
  • 原文地址:https://www.cnblogs.com/onetwo/p/6420085.html
Copyright © 2020-2023  润新知