java 多线程(四)—— 线程同步/互斥=队列+锁

同步、异步、互斥的区别

java 多线程(四)—— 线程同步/互斥=队列+锁_第1张图片

在很多文章中,直接把同步缩小为互斥,并称之为同步。下面也是这样的。

一、线程同步 = 队列 + 锁

同步(这里说的其实是互斥)就是多个线程同时访问一个资源。

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

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

二、 不安全案例

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第2张图片

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第3张图片

2.3 不安全的集合

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第4张图片        

三、同步机制

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

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

1. synchronized 方法

        在方法前面加synchronized 关键字。

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

2. synchronized 块

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

                        ​​​​​​​        java 多线程(四)—— 线程同步/互斥=队列+锁_第5张图片

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

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

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第6张图片

3.1 解决不安全的抢票系统

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第7张图片

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第8张图片

我们来看一下这是为什么。给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;
        }
    }
}

java 多线程(四)—— 线程同步/互斥=队列+锁_第9张图片

注意,睡眠代码的位置也值得思考:
java 多线程(四)—— 线程同步/互斥=队列+锁_第10张图片

java 多线程(四)—— 线程同步/互斥=队列+锁_第11张图片

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

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

java 多线程(四)—— 线程同步/互斥=队列+锁_第12张图片

3.3 解决不安全的集合

java 多线程(四)—— 线程同步/互斥=队列+锁_第13张图片

你可能感兴趣的:(JavaSE,java,多线程,线程同步)