Java多线程--synchronized同步方法与同步块学习(银行取钱问题分析)

1. 概念性知识

1. synchronized关键字

synchronized用于解决线程同步问题,当有多条线程同时访问共享数据时,如果不进行同步,就很可能会发生错误,java提供的解决方案是:只要将操作共享数据的代码在某一时间让一个线程执行完,在执行过程中,其他线程不能执行同步代码,这样就可以保护数据的正确性。

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
(1)修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
(2)修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
(3)修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
(4)修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

今天我们主要学习(1)和(2),即:synchronized同步方法与同步代码块的区别

2. 学习两者区别前,先看一个没有任何同步机制保证多线程安全性的例子:

(1)先建一个实体类:Account,账户类,只有一个属性:余额

class Account{

    private double balance;

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

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

    public double getBalance(){
        return this.balance;
    }

}

(2)再建一个银行类实现Runnable接口,可以对账户进行操作:取钱,减余额操作

class Bank implements Runnable{

    private Account account;

    private double money;

    public Bank(Account account, double money){
        this.account = account;
        this.money = money;
    }

    @Override
    public void run() {
        drawMoney();
    }

    public void drawMoney(){
        while(account.getBalance() >= money){//1
            if(account.getBalance() >= money){
                double balance = account.getBalance() - money;
                System.out.println(Thread.currentThread().getName()
                        + "余额:" + account.getBalance() + ",取钱:" + money + ",剩余:" + balance);
                account.setBalance(balance);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                System.out.println("余额不足");
            }
        }
    }

}

(3)测试类,启动两个线程对账户余额同时操作

public class TestDrawMoney {

    public static void main(String[] args) {
        Account account = new Account(2000);
        Bank bank = new Bank(account, 300);
        Thread thread1 = new Thread(bank, "A-->");
        Thread thread2 = new Thread(bank, "B-->");
        thread1.start();
        thread2.start();
    }

}

(4)运行结果:
情况1:
1236925-20171114221503140-1816225446.png

情况2:
1236925-20171114221539281-1173276665.png

(5)结果分析:
以情况1来说,当A、B线程同时执行到1时,都读取到余额800,A线程先执行-300操作,剩余500,此时,切换线程到B,刚刚B线程读取到的是800,-300,剩余500,这样,就出现了图中所示问题,实际取到了600块钱,余额却只减了300,很显然有问题

从上面分析来看,当两个线程同时对同一数据进行操作的时候,都可以执行减余额的代码,就导致了上述问题的出现,那么,如果当某一线程执行减余额的代码块时,如果能够保证这段代码不同时被其他线程执行呢,是不是就可以保证数据的正确性了?

是的,使用synchronized关键字修饰方法或者代码块就可以做到

3. 同步方法与同步代码块

(1)同步方法
synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如:
public synchronized void drawMoney()

synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。

(2)同步块
synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下:

synchronized(syncObject) {
  //允许访问控制的代码
}

synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。

4. 改进

(1)通过同步方法改进:
将上面drawMoney()方法加上关键字synchronized :

public synchronized void drawMoney2(){
        while(account.getBalance() >= money){
            if(account.getBalance() >= money){
                double balance = account.getBalance() - money;
                System.out.println(Thread.currentThread().getName()
                        + "余额:" + account.getBalance() + ",取钱:" + money + ",剩余:" + balance);
                account.setBalance(balance);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

运行结果:只有以下两种情况
情况1:
1236925-20171114224422640-1868544031.png

情况2:
1236925-20171115101231234-1193798201.png

说明:在上面(1)同步方法中说道同步方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,所以,在线程就绪状态时,A、B两个线程都有机会获得CPU时间片执行,一旦其中一个线程先执行,那么就独占该锁,直到public synchronized void drawMoney2()方法执行完毕,另一线程才有机会执行,当drawMoney2执行完时,只剩200块钱了,小于300,B线程执行不了方法内部的代码,也就没有任何输出,但实际上,B线程也是执行了drawMoney2方法的

从这个运行结果也可以证明synchronized 同步方法将会锁住整个方法的整段代码,运行期间其他线程无法拿到对象锁,会被阻塞,这样效率不是很高

(2)通过同步代码块改进:

public void drawMoney3(){
        while(account.getBalance() >= money){
            synchronized (this){
                if(account.getBalance() >= money){
                    double balance = account.getBalance() - money;
                    System.out.println(Thread.currentThread().getName()
                            + "余额:" + account.getBalance() + ",取钱:" + money + ",剩余:" + balance);
                    account.setBalance(balance);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else {
                    System.out.println("余额不足");
                }
            }
        }
    }

运行结果:
情况1:
1236925-20171114224713031-421022411.png

情况2:
1236925-20171115102345890-494991849.png

说明:可以看到,A、B线程并发操作,并且没有出现账户余额差错的情况,这是由于,同步块被放在while循环判断内部,而同步块锁住的范围比同步方法小,只会锁住synchronized(syncObject) {}大括号中的代码,专业点就是加锁粒度比同步方法小,当一次循环结束后,另外一个线程有机会竞争资源执行synchronized代码块中代码,对比(1)中同步方法写法,while循环取钱操作被放在synchronized同步方法中,一旦一个线程获得对象锁,执行同步方法,就会执行完while循环中的代码,所以会一直取钱,直到条件不满足,退出循环,释放锁

5. 踩坑

(1)由于对“同步方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁”这句话没有理解,所以当时在执行drawMoney2方法时出现的运行结果一直搞不懂,不是多线程吗,为什么总是只有一个线程在执行,按照我的理解,A执行while循环操作时,B获取CPU资源也执行while循环,不就应该是轮流切换取钱吗,然而,这种想法是错的,当A执行drawMoney2方法时,就已经获取到对象锁了,此时如果synchronized同步方法没有执行完毕,是不会释放锁的,所以B线程此时根本拿不到对象锁,也就无法执行取钱操作,直到A同步方法执行完,释放锁,才有机会执行

(2)先看图:
1236925-20171115104729874-1980258447.png

代码1运行结果:
1236925-20171115104808109-1150424731.png

代码2运行结果:
1236925-20171115104841468-1579318593.png

看见有什么不同没,代码1跟drawMoney2运行结果一模一样,其实归根结底还是上面(1)没理解清楚,一样的道理,不多说

(3)对于drawMoney3方法,其实我刚开始写的是这样的:

public void drawMoney3(){
        while(account.getBalance() >= money){//1
            synchronized (this){
                double balance = account.getBalance() - money;
                System.out.println(Thread.currentThread().getName()
                        + "余额:" + account.getBalance() + ",取钱:" + money + ",剩余:" + balance);
                account.setBalance(balance);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

它的运行结果是这样的:
1236925-20171115105336609-1210508401.png

看见和上面drawMoney3有什么不同没,对,if条件判断,刚开始我认为,明明while都已经判断了balance和money了,为什么还要if再判断一遍,不是多此一举吗,balance<money根本就不会减钱嘛,在单线程情况下,是这样的,只需要判断一次,但是,在多线程环境中就不一样了,假如此时余额500,当A线程执行到1处代码时,500>300,此时切换线程到B,500>300,条件也成立,然后又切换到A,执行-300操作,剩余200,再次切换到B,由于刚刚B线程条件判断成立,所以,也会进行-300操作,所以,200-300=-100,就出现了上面不应该出现的错误

找到问题后,就知道办法了,在synchronized (this){}代码块中再判断一下,这样的话,即使A、B线程while判断都成立,但当A线程执行完毕后,B线程执行-300之前还要判断一下钱是否足够,不足够就无法减钱,这样就保证了数据的一致性,问题解决

以上3个问题是我在学习synchronized 关键字踩到的坑,希望对有类似问题的同学能够有所帮助,共同学习

转载于:https://www.cnblogs.com/wang-zai/p/7828680.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值