上面文章(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类(对象锁)进行线程同步。