`

Java 多线程同步的五种方法

 
阅读更多

1、synchronized 

2、volatile 

3、ReentrantLock() 

4、wait() 与 notify()

5、ThreadLocal

6、semaphore 

一、引言

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

二、为什么要线程同步

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

三、不同步时的代码

 

  1. Bank.java 
  2.  
  3. package threadTest;  
  4.  
  5. /** 
  6. * @author ww 
  7. * 
  8. */  
  9. public class Bank {  
  10.  
  11.     private int count =0;//账户余额  
  12.  
  13.     //存钱  
  14.     public  void addMoney(int money){  
  15.         count +=money;  
  16.         System.out.println(System.currentTimeMillis()+"存进:"+money);  
  17.     }  
  18.  
  19.     //取钱  
  20.     public  void subMoney(int money){  
  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.  
  34. SyncThreadTest.java 
  35.  
  36. package threadTest;  
  37.  
  38. public class SyncThreadTest {  
  39.  
  40.     public static void main(String args[]){  
  41.         final Bank bank=new Bank();  
  42.  
  43.         Thread tadd=new Thread(new Runnable() {  
  44.  
  45.             @Override  
  46.             public void run() {  
  47.                 // TODO Auto-generated method stub  
  48.                 while(true){  
  49.                     try {  
  50.                         Thread.sleep(1000);  
  51.                     } catch (InterruptedException e) {  
  52.                         // TODO Auto-generated catch block  
  53.                         e.printStackTrace();  
  54.                     }  
  55.                     bank.addMoney(100);  
  56.                     bank.lookMoney();  
  57.                     System.out.println("\n");  
  58.  
  59.                 }  
  60.             }  
  61.         });  
  62.  
  63.         Thread tsub = new Thread(new Runnable() {  
  64.  
  65.             @Override  
  66.             public void run() {  
  67.                 // TODO Auto-generated method stub  
  68.                 while(true){  
  69.                     bank.subMoney(100);  
  70.                     bank.lookMoney();  
  71.                     System.out.println("\n");  
  72.                     try {  
  73.                         Thread.sleep(1000);  
  74.                     } catch (InterruptedException e) {  
  75.                         // TODO Auto-generated catch block  
  76.                         e.printStackTrace();  
  77.                     }     
  78.                 }  
  79.             }  
  80.         });  
  81.         tsub.start();  
  82.  
  83.         tadd.start();  
  84.     }  
  85.  

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

余额不足  
账户余额:0 

余额不足  
账户余额:100 

1441790503354存进:100  
账户余额:100 

1441790504354存进:100  
账户余额:100 

1441790504354取出:100  
账户余额:100 

1441790505355存进:100  
账户余额:100 

1441790505355取出:100  
账户余额:100

四、使用同步时的代码

(1)同步方法:

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

修改后的Bank.java

 

再看看运行结果:

余额不足  
账户余额:0 

余额不足  
账户余额:0 

1441790837380存进:100  
账户余额:100 

1441790838380取出:100  
账户余额:0  
1441790838380存进:100  
账户余额:100 

1441790839381取出:100  
账户余额:0

瞬间感觉可以理解了吧。

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

(2)同步代码块

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

Bank.java代码如下:

  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.     }  

运行结果如下:

余额不足  
账户余额:0 

1441791806699存进:100  
账户余额:100 

1441791806700取出:100  
账户余额:0 

1441791807699存进:100  
账户余额:100

效果和方法一差不多。

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

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

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

Bank.java代码如下:

 

  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.     }  

运行效果怎样呢?

余额不足  
账户余额:0 

余额不足  
账户余额:100 

1441792010959存进:100  
账户余额:100 

1441792011960取出:100  
账户余额:0 

1441792011961存进:100  
账户余额:100

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

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

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

  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.     }  

运行效果怎么样呢?

余额不足  
账户余额:0 

余额不足  
账户余额:0 

1441792891934存进:100  
账户余额:100 

1441792892935存进:100  
账户余额:200 

1441792892954取出:100  
账户余额:100

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

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

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

Bank.java代码如下:

 

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

运行效果:

余额不足  
账户余额:0 

余额不足  
账户余额:0 

1441794247939存进:100  
账户余额:100 

余额不足  
1441794248940存进:100  
账户余额:0 

账户余额:200 

余额不足  
账户余额:0 

1441794249941存进:100  
账户余额:300

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

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

ThreadLocal与同步机制

a.ThreadLocal与同步机制都是为了解决多线程中相同变量的访问冲突问题
b.前者采用以”空间换时间”的方法,后者采用以”时间换空间”的方式

现在都明白了吧。各有优劣,各有适用场景。

 

原文地址:http://developer.51cto.com/art/201509/490965.htm

分享到:
评论

相关推荐

    java 多线程同步方法的实例

    java 多线程同步方法的实例 java 多线程同步方法的实例 java 多线程同步方法的实例

    Java多线程同步论文.doc

    Java多线程同步论文.doc

    java多线程同步例子

    java多线程同步互斥访问实例,对于初学者或是温故而知新的同道中人都是一个很好的学习资料

    java多线程同步分析

    java多线程同步分析java多线程同步分java多线程同步分析析java多线程同步分析java多线程同步分析

    java多线程同步问题

    多线程注意:wait()方法的调用要有判定条件常用 while () obj.wait(timeout, nanos); ... // Perform action appropriate to condition } synchronized会影响共享数据,但对其他语句的执行不会有规律了!

    Java多线程同步.pdf

    Java多线程同步.pdf

    Java多线程同步具体实例讲解 .doc

    Java多线程同步具体实例讲解 .doc

    Java多线程同步具体实例.doc

    Java多线程同步具体实例.doc

    java多线程经典案例

    java多线程经典案例,线程同步、线程通信、线程阻塞等经典案例

    Java多线程编程总结

    Java线程:线程的同步-同步方法 Java线程:线程的同步-同步块 Java线程:并发协作-生产者消费者模型 Java线程:并发协作-死锁 Java线程:volatile关键字 Java线程:新特征-线程池 Java线程:新特征-有返回值的...

    Java多线程和同步

    Java线程(二):线程同步synchronized和volatile 详细讲解Java 同步的原理技术资料

    java多线程编程总结

    详细的讲述了多线程的各种用法 Java线程:概念与原理 Java线程:创建与启动 Java线程:线程栈模型与线程的变量 Java线程:线程状态的转换 Java线程:线程的同步与锁 Java线程:线程的交互 Java线程:线程的调度-休眠...

    java 多线程同步

    java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建 java.util.concurrent 的目的就是要实现 Collection 框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发...

    Java多线程机制(讲述java里面与多线程有关的函数)

    Java多线程机制 9.1 Java中的线程 9.2 Thread的子类创建线程 9.3 使用Runable接口 9.4 线程的常用方法 9.5 GUI线程 9.6 线程同步 9.7 在同步方法中使用wait()、notify 和notifyAll()方法 9.8 挂起、恢复和终止线程 ...

    java多线程笔记

    Java线程:线程的同步 33 一、 同步方法 35 二、 同步块 36 三、 volatile关键字 38 四、 使用synchronized关键字要注意以下四点 39 五、 关于同步和锁定的一些问题 41 Java线程:并发协作-线程的交互 47 Java线程:...

    java多线程通信图解

    java 多线程 其实就是每个线程都拥有自己的内存空间,多线程之间的通信,比例A线程修改了主内存(main方法的线程)变量,需要把A线程修改的结果同步到主线程中,这时B线程再从主线程获取该变量的值,这样就实现了...

    java 多线程 同步详解

    java 多线程同步详解 synchronized 的使用,锁定对象 ,锁定类 区别

    基于Java多线程同步技术的简易模拟售票系统实现.pdf

    基于Java多线程同步技术的简易模拟售票系统实现.pdf

    java 多线程设计模式 进程详解

    《JAVA多线程设计模式》PDF 下载 《Java线程 高清晰中文第二版》中文第二版(PDF) 前言 第一章 线程简介 Java术语 线程概述 为什么要使用线程? 总结 第二章 Java线程API 通过Thread类创建线程 使用Runable接口...

    Java多线程同步机制在售票系统的实现

    编写多线程程序是一件烦琐的技术而且还涉及很多不安全的因素。Java 的出现,使得编写安全高效的多线程技术变得简单,但 ...本文介绍了长途汽车售票系统中所涉及的问题来说明Java 多线程技术同步机制的实现。

Global site tag (gtag.js) - Google Analytics