JAVA多线程(二)

一、线程间通讯问题

当多个线程同时操作一个对象时,就有可能发生错误,下面我们就通过三个经典案例来具体说明多线程可能遇到的问题。

1.三个经典案例

1.1 案例一: 不安全的买票

// 不安全的买票
public class UnsafeBuyTicket {

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "小明").start();
        new Thread(buyTicket, "黄牛党").start();
    }

}

class BuyTicket implements Runnable {
    private Integer ticketNum = 10;
    private boolean flag = true;
    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    // 买票方法
    public void buy() throws InterruptedException {

        if(ticketNum <= 0) {
            flag = false;
            System.out.println(Thread.currentThread().getName() + "来买票,但是票卖完了!");
            return;
        }
        // 模拟延迟
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "买到了票,剩余" + --ticketNum + "张票");
    }
}

上述代码,我们创建了实现了一个Runnable接口类,然后使用该类的实例化对象启动了三个线程,程序运行结果如下:


不安全的买票.png

从上图中,我们不难发现出现了重复买票以及剩余票数为负的情形!

1.2 案例二:不安全的银行

public class UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(100, "账户基金");


        Thread you = new Drawing(account, 50, "you");
        Thread gf = new Drawing(account, 100, "gf");

        you.start();
        gf.start();

    }

}


class Account {
    private Integer money;    // 余额
    private String  cardNo;   // 卡号

    public Account(Integer money, String cardNo) {
        this.money = money;
        this.cardNo = cardNo;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    public String getCardNo() {
        return cardNo;
    }

    public void setCardNo(String cardNo) {
        this.cardNo = cardNo;
    }
}

class Drawing extends Thread {

   private Account account;

   private Integer drawingMoney;

   private Integer nowMoney  = 0;


   public Drawing(Account account, Integer drawingMoney, String name) {
       super(name);
       this.account = account;
       this.drawingMoney = drawingMoney;
   }


    public void drawMoney() throws InterruptedException {
       if(account.getMoney() < drawingMoney) {
           System.out.println(this.getName() + "取钱," + account.getCardNo() + "余额不足!");
           return;
       }

       // sleep可以放大问题的发生性
       Thread.sleep(1000);
       nowMoney = nowMoney + drawingMoney;
       account.setMoney(account.getMoney() - drawingMoney);

       // this.getName()等价于Thread.currentThread().getName()
       System.out.println(this.getName() + "取了" + drawingMoney + "万元, 手头剩余" + nowMoney  + "万元,账户剩余" + account.getMoney() + "万元!");

   }
    @Override
    public void run() {
        try {
            drawMoney();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

上述代码,我们启动两个线程,操作同一个账户对象,运行结果如下:


不安全的银行运行结果.png

问题:账户中一共只有100万元,结果两人一共取出150万元,账户中仍有50万元!

1.3 案例三: 不安全的数组

public class UnsafeList {

    public static void main(String[] args) {
        List list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()-> {
                  list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());
    }
}
不安全数组运行结果.png

问题:列表长度应该是10000,问什么会少了呢?

2.案例结果分析及解决办法

上述三个案例,都是多个线程操作同一对象。一个线程为执行完自己的逻,另一个线程就又拿到该对象去执行自己的逻辑,这样结果肯定会出现问题!那么我们该如何解决这个问题呢?这里就需要引入线程同步的知识了。

3.线程同步

概念:线程同步是一种等待机制,多个需要同时访问此对象的线程进入这个对象等待池并形成队列,等待前面一个线程使用完毕,下一个再使用!(简而言之,如果多个线程要操作同一对象,请一个个排好队)

实现方式:使用synchronized关键字

带来的问题

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

同步方法与同步块

  1. 同步方法:synchronized关键字添加在方法前面,这个默认锁住的是包含这个方法的对象
  2. 同步块:synchronized (obj) {} 这里表示锁住obj这个对象

下面我们来修改上案例:

案例1:不安全的买票

public class UnsafeBuyTicket {

    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket, "我").start();
        new Thread(buyTicket, "小明").start();
        new Thread(buyTicket, "黄牛党").start();
    }

}



class BuyTicket implements Runnable {
    private Integer ticketNum = 10;
    private boolean flag = true;


    @Override
    public void run() {
        while (flag) {
            try {
                buy();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    // 买票方法
    public synchronized void buy() throws InterruptedException {

        if(ticketNum <= 0) {
            flag = false;
            System.out.println(Thread.currentThread().getName() + "来买票,但是票卖完了!");
            return;
        }
        // 模拟延迟
        Thread.sleep(100);
        System.out.println(Thread.currentThread().getName() + "买到了票,剩余" + --ticketNum + "张票");
    }
}

我们在买票方法上添加了synchronized关键字,相当于是直接锁住了buyTicket这个对象,运行结果如下:


安全的买票.png

结果正常!

案例二:不安全的银行

public class UnsafeBank {

    public static void main(String[] args) {
        Account account = new Account(100, "账户基金");


        Thread you = new Drawing(account, 50, "you");
        Thread gf = new Drawing(account, 100, "gf");

        you.start();
        gf.start();

    }

}


class Account {
    private Integer money;    // 余额
    private String  cardNo;   // 卡号

    public Account(Integer money, String cardNo) {
        this.money = money;
        this.cardNo = cardNo;
    }

    public Integer getMoney() {
        return money;
    }

    public void setMoney(Integer money) {
        this.money = money;
    }

    public String getCardNo() {
        return cardNo;
    }

    public void setCardNo(String cardNo) {
        this.cardNo = cardNo;
    }
}



class Drawing extends Thread {

   private Account account;

   private Integer drawingMoney;

   private Integer nowMoney  = 0;


   public Drawing(Account account, Integer drawingMoney, String name) {
       super(name);
       this.account = account;
       this.drawingMoney = drawingMoney;
   }


    public void drawMoney() throws InterruptedException {
       synchronized (account) {
           if(account.getMoney() < drawingMoney) {
               System.out.println(this.getName() + "取钱," + account.getCardNo() + "余额不足!");
               return;
           }

           // sleep可以放大问题的发生性
           Thread.sleep(1000);
           nowMoney = nowMoney + drawingMoney;
           account.setMoney(account.getMoney() - drawingMoney);

           // this.getName()等价于Thread.currentThread().getName()
           System.out.println(this.getName() + "取了" + drawingMoney + "万元, 手头剩余" + nowMoney  + "万元,账户剩余" + account.getMoney() + "万元!");
       }
   }


    @Override
    public void run() {
        try {
            drawMoney();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我们在drawMoneny方法中添加了同步块,锁住了account对象,运行结果如下:

安全的银行.png

运行结果正常!
注意:这里不能直接在drawMoney()方法前直接添加synchronized关键字,在这里添加表示锁住的对象实Drawing,而我们需要锁住的对像不是Drawing而是account.

案例三:不安全的数组

public class UnsafeList {

    public static void main(String[] args) {
        List list = new ArrayList<>();

        for (int i = 0; i < 10000; i++) {
            new Thread(()-> {
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size());

    }
}

我们在run()方法中添加了synchronized关键字,运行结果如下:

安全的数组.png

结果正常!

4.死锁

概念:多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放资源的情形!某一个同步块拥有两个以上的对象的锁时,就会发生这种情况!(多个线程互相抱着对方需要的资源,然后形成僵持)

public class TestDeadLock {

    public static void main(String[] args) {
        Thread g1 = new MakeUp(0, "H");
        Thread g2 = new MakeUp(1, "B");
        g1.start();
        g2.start();
    }
}

class Mirror {}

class Lipstick {}


class MakeUp extends Thread {
    static Mirror mirror = new Mirror();
    static Lipstick lipstick = new Lipstick();

    Integer choice;

    public MakeUp(Integer choice, String name) {
        super(name);
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            makeUp();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    private void makeUp() throws InterruptedException{
        if(this.choice.equals(0)) {
            synchronized (mirror) {
                System.out.println(this.getName() + "得到了镜子!");
                Thread.sleep(1000);
                synchronized (lipstick) {
                    System.out.println("1s后" + this.getName() + "得到了口红!");
                }
            }
        } else {
            synchronized (lipstick) {
                System.out.println(this.getName() + "得到了口红!");
                Thread.sleep(2000);
                synchronized (mirror) {
                    System.out.println("2s后" + this.getName() + "得到了镜子!");
                }
            }
        }
    }
}

上述代码,H与B两个线程都各自占用了mirror与lipStick两个对象,互相等待对方释放自己的需要的对象,这就形成了死锁!

5.线程协作(生产者消费者模式)

5.1 管程法

  • 生产者生产产品,将产品放入缓存区
  • 消费者从缓存区拿到产品,消费
  • 需要生产者、消费者、缓存区以及产品四个对象
// 测试:生产者消费者模式  --> 利用缓冲区解决:管程法
// 生产者 消费者 消费对象 缓冲区
public class TestPC {

    public static void main(String[] args) {
        SyncContainer syncContainer = new SyncContainer();

        Productor productor = new Productor(syncContainer);
        Consumer consumer = new Consumer(syncContainer);

        new Thread(productor).start();
        new Thread(consumer).start();

    }
}

// 生产者
class Productor implements Runnable {

    private SyncContainer syncContainer;

    Productor(SyncContainer syncContainer) {
        this.syncContainer = syncContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            Chicken chicken = new Chicken(i);
            try {
                syncContainer.push(chicken);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}


// 消费者
class Consumer implements Runnable {

    private SyncContainer syncContainer;

    Consumer(SyncContainer syncContainer) {
        this.syncContainer = syncContainer;
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            try {
                syncContainer.pop();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 产品
class Chicken {
    public int i;

    Chicken(int i) {
        this.i = i;
    }
}

// 缓冲区
class SyncContainer {

    // 产品大小
    Chicken [] chickens = new Chicken[10];
    // 计数标志
    int count = 0;

    // 生产者生产方法
    public synchronized void push(Chicken chicken) throws InterruptedException {
        if(count == chickens.length) {
            // 停止生产
            this.wait();
        }

        chickens[count] = chicken;
        count ++;
        System.out.println("生产者生产了第" + chicken.i + "只鸡");
        // 唤醒消费者消费
        this.notifyAll();
    }


    // 消费者消费方法
    public synchronized Chicken pop() throws InterruptedException {
        if(count == 0) {
            this.wait();
        }
        // 因为生产着生产之后都会让count ++,所以消费者在使用时必须先执行count --
        count --;
        Chicken chicken = chickens[count];
        System.out.println("消费者消费了第" + chicken.i +  "只鸡!");
        // 通知生产者生产
        this.notifyAll();
        return chicken;
    }
}

5.2信号灯法

package com.xdw.gaoji;

// 测试:生产着消费者模式 信号灯法
public class TestPC2 {

    public static void main(String[] args) {
        TV tv = new TV();
        Player player = new Player(tv);
        Watcher watcher = new Watcher(tv);

        new Thread(player).start();
        new Thread(watcher).start();
    }
}


class Player implements Runnable {

    TV tv;

    public Player(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if(i%2 == 0) {
                tv.player("新闻联播!");
            } else {
                tv.player("电视剧!");
            }
        }
    }
}


class Watcher implements Runnable {

    TV tv;

    public Watcher(TV tv) {
        this.tv = tv;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            tv.watch();
        }
    }
}


class TV {
    String voice;
    boolean flag = true;  // flag为true 生产着生产   flag为false 消费者消费

    public synchronized void player(String voice) {
        if(!flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 通知消费者消费
        this.notifyAll();
        this.voice = voice;
        System.out.println("生产者生产了" + this.voice + "!");
        flag = !flag;
    }


    public synchronized void watch() {
        if(flag) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 通知生产着生产
        this.notifyAll();
        System.out.println("消费者消费了" + this.voice + "!");
        flag = !flag;
    }
}

上述两种方法,使用了线程的两个未介绍的方法: wait()noyify()/notifyAll()
wait(): 使线程进入等待状态,会释放锁!
notify(): 唤醒等待队列中的第一个类型
notifyAll(): 唤醒所有等待中的线程

二、线程池

思路: 提前创建好多个线程,放入线程池中,使用时直接取出,使用完毕再放回,避免了线程的频繁创建与销毁。
实现类:使用ExecutorServiceExecutors 两个类!

public class TestPool {
    public static void main(String[] args) {
        // 创建一个线程池服务
        // newFixedThreadPool参数名称为线程池大小
        ExecutorService service = Executors.newFixedThreadPool(4);

        // 运行线程
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        // 关闭服务
        service.shutdown();
    }
}

class MyThread implements Runnable {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

你可能感兴趣的:(JAVA多线程(二))