Java线程的学习_线程同步

多线程编程很容易突然出现“错误情况”,这是由系统的线程调度具有一定随机性造成的,不过即使程序偶然出现问题,那也是由于编程不当引起的。当使用多个线程 来访问数据时,很容易“偶然”出现线程安全问题。

一个线程安全问题——银行取钱问题

实现功能:
-系统判断账户余额是否大于取款金额,如果大于取款金额,则取款成功,否则取款失败。
-模拟两个人使用同一个账户并发取钱。
账户类代码:

public class Account {

    //封装账户编号,账户余额的两个成员变量
    private String accountNo;
    private double balance;
    //构造器
    public Account(String accountNo, double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //set,get方法
    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 class DrawThread extends Thread{

    //模拟账户用户
    private Account account;
    //当前取现线程所希望取的钱数
    private double drawAmout;
    public DrawThread(String name, Account account, double drawAmout){
        super(name);
        this.account = account;
        this.drawAmout = drawAmout;
    }
    //当多个线程修改同一个共享数据时,将涉及数据安全问题
    public void run(){
        //账户余额大于取钱数目
        if(account.getBalance() >= drawAmout){
            //吐出钞票
            System.out.println(getName() + "取钱成功,取钱:" + drawAmout);
            try{
                Thread.sleep(1);
            }catch (InterruptedException ex){
                ex.printStackTrace();
            }
            //修改余额
            account.setBalance(account.getBalance() - drawAmout);
            System.out.println("\t 余额为:" + account.getBalance());
        } else {
            System.out.println(getName() + "取钱失败,余额不足");
        }
    }
}

启动:

public class DrawTest {

    public static void main(String[] args) {
        //创建一个账户
        Account acct = new Account("123456", 1000);
        //模拟两个线程对同一个账户取钱
        new DrawThread("甲", acct, 800).start();
        new DrawThread("乙", acct, 800).start();

    }

}

代码结果:

甲取钱成功,取钱:800.0
乙取钱成功,取钱:800.0
     余额为:200.0
     余额为:-600.0

这里可以看出线程出现了错误,余额仅有1000却能取出1600,并且余额为负值。出现这种错误是因为run()方法的方法体不具有同步安全性——程序中有两个并发线程在修改Account对象。
为了解决这个问题,java的多线程支持引入了同步监视器来解决这个问题。

同步代码块

使用同步监视器的通用方法就是同步代码块。同步代码块的语法格式:

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

在上面语法中,括号内的obj就是同步监视器,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定。

需要注意的一点是,任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行完成后,该线程会释放对该同步监视器的锁定。

同步监视器最大目的:阻止两个线程对同一个共享资源进行并发访问,因此通常把可能被并发访问的共享资源充当同步监视器。在这里我选择account作为同步监视器,修改DrawThread的代码:

public class DrawThread extends Thread {

    //模拟用户账户
    private Account account;
    //模拟取钱线程所希望取钱数
    private double drawAmout;

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

运行结果:

甲取钱成功,取钱:800.0
     余额为:200.0
乙取钱失败,余额不足

同步方法

与同步代码块对应,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰的方法。对于synchronized修饰的实例方法(非static方法)而言,无须显式指定同步监视器,同步方法的监视器就是this,也就是调用该方法的对象。
对于同步方法,选择修改Account类,使其成为线程安全的类。

public class Account {

    //封装账户编号,账户余额的两个成员变量
    private String accountNo;
    private double balance;
    //构造器
    public Account(String accountNo, double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //AccountNo set,get方法
    public String getAccountNo() {
        return accountNo;
    }
    public void setAccountNo(String accountNo) {
        this.accountNo = 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() + "取钱失败,余额不足");
        }
    }
}

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

public void run(){
        //直接调用account对象的draw()方法来执行取钱操作
        //同步方法的同步监视器是this,this代表调用draw方法对象
        //线程进入draw()方法之前,必须先对account对象加锁
        account.draw(drawAmout);
    }
此外synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器、成员变量。

既然获得对同步监视器的锁定,那么什么时候会释放同步监视器的锁定呢?

–当前线程的同步代码块、同步方法执行结束,当前线程即释放同步监视器。
–当前线程在同步代码块、同步方法中遇到break、rerurn终止了该代码块、该方法的继续执行,当前线程即释放同步监视器。
–当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致了该代码块、该方法异常结束时,当前线程即释放同步监视器。
–当前线程执行同步代码块或同步方法时,程序执行了同步监视器对象的wait()方法,则当前线程暂停,并释放同步监视器。

以下是线程不会释放同步监视器的情况:

–线程执行同步代码块或同步方法时,程序调用了Thread.sleep()、Thread.yield()方法来暂停当前线程的执行,当前线程不会释放同步监视器。
–线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放同步监视器。


同步锁(Lock)

    从Java5开始,Java提供了一种功能更加强大的线程同步机制——通过显示定义同步锁对象来实现同步,在这种机制下,同步锁由Lock对象充当。
    Lock提供了比synchronized方法和synchronized代码块更加广泛的锁定操作,Lock允许实现更灵活的结构,可以具有差别很大的属性没并且支持多个相关的Condition对象。
    Lock是控制多个线程对共享资源进行访问的工具。通常锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
    在实现线程安全的控制中比较常用的是ReentrantLock(可重入锁)。使用该Lock对象可以显式地加锁、释放锁,通常使用ReentrantLock的代码如下:
class X{
    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    ...
    //定义需要保证线程安全的方法
    public void m(){
        //加锁
        lock.lock();
        try{
            //需要保证线程安全的代码
        } finally{
            lock.unlock();
        }
    }
}

对Account类使用同步锁:

public class Account {

    //定义锁对象
    private final ReentrantLock lock = new ReentrantLock();
    //封装账户编号,账户余额的两个成员变量
    private String accountNo;
    private double balance;
    //构造器
    public Account(String accountNo, double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }
    //AccountNo set,get方法
    public String getAccountNo() {
        return accountNo;
    }
    public void setAccountNo(String accountNo) {
        this.accountNo = 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();
        }
    }
}

ReentrantLock锁具有可重入性,也就是说,一个线程可以对已被加锁的ReentrantLock锁再次加锁,ReentrantLock对象会维持一个计数器来追踪lock()方法的嵌套调用,线程在每次调用lock()加锁后,必须显示调用unlock()来释放锁,所以一段被锁保护的代码可以调用另一个被相同锁保护的方法。


死锁

当两个线程相互等待对方释放同步监视器时就会发生死锁,Java虚拟机没有监测,也没有采取措施来处理死锁情况,所以多线程编程时应该采取措施避免死锁出现。一旦出现死锁,整个程序既不会发生任何异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。
死锁是很容易发生的,尤其在系统中出现多个同步监视器的情况下。
出现死锁的代码:

class A{
    public synchronized void foo( B b ){
        System.out.println("当前线程名:" + Thread.currentThread().getName() +
                "进入了A实例的foo()方法");
        try{
            Thread.sleep(200);
        }catch (InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName() + 
                "企图调用B实例的last()方法");
        b.last();
    }
    public synchronized void last(){
        System.out.println("进入了A类的last()方法内部");
    }
}
class B{
    public synchronized void bar( A a ){
        System.out.println("当前线程名:" + Thread.currentThread().getName() +
                "进入了B实例的bar()方法");
        try{
            Thread.sleep(200);
        }catch (InterruptedException ex){
            ex.printStackTrace();
        }
        System.out.println("当前线程名:" + Thread.currentThread().getName() + 
                "企图调用A实例的last()方法");
        a.last();
    }
    public synchronized void last(){
        System.out.println("进入了B类的last()方法内部");
    }
}
public class DeadLock implements Runnable{

    A a = new A();
    B b = new B();
    public void init(){
        Thread.currentThread().setName("主线程");
        //调用a对象的foo()方法
        a.foo(b);
        System.out.println("进入了主线程之后");
    }
    public void run(){
        Thread.currentThread().setName("副线程");
        //调用b对象的bar()方法
        b.bar(a);
        System.out.println("进入了副线程之后");
    }
    public static void main(String[] args){
        DeadLock dl = new  DeadLock();
        //以dl为target启动新线程
        new Thread(dl).start();
        //调用init()方法
        dl.init();
    }
}

代码结果:

当前线程名:副线程进入了B实例的bar()方法
当前线程名:主线程进入了A实例的foo()方法
当前线程名:主线程企图调用B实例的last()方法
当前线程名:副线程企图调用A实例的last()方法

上面代码出现了主线程保持着A对象德尔锁,等待对B对象加锁,而副线程保持着B对象的锁,等待对A对象加锁,两个线程互相等待对方先释放,所以出现了死锁。

发布了28 篇原创文章 · 获赞 3 · 访问量 7898
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章