Madao No More

你的努力程度之低,根本轮不到拼天赋.


  • 首页

  • 人文书籍

  • 日记

  • 面试问题

  • Linux

  • 编程语言

  • 服务器应用

  • 各种工具

  • 工作中的问题

  • 归档

  • 关于

  • 搜索
close

8a_线程同步和生产者消费者模型

时间: 2020-11-13   |   分类: 编程语言     |   阅读: 2454 字 ~5分钟
  • 1. 经典案例
  • 2. 分析和解决
    • 2.1. 同步方法
      • 2.1.1. 验证例子
    • 2.2. 静态同步方法
  • 3. 线程安全的类
  • 4. Lock锁
  • 5. 生产者和消费者
    • 5.1. Object类的等待和唤醒方法
    • 5.2. 例子

1. 经典案例

100张票,3个窗口卖票.出票需要时间

出现的问题:

原因是线程的随机性.

  1. 3个线程同时卖同一张如刚启动时的第100张.比如:if(ticket>0){sleep,打印票号,并且--}

    线程依次休息,假设它们按顺序醒来,就会出现同时打印第100张,然后依次–,就直接跳到97了

  2. 可能出现负数的票

    先进入判断后依次醒来,线程1==>1, 线程2==>0,线程3==>-1

2. 分析和解决

安全问题出现的条件

  • 是多线程环境
  • 有共享数据
  • 有多条语句操作共享数据

如何解决多线程安全问题呢?

基本思想:让程序没有安全问题的环境

怎么实现呢?

  • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
  • Java提供了同步代码块的方式来解决
  synchronized(任意对象) {
  	多条语句操作共享数据的代码
  }

synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

public class SellTicket implements Runnable {
  private int tickets = 100;
  private Object obj = new Object();

  @Override
  public void run() {
    while (true) {
      synchronized (obj) {
        if (tickets > 0) {
          try {
            Thread.sleep(100);
            //t1休息100毫秒
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
          tickets--;
        }
      }
    }
  }
}
  • 好处:解决多线程安全问题
  • 坏处:每次都要判断,需要耗费资源.

2.1. 同步方法

就是把synchronized关键字加到方法上

修饰符 synchronized 返回值类型 方法名(方法参数) {
  方法体;
}

同步方法的锁对象是什么呢?

this,上面的代码就可以synchronized(this)

2.1.1. 验证例子

加一个判断,奇数票使用上面代码(修改为this),偶数票把上面代码封装成一个同步方法.

2.2. 静态同步方法

就是把synchronized关键字加到静态方法上

  修饰符 static synchronized 返回值类型 方法名(方法参数) {
  	方法体;
  }

同步静态方法的锁对象是什么呢?

类名.class

3. 线程安全的类

  • StringBuffer
    • 线程安全,可变的字符序列
    • 从版本JDK 5开始,被StringBuilder 替代. 通常应该使用StringBuilder类,因为它支持所有相同的操作,但它更快,因为它不执行同步
  • Vector
    • 从Java 2平台v1.2开始,该类改进了List接口,使其成为Java Collections Framework的成员. 与新的集合实现不同, Vector被同步. 如果不需要线程安全的实现,建议使用ArrayList代替Vector
  • Hashtable
    • 该类实现了一个哈希表,它将键映射到值. 任何非null对象都可以用作键或者值
    • 从Java 2平台v1.2开始,该类进行了改进,实现了Map接口,使其成为Java Collections Framework的成员. 与新的集合实现不同, Hashtable被同步. 如果不需要线程安全的实现,建议使用HashMap代替Hashtable
  • 可以通过Collections.synchronizedList()来把一个不安全的list转换成安全的.

4. Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

  • ReentrantLock构造方法

    ReentrantLock() : 创建一个ReentrantLock的实例

  • 加锁解锁方法
    方法名 说明
    void lock() 获得锁
    void unlock() 释放锁
public class SellTicket implements Runnable {
  private int tickets = 100;
  private Lock lock = new ReentrantLock();

  @Override
  public void run() {
    while (true) {
      try {
        lock.lock();
        if (tickets > 0) {
          try {
            Thread.sleep(100);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(Thread.currentThread().getName() + "正在出售第" + tickets + "张票");
          tickets--;
        }
      } finally {
        lock.unlock();
      }
    }
  }
}

//
  public class SellTicketDemo {
      public static void main(String[] args) {
          SellTicket st = new SellTicket();
  
          Thread t1 = new Thread(st, "窗口1");
          Thread t2 = new Thread(st, "窗口2");
          Thread t3 = new Thread(st, "窗口3");
  
          t1.start();
          t2.start();
          t3.start();
      }
  }

5. 生产者和消费者

生产者消费者模式是一个十分经典的多线程协作的模式

所谓生产者消费者问题,实际上主要是包含了两类线程:

  1. 一类是生产者线程用于生产数据
  2. 一类是消费者线程用于消费数据

为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库

生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为

生产者消费者

5.1. Object类的等待和唤醒方法

方法名 说明
void wait() 导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法
void notify() 唤醒正在等待对象监视器的单个线程
void notifyAll() 唤醒正在等待对象监视器的所有线程

5.2. 例子

public class Box {
  //定义一个成员变量,表示第x瓶奶
  private int milk;
  //定义一个成员变量,表示奶箱的状态
  private boolean state = false;

  //提供存储牛奶和获取牛奶的操作
  public synchronized void put(int milk) {
    //如果有牛奶,等待消费
    if (state) {
      try {
        wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    //如果没有牛奶,就生产牛奶
    this.milk = milk;
    System.out.println("送奶工将第" + this.milk + "瓶奶放入奶箱");

    //生产完毕之后,修改奶箱状态
    state = true;

    //唤醒其他等待的线程
    notifyAll();
  }

  public synchronized void get() {
    //如果没有牛奶,等待生产
    if (!state) {
      try {
        wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }

    //如果有牛奶,就消费牛奶
    System.out.println("用户拿到第" + this.milk + "瓶奶");

    //消费完毕之后,修改奶箱状态
    state = false;

    //唤醒其他等待的线程
    notifyAll();
  }
}

消费者:

public class Customer implements Runnable {
  private Box b;

  public Customer(Box b) {
    this.b = b;
  }

  @Override
  public void run() {
    while (true) {
      b.get();
    }
  }
}

生产者:

public class Producer implements Runnable {
  private Box b;

  public Producer(Box b) {
    this.b = b;
  }

  @Override
  public void run() {
    for(int i=1; i<=30; i++) {
      b.put(i);
    }
  }
}

运行:

public class BoxDemo {
  public static void main(String[] args) {
    //创建奶箱对象,这是共享数据区域
    Box b = new Box();

    //创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作
    Producer p = new Producer(b);
    //创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作
    Customer c = new Customer(b);

    //创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递
    Thread t1 = new Thread(p);
    Thread t2 = new Thread(c);

    //启动线程
    t1.start();
    t2.start();
  }
}
#编程语言 - Java书籍 - 黑马Java#
8_多线程
8b_网络编程
Madao

Madao

人的一切痛苦,本质上都是对自己无能的愤怒.

453 日志
10 分类
69 标签
GitHub E-mail
© 2009 - 2020 Madao No More
Powered by - Hugo v0.79.1
Theme by - NexT
0%