您的位置:首页 > 编程语言 > Java开发

java多线程安全问题

2015-01-05 15:59 155 查看
当两个进程并发修改同一个文件时就有可能造成线程安全问题,为了解决这个问题java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的同步方法就是同步代码块、同步方法、Lock锁(同步锁)

1、同步代码块

synchronized(obj)
{
...
//此处的代码就是同步代码块
}


上面的语法格式中synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

注意:任何时刻只能有一条线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然释放了对该同步监视的锁定。

虽然java程序允许使用任何对象来作为同步监视器,但想一下同步监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器

public class Account
{
//封装账户编号、账户余额两个属性
private String accountNo;
private double balance;
public Account(){}
//构造器
public Account(String accountNo,double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
//此处省略accountNo和balance两个属性的set和get方法
...
//下面两个方法根据accountNo来计算Account的hashCode和判断equals
public int hashCode()
{
return accountNo.hashCode();
}
public Boolean equals (Object obj)
{
if(obj != null && obj.getClass() == Account.class){
Account target = (Account)obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
private double drawAmount'
public DrawThread(String name,Account account,Double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
//当多条线程修改同一共享数据时,讲涉及数据安全问题
public void run(){
//使用account作为同步监视器,任何线程进入下面代码块之前,必须先获得对account账户的锁定--其他线程无法获得锁,也就无法修改它
//这种做法符合:加锁-->修改完成-->释放锁逻辑
synchronized (account)
{
//账户余额大于取钱数目
if(account.getBalance() >= drawAmount)
{
//吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.prinln("/t余额为:"+ account.getBalance());
}
else
{
System.out.prinln(getName() +"取钱失败!余额不足!");
}
}
//同步代码块结束,该线程释放同步锁
}
}

public class TestDraw
{
public static void main(String[] args)
{
//创建一个账户
Account acct = new Account("123456",1000);
//模拟两个线程对同一账户取钱
new Thread("甲",acct,800).start();
new Thread("乙",acct,800).start();
}
}
上面程序使用synchronized讲run方法里方法体修改成同步代码块,该同步代码块的同步监视器account对象,这样的做法符合“加锁-->修改完成-->释放锁”逻辑,任何线程想修改指定资源之前,首先对该资源加锁,在加锁期间其他线程无法修改该资源,当该线程修改完成,该线程释放对该资源的锁定。通过这种方式就可以保证并发线程在任一时刻只有一条线程可以进入修改共同资源的代码区(也被称为临界区),所以同一时刻最多只有一条线程处于临界区从而保证了线程的安全性。

2、同步方法

同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法对于同步方法而言,无需显示指定同步监视器,同步方法的同步监视器是this,也就是对象本身

可变对象需要额外的方法保证其线程安全,那么程序只要把修改的balance的方法修改成同步方法即可

public class Account
{
private String accountNo;
private double balance;
public Account (){};
public Account (String accountNo,double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
//此处省略了accountNo的set/get方法
//因此账户余额不允许随便修改,所以取消balance属性的set方法

public double getBalance()
{
return this.balance;
}
//提供一个线程安全draw方法来完成取钱操作
public synchronized void draw(double drawAmount)
{
//账户余额大于取钱数目
if(balance >= drawAmount)
{
//吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.prinln("/t余额为:"+ account.getBalance());
}
else
{
System.out.prinln(getName() +"取钱失败!余额不足!");
}
}
//此处省略了hashCode和equals两个重写的方法
}
}


上面程序中增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰修改该方法,把该方法变成同步方法。同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一条线程获得Account对象的锁定,然后进入draw()方法执行取钱操作--这样也可以保证多条线程取钱的线程安全

备注:synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、属性等

因为Account类中已经提供了draw方法,而且取消了setBalance()方法,DrawThread线程类需要改写,该类需要调用Account对象的draw方法来执行取钱操作

public class DrawThread extends Thread
{
//模拟用户账户
private Account account;
//当前取钱线程所希望的钱数
private double drawAmount;
public DrawThread(String name,Account account,double drawAmount)
{
super(name);
this.account = account;
this.drawAmount = drawAmount;
}
public void run()
{
//直接调用account对象的draw方法来执行取钱
account.draw(drawAmount);
}
}
上面的DrawThread类无需自己实现取钱操作,而是直接调用account的draw方法来执行取钱。由于我们已经使用了synchronized关键字保证了draw方法的线程安全性,所以多条线程并发调用draw方法也不会出现问题

释放同步监视器的锁定

任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢?程序无法显示释放对同步监视器的锁定,线程会在如下几种情况下释放同步监视器的锁定

-->当前线程的同步方法、同步代码块执行结束,当前线程即释放同步监视器

-->当线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行,当前线程将会释放同步监视器

-->当线程在同步代码块、同步方法中出现了未处理的Error和Exception,导致了该代码块、该方法异常结束时将会释放同步监视器

-->当线程执行同步代码块、或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器

-->当线程执行同步代码块、或同步方法时,程序调用Thread。sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器

-->当线程执行同步代码块,其他线程调用了该线程的suspend方法将该线程挂起,该线程不会释放同步监视器。当然,我们应该避免使用suspend和resume方法来控制线程。

同步锁(Lock)

从JDK1.5之后,Java提供了另外一种线程同步机制:它通过显示定义同步锁对象来实现同步没在这种机制下同步锁应该使用Lock对象充当

通常认为:Lock提供了比synchronized方法和synchronized代码块更广泛的锁定操作,Lock实现允许更灵活的结构,可以具有差别很大的属性,并且可以支持多个相关的Condition对象

Lock是控制多个线程对共享资源进行访问的工具。通常,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。不过,某些锁可能允许对共享资源并发访问,如ReadWriteLock(可重入锁)。使用该Lock对象可以显示加锁、释放锁,通常使用Lock对象的代码格式如下

class L
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//定义需要保证线程安全的方法
public void m()
{
//加锁
lock.lock();
try
{
//需要保证线程安全的代码
//...method body
}
//使用finally快来保证释放锁
finally
{
lock.unlock();
}
}
}


使用Lock对象来进行同步时,锁定和释放锁出线在不同作用范围时,通常建议使用finally快来确保在必要时释放锁。通过使用Lock对象我们可以把Account类改为如下形式,它依然是线程安全的

public class Account
{
//定义锁对象
private final ReetrantLock lock = new ReentrantLock();
private String accountNo;
private double balance;
public Account(){}
public Account(String accountNo,double balance)
{
this.accountNo = accountNo;
this.balance = balance;
}
//此处省略了accountNo的get/set方法
//因此账户余额不允许随便修改,所以取消balance属性的set方法
public getBalance()
{
return this.balance;
}
//提供一个线程安全draw方法来完成取钱操作
public void draw(double drawAmount)
{
//对同步锁进行锁定
lock.lock();
try
{
//账户余额大于取钱数目
if(balance >= drawAmount)
{
//吐出钞票
System.out.println(getName() + "取钱成功!吐出钞票:" + drawAmount);
try
{
Thread.sleep(1);
}
catch (InterruptedException ex)
{
ex.printStackTrace();
}
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.prinln("/t余额为:"+ account.getBalance());
}
else
{
System.out.prinln(getName() +"取钱失败!余额不足!");
}
}
//使用finally块来保证释放锁
finally
{
lock.unlock();
}
}
//此处省略了hashCode和equals两个重写的方法
}
程序中实现draw方法时,进入方法开始后立即请求lock对象进行加锁,接着程序完全实现draw方法的draw方法的取钱逻辑之后,程序使用finally块来确保释放锁

使用Lock与使用同步方法有点相似,只是使用Lock时显示使用Lock对象作为同步锁,而使用同步方式时系统隐式使用当前对象作为同步监视器,同样符合“加锁->访问->释放锁”的操作模式,而且使用Lock对象时每个Lock对象对应一个Account对象,一样可以保证对于同一个Account对象,同一时刻只能有一条线程能进入临界区。

同步方法或同步代码块使用与竞争资源相关的、隐式的同步监视器,并且强制要求加锁和释放锁要出现在同一块结构中,而且当获取了多个琐时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的范围内释放所有锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: