不安全案例与线程同步(二十三)

三大线程不安全案例与线程同步(二十三)

不安全的案例

不安全的集合

在下面的例子中,我们创建了1000个线程往List中添加数据,最后输出这个List的长度。运行一下我们发现,List的长度很少能有1000,经常在九百多。这是因为多个线程同时操作了一个数据,有的List元素被更新了,所以长度达不到期望的1000,这是一个不安全的集合操作。

package com.syc;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}

不安全的抢票系统

这是一个简易的抢票程序,有三个人同时不停的去抢10张票。

package com.syc;

public class UnsafeTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket, "小明").start();
        new Thread(ticket, "小红").start();
        new Thread(ticket, "黄牛").start();
    }
}

class Ticket implements Runnable{
    //票
    private int tickets = 10;
    //标识位
    private boolean flag = true;

    //买票
    public void buyTicket() throws InterruptedException {
        if (tickets<=0) {
            flag = false;
            return;
        }
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
    }
    //重写run
    @Override
    public void run() {
        while (flag) {
            try {
                buyTicket();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

我们来看结果,在结果中出现了-1张票的情况,很显然在最后剩下一张票的时候,几个人同时去操作这个数据,大致了这样的不安全情况。

小明买到了第8张票
黄牛买到了第9张票
小红买到了第10张票
小明买到了第7张票
黄牛买到了第6张票
小红买到了第5张票
黄牛买到了第2张票
小明买到了第4张票
小红买到了第3张票
小红买到了第1张票
黄牛买到了第-1张票
小明买到了第0张票

不安全的银行取钱

和上面的例子相同,银行账户中有100元钱,两个人同时取钱,一个人取50另一个人取100,最终取出来150元,账户剩余-50元,这样的银行岂不是要倒闭。

package com.syc;

public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("投资基金", 100);
        GetMoney you = new GetMoney(account, 50, "You");
        GetMoney me = new GetMoney(account, 100, "Me");
        you.start();
        me.start();
    }
}

class Account {
    String name;
    int money;

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

class GetMoney extends Thread{
    private Account account; //账户
    private int moneyNum; //取钱数

    public GetMoney(Account account, int moneyNum, String name) {
        super(name);
        this.account = account;
        this.moneyNum = moneyNum;
    }

    @Override
    public void run() {
        if (moneyNum>account.money) {
            System.out.println("账户余额不足,取钱失败。");
            return;
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money-=moneyNum;
        System.out.println(Thread.currentThread().getName()+"取了"+moneyNum+"元");
        System.out.println(account.name+"剩余"+account.money);
    }
}
You取了50元
Me取了100元
投资基金剩余-50
投资基金剩余-50

线程同步

为了处理上面的问题,我们有个线程同步synchronized关键字来处理,线程同步实现了排队+锁的功能。就好比排队上厕所的场景,一个人进了厕所会锁上门,上完厕所打开门出来,第二个人再进去,依次处理。

synchronized默认锁的是this,也就是自身对象,如果锁的对象不是自身,可以用synchronized(对象){代码块}的方式来实现。

我们来看看,将上面的不安全例子使用synchronized“上锁”后的情况。

安全的集合

package com.syc;

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                synchronized (list) {
                    // 使用synchronized包裹更新数据的代码,上面的list就是要操作的对象
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        Thread.sleep(1000);
        System.out.println(list.size());
    }
}
1000

Process finished with exit code 0

安全的抢票

package com.syc;

public class UnsafeTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket, "小明").start();
        new Thread(ticket, "小红").start();
        new Thread(ticket, "黄牛").start();
    }
}

class Ticket implements Runnable{
    //票
    private int tickets = 10;
    //标识位
    private boolean flag = true;

    //买票 --> 用synchronized关键字修饰方法
    public synchronized void buyTicket() throws InterruptedException {
        if (tickets<0) {
            flag = false;
            return;
        }
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName()+"买到了第"+tickets--+"张票");
    }
    //重写run
    @Override
    public void run() {
        while (flag) {
            try {
                buyTicket();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
小明买到了第10张票
小明买到了第9张票
黄牛买到了第8张票
小红买到了第7张票
小红买到了第6张票
小红买到了第5张票
小红买到了第4张票
小红买到了第3张票
黄牛买到了第2张票
黄牛买到了第1张票
黄牛买到了第0张票

Process finished with exit code 0

安全的银行

package com.syc;

public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account("投资基金", 100);
        GetMoney you = new GetMoney(account, 50, "You");
        GetMoney me = new GetMoney(account, 100, "Me");
        you.start();
        me.start();
    }
}

class Account {
    String name;
    int money;

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

class GetMoney extends Thread{
    private Account account; //账户
    private int moneyNum; //取钱数

    public GetMoney(Account account, int moneyNum, String name) {
        super(name);
        this.account = account;
        this.moneyNum = moneyNum;
    }

    @Override
    public void run() {
        // 与集合一样,使用synchronized将代码包裹,这里操作的对象是account
        synchronized (account) {
            if (moneyNum>account.money) {
                System.out.println("账户余额不足,取钱失败。");
                return;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            account.money-=moneyNum;
            System.out.println(Thread.currentThread().getName()+"取了"+moneyNum+"元");
            System.out.println(account.name+"剩余"+account.money);
        }
    }
}
You取了50元
投资基金剩余50
账户余额不足,取钱失败。

Process finished with exit code 0

补充

CopyOnWriteArrayList

在第一个不安全集合的例子中,除了使用线程同步的方法,我们还可以使用CopyOnWriteArrayList来实现。CopyOnWriteArrayList是java.util.concurrent中提供的一个安全的集合类,看名字可以理解,他的操作方式是将原来的List复制一份,在复制的新List上进行操作,然后将原来的引用指向这个List。

package com.unsafe;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class SafeList {
    public static void main(String[] args) {
//        List<String> list = new ArrayList<String>();
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(100); // 这里加上等待,避免最后一个线程没跑完就计算长度
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}

看这里的例子,和上面的不安全集合的代码相同,只是将ArrayList改成了CopyOnWriteArrayList,并没有加上线程同步的方式,他的结果也是安全的,如下:

10000

Process finished with exit code 0

Lock

Lock也是java.util.concurrent中提供的一种让线程安全的方式,我们可以自行创建一把锁,在需要保证安全的时候给资源上锁,在用完之后再打开锁。这样的方式相较于synchronized更加的自由,我们还是拿集合的例子来看:

package com.unsafe;

import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock();
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                try {
                    lock.lock();
                    list.add(Thread.currentThread().getName());
                } finally {
                    lock.unlock();
                }
            }).start();
        }
        Thread.sleep(100);
        System.out.println(list.size());
    }
}

结果当然也是安全的,10000,符合我们的预期。

posted @ 2021-03-02 21:40  LucaZ  阅读(43)  评论(0编辑  收藏  举报