目录
1.线程实现方式
一个CPU在某一时刻最多只能执行一个线程,但可以有多条线程(路径)供CPU选择,CPU在多个线程间来回切换,当存在多CPU资源时便可实现真正意义上的并行执行,无论单CPU还是多CPU都会出现线程安全问题。
1.1.并行与并发
- 并发:指两个或多个事件在同一时间段内发生。(cpu交替执行任务)。
- 单线程时cpu先执行任务1,再执行任务2,然后再执行任务1,即并发就是指在一段时间内,cpu在两个或多个任务之间交替执行
- 并行:指两个或多个事件在同一时刻发生。(cpu同时执行任务,速度快)。
- 两个cpu执行两个任务,即并行就是多个cpu同时执行多个任务。
1.2.进程概念
- 进程是指一个内存中运行的应用程序,
- 每一个进程都有一个独立的内存空间,
- 一个应用程序可以同时运行多个进程
- 进程也是程序的一次执行过程,是系统运行程序的基本单位。
- 系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 简而言之,进程就是进入到内存的程序就叫进程。
- 电脑上硬盘存储器也叫永久存储器ROM,在硬盘存储器中现有两个应用程序FeiQ.exe飞秋、idea.exeIDEA,
- 当双击这俩个应用程序时,应用程序就会进入内存,内存也叫临时存储器RAM(当电脑关机后,内存就会清空),所有的应用程序都需要进入到内存中执行,所以内存越大,程序执行速度越快。
- 当点击应用程序后会进入内存中占用一些内存来执行,进入到内存的程序叫进程。
1.3.线程概念
- 线程是进程中的一个执行单元,负责当前进程中程序的执行
- 一个进程中至少有一个线程,一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
- 简而言之,一个程序运行后至少有一个进程,一个进程中可以包含多个线程。
- 可以在电脑底部任务栏,右键打开任务管理器,查看当前任务的进程。
- 简而言之,线程就是应用程序通向cpu的一条路径。
- cpu:cpu也叫中央处理器,对数据进行计算,指挥电脑中软件和硬件干活。
- cpu分类: AMD 、Inter Core(核心)i7 8864 4核心8线程( 8线程:同时执行8个任务)
- 电脑管家软件:
- 当点击运行,该应用程序会进入到内存中执行,此时就是一个进程。
- 电脑管家中有多个功能,当点击功能(病毒查杀、清理垃圾、电脑加速)执行,就会开启一条应用程序到cpu的执行路径,cpu就可以通过这个路径执行功能,这个路径就是线程。
- 线程属于进程,线程是进程中的一个执行单元,负责程序的执行。
- 若cpu是单核心单线程,那么cpu会在多个线程之间做高速的切换,轮流执行多个线程,效率低,切换的速度是1/n毫秒。
- 若cpu是4核心8线程,则可以同时执行8个线程,8个线程在多个任务之间做高速切换,速度是单线程cpu的8倍(每个任务执行到的几率都被提高了8倍)。
- 多线程好处:1.不能提高程序的运行速度,但能够提高程序运行的效率,cpu效率高 2.多个线程之间互不影响。
1.4.线程调度
- 线程调度分类:分时调度和抢占式调度。
- 分时调度:所有线程轮流使用cpu的使用权,平均分配每个线程占用cpu的时间。
- 抢占式调度:优先让优先级高的线程使用cpu,如果线程的优先级相同,那么会随机选择一个 (线程随机性),java使用的就是抢占式调度。
- 设置线程优先级:任务管理器--详细信息中右键设置优先级,谁的优先级高,谁抢夺cpu的执行时 间就长一些,cpu执行它的几率就大一些,比如电脑管家中病毒查杀优先级高一 些,那么病毒查杀功能就会执行5s,垃圾清理和电脑加速功能就会执行3s,
- 抢占式调度详解:
- 大部分操作系统都支持多线程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如现在我们可以边上课边使用编辑器,一边使用录屏软件,同时还开画板等软件,此时这些程序是在同时运行,感觉这些软件好像在同一时刻运行。实际上,是cpu使用了抢占式调度模式在多个线程之间切换速度相对于我们的感觉要快,所以看上去就是在同一时刻运行。
1.5.主线程
- 主线程:即执行主方法(main)的线程。
- 单线程程序:java程序只有一个线程,即执行从main方法开始,从上到下依次执行。
- 主线程原理:
- java虚拟机JVM执行main方法,main方法会进入到栈内存,(执行所有方法都会到栈内存进行),JVM会找操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,这个路径就是main(主)线程,即主线程就是执行主方法的线程。
- 主线程缺陷:单线程容易出现异常导致后面程序无法执行,因此常用java多线程
1.6.创建多线程方式一_继承Thread类
- 1.java使用java.lang.Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。
- 2.每一个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。
- 3.线程是程序中执行线程,java虚拟机允许应用程序并发地运行多个执行线程。每一个线程都有一个优先级,高优先级线程的执行优先于低优先级。
- 3.Java程序属于抢占式调度,哪个线程的优先级高哪个线程优先执行,同一个优先级,随机选择一个执行。
- 4.java中通过继承Thread类来创建并启动多线程的步骤如下:
- 1.定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程需要完成的任务,因此把run方法称为线程执行体。
- 2.创建Thread子类的实例,即创建线程对象。
- 3.调用线程对象的start方法来启动该线程。
//创建线程类:
package Thread;
//1.创建一个Thread类的子类
public class Demo2MulThread extends Thread{
// 2.在Thread类的子类中重写Thread类中的run方法设置线程任务(即开启线程要做什么?)
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.print("run"+i+" ");
}
}
}
创建测试类(主线程):
package Thread;
public class Demo2MulThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类的对象
Demo2MulThread dmt = new Demo2MulThread();
//4.子类对象调用Thread类中的start方法,来开启新的线程,执行run方法。
dmt.start();
for (int i = 0; i < 5; i++) {
System.out.print("main"+i+" ");
}
}
}
- void start()方法使该线程开始执行,java虚拟机调用该线程的run方法,结果是两个线程并发地运行,当前线程(main线程)和另一个线程(创建的新 线程,执行其run方法)。
- 运行结果:main0 run0 run1 run2 run3 run4 main1 main2 main3 main4
- 即当前主线程(主方法)和新创建的线程并发地运行,由优先级高低抢夺cpu使用权。
1.7.多线程执行/内存原理
多线程执行流程
- 1.jvm执行main方法时,找OS开辟一条main方法通向cpu的路径,这个路径叫main线程即主线程,cpu通过这个线程(路径)可以执行main()方法。
- 2.main方法中创建Thread类的子类对象时,又会开辟一条通向cpu的新路径,通过子类对象调用start方法,来执行创建的新线程中的run方法。
- 3.对于cpu来说,就有两条执行路径,cpu就有了选择权利,cpu喜欢谁就会执行哪条路径,我们控制不了cpu, 所以就有了程序的随机打印的结果。
- 4.反过来说就是,两个线程,一个main线程,一个新线程一起抢夺cpu的执行权(执行时间),谁抢到了就执行谁代码。
多线程内存图
- 1.首先在main方法中创建Thread类的子类对象并调用Thread类的start方法来执行新线程中的run方法。新线程中的run方法是一个for循环。
- 2.上述多线程程序的内存:每个线程拥有独立栈空间,共用一个堆内存。
- 首先程序的执行入口是main方法,jvm将main方法压栈执行,开启main线程。
- 压入栈后先创建Thread类的子类对象,在堆内存开辟一块空间存放Thread类的子类对象mt(线程对象);
- 若是直接线程对象mt调用run方法的话会把run方法压入栈中执行,这就变成了单线程程序,
- 通过子类对象调用start 方法,此时就会开辟新的栈空间并在该空间中执行run方法,即此时run方法就不是在主方法所在的栈空间执行。若在主方法中再创建一个Thread类的子类对象并调用start方法,则又会开辟一个栈空间来执行run方法。
- 对于cpu来说,有三个栈内存供其选择,cpu可以执行main方法,也可以执行两个run方法,高速交替执行。
- 可以看出多线程的好处:多个线程之间互不影响(因为多个线程在不同的栈空间)
1.8.获取线程名称两种方法
- 1.使用Thread类中的方法getName()
- String getName():返回该线程的名称,
- String name = getName();
- 2.或先获取到当前正在执行的线程,使用该线程的方法getName获取线程名称
- Thread.currentThread().getName()
package Thread;
//1.创建一个Thread类的子类(创建一个线程类)
public class Demo3MyThread extends Thread {
//2.重写Thread类中的run方法,设置线程任务为获取线程名称
@Override
public void run() {
//获取线程名称方式一
String name = getName();
System.out.println(name);
//获取线程名称方式二
Thread t = Thread.currentThread();
System.out.println(t+" ");//获取当前线程
String name1 = t.getName();//获取当前线程名称
System.out.println(name);
//方式二的链式编程
System.out.println(Thread.currentThread().getName());
}
}
package Thread;
/**
* 线程的名称:
* 主线程:main
* 新线程:Thread-0,Thread-1,Thread-2
*/
public class Demo3MyThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子类对象
Demo3MyThread mt = new Demo3MyThread();
//4.调用start方法,开启新线程0,执行run方法。
mt.start();
//创建一个线程对象,调用start方法,开启新线程1,执行run方法
new Demo3MyThread().start();
//创建一个线程对象,调用start方法,开启新线程2,执行run方法
new Demo3MyThread().start();
//运行结果:Thread-0 Thread-1 Thread-2
//获取主线程的名称:只能使用方式二,因为该类并没有继承Thread类,
//不能直接使用getName()方法,要先获取当前线程,再获取线程名称。
System.out.println(Thread.currentThread().getName());
}
}
1.9.设置线程名称两种方法
设置线程名称的两种方法:(了解)
- 1.使用Thread类中的方法setName(线程名字)。
- void setName(String name)
- 2.创建一个带参数构造方法,参数传递线程名称,方法中调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread类)给子线程起一个名字。
- Thread(String name):分配新的Thread对象
package Thread;
/**
* 修改线程名的测试类
*/
public class Demo4SetNameMyThreadTest {
public static void main(String[] args) {
/**
* 方式一:
* 1.使用Thread类中的方法setName(线程名字)。
* void setName(String name):修改线程名称,使之与参数name相同。
*/
//在主线程(main方法)中创建(开启)多线程(创建线程对象)
Demo4SetNameMyThread mt = new Demo4SetNameMyThread();
//线程对象调用setName(名字)方法设置线程名称
mt.setName("洛映雪");
//开启新线程
mt.start();
/**
* 方式二:
* 2.创建一个带参数的构造方法,参数传递线程的名称,方法中调用父类的带参构造方法,
* 把线程名称传递给父类,让父类(Thread类)给子线程起一个名字。
* Thread(String name):分配新的Thread对象
*/
//再开启一个线程,使用带参数构造方法设置线程名称,并调用start方法启动该线程执行run方法
new Demo4SetNameMyThread("萧炎").start();
}
}
1.10.线程中sleep方法
- public static void sleep(long millis):使当前正在执行的线程以指定的毫秒数暂停,毫秒数结束后,线程继续执行。
package Thread;
public class Demo5SleepMyThread {
public static void main(String[] args) {
//模拟秒表
for (int i = 1; i <= 60; i++) {
System.out.println(i);
//使用Thread类的sleep方法让程序睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
1.11.创建多线程方式二--实现Runnable接口
- 1.java.lang.Runnable接口:Runnable接口应该由那些打算通过某一线程执行其实例的类来实现,类必须定义一个无参数的run方法。
- 2.java.lang.Thread类的构造方法:
- Thread(Runnable target):分配新的Thread对象
- Thread(Runnable target,String name):分配新的Thread对象
- 3.创建多线程程序的第二种方式步骤:
- 1.创建一个Runnable接口的实现类
- 2.在实现类中重写Runnable接口的run方法,并设置线程任务。
- 3.创建一个Runnable接口的实现类对象
- 4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
- 5.调用Thread类中的start方法,开启新线程执行run方法。
package Thread;
//1.创建一个Runnable接口的实现类
public class Demo6RunnableImpl implements Runnable{
//2.在实现类中重写Runnable接口的run方法,并设置线程任务(输出线程名称3次)。
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
package Thread;
public class Demo6Runnable {
public static void main(String[] args) {
//3.创建一个Runnable接口的实现类对象
Demo6RunnableImpl run = new Demo6RunnableImpl();
//4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);
//5.调用Thread类中的start方法,开启新线程执行run方法。
t.start();
//输出当前线程(主线程main)名称3次
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
1.12.Thread和Runnable区别
实现Runnable接口来创建多线程程序的好处:(或创建多线程的两种方式中为什么第二种好)
- 1.避免了单继承的局限性:
- 一个类只能继承一个类,类继承了Thread类就不能继承其它类。而实现Runnable接口的类,还可以继承其它的类,实现其它的接口。
- 2.增强了程序的扩展性,降低了程序的耦合性(解耦):
- 实现Runnable接口的方式创建多线程,把设置线程任务和开启新线程进行了分离(解耦),传递不同的实现类对象实现不同的任务。
- 3.线程池技术,接收Runnable类型任务,不接收Thread类型线程,线程池管理的是任务。
- 4.实现Runnable接口方式创建多线程更适合多个线程执行一个线程任务。
package Thread;
//1.创建一个Runnable接口的实现类(来设置线程任务)
public class Demo6RunnableImpl implements Runnable{
//2.在实现类中重写Runnable接口的run方法,并设置线程任务(输出线程名称3次)。
@Override
public void run() {
for (int i = 0;i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
package Thread;
//1.再创建一个Runnable接口的实现类(来设置线程任务)
public class Demo6RunnableImpl2 implements Runnable{
//2.在实现类中重写Runnable接口的run方法,并设置线程任务(打印helloword3次)。
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("helloworld"+"-->"+i);
}
}
}
package Thread;
public class Demo6Runnable {
public static void main(String[] args) {
//3.创建一个Runnable接口的实现类对象
Demo6RunnableImpl run = new Demo6RunnableImpl();
//4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);
//5.调用Thread类中的start方法,开启新线程执行run方法。
t.start();//打印线程名称
//再创建一个线程对象,来执行Demo6RunnableImpl2线程类中的线程任务
Thread t1=new Thread(new Demo6RunnableImpl2());
t1.start();//打印helloword3次
//上述将设置线程任务与开启新线程进行了分离(解耦),传递不同的实现类,执行不用的任务
//输出当前线程(主线程main)名称3次
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
1.13.匿名内部类简化多线程创建
- 匿名:没有名字 内部类:写在其它类内部的类
- 匿名内部类作用:简化代码
- 把子类继承父类,重写父类的方法,创建子类对象合成一步完成,
- 把实现类实现类的接口,重写接口中的方法,创建实现类对象合成一步完成。
- 匿名内部类的最终产物:是子类/实现类对象,而这个类没有名字
- 格式:
new 父类/接口(){
重写父类/接口中的方法
};
- 匿名内部类是使用父类或接口来替代匿名类
package Thread;
public class Demo7InnerClassThread {
public static void main(String[] args) {
//线程的父类是Thread
//之前方式需要创建Thread类的子类MyThread并重写run方法,然后调用start方法
//即 new MyThread().start();
//若使用匿名内部类可省去创建子类的过程,即:
new Thread(){
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}.start();
//线程的接口是Runnable
//之前使用该接口创建多线程需要创建接口的实现类Demo6RunnableImpl,并创建实现类对象
//作为Thread类的参数,即Runnable r=new Demo6RunnableImpl();这是通过多态创建
//实现类对象,然后new Thread(r).start()创建线程对象调用start方法启动线程;
//下面使用匿名内部类来简化:
Runnable r=new Runnable(){ //匿名内部类+多态,来创建实现类对象并重写接口的run方法
//重写run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
};
new Thread(r).start();//创建线程类对象,传参实现类对象,并调用start方法启动新线程
//简化上述以接口方式创建多线程:即不用接收实现类对象r,通过匿名内部类创建的实现类对象
//直接作为参数传递即:
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName()+"--"+i);
}
}
}).start();
}
}
1.14.Lambda简化多线程创建
1.15.线程中断
- 若当主线程执行完,子线程t1还没执行完,想要中断子线程,可以给t1添加中断标记,即t1.intertupt();当子线程执行以下方法时会触发InterruptException,比如在sleep休眠时,t1会检查自身是否携带中断标记,有的话就会new一个异常抛出被catch捕获,进入到catch块中,至于要不要t1死亡就看程序员在catch块中怎么写的,若要线程t1死亡,直接return结束掉run线程任务即可。
- 由外部干涉一个完整执行流程死亡不合理,所以通过打中断标记,线程触发标记进入catch块中。
public class Thread1 {
public static void main(String[] args) throws InterruptedException { //主线程
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {//中断异常处理了
System.out.println("主线程已执行完,中断子线程t1");
return; //结束线程任务,线程自杀
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
});
t1.start();//开启线程t1
Thread.sleep(5000);//休眠5秒,让子线程先执行会
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}
//主线程执行完,中断子线程t1
t1.interrupt();
}
}
1.16.守护线程
package demo23thread;
/**守护线程
* 线程:分为守护线程和用户线程
* 用户线程:直接创建的线程,当一个进程中不包含任何存活的用户线程,则进程结束。
* 守护线程:守护用户线程的,当最后一个用户线程结束,所有守护线程自动死亡。
*
* 设置线程为守护线程:
* 在启动子线程前设置t1.setDaemon(true)
*/
public class Thread2 {
public static void main(String[] args) throws InterruptedException { //主线程
Thread t1=new Thread(new Runnable() {//使用匿名内部类
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);//休眠1秒,给其他线程抢夺cpu机会
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
});
t1.setDaemon(true);//设置子线程t1为守护线程
t1.start();//启动守护线程
Thread.sleep(5000); //让子线程t1先执行会
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
}//主线程结束
}
}