(See http://www.suneca.com/article.asp?id=55)
六、线程同步
多线程操作给我们带来了很多好处,但也不少问题我们需要去解决,这些问题主要是当多个线程共享数据时,我们应该如何去考虑同步的问题。线程同步问题,可能有些人做开发根本就不会发生,因为在技术框架上,它根本就不可能发生;有些人可能在开发一些应用系统的时候,需要考虑到线程同步的总理,但根本就不去考虑,再说,这种问题也是很少发生;有些人,对线程同步的问题想得太复杂,提到某一些类对象的时候,就马上提出,这个不是线程安全的,应该怎么样怎么样!其实,根本不会发生线程同步的问题,也就是说多线程没有共享数据的时候,你考虑线程安全干嘛?线程安全这一块控制不好,反而会影响整个系统的性能。
多线程共享数据的时候,我们需要考虑到线程同步,以下演示了一个线程共享数据的问题。现在假设有两条线程,分别为线程t1跟线程t2,帐号的余额为1000元,这两条线程都是取钱。假如不考虑线程同步问题,线程1启动后,执行取钱操作,一共取六次,每次一百元;线程2启动后,执行取钱操作,一共也取六次,每次一百元。每次取钱之前都会先检查一下余额,如果余额度小于一百元,则线程退出。最后,在控制台上输出帐号的总额。程序的实现如下:
多线程操作给我们带来了很多好处,但也不少问题我们需要去解决,这些问题主要是当多个线程共享数据时,我们应该如何去考虑同步的问题。线程同步问题,可能有些人做开发根本就不会发生,因为在技术框架上,它根本就不可能发生;有些人可能在开发一些应用系统的时候,需要考虑到线程同步的总理,但根本就不去考虑,再说,这种问题也是很少发生;有些人,对线程同步的问题想得太复杂,提到某一些类对象的时候,就马上提出,这个不是线程安全的,应该怎么样怎么样!其实,根本不会发生线程同步的问题,也就是说多线程没有共享数据的时候,你考虑线程安全干嘛?线程安全这一块控制不好,反而会影响整个系统的性能。
多线程共享数据的时候,我们需要考虑到线程同步,以下演示了一个线程共享数据的问题。现在假设有两条线程,分别为线程t1跟线程t2,帐号的余额为1000元,这两条线程都是取钱。假如不考虑线程同步问题,线程1启动后,执行取钱操作,一共取六次,每次一百元;线程2启动后,执行取钱操作,一共也取六次,每次一百元。每次取钱之前都会先检查一下余额,如果余额度小于一百元,则线程退出。最后,在控制台上输出帐号的总额。程序的实现如下:
程序代码
package
zizz
;
import java .util .Date ;
/**
* 该类演示了多线程的数据共享问题.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:41:38
*/
public class MultiThread {
/**
* @param args
*/
public static void main (String [ ] args ) {
BankThread bank = new BankThread ( ) ;
Thread t1 = new Thread (bank ) ;
Thread t2 = new Thread (bank ) ;
t1 .start ( ) ;
t2 .start ( ) ;
try {
//子线程执行完毕之后,主线程才继续执行.
t1 .join ( ) ;
t2 .join ( ) ;
} catch (InterruptedException e ) {
e .printStackTrace ( ) ;
}
System .out .println ( "帐号余额为:" + BankThread .totalMoney ) ;
}
}
/**
* 共享的线程实例.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:43:01
*/
class BankThread implements Runnable {
/**
* 帐号金额.
*/
static int totalMoney = 1000 ;
/**
* 取钱或转帐每次的金额.
*
*/
final static int FETCH_EACH_TIME = 100 ;
/**
* 线程执行内容.
*/
public void run ( ) {
for ( int i =0 ;i <6 ;i + + ) {
//每次取钱先检查一下帐户的余额.
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
}
/**
* 检查帐号状态.
*
* @return
*/
public boolean checkAccountBalance ( ) {
//如果余额不够,则返false.否则,返回true
if (totalMoney < FETCH_EACH_TIME ) {
return false ;
} else {
return true ;
}
}
/**
* 取钱.假设每次取一百块钱.
*/
public void doFetchMoney ( ) {
System .out .println (Thread .currentThread ( ) .getName ( ) + " 取出" + FETCH_EACH_TIME ) ;
totalMoney = totalMoney - FETCH_EACH_TIME ;
}
/**
* 做日志,该方法目的是为了让线程多干一些事情.
*
*/
public void logOperator ( ) {
for ( int i =0 ;i <100000 ;i + + ) {
//不断让JVM分配内存,该操作目的是为了能让线程同步问题更快发生.
Date date = new Date ( ) ;
}
}
}
import java .util .Date ;
/**
* 该类演示了多线程的数据共享问题.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:41:38
*/
public class MultiThread {
/**
* @param args
*/
public static void main (String [ ] args ) {
BankThread bank = new BankThread ( ) ;
Thread t1 = new Thread (bank ) ;
Thread t2 = new Thread (bank ) ;
t1 .start ( ) ;
t2 .start ( ) ;
try {
//子线程执行完毕之后,主线程才继续执行.
t1 .join ( ) ;
t2 .join ( ) ;
} catch (InterruptedException e ) {
e .printStackTrace ( ) ;
}
System .out .println ( "帐号余额为:" + BankThread .totalMoney ) ;
}
}
/**
* 共享的线程实例.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:43:01
*/
class BankThread implements Runnable {
/**
* 帐号金额.
*/
static int totalMoney = 1000 ;
/**
* 取钱或转帐每次的金额.
*
*/
final static int FETCH_EACH_TIME = 100 ;
/**
* 线程执行内容.
*/
public void run ( ) {
for ( int i =0 ;i <6 ;i + + ) {
//每次取钱先检查一下帐户的余额.
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
}
/**
* 检查帐号状态.
*
* @return
*/
public boolean checkAccountBalance ( ) {
//如果余额不够,则返false.否则,返回true
if (totalMoney < FETCH_EACH_TIME ) {
return false ;
} else {
return true ;
}
}
/**
* 取钱.假设每次取一百块钱.
*/
public void doFetchMoney ( ) {
System .out .println (Thread .currentThread ( ) .getName ( ) + " 取出" + FETCH_EACH_TIME ) ;
totalMoney = totalMoney - FETCH_EACH_TIME ;
}
/**
* 做日志,该方法目的是为了让线程多干一些事情.
*
*/
public void logOperator ( ) {
for ( int i =0 ;i <100000 ;i + + ) {
//不断让JVM分配内存,该操作目的是为了能让线程同步问题更快发生.
Date date = new Date ( ) ;
}
}
}
对程序一共执行了两次,第一次结果为:
该结果正确!
运行第二次,结果为:

该结果不正确!
为什么会出现这个问题呢?我们给出这样的解释:
1)线程t1跟线程t2他们的优先级别相同,他们在线程调度器的调度下,交叉着使用CPU资源。
2)当线程t1处于运行状态时,检查完帐号余额(checkAccountBalance)时,结果是满足条件的,帐号余额还有100元,此时,t1阻塞,CPU资源给线程t2,t2进入运行状态。
3)线程t2检查帐号余额,此时,还是满足条件,则进行日志后,进行扣款。扣款后的余额为0。继续执行循环,不够提款,线程t2退出。
4)线程t1继续执行,执行扣款操作,因为已经跳过了检查帐号途额的操作,所以直接扣款,扣款后的帐号余额为:-100元。
那我们应该如何去解决这个问题呢?此时,我们需要使用到锁的机制来进行相应的操作。
在Java语言中,引入了对象互斥锁(mutual exclusive lock,也简称为对象锁)的概念,来保证共享数据操作的完整性:
1)每个对象都对应于一个可称为“互斥锁”的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
2)关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问。在Java中的两种使用synchronized的方式:
a)放在方法前面,这样,调用该方法的线程均将获得对象的锁。
b)放在代码块前面,它也有两种形式:
synchronized (this){… …}或 synchronized {… …}:代码块中的代码将获得当前对象引用的锁
synchronized(otherObj){… …}:代码块中的代码将获得指定对象引用的锁
对对象加了锁,用完之后,我们需要 释放锁
1)如果一个线程一直占用一个对象的锁,则其他的线程将永远无法访问该对象,因此,需要在适当的时候,将对象锁归还。
2)当线程执行到synchronized()块结束时,释放对象锁。
3)当在synchronized()块中遇到break, return或抛出exception,则自动释放对象锁。
4)当一个线程调用wait()方法时,它放弃拥有的对象锁并进入等待队列。
对于银行存款的例子,我们可以使用锁机制,对其进行改进!
程序代码
package
zizz
;
import java .util .Date ;
/**
* 该类演示了多线程的数据共享问题.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:41:38
*/
public class MultiThread {
/**
* @param args
*/
public static void main (String [ ] args ) {
BankThread bank = new BankThread ( ) ;
Thread t1 = new Thread (bank ) ;
Thread t2 = new Thread (bank ) ;
t1 .start ( ) ;
t2 .start ( ) ;
try {
//子线程执行完毕之后,主线程才继续执行.
t1 .join ( ) ;
t2 .join ( ) ;
} catch (InterruptedException e ) {
e .printStackTrace ( ) ;
}
System .out .println ( "帐号余额为:" + BankThread .totalMoney ) ;
}
}
/**
* 共享的线程实例.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:43:01
*/
class BankThread implements Runnable {
/**
* 帐号金额.
*/
static int totalMoney = 1000 ;
/**
* 取钱或转帐每次的金额.
*
*/
final static int FETCH_EACH_TIME = 100 ;
/**
* 线程执行内容.
*/
public void run ( ) {
for ( int i =0 ;i <6 ;i + + ) {
//每次取钱先检查一下帐户的余额.
synchronized ( this ) {
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
}
}
/**
* 检查帐号状态.
*
* @return
*/
public boolean checkAccountBalance ( ) {
//如果余额不够,则返false.否则,返回true
if (totalMoney < FETCH_EACH_TIME ) {
return false ;
} else {
return true ;
}
}
/**
* 取钱.假设每次取一百块钱.
*/
public void doFetchMoney ( ) {
System .out .println (Thread .currentThread ( ) .getName ( ) + " 取出" + FETCH_EACH_TIME ) ;
totalMoney = totalMoney - FETCH_EACH_TIME ;
}
/**
* 做日志,该方法目的是为了让线程多干一些事情.
*
*/
public void logOperator ( ) {
for ( int i =0 ;i <100000 ;i + + ) {
//不断让JVM分配内存,该操作目的是为了能让线程同步问题更快发生.
Date date = new Date ( ) ;
}
}
}
import java .util .Date ;
/**
* 该类演示了多线程的数据共享问题.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:41:38
*/
public class MultiThread {
/**
* @param args
*/
public static void main (String [ ] args ) {
BankThread bank = new BankThread ( ) ;
Thread t1 = new Thread (bank ) ;
Thread t2 = new Thread (bank ) ;
t1 .start ( ) ;
t2 .start ( ) ;
try {
//子线程执行完毕之后,主线程才继续执行.
t1 .join ( ) ;
t2 .join ( ) ;
} catch (InterruptedException e ) {
e .printStackTrace ( ) ;
}
System .out .println ( "帐号余额为:" + BankThread .totalMoney ) ;
}
}
/**
* 共享的线程实例.
*
* @author <a href='http://www.suneca.com'>ZIZZ</a>
*
* @Create-Time:2008 下午08:43:01
*/
class BankThread implements Runnable {
/**
* 帐号金额.
*/
static int totalMoney = 1000 ;
/**
* 取钱或转帐每次的金额.
*
*/
final static int FETCH_EACH_TIME = 100 ;
/**
* 线程执行内容.
*/
public void run ( ) {
for ( int i =0 ;i <6 ;i + + ) {
//每次取钱先检查一下帐户的余额.
synchronized ( this ) {
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
}
}
/**
* 检查帐号状态.
*
* @return
*/
public boolean checkAccountBalance ( ) {
//如果余额不够,则返false.否则,返回true
if (totalMoney < FETCH_EACH_TIME ) {
return false ;
} else {
return true ;
}
}
/**
* 取钱.假设每次取一百块钱.
*/
public void doFetchMoney ( ) {
System .out .println (Thread .currentThread ( ) .getName ( ) + " 取出" + FETCH_EACH_TIME ) ;
totalMoney = totalMoney - FETCH_EACH_TIME ;
}
/**
* 做日志,该方法目的是为了让线程多干一些事情.
*
*/
public void logOperator ( ) {
for ( int i =0 ;i <100000 ;i + + ) {
//不断让JVM分配内存,该操作目的是为了能让线程同步问题更快发生.
Date date = new Date ( ) ;
}
}
}
在原来的:
程序代码
if
(checkAccountBalance
(
)
)
{
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
增加锁,修改为:
程序代码
synchronized
(
this
)
{
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
if (checkAccountBalance ( ) ) {
//执行日志操作.
logOperator ( ) ;
doFetchMoney ( ) ;
} else {
//余额不足,退出循环
break ;
}
}
问题解决,再次运行程序,结果正确!
使用锁机制也可能会导致死锁,在Step 4我们将会演示一个死锁的例子,这一节我们先了解一下概念!
1)是指两个线程,都相互等待对方释放lock
2)是不可测知或避开的
3)应采取措施避免死锁的出现