多线程-3

线程同步

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()方法也很容易导致死锁,所以不建议使用该方法来暂停线程的执行。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值