第三十讲——多线程
第三十讲——多线程
1——多线程 进程 程序
多线程(Thread)
当一个程序需要多个事件同时进行(或交替运行)时就需要用到多线程;
解决了,在说话的同时能够听的问题。
进程(Process)
在操作系统中运行的程序就是进程,比如正在运行的;QQ、打游戏、爱奇艺、、、
一个进程里可以有多个线程,如视频中、能同时听声音、看图像、看弹幕等待、、
没跑起来叫程序,跑起来就叫进程
-
程序
程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念。
-
进程
进程是程序执行的过程,是一个动态的概念。
进程中至少有一个线程
PS: 许多多线程是模拟出来的,真正的多线程是指多个CPU,即多核,如服务器。
模拟的多线程,即在一个CPU下,在同一个时间点,CPU只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。 就像边吃饭边开手机,其实在一瞬间,你只做了一件事。
计算机处理一道(如两数相加)指令需要约2~4纳秒;ns(nanosecond):纳秒,一秒的十亿分之一
核心概念
- 线程是独立的执行路径。
- 程序运行时,即使没有创建线程,后台也会有多线程,如;主线程、gc线程(守护线程)。
- main() 即主线程(用户进程),为系统的入口,用于执行整个程序。
- 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统密切相关的,先后顺序不是人为干预的。
- 在对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
- 线程会带来额外的开销,如CPU调度的时间,并发控制开销。
- 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致。
2——Thread
共三种方式创建多线程,还有一种 Callable 接口,目前不用
方法一 继承Thread(重点)
Application
package Thread;
// 创建多线程的方式之一; 继承Thread类,重写 run()方法,调用 start 开启线程
public class Thread01 extends Thread{
@Override
public void run() {
// run 方法线程体
for (int i = 0; i < 20; i++) {
System.out.println("晴天");
}
}
// main线程 主线程
public static void main(String[] args) {
// 创建一个线程对象
Thread01 thread01 = new Thread01();
// start 开启线程
thread01.start();
for (int i = 0; i < 2000; i++) {
System.out.println("稻香");
}
// 在稻香里穿插了晴天,说明他们是一起交替运行的,不是线性执行
// 线程开启不一定立即执行,由 CPU 调度。谁先谁后。
}
}
网图下载
利用多线程同时下载多个网图
准备工作
需要用到: conmonsio——下载工具类库
下载压缩包后 将 压缩包中的jar 文件复制到 创建的 Lib 包下;
然后
点击 add as Library ( 创建库) 点击确认
然后在 左上角 File 中点击 Project structure 中的Libraries 就能看到 Lib 的目录 说明添加成功
Application
package Thread;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
// 练习 Thread , 实现多线程同步下载图片
public class Thread02 extends Thread {
private String URL; // 网络图片的地址
private String name; // 保存的文件名
// 只要写 类名的首字 是快捷方法 就会弹出整个类名
// 因为是私有的属性 所以不能用快捷键出构造方法
public Thread02(String url,String name){
this.URL = url;
this.name = name;
}
// 下载图片线程的执行体
@Override
public void run() {
webDownLoader webDownLoader = new webDownLoader();
webDownLoader.downLoader(URL,name);
System.out.println("下载了 全路径名为: "+name);
}
public static void main(String[] args) {
Thread02 t1 = new Thread02("https://pic.cnblogs.com/avatar/2102408/20211029202135.png","E:\\File/1.jpg");
Thread02 t2 = new Thread02("https://pic.cnblogs.com/avatar/2102408/20211029202135.png","E:\\File/2.jpg");
Thread02 t3 = new Thread02("https://pic.cnblogs.com/avatar/2102408/20211029202135.png","E:\\File/3.jpg");
// 线程并不是 按照顺序 执行根据 CPU 进行调度 人为不能干涉 所以下载的顺序可能每次都不一样
t1.start();
t2.start();
t3.start();
}
// 下载器
class webDownLoader{
// 下载方法
public void downLoader (String url , String name){
try {
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常,downLoader 方法出现问题");
}
}
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.Thread02
下载了 文件名为: E:\File/3.jpg
下载了 文件名为: E:\File/2.jpg
下载了 文件名为: E:\File/1.jpg
Process finished with exit code 0
get 新技能 阿帕奇 公司下 conmmons 工具类库 --> FileUtils --> copyURLToFile()方法
FileUtils.copyURLToFile(new URL(URL),new File(fileName));
方法二 实现Runnable 接口(重点)
推荐使用 Runnable 方便灵活 ,方便同一个对象被多个线程使用
Application
package Thread;
/**
* 创建多线程方法 2 ;
* 1. 实现runnable 接口
* 2. 重写 run 方法
* 3. 实例化该实现类,将该类丢入 new Thread() 中 调用 start 方法
*/
// 1. 实现runnable 接口
public class Thread03 implements Runnable {
// 2. 重写 run 方法
// run 方法线程体
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("0 "+i);
}
}
public static void main(String[] args) {
// 3. 实例化该实现类,丢入 new Thread() 中 调用 start 方法
Thread03 thread03 = new Thread03();
// 创建线程对象, 通过线程对象来开启我们的线程,代理
new Thread(thread03).start();
for (int i = 0 ;i<20;i++){
System.out.println("A "+i);
}
}
/**
* Thread 实现了 Runnable 接口
*/
}
特点 ;
- 继承 Thread 类
- 子类 继承 Thread 具备多线程能力
- 启动线程 : 子类对象.start
- 不建议使用,避免 OOP 单继承局限性
- 实现 Runnable 接口
- 实现 RUnnable 具有多线程能力
- 启动线程: 实例化实现类,丢入 Thread 对象. start()
- 推荐使用 : 避免单继承局限性, 灵活方便,方便同一个对象被多个线程使用
Runnable 练习(龟兔赛跑)
- 首先有个赛道距离,离终点越来越近
- 判断比赛是否结束
- 打印胜出者
- 龟兔赛跑开始
- 模拟兔子睡觉
- 乌龟获得胜利
Application
package Thread;
// 模拟龟兔赛跑
public class Race implements Runnable{
// 胜利者
private static String winner;
/**
- 知道这里为什么是 static 了
- 这样设置 保证 多个线程 是只有一个胜者 , 配合 判定游戏是否结束的方法 当胜者不为 null 其他线程就都不能跑了
- 静态变量 保证只有一个胜者 如果不设置 static 的话 每个线程 都是跑自己的 会有多个胜者 */
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
// currentThread() 返回对当前正在执行的线程对象的引用。再getName 模拟的多线程 当前的线程对象只有一个
if (Thread.currentThread().getName().equals("兔子")&i%10==0){
// 相对于 轮到兔子线程的时候就睡 5 毫秒, 就给乌龟跑
try {
// 这个是指兔子的线程
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 判断比赛是否结束
boolean flag = gameOver(i);
// currentThread() 返回对当前正在执行的线程对象的引用。再getName 模拟的多线程 当前的线程对象只有一个
// 比赛结束 退出程序
if (flag){
break;
}
System.out.println(Thread.currentThread().getName()+"-->跑了"+i+"步");
}
}
// 判断比赛是否完成
private boolean gameOver(int steps){
// 判断比赛是否有胜利者
if (winner!=null){ // 已经存在胜利者了
// 为了 多个线程跑的时候 仅有一个胜者 这里要判定 胜者有没有出现 一出现 , 所有线程 都抛出 break 去掉这段会有多个胜利者, 判断条件最好是 静态的变量
return true;
}else { // 如果没有 当steps 为 100 时 看是谁在跑 然后赋值 最后打印退出
if (steps>=100){
winner = Thread.currentThread().getName();
System.out.println("winner is "+winner);
return true;
}
}
return false;
}
public static void main(String[] args) {
Race r1 = new Race();
Thread t1 = new Thread(r1,"乌龟");
Thread t2 = new Thread(r1,"兔子");
t1.start();
t2.start();
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.Race
略...
winner is 乌龟
Process finished with exit code 0
方法三 实现Callable接口( 了解即可 )
- 实现 Callable 接口,需要返回值类型
- 重写 call 方法,需要抛出异常
- 创建目标对象
- 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
- 提交执行: Future
result1 = ser.submit(t1); - 获取结果: boolean r1 = result1.get()
- 关闭服务: ser.shutdownNow();
Application
package Thread;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;
public class TestCallable implements Callable<Boolean> {
private String URL; // 网络图片的地址
private String name; // 保存的文件名
// 只要写 类名的首字 是快捷方法 就会弹出整个类名
public TestCallable(String url, String name) {
this.URL = url;
this.name = name;
}
// 下载图片线程的执行体
@Override
public Boolean call() {
webDownLoader webDownLoader = new webDownLoader();
webDownLoader.downLoader(URL, name);
System.out.println("下载了 文件名为: " + name);
return true;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable("https://pic.cnblogs.com/avatar/2102408/20211029202135.png", "E:\\File/1.jpg");
TestCallable t2 = new TestCallable("https://pic.cnblogs.com/avatar/2102408/20211029202135.png", "E:\\File/2.jpg");
TestCallable t3 = new TestCallable("https://pic.cnblogs.com/avatar/2102408/20211029202135.png", "E:\\File/3.jpg");
// 线程并不是 按照顺序 执行根据 CPU 进行调度 人为不能干涉 所以下载的顺序可能每次都不一样
// 1. 创建执行服务:
ExecutorService ser = Executors.newFixedThreadPool(3);
// 2. 提交执行:
Future<Boolean> r1 = ser.submit(t1);
Future<Boolean> r2 = ser.submit(t2);
Future<Boolean> r3 = ser.submit(t3);
// 3. 获取结果返回值:
boolean rs1 = r1.get();
boolean rs2 = r1.get();
boolean rs3 = r1.get();
System.out.println("----------------");
System.out.println(rs1);
System.out.println(rs2);
System.out.println(rs3);
// 4. 关闭服务:
ser.shutdownNow();
}
// 下载器
class webDownLoader {
// 下载方法
public void downLoader(String url, String name) {
try {
FileUtils.copyURLToFile(new URL(url), new File(name));
} catch (IOException e) {
e.printStackTrace();
System.out.println("IO 异常,downLoader 方法出现问题");
}
}
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestCallable
下载了 文件名为: E:\File/3.jpg
下载了 文件名为: E:\File/1.jpg
下载了 文件名为: E:\File/2.jpg
----------------
true
true
true
Process finished with exit code 0
3——并发问题
抢火车票
Application
package Thread;
// 多个线程 同时操作同一个对象
// 买火车票的例子
// 发现问题 : 多个线程操作同一个资源的情况下,线程不安全,数据紊乱 这是一个并发问题
public class Thread04 implements Runnable{
// 票数
private int ticketNums = 10;
@Override
public void run() {
while (true){
if (ticketNums<=0){
break;
}
// 模拟延时 增加一点延时 因为 CPU 执行太快了 一下子就把 票给了小明 , 所以给它延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// currentThread() 返回对当前正在执行的线程对象的引用。再getName 模拟的多线程 当前的线程对象只有一个
System.out.println(Thread.currentThread().getName()+"-->拿到了第"+ticketNums--+"票");
}
}
public static void main(String[] args) {
Thread04 ticket = new Thread04();
// Thread 的构造方法 前面是调用对象 ticket 后面是线程名字
new Thread(ticket,"啊珍").start();
new Thread(ticket,"啊强").start();
new Thread(ticket,"黄牛").start();
// 推荐使用 : 避免单继承局限性, 灵活方便,方便同一个对象被多个线程使用
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.Thread04
黄牛-->拿到了第8票
啊强-->拿到了第9票
啊珍-->拿到了第10票
黄牛-->拿到了第7票
啊强-->拿到了第6票
啊珍-->拿到了第5票
啊珍-->拿到了第4票
啊强-->拿到了第3票
黄牛-->拿到了第2票
黄牛-->拿到了第1票
啊强-->拿到了第0票
啊珍-->拿到了第-1票
Process finished with exit code 0
4——静态代理模式(StaticProxy)
Application
package Thread;
// 静态代理模式
/**
* 真实对象和代理对象都要实现同一个接口
* 代理对象要代理真实角色
*
* 优点:
* 代理对象可以做真实对象做不了的事情
* 真实对象可以专注的做自己的事
*/
public class StaticProxy {
public static void main(String[] args) {
/**
* Thread( new Runnable() ).start();
*/
new WeddingCompany(new You()).HappyMarry();
new You().HappyMarry();
}
}
interface Marry{
void HappyMarry();
}
// 真实角色
class You implements Marry{
@Override
public void HappyMarry() {
System.out.println("今天我结婚啦!!!");
}
}
// 代理角色 帮助你结婚
class WeddingCompany implements Marry{
private Marry target;
public WeddingCompany(Marry target) {
this.target = target;
}
@Override
public void HappyMarry() {
before();
// 利用代理 把 真实角色"我" 丢进去 然后调用方法
target.HappyMarry(); // 真实角色结婚
after();
}
private void before(){
System.out.println("结婚之前,布置场地");
}
private void after(){
System.out.println("结婚之后,付清尾款");
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.StaticProxy
结婚之前,布置场地
今天我结婚啦!!!
结婚之后,付清尾款
今天我结婚啦!!!
Process finished with exit code 0
5——Lamda 表达式
其实实质属于函数式编程的概念
优点;
- 避免内部类定义过多
- 可以让代码看起来很简洁
- 去掉一堆没有意义的代码,只留下核心逻辑
new Thread (()-->System.out.ptintln("多线程学习 Lambda 表达式...."))
-
理解 Functional Interface (函数式接口)是学习 Java8 Lambda 表达式的关键所在
-
函数式接口的定义
-
任何接口,如果只包含唯一一个抽象方法,那么他就是一个函数式接口
-
public interface Runnable { public abstract void run(); }
-
对于函数式接口,我们可以通过 Lambda 表达式来创建该接口的对象。
-
Application
package Thread;
/**
* 推导 Lambda 表达式
*/
public class TestLambda01 {
// 3. 静态内部类
static class Like02 implements ILike{
@Override
public void lambda() {
System.out.println("i Like Lambda02");
}
}
public static void main(String[] args) {
// 4. 局部内部类
class Like03 implements ILike{
@Override
public void lambda() {
System.out.println("i Like Lambda03");
}
}
ILike like01 = new Like01(); // 第一种方法 接口 new 实现类 调用lambda
like01.lambda();
Like02 like02 = new Like02();// 第二种方法 接口 new 静态内部实现类 调用lambda
like02.lambda();
Like03 like03 = new Like03();// 第三种方法 接口 new 局部内部实现类 调用lambda
like03.lambda();
// 5. 匿名内部类,没有类的名称 必须借助接口或者父类
ILike like04 = new ILike(){ // 第四种方法 接口 new 匿名实现类 调用lambda
@Override
public void lambda() {
System.out.println("i Like Lambda04");
}
};
like04.lambda();
// 6. Lambda 表达式简化 // 第五种方法 lambda 表达式
ILike like05 = ()->{
System.out.println("i Like Lambda05");
};
like05.lambda();
}
}
// 1. 定义一个函数式接口
interface ILike{
public abstract void lambda(); // 默认是抽象方法
}
// 2. 实现类
class Like01 implements ILike{
@Override
public void lambda() {
System.out.println("i Like Lambda01");
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestLambda01
i Like Lambda01
i Like Lambda02
i Like Lambda03
i Like Lambda04
i Like Lambda05
Process finished with exit code 0
Lambda
Application
package Thread;
public class TestLambda02 {
public static void main(String[] args) {
Love love = null;
love = (int a)->{
System.out.println("I Love You -> "+a);
};
love.love(520);
/* Lambda 表达式还能简化, 去中小括号,去 参数类型
love = a-> System.out.println("I Love You -> "+a);
love.love(50);*/
/**
* 总结 ;
* Lambda 表达式只能有一行代码的情况下才能简化成为一行,如果有多行,那么就用代码块包裹(中括号)
* 前提-接口是函数式接口
* 多个参数也可以去掉参数类型
*/
}
}
interface Love {
void love (int a);
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestLambda02
I Love You -> 520
I Love You -> 50
Process finished with exit code 0
6—— 线程方法
线程五大状态
方法 | 说明 |
---|---|
setPriority(int newPriority) | 更改线程的优先级 |
void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响 |
void join() | 等待该线程终止。 |
void yield() | 暂停当前正在执行的线程对象,并执行其他线程.( 礼让 ) |
void interrupt() | 中断线程。(不建议使用) |
boolean isAlive() | 测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。 |
不推荐使用、 stop()、destroy() 方法
- 最好让线程自然停止
- 建议用一个flag 进行中止变量,当 flag = false ,则线程停止运行。
Application 实例
public class Test01 implements Runnable {
// 1. 线程中定义线程体使用的 flag 标识位
private boolean flag = true;
@Override
public void run() {
// 2. 在线程中使用该标识位
while(flag){
System.out.println("Run、、、Thread");
}
}
// 3. 对外提供方法改 flag
public void stop(){
this.flag = false;
}
}
标志位停止线程
Application
package Thread;
// 测试stop
/**
* 1. 建议线程正常停止--> 利用次数,不建议死循环
* 2. 建议是使用标志位--> 设置一个标志位转换方法进行停止程序
* 3. 不使用过时方法
*/
public class TestStop implements Runnable {
// 1. 设置一个标识符
private boolean flag = true;
@Override
public void run() {
int i = 0;
while(flag){
System.out.println("Run、、、Thread"+i++);
}
}
// 2. 设置一个公开的方法停止线程,转换标志位
public void stop(){
this.flag = false;
}
public static void main(String[]args){
TestStop testStop = new TestStop();
new Thread(testStop).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main"+i);
if (i==900){
testStop.stop();
System.out.println("该线程停止了");
}
}
}
}
线程休眠( sleep )
- sleep 存在异常需要抛出
- sleep 可以模拟网络延时,倒计时等等
- 每一个对象都有一个锁,sleep 不会释放锁
// 模拟网络延时:
// 现线程不安全
// 放大问题的发生性
// 模拟延时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 在抢火车票的时候体现出了这一点 出现负数的票
模拟倒计时
Application
package Thread;
// 模拟倒计时、、、
public class TestSleep02 {
public static void main(String[] args) {
try {
tenDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void tenDown() throws InterruptedException{
int num = 10;
while (true){
Thread.sleep(1000);
System.out.println(num--);
if (num==0){
break;
}
}
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestSleep02
10
9
8
7
6
5
4
3
2
1
Process finished with exit code 0
Application
public static void main(String[] args) throws Exception {
// 每隔一秒 打印一次当前系统时间
Date startTime = new Date(System.currentTimeMillis());// 获取系统当前时间
while (true){
Thread.sleep(1000);
System.out.println(new SimpleDateFormat("HH:mm:ss").format(startTime));
startTime = new Date(System.currentTimeMillis());// 更新当前时间 要不然跑的不对
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestSleep02
21:06:53
21:06:54
21:06:55
21:06:56
21:06:57
21:06:58
21:06:59
`````````
Process finished with exit code -1
线程礼让(yield)
礼让线程,让当前正在运行的线程暂停,但不阻塞
将线程从运行状态转为就绪状态(A让出来和B一起重新跑)
让CPU 重新调度,礼让不一定成功!看 CPU 心情
Application
package Thread;
// 测试礼让线程
// 礼让线程不一定成功,看 CPU 心情
public class TestYield {
public static void main(String[]args){
MyYield myYield = new MyYield();
new Thread(myYield,"A").start();
new Thread(myYield,"B").start();
}
}
class MyYield implements Runnable{
public void run(){
/**
* 如果没有礼让 线程开始然后停止是连一起的,有了礼让呢,就会退出来让 B 运行 然后再停止
* 但是实际测试 并不能达到预期情况 总之 礼让就是这个意思 不必在意这个
*/
System.out.println(Thread.currentThread().getName()+"线程开始执行");
//Thread.yield(); // 礼让 如果礼让成功 下面不会打印
System.out.println(Thread.currentThread().getName()+"程序停止执行");
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestYield
A线程开始执行
B线程开始执行
A程序停止执行
B程序停止执行
Process finished with exit code 0
线程插队( Join )
- Join 合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
- 可以想象插队, 插队嘛 当然要在人家没进内存的时候才插队,要不然就来不及了赛 ( 紧跟在 start 后面 霸道的很!)
Application
package Test;
public class TestJoin implements Runnable {
public static void main(String[] args) throws InterruptedException { // 中断异常
TestJoin join = new TestJoin();
Thread thread = new Thread(join);
thread.start();
for (int i = 0; i < 100 ;i++) {
if (i==50){
thread.join(); // main 线程阻塞 当 i = 50 main 线程阻塞 直到 join 跑完
// 就像插队
}
System.out.println("main --->"+i);
}
}
@Override
public void run() {
for (int i = 0; i < 200; i++) {
System.out.println("join --->"+i);
}
}
}
线程状态( state )
State 是 Thread 的一个静态嵌套类 所以用 class. State 调用
Application
package Thread;
// 观察测试线程的状态
public class TestState {
public static void main(String[] args) throws InterruptedException{
Thread thread = new Thread(()->{
for (int i = 0; i < 5 ; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("、、、、、、");
});
// 观察状态
Thread.State state = thread.getState();
System.out.println(state);
// 观察启动后
thread.start(); // 启动线程
state = thread.getState();
System.out.println(state);// Run
while(state!=Thread.State.TERMINATED){ // 只要线程不终止,就一直输出状态
Thread.sleep(100);
state = thread.getState(); // 要更新线程状态 这里更新的值 是给 while 去判断用的
System.out.println(state); // 输出状态
}
// 线程只能启动一次 ,死亡后不能再次启动 会报错
thread.start();
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestState
NEW
RUNNABLE
TIMED_WAITING
TIMED_WAITING
TIMED_WAITING
略、、
TIMED_WAITING
TIMED_WAITING
RUNNABLE
、、、、、、
TERMINATED
Exception in thread "main" java.lang.IllegalThreadStateException
at java.base/java.lang.Thread.start(Thread.java:794)
at Thread.TestState.main(TestState.java:34)
Process finished with exit code 1
线程优先级(Priority)
t2.setPriority(1);// 和join 相反 先设置优先级 再启动
t2.start();
-
JAVA提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行
-
线程的优先级用数字表示,范围从1~10
- Thread.MIN_Priority = 1;
- Thread.Max_Priority = 10;
- Thread.Norm_Priority = 5;
-
getPriority() setPriority ( int i );
最终的结果还是看 CPU 心情,优先级高不一定先执行,只是先执行的概率大一些
package Thread;
public class TestPriority {
public static void main(String[] args) {
// 主线程默认优先级 是改不了的
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
MyPriority myPriority = new MyPriority();
Thread t1 = new Thread(myPriority,"1");
Thread t2 = new Thread(myPriority,"2");
Thread t3 = new Thread(myPriority,"3");
Thread t4 = new Thread(myPriority,"4");
Thread t5 = new Thread(myPriority,"5");
Thread t6 = new Thread(myPriority,"6");
// 设置优先级 再启动
t1.start();
t2.setPriority(1);
t2.start();
t3.setPriority(6);
t3.start();
t4.setPriority(7);
t4.start();
t5.setPriority(Thread.MAX_PRIORITY); // 最大优先级 = 10
t5.start();
t6.setPriority(8);
t6.start();
}
}
class MyPriority implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"--->"+Thread.currentThread().getPriority());
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestPriority
main--->5
5--->10
4--->7
6--->8
1--->5
3--->6
2--->1
Process finished with exit code 0
守护线程 (Daemon)
-
线程分为用户线程和守护线程
-
虚拟机必须确保用户线程执行完毕
-
虚拟机不用等待守护线程执行完毕
这句要和上面一句一起理解,虚拟机不确保守护线程执行到结束,只负责用户执行到结束,一旦守护线程所守护的用户线程结束,那么可能会有一定延时后结束守护线程。
-
如;后台记录操作日志,监控内存,垃圾回收等待机制
Application
package Thread;
public class TestDaemon {
public static void main(String[] args) {
God god = new God();
You01 you = new You01();
Thread thread = new Thread(god);
thread.setDaemon(true); // 默认是 false 是用户线程,正常的线程都是用户线程
thread.start(); // 上帝 守护线程 启动
new Thread(you).start(); // 你 用户线程
}
}
// 上帝
class God implements Runnable{
@Override
public void run() {
while(true){
System.out.println("上帝保佑着你");
}
}
}
// 你
class You01 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 30000; i++) {
System.out.println("你一生都开心的活着");
}
System.out.println("=====goodbye World=======");
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestDaemon
上帝保佑着你
你一生都开心的活着
你一生都开心的活着
上帝保佑着你
上帝保佑着你
上帝保佑着你
你一生都开心的活着
你一生都开心的活着
=====goodbye World=======
上帝保佑着你 // 守护线程在用户线程终止后还会有延时后关闭线程
上帝保佑着你
上帝保佑着你
Process finished with exit code 0
7——线程同步(Synchronized-同步锁)
多个线程操作一个资源
-
并发 ; 同一个对象被多个线程同时操作 | 多个线程访问同一个对象
- 上万人抢十张票
- 两个银行同一个银行卡同时取钱
-
解决 ; 进行排队 在程序中叫队列
-
线程同步; 是一个等待机制,多个需要访问的线程,进入这个 对象的等待池 形成队列,等待前面的线程使用完毕,下一个线程再使用 线程同步 = 队列+锁
- 由于同一进程的多个线程共享同一块储存空间,在带来方便的同时,也带来了访问冲突的问题,为了,保证数据在方法中被访问时的正确性,在访问时,加入锁机制 synchronized,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可,存在以下问题
- 一个线程持有锁会导致其他所有需要此锁的线程挂起;(也就是进入等待状态)
- 在多个线程竞争下加锁,释放锁,会导致比较多的上下文切换 和 调度延时,引起性能问题 (理解为使程序变的繁琐,影响执行效率)
- 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题
例 1 不安全的买票
Application
package Thread.syn;
// 不安全的买票
// 出现了负数 和多出一张票 等情况 说明线程不安全
public class UnsafeBuyTicket {
public static void main(String[] args) {
BuyTicket station = new BuyTicket();
Thread t1 = new Thread(station,"阿珍");
Thread t2 = new Thread(station,"阿强");
Thread t3 = new Thread(station,"我");
Thread t4 = new Thread(station,"阿牛");
t1.start();
t2.start();
t3.start();
t4.start();
}
}
class BuyTicket implements Runnable{
// 票
private int ticketNums = 10 ;
boolean flag = true; // 外部停止方式
@Override
public void run() {
// 买票
while(flag){
buy();
}
}
private void buy(){
// 模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 判断是否有票
if ( ticketNums <=0){
flag = false;
return; // return 不带返回值 退出该方法
}
// 买票
System.out.println(Thread.currentThread().getName() + "拿到"+ticketNums--);
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.syn.UnsafeBuyTicket
阿珍拿到9
阿牛拿到10
阿强拿到8
我拿到10
阿珍拿到7
阿牛拿到6
阿强拿到5
我拿到4
我拿到3
阿牛拿到1
阿强拿到2
Process finished with exit code 0
例 2 不安全的银行
Application
package Thread.syn;
// 不安全的银行
// 两个人取银行取同一个账户
public class UnsafeBank {
public static void main(String[] args) {
// 账户
Account account = new Account(100,"结婚基金");
Drawing you = new Drawing(account,50,"你");
Drawing girlFried = new Drawing(account,100,"girlFried");
you.start();
girlFried.start();
}
}
class Account {
int Money; // 余额
String name; // 卡名
public Account(int money, String name) {
Money = money;
this.name = name;
}
}
// 银行 : 模拟取款
class Drawing extends Thread{ // Drawing 画画
Account account; // 账户
// 取了多少前
int drawingMoney ;
// 现在手里多少钱
int nowMoney;
public Drawing(Account account , int drawingMoney,String name){
super(name);// 传给父类 Thread 相当于给线程 取名字
this.account = account;
this.drawingMoney = drawingMoney;
}
// 取钱
@Override
public void run() {
// 判断有没有钱
if (account.Money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够了 取不了!");
return;
}
// sleep 提高放大问题的发生性 这里加 sleep 就是 两个人都到这个位置等待
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额 = 余额 - 你取的钱
account.Money = account.Money-drawingMoney;
// 你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.Money);
// Thread.currentThread().getName() = this.getName() 继承了 Thread 可以用它的所有方法 可以简化
System.out.println(this.getName()+"手里的钱: "+nowMoney);
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.syn.UnsafeBank
结婚基金余额为:-50
结婚基金余额为:0
你手里的钱: 50
girlFried手里的钱: 100
Process finished with exit code 0
例 3 不安全的集合
Application
package Thread.syn;
import java.util.ArrayList;
import java.util.List;
// 线程不安全集合
public class UnsafeList {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (int i = 0; i <10000 ; i++) {
// 因为 Thread 只有一个抽象方法 run 所以可以用 lambda 表达式
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
// 提高问题的发生性,等待计数完后打印
// 大概意思 : 两个线程 添加到了 list 的同一个地址 就会覆盖所以导致不安全
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(list.size());
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.syn.UnsafeList
9985
Process finished with exit code 0
同步方法 ( Synchronized )
- 方法里面需要修改的内容才需要锁,锁的太多,浪费资源
// 在方法前加 Synchronized 同步方法
Synchronized void buy(){
}
private Synchronized void buy(){
// 模拟延时
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 判断是否有票
if ( ticketNums <=0){
flag = false;
return; // return 不带返回值 退出该方法
}
// 买票
System.out.println(Thread.currentThread().getName() + "拿到"+ticketNums--);
}
同步块 Synchronized ( Obj )
// 锁的对象就是变化的量.需要增删改的量 (obj)
Synchronized ( Obj ){
}
// 取钱
@Override
public void run() {
Synchronized(account){
// 判断有没有钱
if (account.Money-drawingMoney<0){
System.out.println(Thread.currentThread().getName()+"钱不够了 取不了!");
return;
}
// sleep 提高放大问题的发生性 这里加 sleep 就是 两个人都到这个位置等待
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 卡内余额 = 余额 - 你取的钱
account.Money = account.Money-drawingMoney;
// 你手里的钱
nowMoney = nowMoney + drawingMoney;
System.out.println(account.name+"余额为:"+account.Money);
// Thread.currentThread().getName() = this.getName() 继承了 Thread 可以用它的所有方法 可以简化
System.out.println(this.getName()+"手里的钱: "+nowMoney);
}
}
扩充 CopyOnWriteArrayList 先不管它
package Work;
import java.util.concurrent.CopyOnWriteArrayList;
// 测试 JUC 安全类型的集合
public class TestJUC {
public static void main(String[] args) throws Exception{
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>();
for (int i = 0; i < 10000; i++) {
new Thread(()->{
list.add(Thread.currentThread().getName());
}).start();
}
// 原本打印 8845 因为没有统计完全就打印了 所以 这里留点时间统计数据
Thread.sleep(3000);
System.out.println(list.size());
}
}
8 —— 死锁 ( deadLock )
死锁 : 多个线程互相抱着对方需要得资源, 然后形成僵持
Application
package Thread;
public class DeadLock {
public static void main(String[] args) {
Makeup makeup00 = new Makeup(1,"灰姑凉");
Makeup makeup01 = new Makeup(0,"白雪公主");
makeup00.start();
makeup01.start();
}
}
// 口红
class Lipstick {
}
// 镜子
class Mirror{
}
class Makeup extends Thread {
// 需要得资源只有一份. 用 static 来保证只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice; // 选择
String girlName; // 使用化妆品的人
public Makeup(int choice, String girlName) {
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 化妆 , 互相持有对象的锁,就是需要拿到对方的资源
// 死锁 : 双方都把对方需要的锁了,并切还互想需要,就阻塞了, 就是锁中锁,
private void makeup() throws InterruptedException{
if (choice == 0){
synchronized (lipstick){ // 获得口红的锁
System.out.println(this.girlName+"获得口红的锁");
Thread.sleep(1000);
}
// 只要把锁拿出来,避免锁中锁就可以了
/**
* synchronized (mirror){// 一秒钟后想拿镜子的锁
* synchronized (mirror){
*
* }
* }
*/
synchronized (mirror){// 一秒钟后想拿镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
}
}else {
synchronized(mirror){// 获得镜子的锁
System.out.println(this.girlName+"获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick){ // 一秒钟后想拿口红的锁
System.out.println(this.girlName+"获得口红的锁");
}
}
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.DeadLock
白雪公主获得口红的锁
灰姑凉获得镜子的锁
白雪公主获得镜子的锁
灰姑凉获得口红的锁
Process finished with exit code 0
9——Lock (锁)
- java.util.concurrent.lock.Lock 接口是控制多个线程对共享资源进行访问的工具.锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得 Lock 对象.
- ReentrantLock 类实现了Lock , 它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是 ReentrantLock , 可以显式加锁,释放锁.
使用 lock 需要配合 try finally 使用
Application
try {
lock.lock(); // 加锁
if (ticketNums>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
} else {
break;
}
} finally {
lock.unlock(); // 释放锁
}
Application
package Thread;
import java.util.concurrent.locks.ReentrantLock;
// 测试 Lock 锁
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock = new TestLock2();
new Thread(testLock,"1").start();
new Thread(testLock,"2").start();
new Thread(testLock).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10 ;
// 定义 lock 锁
private ReentrantLock lock = new ReentrantLock( );
@Override
public void run(){
while (true) {
try {
lock.lock(); // 加锁
if (ticketNums>0){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
} else {
break;
}
} finally {
lock.unlock();
}
}
}
}
10 ——生产者和消费者问题 ( 线程协作问题 )
把原本互不影响的线程, 进行一个协调协作的操作
- Java 提供了几个方法解决线程之间的通信问题
方法名 | 作用 |
---|---|
wait() | 表示线程一直等待,直到其他线程通知,与sleep不同,会释放锁 |
wait(long timeout) | 指定等待的毫秒数 |
notify | 唤醒一个处于等待状态的线程 |
notifyAll | 唤醒同一个对象上所有调用 wait() 方法的线程,优先级别高的线程优先调度 |
注意 : 均是 Oject 类不是Thread 方法,都只能在同步方法或者同步代码块中使用,非则会抛出异常 IllegalMonitorStateException
两个解决方法
管程法
管程法: 在生产者和消费者中间建立一个缓冲区,用缓冲区来建立方法解决协作问题
Application
package Thread;
// 测试 : 生产者消费者模型--> 利用缓冲区解决 : 管程法
// 生产者, 消费者,产品,缓冲区
public class TestPC {
public static void main(String[] args) {
SynContainer container = new SynContainer();
new Productor(container).start();
new Consumer(container).start();
}
}
// 生产者
class Productor extends Thread{
SynContainer container ;
public Productor(SynContainer container) {
this.container = container;
}
// 生产
@Override
public void run() {
// 准备生产100 只鸡
for (int i = 0; i < 100 ; i++) {
container.push(new Chicken(i));
System.out.println("生产了"+i+"只鸡");
}
}
}
// 消费者
class Consumer extends Thread{
SynContainer container ;
public Consumer(SynContainer container) {
this.container = container;
}
// 消费
@Override
public void run() {
for (int i = 0; i < 100 ; i++) {
System.out.println("消费了-->"+container.pop().id+"只鸡");
}
}
}
// 产品
class Chicken{
int id; // 产品编号
public Chicken(int id) {
this.id = id;
}
}
// 缓冲区
class SynContainer {
// 需要一个容器大小
Chicken[] chickens = new Chicken[10];
// 容器计数器
int count = 0;
// 生产者放入产品
public synchronized void push(Chicken chicken){
// 容器满了, 就需要等待消费者消费
if (count == chickens.length){
// 通知消费者消费 . 生产者等待
try {
this.wait(); // wait() 是一个Object 方法,在被唤醒前,调用该方法(对象)的线程一直等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果没有满,我们就需要丢入产品
chickens[count] = chicken;
count++;
// 可以通知消费者消费了
this.notifyAll();
}
// 消费者消费产品
public synchronized Chicken pop(){
// 判断能否消费
if (count== 0){
// 等待生产者生产,
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 如果可以消费
count--;
// 象征性的把鸡拿走 这里是同步数据,count++对应count-- 就是同一个对象
// 再者 count 与下行并不能对调位置,因为chickens[1]的鸡还没有做出来,会显示空指针异常 null
Chicken chicken = chickens[count];
// 吃完了 通知生产者生产
this.notifyAll();
return chicken;
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestPC
生产了0只鸡
生产了1只鸡
生产了2只鸡
生产了3只鸡
生产了4只鸡
生产了5只鸡
生产了6只鸡
生产了7只鸡
生产了8只鸡
生产了9只鸡
生产了10只鸡
消费了-->9只鸡
生产了11只鸡
消费了-->10只鸡
生产了12只鸡
消费了-->11只鸡
生产了13只鸡
消费了-->12只鸡
生产了14只鸡
消费了-->13只鸡
......
Process finished with exit code 0
信号灯法
信号灯法 : 就是用取反的标志位来解决问题
Application
package Thread;
// 测试生产者消费者问题2: 信号灯法,标志位解决
public class TestPc2 {
public static void main(String[] args) {
TV tv = new TV();
new Player(tv).start();
new Watcher(tv).start();
}
}
// 生产者--> 演员
class Player extends Thread {
TV tv;
public Player(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
if (i%2==0){
this.tv.play("快乐大本营播放中...");
}else {
this.tv.play("抖音: 记录美好生活~");
}
}
}
}
// 消费者 --> 观众
class Watcher extends Thread{
TV tv;
public Watcher(TV tv) {
this.tv = tv;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
this.tv.watch();
}
}
}
// 产品 --> 节目
class TV {
// 演员表演,观众等待 T
// 观众观看,演员等待 F
String voice; // 表演的节目
boolean flag = true;
// 表演
public synchronized void play(String voice){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("演员表演了"+voice);
// 通知观众观看
this.notifyAll();// 通知唤醒
this.voice = voice; // 声音跟新 让观众能看见
this.flag = !this.flag; // 取反
}
// 观看
public synchronized void watch(){
if (flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("观看了: "+voice);
// 通知演员表演
this.notifyAll();
this.flag = !this.flag;
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestPc2
演员表演了快乐大本营播放中...
观看了: 快乐大本营播放中...
演员表演了抖音: 记录美好生活~
观看了: 抖音: 记录美好生活~
演员表演了快乐大本营播放中...
观看了: 快乐大本营播放中...
演员表演了抖音: 记录美好生活~
观看了: 抖音: 记录美好生活~
演员表演了快乐大本营播放中...
.......
Process finished with exit code 0
11——线程池 (理解)
-
背景: 经常创建和销毁, 使用量特别大的资源, 比如并发情况下的线程, 对性能影响很大
-
思路 : 提前创建好多个线程, 放入线程池中, 使用时,直接获取, 使用完放回池中, 可以避免频繁创建销毁,实现重复利用, 类似生活中的公共交通工具
-
好处 :
- 提高相应速度 (减少了创建新线程的时间)
- 降低资源消耗 ( 重复利用线程池中线程, 不需要每次创建)
- 便于线程管理
- corePoolSize : 核心池的大小
- maximumPoolSize : 最大线程数
- keepAliveTime : 线程没有任务时最多保持多长时间后终止.
-
JDK 5.0 起提供了 线程池相关API : ExecutorService 和 Executor
-
ExecutorService : 真正的线程池接口,常见子类 ThreadPoolExecutor
- void execute(Runnable command) : 执行任务|命令 , 没有返回值,一般用来执行 Runnable
Future submit (Callable task) : 执行任务, 有返回值,一般又来执行 Callable - void shutdown() : 关闭连接池
-
Executors : 工具类.线程池的工厂类,用于创建并返回不同类型的线程池
Application
package Thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TestPool {
public static void main(String[] args) {
// 1. 创建服务,创建线程池
// newFixedThreadPool 参数为:线程池的大小
ExecutorService service = Executors.newFixedThreadPool(10);
// 执行
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
service.execute(new MyThread());
// 2. 关闭连接
service.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}
F:\Application\out\production\Application;F:\Application\src\Lib\commons-io-2.11.0.jar Thread.TestPool
pool-1-thread-2
pool-1-thread-1
pool-1-thread-4
pool-1-thread-3
Process finished with exit code 0
新增单词
0 | create Library | 创建库 | 莱伯乱 | |
---|---|---|---|---|
1 | Project structure | 项目结构 | 丝架(tr)可却 | |
2 | ticket | 票 | 踢给特 | |
3 | Race | 赛跑 | 乱丝 | |
4 | winner | 胜利者 | winner | |
5 | gameover | 游戏结束 | ||
6 | steps | 步数 | 丝打破丝 | |
7 | commons io | 下载工具类库 | 康芒丝 io | |
8 | FileUtils.copyURLToFile | 文件工具类 . 复制URL到文件 | ||
9 | StaticProxy | 静态代理模式 | ||
10 | Marry | 结婚 | 马瑞 | |
11 | weddingCompany | 婚庆公司 | ||
12 | target | 目标 | 托get | |
13 | Lambda | 希腊字母11位 | 兰布达 | λ |
14 | yield | 礼让 | 有的 | |
15 | join | 加入 | 插队 | |
16 | state | 线程状态 | 丝得特 | stat 状态 丝打特 |
17 | Runnable | 正在运行 | ||
18 | Blocked | 阻塞 | ||
19 | Waiting | 无限期的等待 | 威丁 | |
20 | Timed Waiting | 有时间的等待 | ||
21 | Terminated | 终止状态 | 檀木内特的 | |
22 | Priority | 优先级 | 派or润踢 | |
23 | Norm | 正常 | ||
24 | Daemon | 守护线程 | 打们 | |
25 | Synchronized | 同步 | 森q纳斯的 | |
26 | Unsafe | 不安全的 | 按塞夫 | |
27 | station | 车站 | ||
28 | Account | 账号 | 额康特 | |
29 | girlFriend | 女朋友 | ||
30 | DeadLock | 死锁 | ||
31 | Lipstick | 口红 | 类噗丝得k | |
32 | Mirror | 镜子 | 妹弱啊 | |
33 | Makeup | 化妆 | ||
34 | choice | 选择(cho) | 雀氏 | |
35 | ReentrantLock | 可重入锁 | 润tr lock | |
36 | unlock | 解锁 | ||
37 | Productor | 生产者 | 噗丢塞 | |
38 | Consumer | 消费者 | 肯素们 | |
39 | Chicken | 鸡 | ||
40 | wait | 等待 | ||
41 | notifyAll | 通知 | 唤醒 | ||
42 | puth | 放入(应该是一种缩写) | 噗徐 | |
43 | pop | 取出(应该是一种缩写) | ||
44 | Player | 演员(选手) | ||
45 | Watcher | 观众 | ||
46 | watch | 观看 | ||
47 | voice | 声音 |