生产者消费者案例
自己实现等待和唤醒
由于CPU的随机性,多个线程在执行的时候是在相互抢夺CPU的执行权。为了让多个线程协同工作,可使用等待和唤醒机制。
消费者和生产者案例就是一个很经典的,利用等待和唤醒,让多个线程达到协同工作的效果。满足的是一种供需关系。
//桌子
public class Desk {
//默认生产10个汉堡包
private int count = 10;
//当前桌子的状态 true有 false:没有
private boolean flag;
//锁对象,final的作用是让锁对象时唯一的,不能更改
private final Object lock = new Object();
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public Object getLock() {
return lock;
}
}
- 生产者
//生产者(厨师)
public class Cooker implements Runnable {
public Desk desk; //桌子
//通过构造方法,给desk变量赋值
public Cooker(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while (true) {
//同步代码块
synchronized (desk.getLock()) {
if (desk.getCount() == 0) {
break;
} else {
//判断桌子上有没有汉堡包
if (desk.isFlag()) { //有
//等待
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else { //没有
System.out.println("生产一个汉堡包");
desk.setCount(desk.getCount() - 1);
//把状态改为true
desk.setFlag(true);
//把消费者线程唤醒
desk.getLock().notifyAll();
}
}
}
}
}
}
- 消费者
//消费者(吃货)
public class Fooder implements Runnable{
private Desk desk;
public Fooder(Desk desk) {
this.desk = desk;
}
@Override
public void run() {
while (true){
synchronized (desk.getLock()){
if(desk.getCount()==0){
break;
}else {
//判断有没有汉堡包
if(desk.isFlag()){ //有
System.out.println("吃货吃了一个包子");
desk.setFlag(false);
desk.getLock().notifyAll(); //把生产者唤醒
}else { //没有
try {
desk.getLock().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
}
- 测试类
public class Demo2 {
public static void main(String[] args) {
Desk desk=new Desk();
//创建两个线程对象
Cooker cooker = new Cooker(desk); //生产者任务
Fooder fooder = new Fooder(desk); //消费者任务
new Thread(cooker).start();
new Thread(fooder).start();
}
}
使用阻塞队列实现
Java的API给开发者提供了一种集合叫做阻塞队列,如图所示。它是以队列结构来进行存取元素的,类似于生活中的排队。当放满了就不能再放了,当取没了也就不能再取了。
生产者
//厨师
public class Cooker implements Runnable{
//阻塞队列
private ArrayBlockingQueue list;
public Cooker(ArrayBlockingQueue list) {
this.list = list;
}
@Override
public void run() {
while (true){
//不断的往阻塞队列中放
try {
list.put("汉堡包");
System.out.println("生产一个汉堡包");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 消费者
//吃货
public class Fooder implements Runnable {
//阻塞队列
private ArrayBlockingQueue<String> list;
public Fooder(ArrayBlockingQueue<String> list) {
this.list = list;
}
@Override
public void run() {
while (true) {
try {
String take = list.take();
System.out.println("取出一个" + take);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
- 测试类
public class Demo3 {
public static void main(String[] args) {
//创建阻塞队列
ArrayBlockingQueue<String> list = new ArrayBlockingQueue<>(3);
//创建生产者任务
Cooker cooker = new Cooker(list);
//创建消费者任务
Fooder fooder = new Fooder(list);
//创建两个线程
new Thread(cooker).start();
new Thread(fooder).start();
}
}
线程池
线程的状态
NEW
至今尚未启动的线程处于这种状态。
RUNNABLE
正在 Java 虚拟机中执行的线程处于这种状态。
BLOCKED
受阻塞并等待某个监视器锁的线程处于这种状态。
WAITING
无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。
TIMED_WAITING
等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。
TERMINATED
已退出的线程处于这种状态。
我们自己频繁的去创建和销毁线程比较消耗系统资源,同时也比较浪费时间。所以Java语言的API给我们提供了线程池的技术,帮我们解决这个问题。
当创建一个线程池,其实就是创建了一个能够存储线程的容器,需要执行线程任务是,就从线程池中拿一个线程出来用,用完之后再还给线程池。
创建默认的线程池
API提供了一个工具类叫Executors,可以用它的方法生成不同特点的线程池。
public static ExecutorService newCachedThreadPool()
创建一个可以根据需要创建新线程的线程池。最多可以创建int最大值的线程数量
public static ExecutorService newFixedThreadPool(int nThreads)
创建一个固定长度的线程池
public static ExecutorService newSingleThreadExecutor()
创建单个线程的线程池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
- 以newCachedThreadPool方法为例,演示向线程池提交任务
//创建默认线程池:
ExecutorService executorService = Executors.newCachedThreadPool();
//提交线程任务
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
});
//提交线程任务
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行了");
}
});
//关闭线程池
executorService.shutdown();
ThreadPoolExecutor创建线程池
线程池可以理解为一个火锅店,店内可以6张桌子是常开的,如果客人比较多时,可以临时再开3张桌子,客人不多时就把这临时的3张桌子收起来。当客人继续增加时常开的桌子和临时的桌子都用完了,就会启用排队机制,其他任务就在阻塞队列中排队。
常开的6张桌子 -- 核心线程数
临时的3张桌子 -- 临时线程数
店内最大桌子数 -- 最大线程数
排队通道 -- 阻塞队列
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
参数解释:
corePoolSize - 核心线程数。
maximumPoolSize - 最大线程数。
keepAliveTime - 临时线程存活的时间。
unit - keepAliveTime 时间单位。
workQueue - 阻塞队列。
threadFactory - 创建线程的工厂。
handler - 如果超过最大线程数,的拒绝方案。
- 创建线程池的演示
ThreadPoolExecutor poolExecutor=new ThreadPoolExecutor(
6, //核心线程数
9, //最大线程数
3, //空闲时间
TimeUnit.SECONDS, //时间单位:秒
new ArrayBlockingQueue<>(10),
Executors.defaultThreadFactory(), //默认的工厂对象,由工厂对象帮我们生产线程
new ThreadPoolExecutor.AbortPolicy() //拒绝的方式
);
- 拒绝策略
当提交的线程任务超过了最大线程数 + 阻塞队列长度,就会触发拒绝策略。
static class ThreadPoolExecutor.AbortPolicy
用于被拒绝任务的处理程序,它将抛出 RejectedExecutionException.
static class ThreadPoolExecutor.CallerRunsPolicy
用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
static class ThreadPoolExecutor.DiscardOldestPolicy
用于被拒绝任务的处理程序,它放弃最旧的未处理请求,然后重试 execute;如果执行程序已关闭,则会丢弃该任务。
static class ThreadPoolExecutor.DiscardPolicy
用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
线程安全细节
volatile 关键字
当多个线程在访问共享数据时,不是直接访问的主内存的数据,而是在线程本地内存创建了一个和主内存一样的变量副本,对副本进行赋值,然后在重新赋值给主内存。
可能会出现多个线程临时存储的变量副本的值不一样,使用volatile修饰这个变量,就可以解决问题,强制让线程每次使用变量时,都从主内存中获取最新的值。
原子性
原子性指意思是多个操作是不可分割的原子项,他们要么同时成功,要么同时失败。 例如:当多线程在执行自增操作时,就不是原子性的。
自增操作,count++为例,其实底层是要完成三个步骤(这三个步骤是不可分割的)
1.从主内存拷贝一个变量副本到线程本地内存
2.对变量副本进行操作
3.把变量副本的值写回主内存
但是由于CPU的随机性,可能一个线程没有完成这个三个步骤,执行权被其他线程抢走了,就破坏了原子性。
AtomicInteger类
直接使用int或者Integer对变量进行自增、修改、获取等操作,不能保证原子性,可能会出现线程安全的问题。
可使用synchronized来保证线程的安全性。这种方式解决问题的方式,是从悲观的角度出发,叫做悲观锁。
解决原子性的问题,Java提供了一系列的原子类。如AtomicInteger类就是其中的一个,它也表示整数,提供了一些方法可以对变量进行自增、获取、修改等操作,但是这些方法可以保证原子性,也能保证线程安全。
public int incrementAndGet()
对AtomicInteger包装的整数,先自增再获取。 //等价于 int num=10; int c = ++num;
public int getAndincrement()
对AtomicInteger包装的整数,先获取再自增。 //等价于 int num=10; int c= num++;
public int getAndAdd(int delta)
对AtomicInteger包装的整数,先获取再增加。 //等价于 int num=10;
// int temp=num; //先获取num的值
// num+=5; //再对num值增加5
public int addAndGet(int delta)
对AtomicInteger包装的整数,先增加再获取 //等价于 int num=10;
// num+=5; /对num增加5
// int temp=num; //赋值后的值
public int getAndSet(int newValue)
对AtomicInteger包装的整数,先获取再设置
AtomicInteger类保证原子性的原理如下图所示
常见的线程安全的类
ArrayList和Vector
ArrayList: 数组结构,线程不安全(效率高)
Vector: 数组结构,线程安全的(效率低)
HashMap和Hashtable
HashMap: 哈希表结构(数组+链表),线程不安全的(效率高)
Hashtable: 哈希表结构(数组+链表),线程安全的(同步代码块,效率低)
ConcurrentHashMap: 哈希表结构(数组+链表+红黑树),线程安全的(同步代码块+CAS算法,效率高)
StringBuilder和StringBuffer
StringBuilder: 线程不安全的(效率高)
StringBuffer: 线程安全的(效率低)
CountDownLatch类
使用场景:当需要某一个线程在其他线程执行完毕之后才执行。
//第一个孩子
public class MyChildThread1 extends Thread {
private CountDownLatch cdl;
//利用构造方法,来给cdl赋值
public MyChildThread1(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//1.孩子吃东西
System.out.println("小刚吃完饺子了");
//2.吃完说一声
cdl.countDown();
}
}
//第二个孩子
public class MyChildThread2 extends Thread {
private CountDownLatch cdl;
//利用构造方法,来给cdl赋值
public MyChildThread2(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//1.孩子吃东西
System.out.println("小黄吃完饺子了");
//2.吃完说一声
cdl.countDown();
}
}
//妈妈线程,等待前面两个孩子线程执行完毕之后再执行。
public class MyMother extends Thread{
private CountDownLatch cdl;
//利用构造方法,来给cdl赋值
public MyMother(CountDownLatch cdl) {
this.cdl = cdl;
}
@Override
public void run() {
//1.先等待
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//2.收拾碗筷
System.out.println("吃完饭洗完");
}
}
Semaphore类
SemaPhore类用来控制正在执行的线程数量,相当于一个管理员的角色,它可以给线程发许可证,得到许可证的线程才能执行,没有得到许可证的线程就必须等待。通过SemaPhore的构造方法,可以指定有多少个线程获得许可。
public class MyRunnable implements Runnable{
//创建对象,控制线程执行的数量为2
Semaphore sp=new Semaphore(2);
@Override
public void run(){
try {
//发同行许可
sp.acquire();
//线程执行的代码
System.out.println(Thread.currentThread().getName()+"执行了");
Thread.sleep(3000);
//释放同行许可
sp.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Demo6 {
public static void main(String[] args) {
MyRunnable mr=new MyRunnable();
for (int i = 0; i < 100; i++) {
new Thread(mr,"线程"+i).start();
}
}
}