今日内容
零、 复习昨日
一、作业
二、线程安全的集合
三、死锁
四、线程通信
五、生产者消费者
六、线程池
零、 复习昨日
见晨考
一、作业
售卖后车票
见代码
二、线程安全的类[了解]
StringBuffer是线程安全的,是因为每个方法都加上synchronized,即都是同步方法
StringBuilder没有加
ArrayList是线程不安全
Vector 是线程安全
HashMap 是线程不安全
Hashtable 是线程安全
比HashMap安全,比Hashtable快,即安全又快的集合ConcurrentHashMap[很重要]
三、死锁[了解]
死锁: 互相持有对方的锁还不释放
public class MyLock {
// 左筷子锁
static Object zuo = new Object();
// 右筷子锁
static Object you = new Object();
}
public class Girl extends Thread{
@Override
public void run() {
synchronized (MyLock.zuo) {
System.out.println("女朋友获得左筷子" );
synchronized (MyLock.you) {
System.out.println("女朋友获得右筷子-吃饭" );
}
}
}
}
public class Boy extends Thread{
@Override
public void run() {
synchronized (MyLock.you) {
System.out.println("男朋友获得右筷子" );
synchronized (MyLock.zuo) {
System.out.println("男朋友获得右左筷子-吃饭" );
}
}
}
}
public class TestDeadLock {
public static void main(String[] args) {
new Boy().start();
new Girl().start();
}
}
四、线程通信[熟悉]
4.1 介绍
线程通信,就是线程之间产生联系.
即通知,例如线程A执行到一定时候会
停下
,同时通知
另外的线程B执行,
线程B执行到一定时候,也停下,通知线程A执行
以上操作需要Object类的方法
- wait() 让当前线程等待
- notify() 唤醒一个处于等待状态的线程
- notifyAll() 唤醒所有处于等待状态的线程
4.2 两个个线程通信
需求: 昨天打印机方法,让print1()和print2()方法交替执行
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 通信
*/
public class Printer {
/**
* 打印机执行的标志
* 此标志如果是1,说明该打印机1执行,否则打印机1停下
* 此标志如果是2,说明该打印机2执行,否则打印机2停下
*/
private int flag = 1;
public synchronized void print1() throws InterruptedException {
if (flag != 1) { // 判断不是自己执行时
// 线程停下不执行
// 锁对象是谁,谁去wait方法
this.wait();
}
System.out.print("1 " );
System.out.print("2 " );
System.out.print("3 " );
System.out.print("4 " );
System.out.print("\r\n" );
// 改变标志
flag = 2;
// 通知其他处于等待状态的线程,起来干活
// 锁对象是谁,谁去notify方法
this.notify();
}
public synchronized void print2() throws InterruptedException {
if ( flag != 2) {
this.wait();
}
System.out.print("A " );
System.out.print("B " );
System.out.print("C " );
System.out.print("D " );
System.out.print("\r\n" );
flag = 1;
this.notify();
}
}
public class TestNotify {
public static void main(String[] args) {
Printer printer = new Printer( );
new Thread(){
@Override
public void run() {
while(true){
try {
printer.print1();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
}
}.start();
new Thread(){
@Override
public void run() {
while(true){
try {
printer.print2();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
}
}.start();
}
}
换用同步代码块实现
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 通信
*/
public class Printer2 {
Object obj = new Object();
private int flag = 1;
public void print1() throws InterruptedException {
synchronized (obj) {
if (flag != 1) { // 判断不是自己执行时
// 线程停下不执行
// 锁对象是谁,谁去wait方法
obj.wait( );
}
System.out.print("1 ");
System.out.print("2 ");
System.out.print("3 ");
System.out.print("4 ");
System.out.print("\r\n");
// 改变标志
flag = 2;
// 通知其他处于等待状态的线程,起来干活
// 锁对象是谁,谁去notify方法
obj.notify( );
}
}
public void print2() throws InterruptedException {
synchronized (obj) {
if (flag != 2) {
obj.wait( );
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.print("\r\n");
flag = 1;
obj.notify( );
}
}
}
总结
- 通信的代码(wait和notify等)需要放在同步方法或者同步代码块里面
- 通信的代码(wait和notify等)必须使用当前锁对象来调用
4.3 练习
创建A1 A2 两个线程,分别打印1-10,11-20,保证A1执行完再 执行A2线程,A2执行完再执行A1
A1 —> 1
A2 —>11
A1 —> 2
A2 —>12
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class TestA1A2 {
static int flag = 1;
public static void main(String[] args) {
Object o = new Object( );
new Thread( ) {
@Override
public void run() {
for (int i = 1; i < 11; i++) {
synchronized (o) {
if (flag != 1) {
try {
o.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.println("A1 -->" + i);
flag = 2;
o.notify( );
}
}
}
}.start( );
new Thread( ) {
@Override
public void run() {
for (int i = 11; i < 21; i++) {
synchronized (o) {
if (flag != 2) {
try {
o.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.println("A2 -->" + i);
flag = 1;
o.notify( );
}
}
}
}.start( );
}
}
4.4 三个线程的通信
如果是notify
第一次
线程3抢到,判断标志是1,不是自己执行,线程3等待.此时1,2有线程都处于就绪状态
假如线程2抢到,判断标志是1,不是自己执行,线程2等待,此时1线程处于就绪状态
只能是线程1执行,执行完后,改标记为2,随机唤醒一条线程,假如是线程2,此时线程1,2处于活跃状态
第二次
假如线程1抢到,线程1执行后发现不是自己执行,线程1等待,此时只有2线程活跃,所以2线程执行,改标记为3,随机唤醒一条线程,假如是线程3,此时线程2,3处于活跃状态
假如线程2抢到,不是自己执行,线程2等待,此时只有3线程能执行,那么3线程执行,改变标记为1,唤醒的是线程2,此时线程2,3就绪,就算线程2,3抢到也会陷入等待状态,此时3个线程全部等待,没有可执行线程…
解决方案: 全部唤醒 ,使用方法notifyAll(),唤醒所有处于等待状态的线程
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class Printer {
int flag = 1;
public synchronized void print1() throws Exception{
while (flag != 1) {
this.wait();// 哪里等待,被唤醒后继续执行,
// 将此处的if改成while,目的是让线程唤醒后继续回头再判断一次
}
System.out.print("1 ");
System.out.print("2 ");
System.out.print("3 ");
System.out.print("4 ");
System.out.print("\r\n");
flag = 2;
//this.notify();// 随机唤醒处于等待状态的一条线程
this.notifyAll();// 唤醒全部处于等待状态的线程
}
public synchronized void print2() throws Exception{
while (flag != 2) {
this.wait();
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.print("\r\n");
flag = 3;
this.notifyAll();
}
public synchronized void print3() throws Exception{
while (flag != 3) {
this.wait();
}
System.out.print("一 ");
System.out.print("二 ");
System.out.print("三 ");
System.out.print("四 ");
System.out.print("\r\n");
flag = 1;
this.notifyAll();
}
}
4.5 总结
特殊的:
- wait和notify方法需要在同步方法或者同步代码块内执行
- wait会让当前线程进入等待状态,让出资源,其他线程可以执行
- wait和notify()方法谁调用? 当前锁对象是谁,就是谁调用该方法
问 wait和sleep有什么区别?
答:
wait是Object类的方法,sleep是Thread类方法
wait和sleep都可以让当前线程进入阻塞状态
但是wait阻塞当前线程,会让出系统资源,其他线程可执行;但是sleep阻塞当前线程,会持有锁不释放,其他线程无法执行
wait需要在同步方法或同步代码快中使用,但是sleep可以在同步或非同步都可以使用
问 为什么wait和notify方法要设计在Object类中?
答: 因为锁可以是任意对象,又因为wait和notify需要被 锁对象调用,那么锁对象是任意的,wait和notify方法也能被任意对象调用,所以就设计在Object类中,因为Object类是所有类的父类
五、生产者消费者
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置⼀个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓 冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到⼀个空的缓冲区中取产品,也不允许生产者向⼀个满的缓冲区中放入产品。
商品类 Phone
商店类 Shop
商店类中要有生产线程,一个消费线程
package com.qf.pc;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 商品
*/
public class Phone {
private String desc;
public Phone(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "Phone{" +
"desc='" + desc + '\'' +
'}';
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
}
package com.qf.pc;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class Shop {
private Phone phone;
// 进货(生产)
public synchronized void product(Phone phone) {
if (this.phone != null) {// 如果商店还有手机,暂时等待消费先不生产
try {
//System.out.println("商店有手机,请来消费" );
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
// 如果商店没手机,就生产
this.phone = phone;
System.out.println("生产中...." );
// 通知消费
this.notify();
}
// 消费
public synchronized void consumer() {
if (this.phone == null) {// 商店没有手机,等待生产
try {
//System.out.println("商店没有手机,请赶紧生产...");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
// 消费完,手机就没啦
this.phone = null;
System.out.println("消费中..." );
// 通知生产
this.notify();
}
public static void main(String[] args) {
Shop shop = new Shop( );
new Thread(){ // 一个线程生产
@Override
public void run() {
for (int i = 1; i < 10; i++) {
shop.product(new Phone("XIAOMI"+i));
}
}
}.start();
new Thread(){ // 一个线程消费
@Override
public void run() {
for (;;) {
shop.consumer();
}
}
}.start();
}
}
六、线程池
6.1 线程池概念
- 如果有非常多的任务需要非常多的线程来完成,每个线程的工作时间不长,就需要创建很多线程,工作完又立即销毁[
线程频繁创建和销毁线程
]- 频繁创建和销毁线程非常消耗性能,那么线程池,就是可以创建一些线程,放在"池子"中,用的时候去池子取一个线程去使用,使用完再放回去,线程可以重用
- 线程池,底层其实就是集合队列,里面存储线程对象,用的时候去抽即可,就不要频繁创建线程了
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资 源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存(OOM Out Of Memory)或者“过度切换”的问题 --> 摘自阿里官方手册
6.2 线程池原理
将任务(task)提交(submit/execute)给线程池(threadpool),由线程池分配线程,运行任务,任务结束后,线程重新放入线程池供后续线程使用
6.3 创建线程池的方式
使用线程池创建线程,执行任务
JDK提供了关于线程池的一些类和接口
- Executor: 线程池的根接口,通过execute(Runnable task) 执行任务
- ExecutorService: Executor的子接口,通过submit(Runnable task) 来提交任务,执行任务,并且可以获得返回值
ThreadPoolExecutor
: ExecutorService的子实现类,通过submit(Runnable task) 来提交任务,执行任务Executors
: 执行器类(线程池工厂类),通过该类提供的静态方法来获得不同特点的线程池对象
6.4 不同特点的线程池
通过Executors调用以下静态方法获得不同特点的线程池对象
方法 类型 解释 newFixedThreadPool 固定大小线程池 池中包含固定数目的线程,空闲线程一直保留。只有核心线程,线程数量固定,任务队列为LinkedBlockingQueue newCachedThreadPool 动态大小的线程池,原则上无上限 无核心线程,非核心线程数量无限,执行完闲置60s后回收,任务队列SynchronousQueue newScheduledThreadPool 可以执行定时任务的线程池 用于调度执行的固定线程池,执行定时或周期性任务。和弦线程数量固定,非核心线程数量无线,执行完闲置10ms后回收,任务队列为DelayedWorkQueue newSingleThreadExecutor 单线程线程池 只有一个线程的池,会顺序执行提交的任务,只有一个核心线程,无非核心线程,任务队列为LinkdBlockingQueue newSingleThreadScheduledExecutor 单线程定时任务线程池 newWorkStealingPool 1.8提供新的方式创建线程池
以上线程池操作在阿里java开发手册中是不建议用的…
说明:Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 2)CachedThreadPool 和 ScheduledThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。 ----------------------- OOM 内存溢出,即系统资源耗尽
线程池执行任务时,可以采用两种方法:
execute(): 没有返回值,无法判断任务是否执行成功
submit():会返回Future对象,通过该对象判断任务是否执行成功
package com.qf.threadpool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class TestThreadPool {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor( );
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行任务" );
}
});
}
threadPool.shutdown();
}
private static void show3() {
ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);
// execute这样没有定时执行
// threadPool.execute(new Runnable( ) {
// @Override
// public void run() {
// System.out.println(Thread.currentThread().getName()+"执行任务" );
// }
// });
/**
* schedule(Runnable r,long delay,TimeUnit 单位)
*/
for (int i = 0; i < 3; i++) {
threadPool.schedule(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行任务" );
}
},3, TimeUnit.SECONDS);
}
threadPool.shutdown();
}
private static void show2() {
// 创建缓冲线程池
ExecutorService cachedThreadPool = Executors.newCachedThreadPool( );
for (int i = 0; i < 5000; i++) {
cachedThreadPool.execute(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行任务..." );
}
});
}
cachedThreadPool.shutdown();
}
/**
* 固定大小线程池
*/
private static void show1() {
// 使用工具类来创建不同类型的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 执行任务
// void execute()
// Future submit()
for (int i = 1; i < 41; i++) {
fixedThreadPool.execute(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"在执行..." );
}
});
}
// 线程池关闭
fixedThreadPool.shutdown();
}
}
6.5 ThreadPoolExecutor[重要]
- ThreadPoolExecutor
很重要,有7个参数
参数名 解释 备注 int corePoolSize 指定线程池的线程数量(核心线程数) 不能小于0 int maximumPoolSize 指定线程池可支持的最大线程数 最大数量>=核心线程数 long keepAliveTime 指定临时线程的最大存活时间 不能小于0 TimeUnit unit 指定存活时间的单位(秒,分,时,天) 时间单位 BlockingQueue workQueue 指定任务队列 ThreadFactory threadFactory 指定哪个线程工厂创建线程 RejectedExecutionHandler handler 指定线程忙,任务队列满的时候新任务来了怎么办?拒绝策略
这几个参数解释(某大型火锅店会例子)
- 核心线程数5, 即店里面的固定员工5个
- 最大线程数15,即突然顾客太多,5个人忙不过来,临时招聘10个人来干活
- 最大存活时间,即顾客不多的时候,这些临时工可以待多长时间
- 时间单位
- 任务队列10,即集合, 固定员工加上临时工还处理不了顾客,在店门口放几10张凳子
- 线程工厂, 如何创建出的线程? 即怎么找到的员工
- 拒绝策略. 当固定员工,临时工,以及门口的凳子都坐满了,不让吃去别的地方
问: 什么是创建临时线程?
答: 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建线程问:什么时候开始拒绝任务?
答:核心线程和临时线程都在忙,任务队列也满了,新的任务过来就会拒绝
public static void main(String[] args) {
ArrayBlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);
// new ThreadPoolExecutor(
// 5,// 核心线程数
// 15,// 最大线程数
// 1, // 存活时间
// TimeUnit.MINUTES,
// queue,// 任务队列
// Executors.defaultThreadFactory(),// 默认的工厂
// new ThreadPoolExecutor.AbortPolicy());// 拒绝策略
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
5,// 核心线程数
15,// 最大线程数
1, // 存活时间
TimeUnit.MINUTES,
queue// 任务队列
);
for (int i = 0; i < 45; i++) {
threadPoolExecutor.execute(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行..." );
}
});
}
System.out.println(queue );
System.out.println(queue.size() );
}