(删)Java线程同步实现一:synchronzied和wait()/notify()
上面文章(2.Java多线程总结系列:Java的线程控制实现)讲到了如何对线程进行控制,其中有一个是线程同步问题。下面我们先来看一个例子:
1、一个典型的Java线程安全例子
package com.chanshuyi.thread;
public class ThreadDemo93 {
    public static void main(String[] args) {
        Account account = new Account(2300);
        new DrawMoneyThread(account).start();
        new DepositeThread(account).start();
    }
}
class DepositeThread extends Thread{
    private Account account;
    public DepositeThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次存200,10次共存2000
        for(int i = 0; i < 10; i++){
            account.deposit(200, i + 1);
        }
    }
}
class DrawMoneyThread extends Thread{
    private Account account;
    public DrawMoneyThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次取100,10次共取1000
        for(int i = 0; i < 10; i++){
            account.withdraw(100, i + 1);
        }
    }
}
class Account{
    //存钱
    public void deposit(double amount, int i){
        try {
            Thread.sleep((long)Math.random()*10000);  //模拟存钱的延迟
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        this.balance = this.balance + amount;
        System.out.println("第" + i + "次,存入钱:" + amount);
        System.out.println("第" + i + "次,存钱后账户余额:" + this.balance);
    }
    //取钱
    public void withdraw(double amount, int i){
        if(this.balance >= amount){
            try {
                Thread.sleep((long)Math.random()*10000);  //模拟取钱的延迟
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            this.balance = this.balance - amount;
            System.out.println("第" + i + "次,取出钱:" + amount);
            System.out.println("第" + i + "次,取钱后账户余额:" + this.balance);
        }else{
            System.out.println("第" + i + "次,余额不足");
        }
    }
    public Account(){
    }
    public Account(double balance){
        this.balance = balance;
    }
    private double balance;
}
上面例子很容易理解,有一张银行卡,里面有2300的余额,程序模拟两个人进行操作,一个人存10次钱每次存200共存2000,一个人取钱取10次每次取100共取1000,这样的话最后的余额应该是3300。多次运行此程序,可能具有多个不同组合的输出结果。其中一种可能的输出为:
第1次,取出钱:100.0
第1次,取钱后账户余额:2200.0
第1次,存入钱:200.0
第1次,存钱后账户余额:2400.0
第2次,取出钱:100.0
第2次,取钱后账户余额:2300.0
第2次,存入钱:200.0
第2次,存钱后账户余额:2500.0
第3次,取出钱:100.0
第3次,取钱后账户余额:2400.0
第3次,存入钱:200.0
第3次,存钱后账户余额:2600.0
第4次,取出钱:100.0
第4次,取钱后账户余额:2500.0
第4次,存入钱:200.0
第4次,存钱后账户余额:2700.0
第5次,取出钱:100.0
第5次,取钱后账户余额:2600.0
第5次,存入钱:200.0
第5次,存钱后账户余额:2800.0
第6次,取出钱:100.0
第6次,取钱后账户余额:2700.0
第6次,存入钱:200.0
第6次,存钱后账户余额:2900.0
第7次,取出钱:100.0
第7次,取钱后账户余额:2800.0
第7次,存入钱:200.0
第7次,存钱后账户余额:3000.0
第8次,存入钱:200.0
第8次,取出钱:100.0
第8次,存钱后账户余额:2900.0
第8次,取钱后账户余额:2900.0
第9次,存入钱:200.0
第9次,存钱后账户余额:3100.0
第9次,取出钱:100.0
第9次,取钱后账户余额:3000.0
第10次,存入钱:200.0
第10次,存钱后账户余额:3200.0
第10次,取出钱:100.0
第10次,取钱后账户余额:3100.0
我们可以看到在第8次存钱和取钱的时候,本来之前的余额是3000元,两个人一个存入200,一个取出100,那么余额应该是3100才是。但是因为发生了两人几乎同时进行存取款操作,导致最后第8次存取款之后余额进程是2900元。经过分析,问题在于Java多线程环境下的执行的不确定性。在存取款的时候,我们应该保证同一账户下不能同时进行存钱和取款操作,否则就会出现数据的混乱。而如果要保证存取款不能同时进行,就需要用到线程中的同步知识。
一般来说,实现线程同步的方式有:synchronized同步方法、synchronized同步代码块以及Lock锁三种,这里我们先介绍前两种,Lock锁的同步方式我们在下篇文章中介绍。
2、synchronized 同步方法
使用synchronized同步方法对线程同步,只需要在方法上synchronized关键字修饰即可。
上面的例子使用synchronized同步方法进行线程同步后的代码如下:
package com.chanshuyi.thread;
public class ThreadDemo93 {
    public static void main(String[] args) {
        Account account = new Account(2300);
        new DrawMoneyThread(account).start();
        new DepositeThread(account).start();
    }
}
class DepositeThread extends Thread{
    private Account account;
    public DepositeThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次存200,10次共存2000
        for(int i = 0; i < 10; i++){
            account.deposit(200, i + 1);
        }
    }
}
class DrawMoneyThread extends Thread{
    private Account account;
    public DrawMoneyThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次取100,10次共取1000
        for(int i = 0; i < 10; i++){
            account.withdraw(100, i + 1);
        }
    }
}
class Account{
    //存钱
    public synchronized void deposit(double amount, int i){
        try {
            Thread.sleep((long)Math.random()*10000);  //模拟存钱的延迟
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        this.balance = this.balance + amount;
        System.out.println("第" + i + "次,存入钱:" + amount);
        System.out.println("第" + i + "次,存钱后账户余额:" + this.balance);
    }
    //取钱
    public synchronized void withdraw(double amount, int i){
        if(this.balance >= amount){
            try {
                Thread.sleep((long)Math.random()*10000);  //模拟取钱的延迟
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            this.balance = this.balance - amount;
            System.out.println("第" + i + "次,取出钱:" + amount);
            System.out.println("第" + i + "次,取钱后账户余额:" + this.balance);
        }else{
            System.out.println("第" + i + "次,余额不足");
        }
    }
    public Account(){
    }
    public Account(double balance){
        this.balance = balance;
    }
    private double balance;
}
运行上面的代码,你会发现无论运行多少次,最终的余额都是3300元,不会发生错误。
3、synchronized 同步代码块
上面的例子用synchronized同步代码块方式实现线程同步后的代码如下:
package com.chanshuyi.thread;
public class ThreadDemo93 {
    public static void main(String[] args) {
        Account account = new Account(2300);
        new DrawMoneyThread(account).start();
        new DepositeThread(account).start();
    }
}
class DepositeThread extends Thread{
    private Account account;
    public DepositeThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次存200,10次共存2000
        for(int i = 0; i < 10; i++){
            account.deposit(200, i + 1);
        }
    }
}
class DrawMoneyThread extends Thread{
    private Account account;
    public DrawMoneyThread(Account account){
        this.account = account;
    }
    @Override
    public void run() {
        //每次取100,10次共取1000
        for(int i = 0; i < 10; i++){
            account.withdraw(100, i + 1);
        }
    }
}
class Account{
    //存钱
    public void deposit(double amount, int i){
        try {
            Thread.sleep((long)Math.random()*10000);  //模拟存钱的延迟
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        synchronized(this){
            this.balance = this.balance + amount;
            System.out.println("第" + i + "次,存入钱:" + amount);
            System.out.println("第" + i + "次,存钱后账户余额:" + this.balance);
        }
    }
    //取钱
    public synchronized void withdraw(double amount, int i){
        try {
            Thread.sleep((long)Math.random()*10000);  //模拟取钱的延迟
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        synchronized(this){
            if(this.balance >= amount){
                this.balance = this.balance - amount;
                System.out.println("第" + i + "次,取出钱:" + amount);
                System.out.println("第" + i + "次,取钱后账户余额:" + this.balance);
            }else{
                System.out.println("第" + i + "次,余额不足");
            }
        }
    }
    public Account(){
    }
    public Account(double balance){
        this.balance = balance;
    }
    private double balance;
}
通过同步代码块方式需要传入一个同步对象,这个对象必须是唯一的,这样才能实现同步。在synchronized(this)中我们传入的对象this,其实就是main方法中声明的Account对象。同样的,运行上面的代码,我们会发现每次的余额都是3300,无论多少次都是一样。
有没有发现我们上面的例子中,每次账户的余额都是2300,但这次我们把账户的初始余额改成0,但是还是存10次200的,取20次100的,看看这次最终的余额会不会是1000。
package com.chanshuyi.thread.part3.part32; /**
* 银行存取款 - 使用synchronized关键字修饰方法实现线程同步
* 实现效果:存取不能同步进行,但可能出现连续几次存或连续几次取
* @author yurongchan
*
*/
public class ThreadDemo1 { public static void main(String[] args) {
Account account = new Account(0);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
} } class DepositeThread extends Thread{ private Account account; public DepositeThread(Account account){
this.account = account;
} @Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
}
}
} class DrawMoneyThread extends Thread{ private Account account; public DrawMoneyThread(Account account){
this.account = account;
} @Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
}
} } class Account{ //存钱
public synchronized void deposit(double amount, int i){
try {
Thread.sleep((long)Math.random()*10000); //模拟存钱的延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance + amount;
System.out.println("第" + i + "次,存入钱:" + amount);
System.out.println("第" + i + "次,存钱后账户余额:" + this.balance);
} //取钱
public synchronized void withdraw(double amount, int i){
if(this.balance >= amount){
try {
Thread.sleep((long)Math.random()*10000); //模拟取钱的延迟
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.balance = this.balance - amount;
System.out.println("第" + i + "次,取出钱:" + amount);
System.out.println("第" + i + "次,取钱后账户余额:" + this.balance);
}else{
System.out.println("第" + i + "次,余额不足");
}
} public Account(){ } public Account(double balance){
this.balance = balance;
} private double balance;
}
运行上面的代码,我们会发现有时候最终的余额有时候并不是1000。那是因为发生了同时存取款的情况吗?不会呀,我们已经用synchronized关键字进行线程同步了。那究竟是什么原因呢?仔细察看输出信息我们可以发现有好几次取款的时候发生了余额不足的情况,也就是说我们再余额为0的时候发生了取款行为,这时候取款当然就会失败了。所以最终余额错误是因为我们忽略了余额为0的这种情况,正确的做法是当余额为0的时候,取款线程放弃锁对象并进入等待状态,等待存钱线程存钱之后进行唤醒。那这就涉及到了线程之间的通信了,在两个线程之间进行通信,我们可以使用wait()和notify()进行通信。
关于传入的锁对象
使用synchronized方法实现线程同步,它使用的是synchronized类所在的内部对象,也就是该类的实例化对象作为唯一的锁对象。而使用synchronized代码块实现线程同步,可以传进各种对象,只要你保证你在竞争的两个线程中使用的是同一个对象就可以了。例如:使用synchronized(this)传入的就是调用本类的那个类对象,即Account对象,在本例中就是在main方法中声明的account对象。使用synchronized(String.class)就是使用String的字节类对象作为锁,这个对象也是绝对唯一的。在deposit()和withdraw中分别使用synchronized("11")其结果也是同步的,因为锁对象其实都是指向字符串池中唯一的一个"11"的字符串对象。如果看不懂,没关系,下一篇文章会也会讲解这个,到时候再回来了解一下就可以了。
4、使用wait()/notify()实现线程间通信
将上面的代码稍微修改,使用wait()/notify()进行通信:
package com.chanshuyi.thread.part3.part34; /**
* 银行存取款 - 用synchronized实现线程同步,用wait()/notify()实现线程通信
* 实现效果:一次存,一次取,一直这样直到结束,不会出现连续几次存或取的情况
* @author yurongchan
*
*/
public class ThreadDemo1 { public static void main(String[] args) {
Account account = new Account(0);
new DrawMoneyThread(account).start();
new DepositeThread(account).start();
} } class DepositeThread extends Thread{ private Account account; public DepositeThread(Account account){
this.account = account;
} @Override
public void run() {
//每次存200,10次共存2000
for(int i = 0; i < 10; i++){
account.deposit(200, i + 1);
//模拟存款的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
} class DrawMoneyThread extends Thread{ private Account account; public DrawMoneyThread(Account account){
this.account = account;
} @Override
public void run() {
//每次取100,10次共取1000
for(int i = 0; i < 10; i++){
account.withdraw(100, i + 1);
//模拟取款的时间间隔
try {
Thread.sleep((long)Math.random()*5);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} } class Account{ //存款
public synchronized void deposit(double amount, int i){
System.out.println("***存款线程" + i + "开始存款.");
try {
Thread.sleep((long)Math.random()*10000); //模拟存款的延迟
this.balance = this.balance + amount;
System.out.println("***第" + i + "次,存入钱:" + amount);
System.out.println("***第" + i + "次,存款后账户余额:" + this.balance);
notifyAll(); //唤醒所有存款进程
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} //取款
public synchronized void withdraw(double amount, int i){
while(this.balance < amount){
try {
System.out.println("---取款线程" + i + "取款时发生余额不足.放弃对象锁,进入Lock Block.");
wait(); //余额不足,等待
System.out.println("---取款线程" + i + "被唤醒,尝试取款操作.");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} System.out.println("---取款线程" + i + "开始存款.");
try {
Thread.sleep((long)Math.random()*10000); //模拟取款的延迟
} catch (InterruptedException e) {
e.printStackTrace();
} this.balance = this.balance - amount;
System.out.println("---第" + i + "次,取出钱:" + amount);
System.out.println("---第" + i + "次,取款后账户余额:" + this.balance);
} public Account(){ } public Account(double balance){
this.balance = balance;
} private double balance;
}
在上面的例子中,我们再取款之前先判断账户余额是否足够,如果余额不足则让线程让出对象锁并等待(调用wait()方法会让线程让出对象锁)。而当有存款线程进行存款操作时,存款线程最后会唤醒所有休眠的线程,让他们尝试去取款。下面是其中一个输出:
###取款线程1取款时发生余额不足.放弃对象锁,进入Lock Block.
***存款线程1开始存款.
***第1次,存入钱:200.0
***第1次,存款后账户余额:200.0
###取款线程1被唤醒,尝试取款操作.
---取款线程1开始存款.
---第1次,取出钱:100.0
---第1次,取款后账户余额:100.0
***存款线程2开始存款.
***第2次,存入钱:200.0
***第2次,存款后账户余额:300.0
---取款线程2开始存款.
---第2次,取出钱:100.0
---第2次,取款后账户余额:200.0
***存款线程3开始存款.
***第3次,存入钱:200.0
***第3次,存款后账户余额:400.0
---取款线程3开始存款.
---第3次,取出钱:100.0
---第3次,取款后账户余额:300.0
***存款线程4开始存款.
***第4次,存入钱:200.0
***第4次,存款后账户余额:500.0
---取款线程4开始存款.
---第4次,取出钱:100.0
---第4次,取款后账户余额:400.0
***存款线程5开始存款.
***第5次,存入钱:200.0
***第5次,存款后账户余额:600.0
---取款线程5开始存款.
---第5次,取出钱:100.0
---第5次,取款后账户余额:500.0
***存款线程6开始存款.
***第6次,存入钱:200.0
***第6次,存款后账户余额:700.0
---取款线程6开始存款.
---第6次,取出钱:100.0
---第6次,取款后账户余额:600.0
***存款线程7开始存款.
***第7次,存入钱:200.0
***第7次,存款后账户余额:800.0
---取款线程7开始存款.
---第7次,取出钱:100.0
---第7次,取款后账户余额:700.0
***存款线程8开始存款.
***第8次,存入钱:200.0
***第8次,存款后账户余额:900.0
---取款线程8开始存款.
---第8次,取出钱:100.0
---第8次,取款后账户余额:800.0
***存款线程9开始存款.
***第9次,存入钱:200.0
***第9次,存款后账户余额:1000.0
***存款线程10开始存款.
***第10次,存入钱:200.0
***第10次,存款后账户余额:1200.0
---取款线程9开始存款.
---第9次,取出钱:100.0
---第9次,取款后账户余额:1100.0
---取款线程10开始存款.
---第10次,取出钱:100.0
---第10次,取款后账户余额:1000.0
从上面的输出我们可以看到一开始的时候第一个取款的线程尝试去取款,但是余额不足,于是它放弃了对象锁并进入阻塞状态。之后存款线程1获得了对象锁,并往账户存入了200,最后调用了notifyAll()方法唤醒了所有的取款线程。此时取款线程1被唤醒,它尝试着继续去取款,判断发现确实账户有余额,于是就进行取款操作。
讲到这里,相信大部分人都会对synchronized和wait()/notify()的作用有一个感性的了解。synchronized只负责实现线程同步,而wait()/notify()方法可以帮助线程在线程同步的基础上实现线程通信,从而实现更加负责的功能。
Synchronized 关键字作用域
synchronized关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod(){}可以防止多个线程同时访问这个对象的synchronized方法(如果一个对象有多个synchronized方法,只要一个线程访问了其中的一个synchronized方法,其它线程不能同时访问这个对象中任何一个synchronized方法)。这时,不同的对象实例的 synchronized方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的synchronized方法;
2)是某个类的范围,synchronized static aStaticMethod{}防止多个线程同时访问这个类中的synchronized static 方法(因为此时用的是 对象.class 作为锁)。它可以对类的所有对象实例起作用。
下篇文章将介绍如何利用ReentrantLock类(对象锁)进行线程同步。
(删)Java线程同步实现一:synchronzied和wait()/notify()的更多相关文章
- java 线程同步 原理 sleep和wait区别
		
java线程同步的原理java会为每个Object对象分配一个monitor, 当某个对象(实例)的同步方法(synchronized methods)被多个线程调用时,该对象的monitor将负责处 ...
 - Java线程同步_1
		
Java线程同步_1 synchronized 该同步机制的的核心是同步监视器,任何对象都可以作为同步监视器,代码执行结束,或者程序调用了同步监视器的wait方法会导致释放同步监视器 synchron ...
 - H2O与Java线程同步
		
Java 5以前的线程同步采用syncronized和wait,notify,notifyAll来实现,比较粗糙.之后有了Lock和Condition.ReentrantLock的简单lock,unl ...
 - Java线程同步之一--AQS
		
Java线程同步之一--AQS 线程同步是指两个并发执行的线程在同一时间不同时执行某一部分的程序.同步问题在生活中也很常见,就比如在麦当劳点餐,假设只有一个服务员能够提供点餐服务.每个服务员在同一时刻 ...
 - java线程 同步临界区:thinking in java4 21.3.5
		
java线程 同步临界区:thinking in java4 21.3.5 thinking in java 4免费下载:http://download.csdn.net/detail/liangru ...
 - JAVA - 线程同步和线程调度的相关方法
		
JAVA - 线程同步和线程调度的相关方法 wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁:wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等 ...
 - Java线程同步的四种方式详解(建议收藏)
		
 Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...
 - (删)Java线程同步实现二:Lock锁和Condition
		
在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步.但在Java中还有一种方式可以实现线程同步,那就是Lock锁. 一.同步锁 ...
 - 【总结】Java线程同步机制深刻阐述
		
原文:http://hxraid.iteye.com/blog/667437 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread). 线程(Thread ...
 
随机推荐
- 8086cpu
			
1. 8086CPU和8088CPU内部结构基本相同,不同之处在于8088有8条外部数据总线,因此为准16位.8086有16条外部数据总线.两个CPU的软件完全兼容,程序的编制也完全相同. 2. ...
 - Java显式锁学习总结之二:使用AbstractQueuedSynchronizer构建同步组件
			
Jdk1.5中包含了并发大神Doug Lea写的并发工具包java.util.concurrent,这个工具包中包含了显示锁和其他的实用同步组件.Doug Lea在构建锁和组件的时候,大多是以队列同步 ...
 - 【Zookeeper】源码分析之网络通信(一)
			
一.前言 前面已经分析了请求处理链中的多数类,接着继续分析Zookeeper中的网络通信模块. 二.总体框图 对于网络通信模块,其总体框图如下所示 说明: Stats,表示ServerCnxn上的统计 ...
 - 第24篇 js小知识和“坑”
			
前面说了说了js的相关知识,基本上除了语法外,把项目常用的知识做了一个梳理,现在说下js的其它方面的知识,这些知识不成体系,属于不理解对于一般开发没什么太多影响,但如果理解清楚,可以更好去开发. js ...
 - 文件的上传(表单上传和ajax文件异步上传)
			
项目中用户上传总是少不了的,下面就主要的列举一下表单上传和ajax上传!注意: context.Request.Files不适合对大文件进行操作,下面列举的主要对于小文件上传的处理! 资源下载: 一. ...
 - Java Web(十三) 使用javamail进行发送邮件,(使用QQ,163,新浪邮箱服务器)
			
加油加油. --WH 一.发送邮件的原理 在了解其原理之前,先要知道两个协议,SMTP和POP3 SMTP:Simple Mail Transfer Protocol,即简单邮件传输协议,发送邮件的协 ...
 - TimerTask实现定期检查数据库操作
			
最近在做一个P2P 的众筹网站,其他的内容还都可以,只是定期检查数库里面的项目是不是到期了,让我费了一些时间,现在写好了,我把它总结下来,以便以后使用.顺便和大家分享一下. Timer可以看成一个定时 ...
 - Laptop Ubuntu16.04/14.04 安装Nvidia显卡驱动
			
笔记本型号 机械革命(MECHREVO)深海泰坦X6Ti-S(黑曜金)15.6英寸 CPU型号 i5-7300HQ 内存 8G 硬盘容量 128SSD+1T机械硬盘 显卡 GeForce GTX 10 ...
 - [LeetCode]Integer Break(Dp或胡搞或推公式)
			
343. Integer Break Given a positive integer n, break it into the sum of at least two positive intege ...
 - 第一章  初始java
			
一.单词 public:公共的 static:静态的 void:空的 class:类 print:打印 line:排 pro ...