今日内容
上课同步视频:CuteN饕餮的个人空间_哔哩哔哩_bilibili
同步笔记沐沐霸的博客_CSDN博客-Java2301
零、 复习昨日
一、作业
二、线程安全的集合
三、死锁
四、线程通信
五、生产者消费者
六、线程池
零、 复习昨日
创建线程的几种方式 1) 继承 2) 实现Runnable 3) callable接口 Future接口 4) 线程池
启动线程的方法 start()
线程的几种状态
什么是线程不安全
setName getName
Thread.currentThread()
join
sleep
synchronized
一、作业
售卖后车票
package com.qf.homework;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class Window implements Runnable {
// 票(加static,被该类所有对象共享)
private static int ticket = 100;
// 售票任务
@Override
public void run() {
while (true) {
synchronized (Window.class) {
if (ticket > 0) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace( );
}
System.out.println(Thread.currentThread( ).getName( ) + "有余票,正在售出" + ticket);
ticket--;
} else {
System.out.println("票已售完");
return;
}
}
}
}
}
public static void main(String[] args) {
new Thread( new Window() ,"窗口1").start();
new Thread( new Window() ,"窗口2").start();
new Thread( new Window() ,"窗口3").start();
}
二、线程安全的集合[了解]
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 Boy extends Thread{
@Override
public void run() {
synchronized (MyLock.zuo){
System.out.println("男朋友-拿到左筷子" );
synchronized (MyLock.you) {
System.out.println("男朋友-拿到右筷子,开吃" );
}
}
}
}
public class Girl 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();
}
}
男生先拿到zuo锁,再去获得you锁即可吃饭
但是you锁在女生那里,女生需要获得zuo锁才能吃饭
即 男生需要的you锁被女生拿着,女生需要的zuo锁被男生拿着
互相持有对方的锁,还不释放,就会出现"死锁" 程序卡死,不往下执行,持续阻塞
四、线程通信[熟悉]
4.1 介绍
线程通信,就是线程之间产生联系.
即通知,例如线程A执行到一定时候会
停下
,同时通知另外的线程B执行,
线程B执行到一定时候,也停下,通知线程A执行
以上操作需要Object类的方法
- wait() 让当前线程等待
- notify() 唤醒一个处于等待状态的线程
- notifyAll() 唤醒所有处于等待状态的线程
4.2 两个个线程通信
需求: 昨天打印机方法,让print1()和print2()方法交替执行
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 打印机类
*/
public class Printer {
// 具体哪台打印机执行的标志
private int flag = 1;
// 现在使用同步方法,print1和print2方法由同一个对象打印机对象调用
// print1方法和print2方法锁是同一个,是this,即打印机对象
public synchronized void print1() {
if (flag != 1) {
try {
// 锁是谁,就用谁调用wait
// 当前线程就陷入等待,会让出资源释放锁
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
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();
}
public synchronized void print2() {
if (flag != 2) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
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){
printer.print1();
}
}
}.start();
new Thread(){
@Override
public void run() {
while (true){
printer.print2();
}
}
}.start();
}
}
换用同步代码块实现
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 打印机类
*/
public class Printer {
// 锁对象
private Object obj = new Object();
// 具体哪台打印机执行的标志
private int flag = 1;
// 现在使用同步方法,print1和print2方法由同一个对象打印机对象调用
// print1方法和print2方法锁是同一个,是this,即打印机对象
public void print1() {
// 同步代码块,现在锁是字节码文件
synchronized(Printer.class) {
if (flag != 1) {
try {
// 锁是谁,就用谁调用wait
// 当前线程就陷入等待,会让出资源释放锁
// 用字节码锁来调用wait方法
Printer.class.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.print("1 ");
System.out.print("2 ");
System.out.print("3 ");
System.out.print("4 ");
System.out.print("\r\n");
// 干完活,修改标志
flag = 2;
// 通知另外一个处于等待状态的线程
// 只能唤醒在此对象监视器(加过锁的)上等待的单个线程.
// 如果没有加锁,直接调用该方法唤醒线程,会报错IllegalMonitorStateException
// 锁是谁,用谁调用方法
Printer.class.notify( );
}
}
public void print2() {
synchronized( Printer.class) {
if (flag != 2) {
try {
Printer.class.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.print("\r\n");
flag = 1;
Printer.class.notify( );
}
}
}
4.3 练习
创建A1 A2 两个线程,分别打印1-10,11-20,保证A1执行完再 执行A2线程,A2执行完再执行A1
A1 —> 1
A2 —>11
A1 —> 2
A1 —>12
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc
*/
public class A1 extends Thread{
@Override
public void run() {
synchronized (TestA1A2.class) {
for (int i = 1; i < 11; i++) {
if (TestA1A2.flag != 1) {
try {
TestA1A2.class.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.println("A1 --> " + i);
TestA1A2.flag = 2;
TestA1A2.class.notify();
}
}
}
}
class A2 extends Thread{
@Override
public void run() {
synchronized (TestA1A2.class) {
for (int i = 11; i < 21; i++) {
if (TestA1A2.flag != 2) {
try {
TestA1A2.class.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.println("A2 --> " + i);
TestA1A2.flag = 1;
TestA1A2.class.notify();
}
}
}
}
class TestA1A2{
static int flag = 1;
public static void main(String[] args) {
new A1().start();
new A2().start();
}
}
4.4 三个线程的通信
第一次
线程3抢到,判断标志是1,不是自己执行,改动标志为2,随机唤醒一条线程,但是此时所有线程都处于就绪状态,唤醒不唤醒无所谓…
第二次
假如线程2抢到,判断标志是2,自己执行,改动标志为3,随机唤醒一条线程,但是此时所有线程都处于就绪状态,唤醒不唤醒无所谓…
第三次
假如线程3抢到,判断标志是3,自己执行,改动标志为1,随机唤醒一条线程,但是此时所有线程都处于就绪状态,唤醒不唤醒无所谓…
第四次
假如这次又是线程3抢到,判断标志是1,不是自己执行,线程3陷入等待状态,让出资源
如果此时线程2抢到,判断标志是1,不是自己执行,线程2陷入等待状态,让出资源
此时只能线程1执行,判断标志是1,自己执行,改动标志为2,随机唤醒一条线程,如果唤醒是线程3,线程3从等待状态起来后通过while继续判断标志,发现不是自己,继续等待.此时只有线程1活跃,那么线程1执行,判断标志发现不是自己,陷入等待
解决方案: 全部唤醒 ,使用方法notifyAll(),唤醒所有处于等待状态的线程
package com.qf.notify;
/**
* --- 天道酬勤 ---
*
* @author QiuShiju
* @desc 打印机类
*/
public class Printer2 {
private int flag = 1;
public void print1() {
synchronized(Printer2.class) {
// 改成while,让线程被唤醒后,继续判断
// 如果是自己就执行,不是自己的标志就继续休眠
while (flag != 1) {
try {
// 哪里等待,哪里起来继续执行
Printer2.class.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.print("1 ");
System.out.print("2 ");
System.out.print("3 ");
System.out.print("4 ");
System.out.print("\r\n");
flag = 2;
// 唤醒的是一条处于等待状态的线程(随机唤醒一条)
// Printer2.class.notify( );
// 唤醒所有等待的线程
Printer2.class.notifyAll( );
}
}
public void print2() {
synchronized( Printer2.class) {
while (flag != 2) {
try {
Printer2.class.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.print("A ");
System.out.print("B ");
System.out.print("C ");
System.out.print("D ");
System.out.print("\r\n");
flag = 3;
Printer2.class.notifyAll( );
}
}
public void print3() {
synchronized( Printer2.class) {
while (flag != 3) {
try {
Printer2.class.wait( );
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
System.out.print("一 ");
System.out.print("二 ");
System.out.print("三 ");
System.out.print("四 ");
System.out.print("\r\n");
flag = 1;
Printer2.class.notifyAll( );
}
}
}
第一次
线程3抢到,判断标志是1,不是自己执行,线程3陷入等待状态,让出资源
第二次
线程2抢到,判断标志是1,不是自己执行,线程2陷入等待状态,让出资源
第三次
线程1抢到,判断标志是1,自己执行,改动标志为2,随机唤醒一条线程
假如唤醒的是线程2,判断标志是2,自己执行,改动标志为3,唤醒线程3
但是假设线程2抢到,判断标志不是自己,线程2等待
假设线程1抢到,判断标志为3,不是自己,线程1也等待
线程3抢到资源开始执行,改动标志为1
随机唤醒一条线程,假如唤醒的线程2,现在判断标志是1,不是自己执行
线程2继续等待,现在活着的是线程3,抢到资源,判断标志不是自己,继续等待
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 {
String name;
public Phone(){}
public Phone(String name){
this.name = name;
}
}
class Shop {
Phone phone;
// 进货(生产)
public synchronized void putPhone(Phone phone) {
if (this.phone != null) {
// 有货,先等待,让别人消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
// 进货
this.phone = phone;
System.out.println("进货,"+phone.name );
// 通知消费
this.notify();
}
// 售货(消费)
public synchronized void sellPhone() {
if (this.phone == null) {
// 没有商品,不能消费,先等待生产
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace( );
}
}
// 消费
System.out.println("消费,"+phone.name );
this.phone = null;
// 通知生产
this.notify();
}
}
class TestPC {
public static void main(String[] args) {
Shop shop = new Shop( );
// 生产者线程
new Thread(){
@Override
public void run() {
for (int i = 1; i < 6; i++) {
shop.putPhone(new Phone("IPhone"+i));
}
}
}.start();
// 消费者线程
new Thread(){
@Override
public void run() {
for (int i = 1; i < 11; i++) {
shop.sellPhone();
}
}
}.start();
}
}
六、线程池
6.1 线程池概念
- 如果有非常多的任务需要非常多的线程来完成,每个线程的工作时间不长,就需要创建很多线程,工作完又立即销毁[
线程频繁创建和销毁线程
]- 频繁创建和销毁线程非常消费性能,那么线程池,就是可以创建一些线程,放在"池子"中,用的时候去池子取一个线程去使用,使用完再放回去,线程可以重用
- 线程池,底层其实就是集合队列,里面存储线程对象,用的时候去抽即可,就不要频繁创建线程了
使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资 源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者 “过度切换”的问题 --> 摘自阿里官方手册
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对象,通过该对象判断任务是否执行成功
public static void main(String[] args) {
// 固定大小线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 50; i++) {
fixedThreadPool.execute(new Runnable( ) {
@Override
public void run() {
// System.out.println(Thread.currentThread().getName()+"干活" );
}
});
}
// 动态调整大小线程池
ExecutorService fixedThreadPool2 = Executors.newCachedThreadPool();
for (int i = 0; i < 50; i++) {
fixedThreadPool2.execute(new Runnable( ) {
@Override
public void run() {
// System.out.println(Thread.currentThread().getName()+"干活" );
}
});
}
// 定时线程池
ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 5; i++) {
scheduledExecutor.schedule(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"干活" );
}
},3, TimeUnit.SECONDS);
}
}
6.5 ThreadPoolExecutor[重要]
- ThreadPoolExecutor
很重要,有7个参数
参数名 解释 备注 int corePoolSize 指定线程池的线程数量(核心线程数) 不能小于0 int maximumPoolSize 指定线程池可支持的最大线程数 最大数量>=核心线程数 long keepAliveTime 指定临时线程的最大存活时间 不能小于0 TimeUnit unit 指定存活时间的单位(秒,分,时,天) 时间单位 BlockingQueue workQueue 指定任务队列 ThreadFactory threadFactory 指定哪个线程工厂创建线程 RejectedExecutionHandler handler 指定线程忙,任务队列满的时候新任务来了怎么办?拒绝策略
问: 什么是创建临时线程?
答: 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建线程问:什么时候开始拒绝任务?
答:核心线程和临时线程都在忙,任务队列也满了,新的任务过来就会拒绝
LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(10);
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 15, 10, TimeUnit.SECONDS, queue);
for (int i = 0; i < 50; i++) {
poolExecutor.submit(new Runnable( ) {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"干活" );
}
});
}
今天这些都是会说,理解,能讲出来,不在于敲代码