Java多线程(四)线程同步

线程同步

    系统的线程调度有一定的随机性,当多个线程访问同一个数据时,不作处理的话,很容易出现线程安全问题,我们可以通过线程同步来解决这个问题。
     我们用银行取钱的场景的来模拟这种情况:

银行类,可以设置账户上的余额,提供一个取钱的方法,只要没有超过账户上的钱就扣除对应的钱数:

import com.sun.scenario.effect.impl.prism.PrImage;

/**
 * Created by liujiawei on 2018/7/7.
 */
public class Bank{
    private int balance; //账户余额



    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }


    public void getMoney(String name, int money){
        balance = balance - money;
        if(balance > 0){
            System.out.println(name + "取了" + money + ",还剩" + balance);
            setBalance(balance);
        }else{
            System.out.println(name + "想取" + money +",余额不足");
        }
    }

}

操作类,用来模拟多个人取钱的操作,sleep(500)模拟取钱的耗时:

package com.ljw.Thread.BankGetMoney;

/**
 * Created by liujiawei on 2018/7/8.
 */
public class GetMoney implements Runnable{

    Bank bank;

    int count;

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public Bank getBank() {
        return bank;
    }

    public void setBank(Bank bank) {
        this.bank = bank;
    }

    @Override
    public void run() {
        try {
            bank.getMoney(Thread.currentThread().getName(),count);
            Thread.currentThread().sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类:

package com.ljw.Thread.BankGetMoney;

/**
 * Created by liujiawei on 2018/7/7.
 */
public class TestBank {
    public static void main(String[] args) {
        Bank bank = new Bank();
        bank.setBalance(1000);
        GetMoney getMoney = new GetMoney();
        getMoney.setBank(bank);
        getMoney.setCount(600);

        GetMoney getMoney2 = new GetMoney();
        getMoney2.setBank(bank);
        getMoney2.setCount(600);

        Thread thread = new Thread(getMoney,"甲");
        Thread thread2 = new Thread(getMoney2,"乙");

        thread.start();
        thread2.start();



    }
}

    账户余额设置为了1000,两个线程每次都是取600,正常情况下应该是一个取完钱,另一个就会提示余额不足,我们看下运行结果是不是也是这样:


看起来好像没有问题,但是涉及到金钱都是需要比较谨慎的,我们再多运行几次:




   可以看到,其中有一次提示两次都取钱成功了,账户上只剩了-200元,这就是我们上面所说到的线程安全问题。

 同步代码块:
为了解决多个线程操作同一个数据可能会带来的线程安全问题,java中引入了同步监视器。
使用同步监视器的通用方法是同步代码块:
synchronized (obj){
    //对obj的操作
}
    对可能会被多个线程同时访问的资源加上synchronized关键字,当线程访问被synchronized修饰的资源时,它会获取到对应的同步监视器,此时其他线程访问该资源时,就会因为无法获取同步监视器而操作不了数据。
    到了这里,我们对上面取钱会出现的那种情况已经有了解决方法,只要对访问的资源加上synchronized就行了,因为synchronized用来修饰对象,所以我们稍微修改下getMoney() 方法:
public void getMoney(String name, int money) {
    synchronized (this) {
        this.balance = this.balance - money;
        if (this.balance > 0) {
            System.out.println(name + "取了" + money + ",还剩" + this.balance);
            setBalance(this.balance);
        } else {
            System.out.println(name + "想取" + money + ",余额不足");
        }
    }
}

    我们对当前银行这个对象做同步监视,通过this.balance的方式获取账户余额,重新运行下我们的测试类:




    从运行结果中可以看到,再也没有出现过余额为负的情况的出现。

    通过同步代码块,线程访问指定资源时,会按照加锁-操作数据-释放锁这样的顺序来进行。



同步方法:
除了同步代码块以外,java还提供了同步方法用来保证线程安全。使用方法就是用synchronized关键字来修饰方法(非static方法),同步方法不需要显示指定同步监视器,她的同步监视器是this对象。

使用同步方法来对银行取钱的代码进行改造,修改getMoney()方法:

public synchronized  void getMoney(String name, int money) {
        this.balance = this.balance - money;
        if (this.balance > 0) {
            System.out.println(name + "取了" + money + ",还剩" + this.balance);
            setBalance(this.balance);
        } else {
            System.out.println(name + "想取" + money + ",余额不足");
        }
}

    重新运行我们的测试类:




    可以看到,使用同步方法一样解决了线程安全的问题。



同步监视器的释放
线程在进入同步代码块或者同步方法之前,都需要获取对同步监视器的锁定,但是程序不会显示释放同步监视器的锁定,线程会在以下几种情况释放同步监视器的锁定:
(1) 同步方法/同步代码块内的方法正常执行完毕;
(2)同步方法/同步代码块内有break/return等可以结束方法;
(3)同步方法/同步代码块内有未捕获的Exception或Error;
(4)同步方法/同步代码块内调用了同步监视器的wait()方法;
但是以下情况不会释放同步监视器的锁定:
(1) 调用线程的sleep()和yield()方法不会释放同步监视器的锁定;
(2)其他线程调用该线程的suspend()方法也不会释放同步监视器的锁定;


同步锁
除了同步监视器,java还有一种机制可以保证线程安全,通过同步锁对象(Lock)显式的加锁/释放锁来实现。
Lock比synchronized更加灵活,可以支持属性差别很大的数据,还可以支持多个相关的condition对象(有关condition的介绍在后面)。

通常使用最多的锁是重入锁(ReentrantLock),我们看下如果用ReentrantLock对象来修改之前的代码是怎么做的:

ReentrantLock lock = new ReentrantLock();
    public void getMoney(String name, int money) {
        lock.lock();
        try {
            this.balance = this.balance - money;
            if (this.balance > 0) {
                System.out.println(name + "取了" + money + ",还剩" + this.balance);
                setBalance(this.balance);
            } else {
                System.out.println(name + "想取" + money + ",余额不足");
            }
        }
        finally {
            lock.unlock();
        }
}
    首先需要声明一个ReentrantLock对象,在需要实现线程安全的地方通过lock()方法加上锁,在结束的地方通过unlock() 方法释放锁,重入锁通常会和tryfinally代码块结合使用,保证同步锁的释放。

    重入锁支持多个锁同时存在,只要全部显式释放就可以,如下:

ReentrantLock lock = new ReentrantLock();
    public void getMoney(String name, int money) {
        lock.lock();
        try {
            lock.lock();
            this.balance = this.balance - money;
            if (this.balance > 0) {
                System.out.println(name + "取了" + money + ",还剩" + this.balance);
                setBalance(this.balance);
            } else {
                System.out.println(name + "想取" + money + ",余额不足");
            }
        }
        finally {
            lock.unlock();
            lock.unlock();
        }
}

死锁
当两个线程互相等待对方释放同步监视器的时候,这个时候就会发生死锁,没有报错,没有异常,但是所有的线程都进入了阻塞状态,我们编写代码时,要尽量编码死锁的情况。
Thread的suspend()将线程挂起,就很容易导致死锁,所以不建议使用suspend()。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值