上面文章(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()的更多相关文章

  1. java 线程同步 原理 sleep和wait区别

    java线程同步的原理java会为每个Object对象分配一个monitor, 当某个对象(实例)的同步方法(synchronized methods)被多个线程调用时,该对象的monitor将负责处 ...

  2. Java线程同步_1

    Java线程同步_1 synchronized 该同步机制的的核心是同步监视器,任何对象都可以作为同步监视器,代码执行结束,或者程序调用了同步监视器的wait方法会导致释放同步监视器 synchron ...

  3. H2O与Java线程同步

    Java 5以前的线程同步采用syncronized和wait,notify,notifyAll来实现,比较粗糙.之后有了Lock和Condition.ReentrantLock的简单lock,unl ...

  4. Java线程同步之一--AQS

    Java线程同步之一--AQS 线程同步是指两个并发执行的线程在同一时间不同时执行某一部分的程序.同步问题在生活中也很常见,就比如在麦当劳点餐,假设只有一个服务员能够提供点餐服务.每个服务员在同一时刻 ...

  5. java线程 同步临界区:thinking in java4 21.3.5

    java线程 同步临界区:thinking in java4 21.3.5 thinking in java 4免费下载:http://download.csdn.net/detail/liangru ...

  6. JAVA - 线程同步和线程调度的相关方法

    JAVA - 线程同步和线程调度的相关方法 wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁:wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等 ...

  7. Java线程同步的四种方式详解(建议收藏)

    ​ Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式@mikechen 目录 什么是线程同步 线程同步的几种方式 1.使用sync ...

  8. (删)Java线程同步实现二:Lock锁和Condition

    在上篇文章(3.Java多线程总结系列:Java的线程同步实现)中,我们介绍了用synchronized关键字实现线程同步.但在Java中还有一种方式可以实现线程同步,那就是Lock锁. 一.同步锁 ...

  9. 【总结】Java线程同步机制深刻阐述

    原文:http://hxraid.iteye.com/blog/667437 我们可以在计算机上运行各种计算机软件程序.每一个运行的程序可能包括多个独立运行的线程(Thread). 线程(Thread ...

随机推荐

  1. asp.net中listview下嵌套gridview

    最近在上软件工程实践课程,想做一个类似于QQ空间或者朋友圈一样的效果.即显示所有好友发送的动态以及动态下回复的信息. 自己YY了一种方法,一开始以为不能达到效果,研究了2个小时终于实现了,感觉效果还是 ...

  2. 【转】对于HttpClient和HtmlUnit的理解

    原文地址:http://www.haohaoblog.com/?p=1327&utm_source=tuicool 做Java编程的人其实,很多不懂SEO,也不知道如何让百度收录等等,当然,对 ...

  3. 架构师之路——里氏替换原则LSP

    定义: 如果对每一个对类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型. 内容: 里氏替换原则通 ...

  4. javascipt : reduce

    $scope.totalPrice = function () { return $scope.addcartProduct.reduce(function (money, product) { re ...

  5. MYSQL中添加时间

    1.在创建新记录和修改现有记录的时候都对这个数据列刷新:TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 2.在创建新记录 ...

  6. C++ 友元函数的函数指针

    成员函数的指针 讲友元之前先讲普通的成员函数的函数指针 class Std_interface { public: virtual void suspend() = 0; }; // define t ...

  7. Window下JDK安装与配置

    今天项目组开会,由于.Net平台的限制无法满足现有业务需求,项目计划从.Net平台转Java平台,采用Java+Spark+Hadoop,之前关于Java和Hadoop的书也买的有只是平时看的少,最近 ...

  8. 关于Java空指针的控制(转)

    1)在已经的String(字符串)调用 equal()和 equalsingnoreCase()而不是未知的对象 通常在已经的非空字符串在调用equals().因为equal()方法是对称的,调用a. ...

  9. jquery 日期获取

    来自:http://blog.csdn.NET/liujun198773/article/details/7554628  感谢 $(function(){ var mydate = new Date ...

  10. 3522: [Poi2014]Hotel

    3522: [Poi2014]Hotel Time Limit: 20 Sec  Memory Limit: 128 MBSubmit: 253  Solved: 117[Submit][Status ...