JavaEE & 进程与线程
1. 多线程
一个进程即同一个程序,内部有多个任务,多线程更合适,因为互相影响也仅限于此程序,这样就提高了效率~
例如:QQ的聊天,邮箱,游戏,录制,截图,QQ空间~
多个程序,即多个进程,那么他们之间就是相互隔离的,一个出bug,其他进程不受影响~
例如:CCtalk,QQ,微信~
有可能有人会说,微信摄像头和QQ摄像头或者CCtalk摄像头这些不同进程的线程之间也会影响~
这只是因为你摄像头就只有一个~
1.1 线程越多,越好?
由于CPU核心数有限,即打工仔有限
线程太多,打工仔们也分身乏术~
并且,线程是需要有系统资源的~
一些线程等不及了,就要挤掉一些线程~
也就是说,线程之间的并发,并不满足肉眼“同时”~
“多开一个进程”,核心数没变
多加一个主机,多一个CPU就可以解决,即“分布式系统”~
这样很容易导致一个线程异常了,整个进程也就狗带了~
举个栗子:
《一群人吃猪扒》,一张大桌子有100份猪扒(进程),人数高达1000个,每个人要吃若干量猪扒(线程),如果有一个人一直吃不到,直接干架了,把桌子给掀了,“得了,都别吃了”
1.2 进程与线程的区别总结
进程包含线程
进程有自己独立的内存空间和文字描述符表
一个进程的多个线程之间共用~
进程是操作系统分配内存的基本单位
线程是操作系统调度执行的基本单位
宏观上:
微观上:
进程之间有“隔离性”,一个进程挂了并不会影响别的进程
同一进程不同线程共用同一份地址空间和文件描述符表,一个线程挂了,可能会导致整个进程直接崩了~
2. Java与多线程
Java不提倡多进程编程
Java的jdk并没有封装多少多进程API,有也很简陋~
Java很提倡多线程编程
Java的jdk有很多多线程API,很完善~
接下来的内容,新概念会很多,学习并且习惯就好~
2.1 Java标准库提供的一个类 Thread(普通类)
通过这个Thread类表示线程
我这边提供五种语法,即描述一个线程的方式,以及启动一个线程的方式~
5.1.1 实例化子类法
class MyThread extends Thread { @Override public void run() { while (true) { System.out.println("好耶 ^v^"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Test1 { public static void main1(String[] args) throws InterruptedException { int x = 1; Thread t = new MyThread(); t.start(); while(true) { System.out.println("不好耶 T.T"); Thread.sleep(100); } } }
main方法是一个线程
我们有时会遇到,不同类可以重复定义main方法
而这多个main是不同的线程
并且毫不相干
自己建立的线程也是一个线程
两个线程并发执行
为了体现并发性,我写了两个死循环
补充知识:sleep方法,即休眠,让线程进入阻塞态~
* 该状态下,线程无法进行执行
* 输入一个long类型值(单位ms),表示休眠时间~
* 这个类是Thread系统类的一个静态方法,直接通过类名进行引用
* 这个方法附带一个“编译时异常”,InterruptedException,这个异常在这里不做赘述,但是在后面很重要,现在浅浅认为它就是个异常而已~
处理异常:(alt + 回车)
try catch 环绕
throws
main线程是可以用的
但是自己创建的线程不行,因为run方法可以throws,但是start方法并没有进行异常处理,也没有进行throws~
调用start启动线程,run方法会被Java自动调用~
run方法可以看成是别的线程的main方法!
怎么实现与main线程并发的,不是我们需要想的~
run方法是线程运行,被核心调度的一个方法
start方法是启动一个线程,让Java自己去调用run,自己完成并发
不难看出,run方法一旦被调用,就只会让run方法执行完,即该线程执行完,才能轮到下一个语句,其他线程全部都处于阻塞态~
而start方法则是让CPU的核心调度线程的时候再自动调用run方法,完成并发编程~
本质上,五种语法都是通过重写run方法~
测试:(按ctrl + F2结束程序)
两个线程“同时”执行:
调用run方法,则只执行一个线程,等该线程结束,才能继续执行后续操作~
找到jconsole.exe,这是jdk提供的根据,可以观看也仅能观看Java中线程的详情~
双击~
可能会出现,啥都没有的情况,我们需要用管理员的身份打开~
双击我们自己写的线程~
本来就不安全,知识提醒你罢了,点了~
允许~
观察详情(要让线程一直运行才能看到,否则会连接失败)
可以看出,我们代码层面看到的是两个线程,但是实际上不止两个~
只有这两个是我们自己写的,其他都是jvm自己创建和启动的~
jvm启动这些去干脏活累活~
点击两个线程~
显示的信息就是执行情况~
比如执行到哪里~
用这个Java监视器 jconsole去调试是一个普遍的方法~
5.1.2 实例化子类法 & 匿名内部类法
public class Test2 { public static void main1(String[] args) throws InterruptedException { Thread t = new Thread(){ @Override public void run() { while(true) { System.out.println("好耶 ^v^"); try { Thread.sleep(100); } catch (InterruptedException e) {//sleep抛出中断异常,我们要让jvm去安抚它~ e.printStackTrace(); } } } }; t.start(); while(true) { System.out.println("不要耶~~~"); Thread.sleep(100); } } }
不做过多阐述,就是用匿名内部类对类进行重写的基本操作~
本质上跟上面那个方法完全一样
只不过我不需要专门写一个类去继承,系统不需要加载多一个类
我们也不需要编个名字哈哈 ^ v ^
测试:(按ctrl + F2结束程序)
两个线程“同时”执行:
5.1.3 Thread提供的构造方法,”传入工具“法
在之前构造搜索二叉树的时候,传入比较器,就是一个“传入工具”法~
类似的,Thread系统类也提供了几个构造方法
重点掌握如图两种~
单纯传入一个“线程可执行器”
这样通过这个“线程可执行器”,就可以重写run方法了~
这个构造方法内部就会利用这个工具去重写run~
我们的工具类要实现Runnable接口~
Runnable是个函数式接口~【铺垫】
多传入一个String name,别名
这个的用处就是给线程起一个别名,更加显眼~
这个别名会在jconsole监视会很显眼~
class Goble implements Runnable{ @Override public void run() { while(true) { System.out.println("好耶 ^v^"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public class Test3 { public static void main1(String[] args) { Thread t = new Thread(new Goble()); //传一个工具过去 t.start(); while(true) { System.out.println("不好耶T.T"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
测试跟上面是一样的,正常
看一看别名的那个案例吧~
是不是很显眼 ^ v ^
5.1.4 "传入工具"法 + 匿名内部类
不做赘述~
public class Test4 { public static void main1(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { @Override public void run() { while(true) { System.out.println("好耶^v^"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }); t.start(); while(true) { System.out.println("不好耶T.T"); Thread.sleep(100); } } }
5.1.5 lambda表达式法
这个很重要,也是最常使用的方法
lambda表达式可以模拟匿名内部类~
并且这必须是函数式接口
所以5.1.2 的那个方法是不能写成lambda表达式的,因为Thread不是函数式接口
而Runnable就是一个函数式接口~
lambda表达式会根据对应环境,确定函数式接口
并且对那个单一的方法进行重写~
public class Test5 { public static void main1(String[] args){ Thread t = new Thread(() -> { while(true) { System.out.println("好耶^v^"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); break; } } }); t.start(); try { System.out.println("不好耶T.T"); Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); break; } } }
测试结果一致~
最简洁的一个方式~
lambda表达式知识点所在博客:JAVA数据结构 & Java语法完善补充_java数据结构语法_s:103的博客-CSDN博客
有时候测试的时候会发现要么main线程不见了,要么个别线程不见了,原因是执行太快,立马结束了~
3. Thread常见属性
属性 封装方法
ID线程编号 getId()
名称 getName()
状态 getState()
优先级 getPriority()
是否为后台线程 isDaeMon()
是否存活 isAlive()
是否被中断 isInterrupted()
3.1 后台线程
我们创建的线程都是前台线程,前台线程会阻止Java程序结束
而jvm自动创建的后台线程,不会阻止Java程序结束,一旦前台线程结束,后台线程无论是否执行完,都会结束程序~
创建的线程可以设置的后台,setDeMon(true)
设置后,不影响程序结束~
3.2 是否存活
如果一个线程引用对应的线程未执行或者已经执行完毕,那么在jconsole里也不会有这个线程,那么这个引用调用isAlive,就显示false或者,执行中都是true~
问题来了,在run方法内怎么获得线程引用呢,因为线程引用是在run方法重写后产生的,怎么获得呢?
Thread.currentThread()调用这个Thread的静态方法(一样会抛异常,但是我们已经解决了)
测试:
只有在执行前,执行完毕是false
执行中都是true
public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { System.out.println("好耶 ^ v ^"); }); t.start(); Thread.sleep(1000); System.out.println(t.isAlive()); }
对于执行完毕,有如上代码
几乎每次运行结果都是这样的
但是极端情况下,如果该线程在main线程打印t.isAlive()的时候都没到达CPU,那么就会是
true[回车]好耶 ^ v ^
特别极端,但是线程那么多,还真有可能
主要原因是,线程的调度是随机的~
3.3 线程的中断
3.3.1 自己的方法去中断
线程的中断主要是中断循环
而需要一个变量充当循环条件
这个循环变量应该在特定的时候被置为假~
所以有以下算法
全局性质的静态变量:flag
当flag为true,表示t1线程可以中断:结束
注意:由于lambda/匿名类 的变量捕获机制
局部变量必须是”final“ / ”实际final“
而全局性质的变量【属性】,不需要这个要求
public class Test5 { public static boolean flag; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(!flag) { System.out.println("好耶^v^"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); Thread.sleep(3000); flag = true; System.out.println("不好耶T.T"); } }
测试:
绝大多数结果是这样的:
3.3.2 线程引用自带的一些方法
Thread.currentThread() //获取当前线程的引用
isInterrupted() //判断线程引用内置的标识符是真是假 //----反应是否可以被中断
interrupt() //线程引用变量的标识符进行取非
初步设计
public static void main1(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(!Thread.currentThread().isInterrupted()) { System.out.println("好耶^v^"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); t1.start(); Thread.sleep(3000); t1.interrupt(); System.out.println("不好耶T.T"); }
测试:
貌似main线程已经结束了,但是t1线程似乎还没有中断~
极大概率是这样的~
恰好“睡醒”也不好说~
因为睡眠时间占99.999%d的时间~
原因就是:
interrupt()方法将线程引用的标识符改为了true
而线程的阻塞就会立即被疏通,或者说线程会立即醒来~
但是线程醒来,会将标识符情况成默认值(false)
并且抛出InterruptedException异常
catch到后打印异常
此时的循环判断仍然为真~
解决:在捕获到异常后,直接break~
测试:
3.4 join方法
join方法为等待方法,代表某线程XX时间,本线程再继续执行
join(long time), 最多等待time 毫秒
提前结束就不再等了~
join(), 等待线程完全结束~
确定此语句出现在哪个线程
调用此方法的线程引用对应的线程
前者等后者~
即前者线程进入阻塞态
等待后者线程XX时间
再转化为就绪态
注意:千万不要搞个”死锁bug“,就是“他等他,他又等他”
这不是本章节的问题,以后会讲~
public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { while(!Thread.currentThread().isInterrupted()) { System.out.println("好耶^v^"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); break; } } }); t1.start(); t1.join(5000); //在这里不带参数,那该t1线程就死循环了,无法结束的~ Thread.sleep(3000); t1.interrupt(); System.out.println("不好耶T.T"); }
测试:
3.5 线程的状态
没有测试案例
操作系统里的线程,自身是有一个状态的
但是Java Thread 是对系统线程的封装,把这里的状态又细致化了~
状态获取:getState();
NEW 线程还没被创建出来,只是存在这个线程对象~
start 创建 + 启动
也就是说start之前~
TERMINATED 系统中的线程已经执行完毕~
但是线程引用还在~
RUNNABLE 就绪状态
正在CPU上运行
准备好,随时可以去CPU运行
这两种情况,基本“同时”
TIMED_WAITING
被指定时间的等待—sleep
对于join方法
即使正在等待的线程是完全阻塞的状态
我们可以通过全局性质的静态变量去获得线程引用并在lambda表达式中被捕获到~
如果有时间限制就是TIMED_WAITING~
否则是WAITING~
BLOCKED
表示等待锁出现的状态
不在本章中讲解
WAITING
使用 wait 方法出现的状态
不在本章中讲解
掌握线程的状态,可以更好的进行多线程代码的调试,知己知彼百战百胜