回顾
首先回顾一下线程和进程,进程是系统分配资源的基本单位,线程是调度执行的基本单位,一个进程可以包含多个线程,每一个线程都是一个单独的执行流,都有各自单独的任务,线程之间会相互影响,而进程不会,我们把线程称为轻量级进程,让同一个进程的多个线程去共享一份系统资源(创建线程,能够在一定程度上减少申请资源和销毁线程时释放资源带来的开销);
每个线程都是一份单独的执行流,都可以单独的参与CPU的调度。共享一份系统资源(文件描述符表和内存指针)。
操作系统,提供了一些操作线程的API,用c或c++实现,通过JDK封装成java风格的api。下面我们讲讲如何使用这些API。
我们将Thread类称为线程,而里面的run方法是线程执行的入口。每个线程跑起来都会去执行一定的逻辑。
所谓线程,就是在操作操作系统的内核,操作系统最核心的部分。
操作系统的内核态:有一些程序,需要针对一些系统提供的软硬件资源进行操作,这些操作需要调用系统提供的API,进一步在内核中完成这样的操作。
接下来我们讲线程是如何创建的
class Mythread extends Thread{//我们自己创建的线程需要继承类Thread,重写里面的run方法,我们可以把run方法当作线程执行的入口。
pubic void run(){
while(true){
System.out.println("hello thread!");
}
}
}
public class Main(){
public static void main(){
Thread t=new Mythread();//通过这个操作来创建线程
t.start();//通过start方法来进行该线程的启动
while(true){
System.out.println("hello Main!");
}
}
}
两个线程,一个是Main方法,一个是自己创建的t线程,两个各自执行各自的,互不干扰。
********************************************************
这里有一个点需要注意,当有多个线程的时候,这些线程执行的先后顺序,是不确定的。
因为在操作系统内核中,有一个“调度器模块”。这个模块的实现方式,是一种类似于“随机调度”的效果。
随机调度
1.一个线程,什么时候被调度到CPU上执行,时机是不确定的。
2.一个线程,什么时候从cpu上下来,让其他线程被调度,时机是不确定的。
我们称为“抢占式先机”,为后面多线程的线程安全问题埋下伏笔。
另外关于谁先执行的问题,主线程,调用start方法之后,就立刻往下打印了。于此同时,内核需要通过刚才线程的API构建出线程来,并且去执行run方法。
一般来说,是主线程执行的更快,因为创建线程本身也有一定的开销,不能说为0,在第一轮打印,创建线程开销本身影响下,导致hello Thread!比hello Main!略慢一筹。
进程创建线程第一线程是最大的,剩下的开销都比较小,但都不是0;
线程创建的五种方式:
1.继承类Thread来创建线程
class MyThread extends Thread{
@Override
public void run(){
while(true)
{
System.out.println("hello Thread!");
}
}
}
public class Main{
public static void main(String[]args){
Thread t=new MyThread();
t.start();
while(true)
{
System.out.println("hello Main!");
}
}
}
2.使用Runnable接口来创建线程
public class Main{
public class MyThread implents Runnable{
public void run(){
while(true)
{
try{Thread.sleep(1000);
}
catch(InterruptedException e)
{throw new RuntimeException(e);
}
System.out.println("hello Thread!");
}
}
}
public static void main(String[]args)
{
Thread t=new Thread(new MyThread());
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
Runnable接口可以抽象出一段可以实现的代码
3.使用匿名内部类来创建线程
public class Main{
public static void main(String[]args){
Thread t=new Thread(){
public void run()
{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
};
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
4.使用接口匿名
public class Main{
public static void main(String[]args){
Thread t=new Thread(new Runnable(){
public void run()
{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
5.最常用的,lamab表达式来创建
public class Main{
public static void main(String[]args){
Thread t=new Thread(()->{
while(true){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
while(true){
System.out.println("hello main");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
Thread的创建方法
Thread();
Thread(String name);//创建带名字的线程
Thread(Runnable target);使用RunNbale来创建线程
Thread(Runnable target,String name);
Thread(ThreadGroup group,Runnable target);ThreadGroup为线程组,可以分组管理
Thread的相应属性
Thread t=new Thread();//创建线程
t.getId();//获取线程ID;
t.getName();//获取线程名称
t.getState();//获取线程状态
t.getPriority();//获取优先级
t.isDaemon();//是否为后台进程
t.isAlive();//判断线程是否存货
t.isInterrupted();//判断线程是否阻塞
public class ThreadDemo7 {
public static void main(String[]args) throws InterruptedException {
Thread t=new Thread(()->{
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("hello Thread!");
}
},"这是我的线程");
t.start();
Thread.sleep(1000);
System.out.println(t.getName());
}
}
运行结果如上图所示
咱们在代码创建线程的时候,默认是前台进程,前台进程会阻止进程结束,只要前台进程没执行完,进程就不会结合,即使Main线程已经执行完了.
t.setDaemon(true);//注意,setDaemon要放在start之前,不然会报错
t.start();
之后的运行结果.
在使用setDaemon(true)之后,true是后台,不会阻止进程结束.只要前台进程一结束,后台进程无论结束没结束都必须跟随前台进程结束.
Thread创建出来的线程是有生命周期的
public class Threaddemo8 {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
System.out.println("start之前的存活"+t.isAlive());
t.start();
System.out.println("start之后的存活"+t.isAlive());
Thread.sleep(2000);
System.out.println("2000ms之后的存活"+t.isAlive());
}
}
运行结果,可以使用isAlive函数来进行查看线程是否哦存活.
当线程执行完之后,内核中的线程就会释放了(pcb中的内核就会释放了).
*******************在这里要注意一点,start方法只能执行一次*************
中断线程
要想中断一个线程,必须要让他的代码逻辑符合中断的一个机制,就需要你main线程的配合,不然这是没有办法进行终止的,我们可以来设置标志位来中断线程,比如:
public class ThreadDemo9 {
private static boolean isQuit=false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while(!isQuit){
System.out.println("我是一个线程,正在工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("线程工作执行完毕");
});
t.start();
Thread.sleep(3000);
isQuit=true;
}
}
这是运行结果.
设置标志位来中断线程
public class ThreadDemo10 {
private static boolean isQuit=false;
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
while(!Thread.currentThread().isInterrupted()){
System.out.println("我是一个线程,正在工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("线程工作执行完毕");
});
t.start();
Thread.sleep(3000);
System.out.println("让线程结束");
t.interrupt();
}
}
这里我们使用Thread.curreThread()来获取当前实例
这里我们可以看出打印了异常,并且线程并没有被终止.
为什么呢?
在这里,我们通过interrupt方法设置了标志位,但确将sleep函数提前唤醒了,这使得发生了异常,sleep函数自动清除还原了标志位.因此,循环并没有并中断(这里要注意,之后或许并不只有一种让标志位重塑),
提前唤醒,会做两件事
1.抛出Inter若特点Exception(紧接着就会被catch捕捉到)
2.清除Thread对象的isInterrupted标志位,通过interrupt方法已经将标志位设置为true了,但是sleep唤醒,又将标志位设回false了,所以循环会继续进行执行.
要想让线程结束,只需在catcj里加上break就行.
这里的sleep清除是有一些逻辑在里面的,是为了给我们操作空间,你到底是想让线程沉睡,还是让他唤醒,这里有歧义,所以sleep会将标志位还原,之后怎么设置,我们可以在catch里面加上自己的一些逻辑.
1)让线程立即结束,加上break;
2)不让线程结束,继续执行,不加break;
3)让线程执行一些逻辑之后,再结束,写一些其他代码,再break;
t.interrupt();让线程结束
Thread.currentThread().isInterrupted();判断标志位是否为true.
对于一个服务器程序来说,稳定性是最重要的.
无法保证服务器一直不出问题,这些所谓的问题就会在java代码中,以异常的形式体现出来.
可以通过catch语句,对这些语句进行处理
实际开发中的catch做的代码
1)可能会尝试自动恢复
能自动恢复就尽量自动恢复,比如出现了一个网络通信相关的异常,就可以在catch中尝试重连网络.
2)记录日志(异常信息记录到文件里面)
有些情况,并非很严重的问题,只需要把这个问题记录下来,(并不需要立即解决),等之后有空了再去解决.
3)发出报警
针对一些比较严重的问题,包括但不限于发短信邮件,打电话
4)也有少数的正常的业务逻辑,会依赖到catch,比如文件操作中有的方法,就是要通过catch结束循环
(非常规用法)
Java中,线程的终止是一种"软性操作".必须要对应的线程配合,才能把终止执行下去.
相比之下,系统原生的API其实提供了强制了终止线程的操作,无论你线程是否配合,无论线程执行到哪个代码,都能强行把这个线程给干掉.
java的api并没有提供这样的操作.
这种操作,其实是弊大于利,如果强行干掉一个实验,很有可能线程执行到一半,就可能出现一些残留的临时性质的"错误"的数据.
例子:加入这个线程正在执行写文件的操作,写文件的数据有一定的格式要求(写一个图片文件)
如果图片写到一半,线程嘎了.图片文件就无法正确打开了.
lambda表达式/匿名内部类是可以访问到外面定义的局部变量的(变量捕获语法规则)
lambda要求捕获的临时变量是final类型或者事实final(即要求没有修改)
当写成成员变量的时候就可以,因为内部类本来就可以访问外部类成员.
这里不能让局部变量修改是因为生命周期可能不一样.
以局部变量boolean isQuit为例
java在变量捕获中,是在lambda表达式在自己的栈帧中创建一个新的isQuit,并把外面的值拷贝过来,
为了避免isQuit的值不同步,java干脆就不让你修改isQUit的值.
如何等待线程结束
t.join();
package thread;
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(()->{
int n=5;
for(int i=0;i<n;i++)
{
System.out.println("我是一个线程,正在工作中");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();//该操作让线程结束
System.out.println("这是主线程, 期望这个日志在 t 结束后打印");
}
}
可以看到执行的一个结果
多个线程的执行顺序是不确定的(抢占式执行,随机调度)
虽然程序底层的调度是无序的,但是可以在程序中,调用一些api来影响到线程执行的顺序.
join就是一种方式
影响线程执行的先后顺序.
在main线程中调用t.join()就是让main线程等待t线程结束.
执行join的时候,就看t是否在正常运行
如果t在运行中,那么main就会阻塞(main线程暂时不会参与到cpu的调度中)
如果t运行结束,main线程就会从阻塞中恢复过来,并且往下执行~
(阻塞,让这两个线程结束的先后时间,产生了先后关系)
*********************
一个典型情况就是使用多个线程并发进行一系列的计算
用一份线程阻塞等待上述计算线程,等到所有的线程都计算完了,最终这个线程汇总结果.
线程执行顺序是不确定的,线程执行的任务的时间也是不可预估的
如果单个线程,无法发挥出多核cpu的优势
如果是多个线程,势必要有一个线程进行汇总结果.