java 多线程—— 线程同步=队列+锁

Posted 玛丽莲茼蒿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 多线程—— 线程同步=队列+锁相关的知识,希望对你有一定的参考价值。

一、线程同步 = 队列 + 锁

同步就是多个线程同时访问一个资源。

那么如何实现? 队列+锁。

想要访问同一资源的线程排成一个队列,按照排队的顺序访问。访问的时候加上一个锁(参考卫生间排队+锁门),访问完释放锁。

二、 不安全案例

2.1 不安全的抢票系统

之前我们实现过这个例子。

package Unsafe;

public class RailwayTicketSystem

    public static void main(String[] args) 
        BuyTicket buyer = new BuyTicket();
        new Thread(buyer,"黑黑").start();
        new Thread(buyer,"白白").start();
        new Thread(buyer,"黄牛党").start();
    


class BuyTicket implements Runnable
    private int ticketNums = 10;  //系统里有10张票

    //抢票行为
    @Override
    public void run() 
        while(ticketNums>0)
            try 
                Thread.sleep(100);  //模拟延时,放大问题的发生性
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(Thread.currentThread().getName()+"--->抢到了第"+ticketNums+"张票");
            ticketNums--;
        
    

2.2 不安全的银行取钱 

场景: 黑土有一张存款为100万的卡,黑土去银行柜台取钱50万,同一时刻,黑土的老婆白云也要通过网上银行从卡里取走100万。

因为取钱是用户各自取各自账户里的钱,不存在多个线程操作同一个对象(所有用户都去抢系统里的票),所以可以用extends Thread。

package Unsafe;

public class UnsafeBank 
    public static void main(String[] args) 
        //黑黑的卡里一共有100万
        Account  黑土的卡 = new Account("黑土的卡",100);

        //黑黑要从卡里取走50万
        DrawMoney 黑土 = new DrawMoney(黑土的卡,50);
        黑土.start();
        //同时,白白也来到了银行,白白要从卡里取走100万
        DrawMoney 白云 = new DrawMoney(黑土的卡, 100);
        白云.start();
    



//银行卡
class Account
    private String name; //持卡人
    private int money ; //余额

    public Account(String name, int money) 
        this.name = name;
        this.money = money;
    

    public String getName() 
        return name;
    

    public void setName(String name) 
        this.name = name;
    

    public int getMoney() 
        return money;
    

    public void setMoney(int money) 
        this.money = money;
    


//银行:模拟取款
class DrawMoney extends Thread
    Account account; //账户
    int drawMoney; //要取多少钱
    public DrawMoney(Account account,int drawMoney)
        this.account = account;
        this.drawMoney = drawMoney;
    

    //取钱
    @Override
    public void run() 
        if(account.getMoney()-drawMoney<0)
            System.out.println("余额已不足,【"+Thread.currentThread().getName()+"】无法取钱");
            return;
        

        //延时,放大问题的发生
        try 
            Thread.sleep(100);
         catch (InterruptedException e) 
            e.printStackTrace();
        

        //余额变动
        account.setMoney(account.getMoney() - drawMoney);
        System.out.println(Thread.currentThread().getName()+"取走了"+drawMoney);
        //输出余额
        System.out.println(account.getMoney());
    

2.3 不安全的集合

这里以ArraryList为例,我们知道ArraryList的底层是用数组存储的。当多个线程同时执行add方法时,会出现多个线程向数组的同一个位置存放数据的情况。

        

三、同步机制

由于我们可以用private关键字来保证变量只能被方法访问,所以我们只需要针对类似于getXx()方法提出一套机制,这套机制就是synchronized关键字,synchronized就能实现队列+锁机制。它包括两种用法

所以说,synchronized 锁的既是对象(的资源/成员变量)(临界资源),也是一段代码(临界区)

1. synchronized 方法

        在方法前面加synchronized 关键字。

        同步方法所属类所创建的每个对象,都有一把锁。

2. synchronized 块

        如何使用?中括号括起来临界区,小括号内填上临界资源。

                ​​​​​​​        ​​​​​​​        

         一个方法中同时存在读取和增删改的代码,但是读取不属于同时操作资源。假如一个方法有1000行,里面只有5行代码是增删改,需要同步,剩下的995行不需要同步,那么使用synchronized声明整个方法会造成线程不必要的等待,浪费时间。所以出现了synchronized块。

        顾名思义就是把一个代码段声明为synchronized。

        可以指定要锁定的对象,如果不指定的话默认锁的是this。

3.1 解决不安全的抢票系统

我们给将run方法声明为synchronized,发现虽然结果不会出现负数的情况。

但是票都被同一个人抢去了。

我们来看一下这是为什么。给run方法上锁,意味着所有进入run方法的对象都要把run方法执行完才能释放这个锁给下一个排队的对象用。在我们的代码中,一旦某个对象进入了run方法就要一直抢票,直到 ticketNums<0,也就是意味着一张票也没有了,才会退出run方法。所以,除了第一个被执行的线程能抢到票且抢走了所有票,其他的线程一张票都抢不到。

可是这不是我们的目的呀!

错就错在,我们想要锁的操作是“抢一张票”,而我们上面锁的是“抢完所有票”。

所以应该把抢一张票的逻辑单独写成一个方法,然后加上synchronized关键字。

package Unsafe;

public class RailwayTicketSystem

    public static void main(String[] args) 
        BuyTicket system= new BuyTicket(); //镜像
        new Thread(system,"黑黑").start();  //容器1
        new Thread(system,"白白").start();  //容器2
        new Thread(system,"黄牛党").start(); //容器3
    


class BuyTicket implements Runnable
    private int ticketNums = 10;  //系统里有10张票
    private boolean flag = true;  //系统初始化是开放的

    //抢票行为
    @Override
    public void run() 
        while(flag==true)
            try 
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            buy();
        
    

    public synchronized void buy()
        if(ticketNums>0)
            System.out.println(Thread.currentThread().getName()+"--->抢到了第"+ticketNums+"张票");
            ticketNums--;
            if(ticketNums == 0) flag = false;
        
    

注意,睡眠代码的位置也值得思考:

3.2 解决不安全的银行取钱系统 

在需要同步的代码中,发生变动(增删改)的是account,而不是run方法所在的DrawMoney类。所以要指定锁的对象account,如果不指定的话默认锁的是所在类。此时我们不能给方法加synchronized了,因为方法无法指定被锁的对象。我们使用同步块:

3.3 解决不安全的集合

以上是关于java 多线程—— 线程同步=队列+锁的主要内容,如果未能解决你的问题,请参考以下文章

第七天学习多线程同步和锁

第七天学习多线程同步和锁

秋招之路8:JAVA锁体系和AQS抽象队列同步器

Java多线程的同步机制(synchronized)

Java多线程-synchronized同步方法及同步块简述

Python的多线程锁跟队列