万字狂淦多线程__(多线程学习笔记)
Posted z啵唧啵唧
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了万字狂淦多线程__(多线程学习笔记)相关的知识,希望对你有一定的参考价值。
文章目录
多线程学习总结
多线程概述
- 什么是进程?什么是线程?
一个进程是一个应用程序,或者说是一个软件
线程是进程中的执行场景/执行单元
一个进程可以启动多个线程
- 举个例子
在dos窗口中当输入java HelloWorld 回车之后 会先启动JVM,而JVM就是一个进程。
JVM会再启动一个主线程调用main方法,同时还会启动一个垃圾回收线程来负责看护回收垃圾
此时最起码再进程当中有两个线程是并发的,一个是执行main方法的主线程 一个是执行垃圾回收的线程
- 进程和线程之间的关系
进程可以看作现实生活当中的公司
线程可以看作是公司当中的某个员工
比如:
公司:京东就可以看作是一个进程
员工:强东 就可以看作是一个线程
员工:妹妹 也可以看作是一个线程
- 注意
1、假如有两个进程A和B进程A和进程B他们之间的内存是不能够共享的,他们是独立的
比如:QQ音乐和网易云音乐是两个独立的进程他们之间内存不能够共享,是相互独立的
2、假如有两个线程A、B
在java当中,线程A、B之间共享堆内存和方法区,但是他们两个的栈内存是独立的,一个线程一个栈,每个线程之间执行互不干扰,各自执行各自的,这个就是多线程
3、java中存在多线程机制的原因:提高程序的处理效率
- 注意一个问题
采用了多线程机制之后,main方法执行完毕之后程序不一定会执行结束,因为main方法执行完毕之后,只是主线程结束了,只是主栈空了其他的栈可能还在弹栈、压栈
- 图解线程和线程之间的关系
多线程并发的理解
- 分析一个问题对于单核的CPU能够真正的做到多线程并发吗?
答案:不能!但是单核的CPU可以给人一种多线程并发的错觉
实际上对于单核的CPU在某一个时间节点上,只能处理一件事情,但是由于CPU处理的速度极快,线程和线程之间切换频繁,给人的感觉是多个线程同时执行
比如:以前老的电影院播放电影采用胶卷其实就是一张张的照片进行切换,只不过速度很快让人感觉到是连续的
但是对于多核CPU来说,比如4核CPU,可以真正做到4个线程并发
分析程序当中存在几个线程
分析一下下面的代码中有几个线程
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:20
**/
public class ThredTest1 {
public static void main(String[] args) {
m1();
}
private static void m1()
{
m2();
}
private static void m2()
{
m3();
}
private static void m3()
{
}
}
虽然在这份代码当中main()方法调用了m1方法,m1方法调用了m2方法,m2方法调用了m3方法,但是这些方法全部都是在main方法的主栈当中执行的,只有一个方法栈,所以在这份代码当中只有一个线程。
- ThredTest1类中方法执行分析图
实现线程的第一种方式
- 编写一个类,让这个类直接继承java.lang.Thread类重写run方法
- 怎么创建线程对象? 直接new我们的分支线程对象即可
- 怎么启动线程?调用start方法启动线程,调用start方法会启动一个分支栈会在JVM当中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了
- 调用start方法只是开辟了一个新的栈空间,只要新的栈空间一开辟,start方法就结束了。分支线程也就启动成功了
- 当线程启动成功之后就会自动调用run方法,并且run方法在新的栈空间的底部(压栈)。
- run方法在分支栈的底部,main方法在主栈的底部,main方法的run方法是平级的
- 如果用我们的线程对象直接调用run方法:myThread.run();会怎么样?=====>不会启动多线程,不会分配新的栈空间(这种方式其实就是一个单线程)
- **注意一个亘古不变的道理:**方法体中的代码永远都是自上而下的顺序逐行依次执行
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
myThread.start();
//这下面的代码还是运行在主线程当中
for (int i=0;i<999;i++)
{
System.out.println("这段代码还是运行在主线程当中");
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<999;i++)
{
System.out.println("这段程序运行在分支栈中");
}
}
}
strat和run的区别
- 调用start方法才是真正的多线程并发,在调用start方法的时候,会在mian方法的这个主栈的外边新开辟一个栈,在这个栈空间开辟成功之后,这个方法立即失效然后JVM会默认自行调用run方法,将这个run这个方法压在新开辟的栈底当中。
- 调用run方法本质还是个单线程,他还是在main方法中执行的并没有开辟新的栈空间
- 有这样一份代码
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
myThread.start();
//调用run方法,对比一下调用start方法的区别
//这下面的代码还是运行在主线程当中
for (int i=0;i<100;i++)
{
System.out.println("主方法栈=====>"+i);
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<100;i++)
{
System.out.println("新开辟的栈=====>"+i);
}
}
}
从这个运行结果可以看出这是调用了start真正的实现了多线程,他的新开辟的栈和主栈时交替运行的,充分说明了这是两个线程并发的下面看看他的运行原理图
- 另外一份代码
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 17:32
**/
public class ThredTest2 {
public static void main(String[] args) {
//这是main方法,这段代码属于主线程,运行在主栈当中
//现在新建一个线程对象
MyThread myThread = new MyThread();
//调用start方法启动线程
//myThread.start();
//调用run方法,对比一下调用start方法的区别
myThread.run();
//这下面的代码还是运行在主线程当中
for (int i=0;i<100;i++)
{
System.out.println("主方法栈=====>"+i);
}
}
}
class MyThread extends Thread
{
@Override
public void run() {
//编写程序,输出这段程序运行在分支栈中
for (int i = 0; i<100;i++)
{
System.out.println("新开辟的栈=====>"+i);
}
}
}
从这个调用了run方法执行的结果分析:分明显并没有开启多线程,因为他是从上向下依次调用了run方法,执行完run方法中中的输出才开始执行mian方法中的输出,下面看看他的运行结构图
- 总结一下调用run方法只是new了一个继承了线程的类,应没有启动多线程,它的本质还是一个单线程
实现多线程的第二种方式
-
采用接口的方式实现多线程
-
编写一个类实现java.lang.Runnable接口重写run方法
-
需要注意的是自定义的这个实现了Runnable接口的方法并不是一个多线程对象,他需要封装到Thread类中才能获得这个多线程对象
-
然后拿这个多线程对象,调用start方法,就能成功开启多线程了
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 22:54
**/
public class ThredTest3 {
public static void main(String[] args) {
//new 一个可运行的对象
MyRunnable myRunnable = new MyRunnable();
//将可运行的对象封装成一个线程对象
Thread thread = new Thread(myRunnable);
//调用start方法启动多线程
thread.start();
for (int i = 0;i<99;i++)
{
System.out.println("主方法栈===>"+i);
}
}
}
//创建一个类实现Runnable接口
class MyRunnable implements Runnable{
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println("分支栈===>"+i);
}
}
}
- 执行结果
明显看出分支栈和主方法栈并发执行,这就是开启多线程的第二种方法
- 注意以上两种实现多线程的方式我们推荐实现Runnable接口的方式,因为用实现接口的方式,这个类还可以继承别的类,更加灵活然而采用继承的方式就不能再实现别的类了
采用匿名内部类的方式
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/25 23:18
**/
public class ThredTest4 {
public static void main(String[] args) {
//采用匿名内部类的方式来开启多线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println("分支栈===>"+i);
}
}
});
//调用start方法开启多线程
thread.start();
for (int i=0;i<99;i++)
{
System.out.println("主方法栈===>"+i);
}
}
}
- 成功开启多线程
线程的生命周期
从上面一系列代码可以总结出输出结果的一些特点:
- 分支栈和主方法栈时有先有后
- 有多有少
分析这个问题就需要了解一下线程的生命周期
包含六个周期:
- 新建状态:刚new出来线程对象
- 就绪状态:新建状态调用了start方法后就会进入到就绪状态,就绪状态的线程又叫做可运行状态,表示当前线程具有抢夺cpu时间片的权利(cpu时间片就是执行权力)当一个线程抢到时间片之后,就可以开始执行run方法,run方法执行成功之后标志着线程进入到了运行状态。
- 运行状态:run方法开始执行就标志着线程进入到运行状态,当之前占有的cpu时间片执行完之后,会重新回到就绪状态,继续抢夺cpu时间片,当再次抢夺到cpu时间片之后,就会再次进入到运行状态,接着执行run方法,继续往下执行
- 阻塞状态:当一个线程遇到阻塞事件之后,例如接受用户从键盘输入,或者sleep方法等,此时线程会进入到阻塞状态,阻塞状态的线程会放弃之前占用的cpu时间片,当阻塞解除的时候,需要这个线程继续回到就绪状态继续抢夺cpu时间片
- 死亡状态:运行状态时当run方法执行结束就会进入死亡状态,整个线程也就到此结束了
获取当前线程
- 获取线程名字的方法通过new的线程对象调用myThreTest1.getName()方法
- 如果自己不设置线程的名字的话,线程的名字默认是Thread-x,第一个是Thread-0,第二个是Thread-1,第三个是Thread-2 …
- 设置线程名字的方法通过new的线程对象调用setName()方法
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 0:30
**/
public class ThresTest5 {
public static void main(String[] args) {
myThreTest myThreTest1 = new myThreTest();
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
String name2 = myThreTest2.getName();
System.out.println(name1);
System.out.println(name2);
}
}
class myThreTest extends Thread{
@Override
public void run() {
for (int i=0;i<99;i++)
{
System.out.println();
}
}
}
- 默认的线程名称
- 通过调用setName方法来设置线程名称
myThreTest myThreTest1 = new myThreTest();
myThreTest1.setName("线程一");
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
myThreTest2.setName("线程二");
String name2 = myThreTest2.getName();
System.out.println(name1);
System.out.println(name2);
- 获取当前线程,调用thread.currentThread方法
- 在哪个线程中调用thread.currentThread就获取到哪个线程
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 0:30
**/
public class ThresTest5 {
public static void main(String[] args) {
Thread thread = new Thread();
Thread cur = thread.currentThread();
System.out.println("当前线程的名称:"+cur.getName());
myThreTest myThreTest1 = new myThreTest();
myThreTest1.setName("线程一");
String name1 = myThreTest1.getName();
myThreTest myThreTest2=new myThreTest();
myThreTest2.setName("线程二");
String name2 = myThreTest2.getName();
/* System.out.println(name1);
System.out.println(name2);*/
myThreTest1.start();
myThreTest2.start();
}
}
class myThreTest extends Thread{
Thread thread = new Thread();
@Override
public void run() {
//在分支线程当中调用currentThread就获取到分支线程
System.out.println("获取到分支线程名称:"+thread.currentThread().getName());
/* for (int i=0;i<99;i++)
{
System.out.println();
}*/
}
}
- main方法对应的线程如果不修改名称,获取到的线程对象名字就叫做main
- 总结就是一句话:thread.currentThread在哪个线程中调用返回的就是哪个线程
sleep方法
-
参数是毫秒
-
是一个静态方法
-
sleep方法在哪个进程当中调用就在让那个进程进行休眠
-
实例一个方法,每隔2秒获取一下当前线程的名称获取10次
package com.zb.test;/** * @Author 啵儿 * @Email 1142172229@qq.com * @date 2021/10/26 1:13 **/public class ThredTest6 { public static void main(String[] args) { myRunnable run = new myRunnable(); Thread thread = new Thread(run); thread.start(); }}class myRunnable implements Runnable{ @Override public void run() { for (int i=0;i<10;i++) { //每隔5秒输出分支线程的名称 try { Thread.sleep(1000*2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("分支线程的名称"+Thread.currentThread().getName()); } }}
- 关于sleep方法的一道面试题
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 1:25
**/
public class ThredTest7 {
public static void main(String[] args) {
Thread t = new myThred();
t.setName("分支线程1");
t.start();
try {
t.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class myThred extends Thread{
@Override
public void run() {
System.out.println("====>分支线程执行");
}
}
-
在这份代码中myThred这个分支线程会不会睡眠
-
答案:不会的!因为sleep是一个静态方法,他在哪个线程中调用就让哪个线程睡眠,更哪个对象去调用没有关系,主要看的是他在哪个线程中被调用!
-
终止一个线程的睡眠(注意是终止线程的睡眠,不是终止整个线程),调用thread.interrupt()方法
-
测试用例,现在有一个一睡睡一年的线程,现在想办法在主线程当中终止它
package com.zb.test;
/**
* @Author 啵儿
* @Email 1142172229@qq.com
* @date 2021/10/26 8:34
**/
public class ThredTest8 {
public static void main(String[] args) {
Thread thread = new Thread(new myRunnable1());
thread.setName("分支线程");
//启动分支线程
thread.start();
//设计一下希望5秒之后终止分支线程睡眠
try {
thread.sleep(2*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//终止分支线程睡眠,这种终止方式,依靠java的异常处理机制
thread.interrupt();
for (int i = 0;i<10;i++)
以上是关于万字狂淦多线程__(多线程学习笔记)的主要内容,如果未能解决你的问题,请参考以下文章