Java线程(二)

来源:互联网 发布:数据包与数据帧的区别 编辑:程序博客网 时间:2024/06/06 08:01

线程有它自己的生命周期,也就是五种状态:被创建,运行(start()),临时状态、阻塞(具备运行资格,但没有执行权),冻结(放弃了执行资格),消亡(stop()、run()方法执行完成)。这里主要就是了解和掌握几个函数的应用和区别,例如我之前的一篇博客http://blog.csdn.net/lxjstudyit/article/details/52443872,sleep()和yield()方法区别。本篇主要探讨线程同步问题。
其实线程安全问题,怎么引发的,当多条线程并发修改共享资源就容易引发线程安全问题。
一个比较经典的线程安全问题就是–银行取钱问题。
步骤:
1.用户输入账户、密码,系统判断用户的账户、密码是否匹配
2.用户判断取款金额
3.系统判断账户余额是否大于取款金额
4.如果余额大于取款金额,则取款成功;如果余额小于取款金额,则取款·失败


按上面的流程编写取款程序,并使用两个线程来模拟取钱操作。模拟两个人使用同一个账户并发现取钱问题。写三个java文件,下面先定义一个账户类,该账户封装了账户编号和余额两个实例变量

public class Account {    private String accountNo;//账户编号    private double balance;//账户余额    public String getAccountNo() {        return accountNo;    }    public void setAccountNo(String accountNo) {        this.accountNo = accountNo;    }    public double getBalance() {        return balance;    }    public void setBalance(double balance) {        this.balance = balance;    }    public Account(){}    public Account(String accountNo,double balance)    {        this.accountNo = accountNo;        this.balance = balance;    }    //下面两个方法根据accountNo来重写hashCode()和equals()方法    public int hashCode()    {        return accountNo.hashCode();    }    @Override    public boolean equals(Object obj) {        if(this == obj)            return true;        if(obj!=null && obj.getClass() == Account.class)        {            Account target = (Account) obj;        }        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()    {        //账户余额大于取钱数目        if(account.getBalance() >= drawAmount)        {            System.out.println(getName()+"取钱成功!吐出钞票:"+drawAmount);            //修改余额            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) {        // TODO Auto-generated method stub        //创建一个账户        Account acct = new Account("123",1000);        //模拟两个账户对同一个账户收钱        new DrawThread("甲",acct,800).start();        new DrawThread("乙",acct,800).start();    }}

多次运行上面程序,很有可能看到如图所示的结果
这里写图片描述
这正式多线程编程突然出现的偶然错误–因为线程调度的不确定性。
Java的多线程支持引入了同步监视器来解决这个问题,使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式如下;

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

synchronized后括号里的obj就是同步监视器,上面代码的含义是:线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。对象如同锁,持有锁的线程可以在同步中执行;没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。
选择监视器的目的:阻止两条线程对同一个共享资源进行并发访问。因此通常推荐使用可能被并发访问的共享资源充当同步监视器。对于上面的取钱模拟程序,我们应该考虑使用账户(account)作为同步监视器。

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);                //修改余额                account.setBalance(account.getBalance() - drawAmount);                System.out.println("\t余额为:"+account.getBalance());            }            else{                System.out.println(getName()+"取钱失败!余额不足!");            }        }    }   }

通过这种方式就可以保证并发线程在任一时刻只有一个线程可以进入修改共享资源的代码区(也称为临界区),所以同一时刻最多一个线程处于临界区,从而保证了线程的安全区。修改后,多次运行程序,总可以看到如下图所示结果
这里写图片描述
Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器是this,也就是调用方法的对象。上面的Account类就是一个可变类,它的accountNo和balance两个成员变量都可以被改变,当两个线程同时修改Account对象的balance成员变量的值时,程序就出现了异常。下面将Account类对balance类的访问设置成安全的,那么只要把修改balance的方法变成同步方法即可。对Account.java文件进行修改。

public class Account {    private String accountNo;//账户编号    private double balance;//账户余额    public String getAccountNo() {        return accountNo;    }    public void setAccountNo(String accountNo) {        this.accountNo = accountNo;    }    public double getBalance() {        return balance;    }    public Account(){}    public Account(String accountNo,double balance)    {        this.accountNo = accountNo;        this.balance = balance;    }    //提供一个线程安全的draw()方法完成取钱操作    public synchronized void draw(double drawAmount)    {        //账户余额大于取钱数目        if(balance >= drawAmount)        {            System.out.println(Thread.currentThread()+"取钱成功!吐出钞票:"+drawAmount);            //修改余额            balance -= drawAmount;            System.out.println("\t余额为:"+balance);        }        else{            System.out.println(Thread.currentThread()+"取钱失败!余额不足!");        }    }    //省略了hashCode()和equals(方法)

上面程序增加了一个代表取钱的draw方法,并使用了synchronized关键字修饰该方法,把该方法变成同步方法。该同步方法的同步监视器是this,因此对于同一个账户而言,任意时刻只能有一个线程获得对Account对象的锁定,然后进入draw()方法执行取钱操作–这样可以保证多个线程并发取钱的线程安全。
因为Account类中已经提供了draw()方法,而且取消了setbalance()方法,DrawThread线程类需要改写,该线程类的run()方法只要调用Account对象的draw()方法即可执行取钱操作。run()方法代码片段如下:

public void run()    {        // 直接调用account对象的draw方法来执行取钱        // 同步方法的同步监视器是this,this代表调用draw()方法的对象。        // 也就是说:线程进入draw()方法之前,必须先对account对象的加锁。        account.draw(drawAmount);    }

上面的drawThread类无须实现取钱操作,而是直接调用account的draw()方法来执行取钱操作。由于已经使用synchronized关键字修饰了draw()方法,同步方法的监视器是this,而this总代表调用该方法的对象–在上面实例中,调用draw()方法的对象是account,因此多个线程并发修改同一份account之前,必须先对account对象加锁,这也符合了”加锁”-“修改”-“释放锁”的逻辑。


最后做个小总结:
同步的前提:
1. 必须要有两个或者两个以上的线程
2. 必须是多个线程使用同一个锁
必须保证同步中只能有一个线程在运行。
好处:解决多线程的安全问题
弊端:多个线程都需要判断锁,较为消耗资源。

如何找程序是否有安全问题:
1. 明确哪些代码是多线程运行代码
2. 明确共享数据
3. 明确 多线程运行代码中哪些语句是操作共享数据的。

同步函数用的是哪个锁呢?
函数需要被对象调用,那么函数都有一个所属对象引用,就是this,所以同步函数用的锁是this。
拓展:
如果同步函数被static修饰后,使用的锁是什么?
用过验证,发现不再是this,因为静态方法中不可以定义this,静态进内存时,内存中没有本类 对象,但是一定有该类对应的字节码文件对象,该对象的类型是class,静态的同步方法,使用的锁是该方法所在类的字节码文件对象,类名.class

0 0
原创粉丝点击