前言
接上一篇内容......
中断线程~
中断线程就是让线程结束~(让内核里面的CPU被销毁)
让线程结束的关键,就是让线程对应的入口方法,执行完毕.
入口方法:
1.继承Thread重写的run方法
2.实现Runnable重写run方法
3.lambda
这些都算是它的入口方法,只要入口方法执行完了,线程就随机结束了
像这种情况:
//Thread是 Java 标准库中描述一个关于线程的类 //常用的方法就是自己定义一个类继承 Thread //重写 Thread 中的 run 方法. run方法就是表示线程要执行的具体任务(代码). class MyThread extends Thread{ @Override public void run() { System.out.println("hello,Thread"); } } public class Test { public static void main(String[] args) { Thread thread = new MyThread(); //stat 方法,就会在操作系统中真的创建一个线程出来,(内核中搞个PCB,加入到双链表中) //这个新的线程,就会执行 run中所描述的代码 thread.start(); //thread.run() } } 像这种情况
像这种情况,只要run执行完,线程就随之结束了
更多情况下,线程不一定这么快就能执行完run方法:
public class TestDome1 { public static void main(String[] args) { Thread t = new Thread(new Runnable() { @Override public void run() { while (true){ //打印当前线程的名字 //Thread.currentThread()这个静态方法,获取到当前线程实例 //哪个线程调用这个方法,就能获取到当前线程实例 System.out.println(Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } },"myThread"); t.start(); //在这里也打印一下这个线程的属性 System.out.println("id: " + t.getId() ); System.out.println("name: " + t.getName()); System.out.println("state: " + t.getState()); System.out.println("priority: " + t.getPriority()); System.out.println("isDaemon" + t.isDaemon()); System.out.println("isInterrupted: "+t.isInterrupted()); System.out.println("isAlive " + t.isAlive()); } }
如果run里面带的是一个死循环,此时这个线程就会一直持续运行,直到整个进程结束.
实际开发中,并不希望线程run就是一个死循环,更希望能够控制这个线程,按照咱们的需要随时结束.
为了实现这样的效果,就有一些办法:
1.简单粗暴的办法,就是使用一个boolean变量来作为循环结束标记
public class TestDome { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { while (flag){ System.out.println("线程运行中....."); try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程结束!"); } }; t.start(); //主循环中也等待个三秒 Thread.sleep(3000); //三秒之后,就把flag 改成 false flag = false; } }
2.刚才是使用程序猿自己定义的变量作为循环标记,还可以使用标准库里内置的标记
获取线程内置的标记位:线程的isinterupted()(打断的意思)判定当前线程是不是应该结束循环
修改线程的内置标记位:Thread.interrupt()来修改这个标记位
public class TestDome2 { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { //默认情况下isInterrupted(打断的意思)这个方法的值为false(默认是一个未被打断的状态,所以说要取反) while (!Thread.currentThread().isInterrupted()){ System.out.println("线程运行中..."); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); //在这里再加个break 就可以保证循环能结束了 break; } } } }; t.start(); //在主线程中,通过t.interrupt()方法来设置这个标记位 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //这个操作就是把Thread.currentThread().isInterrupted()给设置成true t.interrupt(); } }
打印结果:
当我们三秒之后,insterrupt方法貌似并没有修改这个标记位......循环看起来再继续
同时这里还有个异常:
就是这个异常:
interrupt方法并没有修改这个标记位,而是直接抛了个异常
这里的interrupt方法可能有两种行为:
1.如果当前线程正在运行,此时就会修改Thread.currentThread().isInterrupted()标记位为true
2.如果当前线程正在sleep/wait/等待锁.......此时就会触发interruptedException
在这里加上break就可以保证循环结束了
isInterrupted()这个是Thread的实例方法
和这个方法还有一个类似的
interrupted()这个也是Thread的类方法(static)
都能去判断当前的标记位是不是被设置上了
使用两者的区别:
使用这个静态方法,会自动清楚标记位
例如:调用interrupt()方法,把标记位设为true,就应该结束循环
当调用静态的interrupted()来判定标记位的时候,就会返回true,同时会把标记位再改回false,下次再调用interrupted()就返回false
如果是调用非静态的isinterrupted()来判定标记位,也会返回true.同时不会对标记位进行修改.后面再调用isInterrupted()的时候就仍然返回true
线程等待 ~
线程和线程之间,调度顺序是完全不确定的(取决于操作系统调度器自身的实现)
但是有的时候,希望这里的顺序是可控的
此时,线程等待就是一种办法.
这里的线程等待,主要就是控制线程结束的先后顺序~
一种常见的逻辑:有一个t1线程,创建t2,t3,t4.让这三个新的线程分别执行一些任务,然后t1线程最后在这里汇总结果~~
这样的场景我们就需要t1结束时机必须比t2,t3,t4都迟
比如现在有一个线程实例:
Thread t = new Thread()
当我们调用 t.join() 的时候,执行到这个代码,此时调用这个代码的线程就会阻塞等待,代码就不继续往下走了.具体说就是操作系统短时间内不会把这个线程调度到CPU上了
以往的代码,只要代码一写好跑起来,此时就会快速的按照顺序执行...
但是join就会触发阻塞等待
当我们执行start()方法的时候,就会立刻创建出一个新的线程来.
同时main这个线程也立刻往下执行,就执行到t.join
执行到t.join的时候就发现,当前t线程还是在运行中的.....
只要t在运行中,join方法就会一直阻塞等待,一直等到t线程执行结束(run方法执行完了)
public class TestDome3 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { int count = 0; while (count < 5){ count++; System.out.println("线程运行中"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t.start(); //此处的join就会阻塞等待 System.out.println("join开始执行"); t.join(); System.out.println("join执行结束"); } }
查看结果:
假设调用join的时候,t线程已经结束了,会咋样?
public class TestDome3 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { int count = 0; while (count < 5){ count++; System.out.println("线程运行中"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } }; t.start(); Thread.sleep(7000); //此处的join就会阻塞等待 System.out.println("join开始执行"); t.join(); System.out.println("join执行结束"); } }
打印结果:
join方法:
join无参数版本:相当于死等.
join有参数版本,参数就是最大等待时间.
在实际开发中,使用死等操作,往往是比较危险的
典型的就是网络编程中
发了一个请求,希望得到一个回应~~
由于种种原因,回应没有到达.....
此时我们的程序就不应该一直死等
获取当前线程引用
getCurrentThread() 能够获取到当前线程对应的Thread实例的引用
public class TestDome4 { public static void main(String[] args) { Thread t = new Thread(){ @Override public void run() { System.out.println(Thread.currentThread().getId()); System.out.println(this.getId()); } }; //在这个代码中,Thread.currentThread()得到的就是t这个引用,也就相当于是在run中直接使用this t.start(); } }
打印结果:
在这个代码中看起来就好像是this 和 Tread.currentThread没啥区别
但是实际上,没区别的前提,是使用这种继承Thread,重写run的方式创建线程,才是没区别
如果当前是通过Runnable或者lambda的方式,就不行了
这个代码中this指向的是Runnable实例,而不是Thread实例了.此时也就没有getId这样的方法了
休眠当前线程
sleep这个方法,本质上是把线程PCB给从就绪队列,移动到了阻塞队列~
操作系统管理线程
1.描述:PCB
2.组织:双向链表(其实不仅仅是一个)
线程的状态
用于辅助系统对于线程进行调度这样的属性
public class TestDome5 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { while (!Thread.currentThread().isInterrupted()){ } } }; System.out.println(t.getId() + ":" + t.getState()); t.start(); System.out.println(t.getId() + ":" + t.getState()); Thread.sleep(1000); t.interrupt(); System.out.println(t.getId() + ":" + t.getState()); } }
打印结果:
**NEW:**当前已经把Thread对象创建出来了,但是内核里面的PCB还没创建出来(意思是当前只是把任务给它创建出来,但是没有任务去具体执行
**RUNNABLE:**当前的PCB也创建出来了,同时这个PCB随时待命(就绪)这个线程可能是正在CPU上运行,也可能是在就绪队列中排队...
public class TestDome5 { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { while (!Thread.currentThread().isInterrupted()){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break; } } } }; System.out.println(t.getId() + ":" + t.getState()); t.start(); Thread.sleep(1000); System.out.println(t.getId() + ":" + t.getState()); Thread.sleep(1000); t.interrupt(); Thread.sleep(1000); System.out.println(t.getId() + ":" + t.getState()); } }
打印结果:
**TIME_WAITING:**表示当前的PCB在阻塞队列中等待~
这样的一个等待是一个"带有结束时间"的等待
这个操作就会触发这个状态~~
**TERMINATED:**这个状态就表示当前PCB已经结束了.Thread对象还在,此时调用获取状态,得到的就是这个状态
**WAITING:**线程中如果调用了wait方法,也会阻塞等待.此时处在WAITING状态.(死等)除非是其他线程唤醒了该线程
**BLOCKED:**线程中尝试进行加锁,结果发现加锁已经被其他线程占用了.此时该线程也会阻塞等待.这个等待就会在其他线程释放锁之后,被唤醒
yield();这个方法并不会经常用到.效果是让线程主动让出CPU,但是不改变线程状态
这个操作咱们在Java很少会使用
当前这个几个状态,都是Java的Thread类的状态和操作系统内部PCB里面的状态的值并不完全一致的
线程安全
代码举例:
预期让count变量能够自增十万次
public class TestDome6 { static class Counter{ public int count = 0; public void increase(){ count ++; } } static Counter counter = new Counter(); public static void main(String[] args) { //此处创建两个线程,分别针对 count 自增5w次 Thread t1 = new Thread(){ @Override public void run() { for (int i = 0; i < 50000; i++) { counter.increase(); } } }; t1.start(); Thread t2 = new Thread(){ @Override public void run() { for (int i = 0; i <50000 ; i++) { counter.increase(); } } }; t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(counter.count); } }
多次打印结果:
预期能够自增10w次
但实际上自增的次数,无法确定,每次运行结果都不一样
写代码最害怕的就是"不确定'
显然,这个代码我们视为一个"bug"
为啥会产生这个情况
大概率是和并发执行相关
由于多线程并发执行,导致代码中出现了BUG,这样的情况就称为"线程不安全"
分析程序执行的过程~
count++的详细过程;分成三个步骤~~
2.执行++操作(就是在CPU里面把0给它变成1)
3.把CPU的值写回到内存中~
此时我们这1次++就完成了
如果是两个线程并发的执行count++,就容易出现问题~~
我们来看一下两个线程并发执行的一个情况
我们给前面三个步骤分别起名为A,B,C
假设两个线程分别在不同的CPU上执行~~
产生线程不安全的原因:
1.线程之间是抢占式执行的(根本原因,线程不安全的万恶之源)
抢占式执行,导致两个线程的里面操作的先后顺序无法确定
这样的随机性,就是导致线程安全问题的根本存在
我们无力改变,这是操作系统内核实现的
2.多个线程修改同一个变量(和代码的写法密切相关)
一个线程修改同一个变量,没有线程问题!!不涉及并发,结果就是确定的
多个线程读取同一个变量,也没有线程安全问题!!读只是单纯的把数据从内存放到CPU中~,不管怎么读,内存的数据始终不变~~
多个线程修改不同的变量,也没有线程安全问题!!其实就认为就类似于第一个情况,一个线程修改一个变量
所以为了规避线程安全问题,就可以尝试变换代码的组织形式,达到一个线程只改变一个变量.....
有的场景下能这样变换,但是有的场景下不能这样变换
3.原子性~
像++这样的操作,本质上是三个步骤,是一个"非原子"的操作
像 = (赋值)操作,本质上就是一个步骤,认为是一个"原子"的操作
像当前,咱们的++操作本身不是原子的,可以通过加锁的方式,把这个操作编程原子的~~
4.内存可见性~
和原子性有点类似
例如:
线程1,读取变量
线程2,对变量自增
这样读,线程1就读到的是修改之前的值
这样读,线程1读到的就是修改之后值
像上面的两种情况都是OK的
1
3
4
如果我们的线程2更复杂了是循环的进行自增
比如说是按照这样一个方式来执行
线程2循环执行很多次自增
很多次自增就会涉及到很多次A和C
CPU执行B操作的速度比执行A和C要快1w倍~
这个时候线程2为了能够算的更快,于是就偷懒了~~
线程2这里面就不会每次自增都去A和C了
而是变成这个样子:
为了提高程序的整体效率,于是线程2就会把中间的一些A和C操作省略掉
这个省略操作是编译器(javac)和JVM(java)总和配合达成的效果~~
如果只是单线程下,这样的优化,没有任何副作用和
如果是多线程下,另外一个线程也尝试读取/修改这个数据
此时就出大事了
按照刚才的画法:
预期线程1读到的数据应该是1
但是实际上由于这样的优化可能导致线程1读到的是0了
如果把中间的A和C操作都省略掉,之后最后有一个C操作
这个时候A读到的其实还是内存中原来的值
这里的三次B操作都是在CPU内部来完成的,并没有把这样的结果写回到内存中
于是线程1再来去读内存就发现读到是旧的结果
1
至于这样的问题如何解决?
volatile关键字,就是用来解决这个问题的
我们下一篇继续.......
版权归原作者 K稳重 所有, 如有侵权,请联系我们删除。