线程同步
Vector 、 Hashtable都是线程安全的
竞争资源(共享资源) - 如果有多条线程需要并发访问、并修改某个对象,该对象就是“竞争资源”。
为了避免多个线程“自由竞争”修改共享资源所导致的的不安全问题。 于是就考虑 “加锁”
- 第一种方式: 用同步代码块
++它需要显示指定同步监视器++
同步代码块的语法格式如下:
synchronized(obj)
{
...
//此处的代码就是同步代码块
}
上面语法格式中synchronized后面括号中的obj就是同步监视器,上面代码的含义是: 线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。
synchronized关键字可以修饰方法,可以修饰代码块,但不可以修饰构造器、成员变量等。
==任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。==
==++虽然Java程序允许适用任何对象作为同步监视器,但同步监视器的目的是:组织两个线程对同一个共享资源进行并发访问。 因此推荐使用可能被并发访问的共享资源充当同步监视器。++==
程序实例
先定义一个账户类,该账户类封装了账户编号和余额两个实例变量。
public class Account {
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return accountNo;
}
public void setBalance(double balance) {
this.balance = balance;
}
public double getBalance() {
return balance;
}
//下面两个方法根据accountNo来重写hashCode()和equals方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
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() {
synchronized (account) {
//账户余额大于取钱数目
if (account.getBalance() >= drawAmount) {
System.out.println(getName() + "取钱成功,吐出钞票" + drawAmount);
/*
try
{
Thread.sleep(1);
}
cath (interruptedException ex)
{
ex.printStackTrace();
}
*/
//修改余额
account.setBalance(account.getBalance() - drawAmount);
System.out.println("\t 余额为: " + account.getBalance());
} else {
System.out.println(getName() + "取钱失败!余额不足!");
}
}
}
最后是该程序的主程序,创建一个账户,并启动两个线程从该账户中取钱。
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("1234567", 1000);
//模拟两个线程对一个账户取钱
new DrawThread("甲", acct, 800).start();
new DrawThread("乙", acct, 800).start();
}
}
第二种方式: 同步方法
同步方法就是使用synchronized关键字修饰某个方法。++如果方法是实例方法,相当于this作为同步监视器,如果方法是类方法,相当于以类作为同步监视器。++
它们的实现机制是完全相同的:当线程要进入某个被 “同步监视锁” 监视的代码之前,线程必须先获得“同步监视锁”。
这样就保证在任意一个时刻,只有一条线程能进入被“同步监视锁”监视的代码。
通过使用同步方法可以非常方便的实现线程安全的类,线程安全的类具有如下特征。- 该类的对象可以被多个线程安全地访问。
- 每个线程调用该对象的任意方法之后都将得到正确的结果。
- 每个线程调用该对象的任意方法之后,该对象状态依然保持合理状态。
不可变类总是线程安全的,因为他的对象状态不可变;但可变对象需要额外的方法来保证其线程安全。 例如Account就是一个可变类,他的accountNo和balance两个成员变量都可以被改变,当两个线程同时修改balance成员变量的值时,程序就可能出现异常。下面将Account类对balance的访问设置成线程安全的。只需要把修改balance的方法变成同步方法即可。
程序实例
public class Account {
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return accountNo;
}
//因为账户余额不允许随便修改,所以只为balance提供getter方法
public double getBalance() {
return balance;
}
//提供一个线程安全的draw方法来完成取钱操作
public synchronized void draw(double drawAmount){
//账户余额大于取钱数目
if (balance >= drawAmount){
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票: " + drawAmount);
try{
Thread.sleep(1);
}
catch (InterruptedException ex){
ex.printStackTrace();
}
//修改余额
balance -= drawAmount;
System.out.println("\t 余额为: " + balance);
}
else{
System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
}
}
//下面两个方法根据accountNo来重写hashCode()和equals方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
上面程序中增加了一个代表取钱的draw()方法,并使用了synchronized关键字修饰该方发,把该方法变成同步方法,该同步方法的同步监视器是this,因此对于同一个Account账户而言,任意时刻只能有一个线程获得对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()方法来执行取钱操作
//同步方法的同步监视器是this,this代表调用draw()方法的对象
//也就是说,线程进入draw()方法之前,必须先对account对象加锁。
account.draw(drawAmount);
}
public class DrawTest {
public static void main(String[] args) {
Account acct = new Account("1234567", 1000);
//模拟两个线程对一个账户取钱
new DrawThread("甲", acct, 800).start();
new DrawThread("乙", acct, 800).start();
}
}
从语法角度来看,任意对象都可作为同步监视锁。
/—从程序逻辑来看,==选择“竞争资源”作为同步监视锁==。
线程同步的关键:
任意的线程,进入【同步监视器】监视的代码之前,必须先对【同步监视器】加锁。
这种做法符合 加锁—修改—-释放锁的逻辑。
可变类的线程安全是以降低程序的运行效率作为代价的,为了减少线程安全所带来的负面影响:
* 不要对线程安全类的所有方法都进行同步,只对那些会改变竞争资源的方法进行同步。
* 如果可变类有两种运行环境:单线程环境和多线程环境,则应该为该可变类提供两种版本,即线程不安全版本和线程安全版本。 在单线程环境使用线程不安全版本以保证性能,在多线程环境使用线程安全版本。
何时释放对【同步监视器】的加锁
程序无须显式释放对同步监视器的锁定,线程会在如下几种情况下释放对同步监视器的锁定。
* A。 同步代码块或同步方法执行完成
* B. 在代码块遇到break、return跳出了改代码块时。
* C. 执行同步代码块或同步方法时遇到未捕获的异常
* D. 调用了同步监视器的wait()方法。
下面这些情况不会释放
* A. sleep()、yield(0都不会释放。
* B. supend()也不会释放。
同步锁
通常使用ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码格式如下:
class X
{
//定义锁对象
private final ReentrantLock lock = new ReentrantLock();
//...
//定义需要保证线程安全的方法
public void m()
{
//加锁
lock.lock();
try
{
//需要保证线程安全的代码
//...method body
}
//使用finally块来保证释放锁
finally
{
lock.unlock();
}
}
}
==使用ReentrantLock对象来进行同步,加锁和释放锁出现在不同的作用范围内时,通常建议使用finally块来确保在必要时释放锁。==
使用Lock与使用同步方法有点相似,只是使用Lock时显式使用Lock对象作为同步锁,而使用同步方法时系统隐式使用当前对象作为同步监视器。同样都符合加锁—修改–释放锁的操作模式。
public class Account {
//定义锁对象
private final ReentrantLock lock =new ReentrantLock();
// 封装账户编号、账户余额两个成员变量
private String accountNo;
private double balance;
public Account() {
}
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}
public String getAccountNo() {
return accountNo;
}
//因为账户余额不允许随便修改,所以只为balance提供getter方法
public double getBalance() {
return balance;
}
//提供一个线程安全的draw方法来完成取钱操作
public void draw(double drawAmount){
//加锁
lock.lock();
try {
//账户余额大于取钱数目
if (balance >= drawAmount) {
System.out.println(Thread.currentThread().getName() + "取钱成功!吐出钞票: " + drawAmount);
try {
Thread.sleep(1);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
//修改余额
balance -= drawAmount;
System.out.println("\t 余额为: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
}
}
finally {
//修改完成,释放锁
lock.unlock();
}
}
//下面两个方法根据accountNo来重写hashCode()和equals方法
public int hashCode() {
return accountNo.hashCode();
}
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}
死锁
当两个线程相互等待对方释放同步监视器时就会发生死锁,Java没有相关措施来处理死锁,因此多线程编程时需要避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态下,无法继续下去。
由于Thread类的suspend()方法也很容易导致死锁,所以不建议使用该方法来暂停线程的执行。