读书笔记:java多线程之线程同步

阅读的书籍:《java疯狂讲义》

关键词:线程安全问题,同步代码块,同步方法,释放同步监视器的锁定,同步锁,死锁

线程安全问题:当使用多个线程来访问同一个数据时,会导致一些错误情况的发生

到底什么是线程安全问题呢,先看一个经典的案例:银行取钱的问题

 

模拟步骤:

1.匹配用户账户的正确性(这里就简化了)

2.用户输入取款金额

3.系统判断账户余额是否大于取款金额

4.返回取款成功或者失败

              

案例中一共就三个类,首先是Account:

/**
 * 银行账户
 */
public class Account {

    private String accountNo;//账户名
    private double balance;//账户余额

    public Account(String accountNo,double balance){
        this.accountNo = accountNo;
        this.balance = balance;
    }

    @Override
    public int hashCode() {
        return accountNo.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == this){
            return true;
        }

        if (obj != null && obj.getClass() == Account.class){
            Account target = (Account) obj;
            return target.getAccountNo().equals(accountNo);
        }

        return false;

    }

    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;
    }
}

 然后是DrawThread:

/**
 * 模拟取钱过程的线程
 */
public class DrawThread extends Thread {

    private Account mAccount;//账户信息
    private double mDrawAmount;//要取的金额

    public DrawThread(String name, Account account, double drawAmount) {
        super(name);
        mAccount = account;
        mDrawAmount = drawAmount;
    }

    @Override
    public void run() {

        if (mAccount.getBalance() >= mDrawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
            System.out.println(getName() + "取钱成功,取出金额:" + mDrawAmount);
            mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新账户余额
            System.out.println("账户余额:" + mAccount.getBalance());
        } else {
            System.out.println(getName() + "取钱失败,余额不足");
        }
    }
}

最后用DrawThread进行测试:

public class DrawTest {

    public static void main(String[] args){
        Account account = new Account("王尼玛",1000);
        new DrawThread("小偷甲",account,800).start();
        new DrawThread("小偷乙",account,800).start();
    }

}

如果只是按上面的代码来运行的话,运行结果是这样的:

可以看到,这里就存在着一个问题:一开始账户中有1000块钱,小偷甲取走800后剩下200,若小偷乙继续取的话余额就不够了,可是这里却没有返回 “取钱失败,余额不足”,而是输出了一个负数:-600。两个并发线程同时在修改Account对象,出现了问题。

这就是线程安全问题的一个体现。那么如何解决呢?——使用同步监视器

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

语法格式:

synchronized(obj){

         //需要同步的代码块

}

线程在开始执行同步代码块之前,必须先获得对同步监视器的锁定。

任何时刻只能由一个线程可以获得对同步监视器的锁定,当同步代码块执行完成之后,该线程会释放对该同步监视器的锁定

 

明白了这个原理之后,对DrawThread中的run()方法小改一下:

@Override
    public void run() {

        synchronized (mAccount) {//看这里
            if (mAccount.getBalance() >= mDrawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
                System.out.println(getName() + "取钱成功,取出金额:" + mDrawAmount);
                mAccount.setBalance(mAccount.getBalance() - mDrawAmount);//更新账户余额
                System.out.println("账户余额:" + mAccount.getBalance());
            } else {
                System.out.println(getName() + "取钱失败,余额不足");
            }
        }
    }

可以看到,此时取钱的这块逻辑就是同步代码块,mAccount就是同步监视器了,取钱就是对mAccount进行操作,而经过这么一包裹之后,对于同一个账户来说,同一时间就只能有一个人对它进行取钱的操作了。再看看此时输出的结果:

嗯,小偷乙这会儿就取钱失败了,问题终于得到了解决。

同步方法:使用synchronized修饰某个方法来达到多线程安全

同步方法的同步监视器是调用该方法的对象,通过使用同步方法可以很方便地实现线程安全的类

线程安全的类的特点:

  • 该类的对象可以被多个线程安全地访问
  • 每个线程调用该对象的任意方法之后都将得到正确的结果,并且该对象状态依然保持合理状态

用同步方法对案例代码也进行小改:

    //在Account类中加入这个方法
    public synchronized void draw(double drawAmount){

        if (getBalance() >= drawAmount) {//如果 余额 >= 要取的数额,则进入取钱流程
            System.out.println(Thread.currentThread().getName() + "取钱成功,取出金额:" + drawAmount);
            setBalance(getBalance() - drawAmount);//更新账户余额
            System.out.println("账户余额:" + getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
        }

    }

然后回到DrawThread中调用:

 @Override
    public void run() {
        mAccount.draw(mDrawAmount);
    }

此时输出结果也是正确的,就不再重复贴图了

释放同步监视器的锁定:线程会在如下几种情况释放对同步监视器的锁定

  • 当前线程的同步代码块,同步方法正常执行结束
  • 遇到break,return终止了该同步代码块,同步方法的执行
  • 出现未处理的Error或Exception,该方法异常结束
  • 程序执行了同步监视器对象的wait()方法

与释放对应的是,当线程执行同步代码块,同步方法时,如下几种情况线程是不会释放同步监视器的:

  • 程序调用Thread.sleep(),Thread.yield()
  • 其他程序调用了该线程的suspend()方法

同步锁:通过显示定义同步锁对象(Lock)来实现同步

Lock是控制多个线程对共享资源进行访问的工具,通常每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前先要获得Lock对象

之所以说通常,是因为某些锁可能允许对共享资源并发访问,如ReadWriteLock

在实现线程安全的控制中,比较常用的是ReentrantLock(可重入锁)

死锁:当两个线程互相等待对方释放同步监视器时就会发生死锁,导致所有线程处于阻塞状态,无法继续

 由于篇幅有限,关于死锁就不展开了,后面会专门写一篇关于死锁的笔记或文章

注意事项:

  1. synchronized关键字可以修饰方法和代码块,但不能修饰构造器和成员变量等
  2. Thread类的suspend()方法很容易导致死锁,故Java不推荐使用这个方法

 

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值