【JavaSE】多线程4

①.线程同步

并发:同一个对象多个线程同时操作
上万人同时抢票,两个银行同时取钱

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。这个时候我们就需要线程的同步。线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用。
  • 线程同步的形成条件:对象 + 锁,以保证线程的安全性

由于同意进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入 - 锁机制 - synchronized,当一个线程获得对象的排它锁,独占资源,其它线程必须等待,使用后释放锁即可。可能存在以下问题:

  • 一个线程持有锁会导致其它所有需要此锁的线程挂起;
  • 在多线程竞争下,加锁→释放锁 会导致比较多的上下文切换和调度延时,引起性能问题;
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

②.三大线程不安全案例

-抢票不安全的案例

多人(多线程)同时抢票,操作票数时会造成票数的错误

//不安全的买票
//线程不安全,可能会有复数,重复的抢票
public class UnsafeByTicket {

    public static void main(String[] args) {

        ByTicket station = new ByTicket();  //拿掉抢票类型的实列对象

        //创建多个线程来同时操作这个对象
        Thread user1 = new Thread(station,"小明");
        Thread user2 = new Thread(station,"张三");
        Thread user3 = new Thread(station,"王五");
        Thread user4 = new Thread(station,"小李");

        //开启现线程
        user1.start();
        user2.start();
        user3.start();
        user4.start();
    }
}

class ByTicket implements Runnable{

    //票数
    private int ticket = 20;
    boolean flag = true;   //外部停止方式

    @Override
    public void run() {
        //买票
        while(flag){
            buy();
        }
        System.out.println("票抢完了!");
    }

    //买票的方法
    private void buy(){
        //判断是否有票
        if(ticket <= 0) {
            flag = false;
            return;
        }
        //买票
        System.out.println(Thread.currentThread().getName() + " 抢到了第 " + ticket-- + " 张票");
        new MySleep().Asleep(1000);  //模拟网络延时
    }
}

-取钱不安全的案例

两个对象同时操作一个数据,可能直接导致数据为负,造成损失

//不安全的取钱
//两个人去银行取钱
public class UnsafeBank {

    public static void main(String[] args) {

        //创建账户
        Account account = new Account(100,"基金");

        //创建两个线程,来进行取钱操作
        Drawing you = new Drawing(account,50,"你自己");
        Drawing wife = new Drawing(account,100,"你妻子");

        you.start();
        wife.start();
    }
}

//账户
class Account {
    double money; //账户余额
    String name; //卡名

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

//银行:模拟取款
class Drawing extends Thread{
    Account account;  //需要有一个账户
    double drawingMoney; //取了多少钱
    double yourMoney;  //拿在手里的钱

    public Drawing(Account account,double drawingMoney,String name){
        super(name);  //调用父类的有参构造,传入线程的名字
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        //判断有没有钱
        if(account.money - drawingMoney < 0){
            System.out.println(Thread.currentThread().getName() + "账户余额不足!");
            return;
        }
        //sleep可以放大问题的发生性
        new MySleep().Asleep(1000);  //模拟延迟,让两个人都能见到100的余额

        account.money -= drawingMoney;  //计算卡内余额
        yourMoney += drawingMoney;  //计算现在你手里的现金

        System.out.println(account.name + "余额为:" + account.money);
        System.out.println(this.getName() + "手里的钱:" + yourMoney);

    }
}

列表不安全的案例

多线程操作列表中的元素,不安全

//数据不安全的集合
public class UnsafeList {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i <= 10000; i++) {
            new Thread(() -> {  //创建10000个线程来将线程的名字写入列表
               list.add(Thread.currentThread().getName());
            },i + " ").start();
        }
        //new MySleep().Asleep(2000);
        System.out.println("列表里一共有:" + list.size() + "条数据");  //输出的数据是不会达到10000的
        //不同的线程有可能在同一瞬间往列表里添加数据,这样就导致了最终链表数据的不足
    }
}

③.线程同步

同步方法:

  • 就像我们可以通过private关键字来保证数据对象只能被方法访问一样,我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,他包括两种用法:synchronized方法synchronized块
    同步方法:public synchronized void method( int args ){ }
  • synchronized 方法控制对”对象“的访问,每个对象对应一把锁,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行。
    优点:一定程度上保证了线程的安全
    缺点:若将一个大的方法声明为synchronized将会影响效率
    注意:方法里边需要修改的内容才需要锁,锁太多会浪费资源
  • 同步块synchronized(Obj) { }
    Obj称之为同步监视器
    Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身,或者是class[反射中的内容]
    同步监视器的执行过程
    第一个线程访问,锁定同步监视器,执行其中的代码。
    第二个线程访问,发现同步监视器被锁定,无法访问。
    第一个线程访问完毕,解锁同步监视器。
    第二个线程访问,发现同步监视器没有锁,然后锁定并访问。

-抢票案例的优化

使用同步之后,没有再出现票数为负的状况,各个线程依次访问对象

//不安全的买票
//线程不安全,可能会有复数,重复的抢票
public class UnsafeByTicket {
    public static void main(String[] args) {
        ···
    }
}

class ByTicket implements Runnable{
    //票数
    private int ticket = 20;
    boolean flag = true;   //外部停止方式
    @Override
    public void run() {
        //买票
        ····
    }

    //买票的方法  设置成同步方法,锁的是this(ByTicket类)
    private synchronized void buy(){
        //判断是否有票
        if(ticket <= 0) {
            flag = false;
            return;
        }
        //买票
        System.out.println(Thread.currentThread().getName() + " 抢到了第 " + ticket-- + " 张票");
        new MySleep().Asleep(100);  //模拟网络延时
    }
}

-银行取钱案例的优化

二者同时看到余额后必须要排队才能进行取钱操作,如果余额不足是没有办法取钱的,避免了重复的操作

  • 注意:synchronized方法默认锁的是this对象,当需要加锁的对象不在this方法里,就需要只用同步块来进行线程同步操作,括号中的同步监视器也就是需要同步的那一个对象。
  • 一般加锁的对象是需要 进行 增、删、改 的对象
//取钱 Runnable 实现
public class UnsafeBank2 {
    public static void main(String[] args) {

        //获取账户的实列对象
        Account2 account2 = new Account2(100,"小基金");

        //创建两个线程来执行取钱的操作
        Bank you = new Bank(account2,50);
        Bank wife = new Bank(account2,100);

        new Thread(you,"你自己").start();
        new Thread(wife,"你老婆").start();
    }
}

//银行账户类
class Account2{
    double money;
    String name;

    public Account2(double money, String name) {
        this.money = money;
        this.name = name;
    }
}

class Bank implements Runnable{
    Account2 account2; //需要实例的取钱账户的对象
    private double drawingMoney; //需要知道取了多少钱
    private double handMoney;  //可以返回你的手里有多少钱

    public Bank(Account2 account2,double drawingMoney){  //Bank类的构造器,获取对象时就传入参数
        this.account2 = account2;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {

        //synchronized默认锁的是this对象,当需要锁定其它对象的时候就需要使用同步块,括号中传入的是需要锁的对象名,即同步监视器
        //锁的对象一定是需要 增、删、改 的量
        synchronized (account2){
            if(account2.money - drawingMoney < 0){
                System.out.println(Thread.currentThread().getName() + "账户余额不足");
                return;
            }
            new MySleep().Asleep(1000);  //模拟网络延时

            account2.money -= drawingMoney;  //计算账户中的余额
            handMoney += drawingMoney;   //计算现在你手中的现金

            System.out.println(Thread.currentThread().getName() + "取走了:" + drawingMoney);
            System.out.println(account2.name + "余额:" + account2.money);
        }
    }
}

-列表的优化

//集合不安全的优化
public class UnsafeList {

    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {  //创建10000个线程来将线程的名字写入列表
                synchronized (list){  //用同步块将对象锁住
                    list.add(Thread.currentThread().getName());
                }
            },i + " ").start();
        }
        new MySleep().Asleep(2000); //这里不加sleep方法会导致主线程提前跑完
        System.out.println("列表里一共有:" + list.size() + "条数据");  //输出:10000
    }
}

-CopyOnWriteArrayList

JUC当中一个安全类型的集合

//测试JUC安全类型的集合
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        new MySleep().Asleep(1000);
        System.out.println("列表里一共有:" + list.size() + "条数据");
    }
}

④.死锁

多个线程各自占有一些共享资源,并且互相等待其它线程占有的资源才能运行,而导致两个或多个线程都在等待对方释放资源,都停止执行的情形。
在某一个同步块同时拥有“两个以上的对象的锁”时,就会发生死锁问题。

//死锁:多个线程互相持有对方需要的资源,形成僵持
public class DeadLock {
    public static void main(String[] args) {
        MakeUp girl1 = new MakeUp(0,"大姐");
        MakeUp girl2 = new MakeUp(1,"二姐");
        girl1.start();
        girl2.start();
    }
}
//口红
class Lipstick{}
//镜子
class Mirror{}

//化妆
class MakeUp extends Thread{
    //化妆需要的资源只有一份  用static修饰
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();

    int choice;  //选择
    String girlName;  //化妆的人名

    MakeUp(int choice,String girlName){
        this.choice = choice;
        this.girlName = girlName;
    }
    @Override
    public void run() {  //化妆
        makeUp();
    }
    //互相持有对方的锁,需要拿到对方的资源
    private void makeUp(){
        if(choice == 0){
            synchronized (lipstick){  //获得口红的锁
                System.out.println(this.girlName + "获得了口红的锁");
                new MySleep().Asleep(1000);
                synchronized (mirror){  //一秒种后,在拿着口红的状态下,想获得镜子
                    System.out.println(this.girlName + "获得了镜子的锁");
                }
            }
        }else{
            synchronized (mirror){  //获得镜子的锁
                System.out.println(this.girlName + "获得了镜子的锁");
                new MySleep().Asleep(2000);
                synchronized (lipstick){  //一秒种后,在拿着镜子的情况下,想获得口红
                    System.out.println(this.girlName + "获得了口红的锁");
                }
            }
        }
    }
}

产生死锁的四个必要条件条件:
互斥条件:一个资源每次只能被一个进程使用
请求互斥条件:一个进程因请求资源而阻塞时,对方已获得的资源保持不放
不剥夺条件:进程已经获得资源,在未使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

  • 只要想办法破除了其中一个条件就可以避免死锁的发生
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值