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<String> 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<String> 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());
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容