Java-Day-23
线程终止
- setLoop()
基本说明
- 当线程完成任务后,会自动退出
- 还可以通过使用变量来控制 run 方法退出的方式停止线程,即通知方式
练习使用
public class test1 {
public static void main(String[] args) throws InterruptedException {
T t1 = new T();
t1.start();
// 如果希望main线程控制t1线程的终止,必须可以修改loop
// 让t1退出run方法,从而终止t1线程 —> 通知方式
// 让主线程休眠 10 秒,再通知t1线程退出
System.out.println("线程main休眠10s,休眠结束就执行false操作");
Thread.sleep(10 * 1000);
t1.setLoop(false); //
}
}
// 使用 Thread 方式
class T extends Thread {
int count = 0;
// 设置一个控制变量
private boolean loop = true;
@Override
public void run() {
while (loop) { // 控制循环
try {
Thread.sleep(50); // 休眠50ms
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("T 运行中......" + (++count));
}
}
public void setLoop(boolean loop) {
this.loop = loop;
}
}
常用方法
常用方法第一组
- setName:设置线程名称,使之与参数 name 相同
- getName:返回该线程的名称
- start:使该线程开始执行 ( Java 蓄奴就底层调用该线程的 start0 方法 )
- run:调用该线程对象 run 方法
- setPriority:更改线程的优先级
- getPriority:获取线程的优先级
- sleep:在指定的毫秒数内让当前正在执行的线程休眠 ( 暂停执行 )
- interrupt:中断线程
public class test {
public static void main(String[] args) throws InterruptedException {
// 测试相关的方法
T t = new T();
t.setName("zyz 的你");
t.setPriority(Thread.MIN_PRIORITY); // 设置其优先级为最低:1
t.start(); // 启动子线程
// System.out.println(t.getName());
for (int i = 0; i < 5; i++) {
Thread.sleep(1000); // 每隔一秒钟输出一个 hi
System.out.println("hi ~ " + i);
}
System.out.println(t.getName() + "线程的优先级 = " + t.getPriority());
// 中断线程
t.interrupt(); // t线程在吃包子休眠还没到10秒就被终止了休眠状态,while又开始循环了
}
}
// 使用 Thread 方式
class T extends Thread {
@Override
public void run() {
while (true) {
for (int i = 0; i < 100; i++) {
// Thread.currentThread().getName() 获取当前线程的名称
System.out.println(Thread.currentThread().getName() + "恰饭" + i);
}
try {
System.out.println(Thread.currentThread().getName() + "睡了");
Thread.sleep(10000); // 输出100个后,休眠10秒
} catch (InterruptedException e) {
// 当该线程执行到一个 interrupt 方法时,就会 catch 一个异常,可以加入自己的业务代码
// InterruptedException 是捕获到一个中断异常
System.out.println(Thread.currentThread().getName() + "被 interrupt 了");
}
}
}
}
注意细节
-
start 底层会创建新的线程,调用 run,run 就是一个简单的方法调用,不会启动新线程 ( start0 才是真正实现 )
-
线程优先级的范围 ( public final static int )
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
- MAX_PRIORITY = 10
-
interrupt:中断线程,但并没有真正的结束线程。所以一般用于中断正在休眠线程
-
sleep:线程的静态方法,使当前线程休眠
常用方法第二组
-
yield:线程的礼让 ( 调用自己的 )。让出 cpu,让其他线程执行,但礼让的时间不确定,所以也不一定礼让成功 ( 主是操作系统 cpu 决定的,看资源是否紧张,资源够多就不会执行礼让 )
-
join:线程的插队 ( 调用对方的 )。插队的线程一旦插队成功,则肯定先执行完插入的线程的所有的任务 ( 然后才会回到一开始执行的线程 )
-
创建一个子线程,每隔 1s 输出 hello,输出 10 次,主线程每隔 1s 输出 hi,输出 10 次 ( 要求:两个线程同时执行,当主线程输出 5 次后,就让子线程运行完毕,主线程再继续 )
public class test { public static void main(String[] args) throws InterruptedException { T t = new T(); t.start(); // 启动子线程 for (int i = 1; i <= 10; i++) { Thread.sleep(1000); // 每隔一秒钟输出一个 hi System.out.println("主线程:hi ~ " + i); if (i == 5) { System.out.println("主线程让步于子线程,即插队"); t.join(); // Thread.yield(); // 礼让不成功 System.out.println("子线程运行完毕"); } } } } class T extends Thread { @Override public void run() { for (int i = 1; i <= 10; i++) { try { Thread.sleep(1000); //休眠1秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("子线程:hello ~ " + i); } } }
-
用户线程 ( 通知方式 )、守护线程
-
用户线程:也叫工作线程,当线程的任务执行完毕或以通知的方式结束
- 通知方式前提:一个线程持有另一个线程的变量
-
守护线程:一般是为工作线程服务的,当所有的用户线程结束,守护线程自动结束
-
常见经典的守护线程:垃圾回收机制
-
将一个线程设置成守护线程 ( 若是一个主线程创建一个无限循环的子线程,就算是主线程退出了,子线程还在执行,所以设置成守护线程,使之就算是无限循环,也会在主线程执行完毕后就退出 )
public class test { public static void main(String[] args) throws InterruptedException { MyDaemonThread myDaemonThread = new MyDaemonThread(); // 想在main主线程狗作完了,子线程的铲屎官就结束休息,就将子线程设置为守护线程即可 myDaemonThread.setDaemon(true); // 先设置再启动 myDaemonThread.start(); for (int i = 1; i <= 10; i++) { System.out.println("小小狗wu~哇哈哈哈哈~~~ " + i); Thread.sleep(1000); } } } class MyDaemonThread extends Thread { // Daemon:守护线程 int count = 0; @Override public void run() { for (; ;) { // 无限循环 try { count++; Thread.sleep(1000); //休眠1秒 (毫秒与秒是千位进) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("辛勤工作的铲屎官 " + count); } } }
-
线程的生命周期
- JDK 中用 Thread.State 枚举表示了线程的六种状态 ( 官方 )
- 实则有的是七种:Runnable 细化了 ( 内核部分编码看不出 )
- new一个线程,NEW 状态在调用 start0 后,进入可运行状态,即 Runnable 状态 ( 细化为就绪 Ready 状态、运行 Running 状态,真正运行还要取决于进程调度器 — 内核,操作系统决定的 );
- 运行 Running 状态 — ( 线程挂起或 Thread.yeild — ) —> 就绪 Ready 状态
- 若是运行 Running 完毕的就进入终止 Teminated 状态
- 可运行 Runnable 状态时
- 若是进入同步代码块 ( 获取一把锁 ) 就进入阻塞 Blocked 状态,
- 调用 wait()、join() ... 就进入等待 Waiting 状态
- 调用 sleep() ... 进入超时等待 TimedWaiting 状态
- 随后各自经过一些操作再回到可运行 Runnable 状态 ( 并非一定是 运行状态,也可能是就绪状态 )
public class test1 {
public static void main(String[] args) throws InterruptedException {
T t = new T(); // NEW
System.out.println(t.getName() + " 状态 " + t.getState());
t.start(); // RUNNABLE
// 只要不是终止就不断执行看处于何状态
while (Thread.State.TERMINATED != t.getState()) {
System.out.println(t.getName() + " 主状态 " + t.getState()); // T:sleep 主:sleep
Thread.sleep(500);
}
System.out.println(t.getName() + " 状态 " + t.getState()); // break:TERMINATED
}
}
class T extends Thread {
int count = 0;
@Override
public void run() {
while (true) {
for (int i = 0; i < 10; i++) {
System.out.println("T~hi~" + i);
try {
Thread.sleep(1000); // TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
}
break; // TERMINATED
}
}
}
Synchronized
- 在多线程编程,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何同一时刻,最多有一个线程访问,以保证数据的完整性
- 也可以理解为:线程同步,即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作
方式一
synchronized(对象) { // 得到对象的锁,才能操作同步代码
// 需要被同步代码;
}
- 拿到锁才能操作 ( 需要被同步代码 ),然后放回锁,另一个对象再来拿到锁,才能再进行操作
方式二
// synchronized 放方法声明中,表示整个方法 ——> 为同步方法
public synchronized void m (String name) {
// 需要被同步的代码
}
- 同一时刻只能有一个对象执行此方法
- 类似于试衣间,进一人上锁拴上,各操作后,出来开门栓,由下一个进入的人再上锁拴上
售票优化
public class test1 {
public static void main(String[] args) throws InterruptedException {
// 使用 synchronized 方式就会防止超卖现象
SellTicket001 sellTicket = new SellTicket001();
// 使用实现接口的方式来售票
new Thread(sellTicket).start(); // 第一个窗口(简写方式)
new Thread(sellTicket).start(); // 第二个窗口
new Thread(sellTicket).start(); // 第三个窗口
}
}
class SellTicket001 implements Runnable {
private int ticketNum = 200;
private boolean loop = true;
public synchronized void sell() { // 改成同步方法,在同一时刻只能有一个线程来操作执行此方法
if (ticketNum <= 0) {
System.out.println("售票结束..."); // 三个线程都要进来一次,所以输出三次售票结束
loop = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
+ "剩余票数 = " + (--ticketNum));
}
@Override
public void run() {
// run是重写的都有的方法,因内带while死循环(票没买完前),进一个线程上锁就不出去了,所以把 synchronized 用在其调用的售票方法里
// 使之可多个进while,但sell()同一时刻只能进一个
while (loop) { // 卖完就false
sell();
}
}
}
- 若是只出现一个或两个窗口的售票现象,那就把票数拉高、休眠拉低 ( 就会发现三个窗口都会出现售票情况且最好都会显示售票结束,不会超卖 )
分析同步原理
- 对象锁靠抢,若是窗口一抢到售票后返回然后还回锁,仍继续是三个窗口再抢
互斥锁
了解
- Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性
- 每个对象都对应于一个可称为 “ 互斥锁 ” 的标记 ( 在底层某位置 ),这个标记用来保证在任一时刻,只能有一个线程访问该对象
- 关键字 synchronized 来与对象的互斥锁联系,当某个对象用 synchronized 修饰时,表明该对象在任意时刻只能由一个线程访问
- 同步的局限性:导致程序的执行效率要降低 ( 类似 ETC 一杆一车,除了获取锁的线程外,其他线程都阻塞 )
- 同步方法 ( 非静态的 ) 的锁可以是 this 本身 (代码见 — 1. 与 2.1 ),也可以是其他对象 ( 要求是同一个对象 ) ( 代码见 — 2.2 )
- 同步方法 ( 静态的 ) 的锁为当前类本身 ( 类.class,代码见 — *1. 与 *2. )
public class test1 {
public static void main(String[] args) throws InterruptedException {
// 使用 synchronized 方式就会防止超卖现象
SellTicket001 sellTicket = new SellTicket001();
// 使用实现接口的方式来售票
new Thread(sellTicket).start(); // 第一个窗口(简写方式)
new Thread(sellTicket).start(); // 第二个窗口
new Thread(sellTicket).start(); // 第三个窗口
}
}
class SellTicket001 implements Runnable {
private int ticketNum = 200;
private boolean loop = true;
// 2.3.1 因为虽然三个窗口,但都是sellTicket这个对象,所以创建的object实际上也都是一个(实际就相当于把object当作锁了)
Object object = new Object(); // 2.2.1 可以是this当前(sellTicket)对象, 2.3.2 也可以是此对象里相同的object对象(只创建了一次)
// 1. sell() 加了 synchronized ,为同步方法,这时,锁在 this 当前(new了的)对象
// 2. 也可以在代码块上写synchronized,为同步代码块,互斥锁还是在this当前对象,或者是其他(同一个对象的)对象--(同一个sellTicket对象的其他(相对于sellTicket来说)对象:object)
public /* synchronized */ void sell() { // 1.1 改成同步方法,在同一时刻只能有一个线程来操作执行此方法
// synchronized (this) { // 2.2 同步代码块 this(本身为锁)
synchronized (/* this */ object) { // 2.3 同步代码块 其他对象(从本身提取出来一个object充当锁)
if (ticketNum <= 0) {
System.out.println("售票结束..."); // 三个线程都要进来一次,所以输出三次售票结束
loop = false;
return;
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("窗口 " + Thread.currentThread().getName() + " 出售了一张票 "
+ "剩余票数 = " + (--ticketNum));
}
}
// 若是如下:
public synchronized static void m1(){}
// *1. 静态的同步方法(锁是加在 SellTicket001.class 对象里)
// *2. 想在静态方法里面加(实现)一个同步代码块
public static void m2(){
synchronized (SellTicket001.class){ // *2.1 拿类名(而非this)
System.out.println("m2");
}
}
@Override
public void run() {
// run是重写的都有的方法,因内带while死循环(票没买完前),进一个线程上锁就不出去了,所以把 synchronized 用在其调用的售票方法里
// 使之可多个进while,但sell()同一时刻只能进一个
while (loop) { // 卖完就false
sell();
}
}
}
注意细节
- 同步方法如果没有使用 static 修饰,默认锁对象:this
- 如果方法使用 static 修饰,默认锁对象:当前类.class
- 且锁只在代码块时需要写出来 ( 显式体现出来 )
- 实现的落地步骤:
- 需要先分析上锁的代码 ( 如:售票时判断票数的时候 )
- 选择用同步代码块 ( 推荐,同步范围越小,效率越高 ) 或者是同步方法 ( 一大块方法 )
- 要求多个线程的锁对象为同一个 ( 如:售票里的 )
- 如,使用 Thread 时,底层是 new,两个窗口就是两个对象 ( 不同地址 )、两个 this 了 ( 锁也就是两个了,没有争夺的必要了 ),所以必须保证对象是同一个、共享的 ( 这样 this 锁只有一个,才会争夺,才能锁住 )
线程的死锁
- 多个线程都占用了对方的锁资源,但不肯相让,导致了死锁,在编程是一定要避免死锁的发生
- 案例:母亲说小明要先写完作业才能玩手机,小明说要让我先玩手机,才去写作业
public class test1 {
public static void main(String[] args) {
// 模拟死锁
DeadLockDemo o1 = new DeadLockDemo(true);
DeadLockDemo o2 = new DeadLockDemo(false);
o1.setName("A");
o2.setName("B");
o1.start();
o2.start();
}
}
class DeadLockDemo extends Thread {
static Object o1 = new Object();
static Object o2 = new Object();
boolean flag;
public DeadLockDemo(boolean flag) { //构造器
this.flag = flag;
}
@Override
public void run() {
// T 的话,线程A就会先得到、持有 o1 对象锁,然后去尝试获取 o2 对象锁
// 拿不到 o2 就会 Blocked
if (flag) {
synchronized (o1) { // o1:对象互斥锁,下面的都是同步代码块
System.out.println(Thread.currentThread().getName() + "进入1");
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入2");
}
}
} else {
synchronized (o2) {
System.out.println(Thread.currentThread().getName() + "进入3");
synchronized (o1) {
System.out.println(Thread.currentThread().getName() + "进入4");
}
}
}
}
}
-
输出:( 始终卡着也不终止 )
A进入1
B进入3
释放锁分析
会释放
- 当前线程的同步方法、同步代码块执行结束
- 上厕所,上完出来
- 当前线程的同步方法、同步代码块中遇到 break、return
- 没完事,经理喊出来修改 bug,不得已出来
- 当前线程的同步方法、同步代码块中出现了未处理的 Error 或者 Exception,导致异常结束
- 没有正常完事,发现忘带纸了,不得已出来
- 当前线程的同步方法、同步代码块中执行了线程对象的 wait() 方法,当前线程暂停,并释放锁
- 没有正常完事,觉得需要酝酿下,所以出来等会再进去
不会释放
- 线程执行同步代码块或者同步方法时,程序调用 Thread.sleep ( 进入 TimeWaiting 状态 )、Thread.yield ( 切换为 Ready 状态 ) 方法暂停当前线程的执行,不会释放锁
- 上厕所太困了,在坑位上睡了一会
- 线程执行同步代码块时,其他线程调用了该线程的 suspend() 方法将该线程挂起 ( 切换为 Ready 状态 ),该线程不会释放锁
- 注意:应尽量避免使用 suspend() 和 resume() 来控制线程,方法不再推荐使用
练习
题一
- 在 main 方法中启动两个线程,第一个线程循环随机打印 100 以内的整数,直到第二个线程从键盘读取了 " Q " 命令。
public class test {
public static void main(String[] args) {
AA a = new AA();
BB b = new BB(a);
b.start();
a.start();
}
}
// A不会继承别的类,所以 extends 也无妨
class AA extends Thread {
private boolean loop = true;
@Override
public void run() {
while (loop) {
// 输出0~100
System.out.println((int)(Math.random() * 100 + 1));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("a线程退出");
}
// 才能方便被别的线程以通知方式结束线程
public void setLoop(boolean loop) {
this.loop = loop;
}
}
// 直到第二个线程从键盘读取了"Q"命令
class BB extends Thread {
private AA a;
private Scanner scanner = new Scanner(System.in);
public BB(AA a) { // 直接在构造器中传入AA类对象
this.a = a;
}
@Override
public void run() {
while (true) {
// 接收用户的输入
System.out.println("请输入你的指令(注意要在输入后及时按下回车)--Q表示退出");
char key = scanner.next().toUpperCase().charAt(0); // 卡住等待接收(toUpper转大写)
if (key == 'Q') {
// 以通知的方式结束a线程
a.setLoop(false);
System.out.println("b线程退出");
break;
}
System.out.println("输入非可接受的指令");
// 若输入不是 Q,就不会break退出,就会再提示输入
}
}
}
-
例输出:
请输入你的指令(注意要在输入后及时按下回车)--Q表示退出
62
65
c
输入非可接受的指令
请输入你的指令(注意要在输入后及时按下回车)--Q表示退出
90
24
q
b线程退出
a线程退出
题二
- 有两个用户分别从同一个卡上取钱 ( 总额:10000 ),每次都取 1000,当余额不足时,就不能取款了,不能出现超取的现象 ( 即:线程同步问题,在取钱前加一个锁供争抢获取,Blocked 时抢到了锁的才能取钱 )
public class test1 {
public static void main(String[] args) {
T t = new T();
Thread thread1 = new Thread(t); // t放进线程内
thread1.setName("t1");
Thread thread2 = new Thread(t);
thread2.setName("t2");
thread1.start();
thread2.start();
}
}
// 因为这里涉及到多个线程共享资源,所以我们使用实现 Runnable方式
// 每次取 1000
class T implements Runnable {
private int money = 10000;
@Override
public void run() {
while (true) {
// 解读这里使用 synchronized 实现了线程同步
// 当多个线程执行到这里时,就会去争夺this对象锁(前提是同一个对象t)
// 哪个线程争夺到(获取)this对象锁,哪个就执行 synchronized 代码块,执行完后,会释放this对象锁
// 争夺不到this对象锁的,就blocked,准备继续争夺
// 锁释放了,再一起同起跑线争夺(this:非公平锁,1抢到后可能下一个还是1抢到了)
synchronized (this) {
// 判断余额是否够
if (money < 1000) {
System.out.println("余额不足");
break;
}
money -= 1000;
System.out.println(Thread.currentThread().getName() + "取出了1000 当前余额" + money);
}
// 休眠1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
-
输出:
t1取出了1000 当前余额9000
t2取出了1000 当前余额8000
......
t2取出了1000 当前余额1000
t1取出了1000 当前余额0
余额不足
余额不足- 输出工整、不乱、不错