一、多线程相关的三组概念
(一)程序和进程
1、程序:一个固定的存储有逻辑和数据的集合,是一个静态的状态,存储在磁盘上
2、进程:一个正在运行着的程序,是一个动态的概念,一般运行在计算机的内存中
3、查看计算机进程:ctrl + shift +ESC
(二)进程和线程
1、进程:是一个正在运行的程序,会分配一部分系统资源,是一个独立的资源分配单位
2、线程:一条正在独立执行的路径。多线程,再执行某个程序的时候,该程序与多个子任务,每个线程都可以独立的完成其中一个子任务。在子任务之间,没有什么依赖关系,可以独立执行。
3、进程和线程的关系:
(1)进程适用于分配系统资源的单位
(2)一个进程中,可以有多条线程,但是一个进程中,至少有一条线程
(3)线程不会独立分配资源,一个进程中的所有线程,共享的是同一个进程的资源
(三)并行和并发
1、并行:在一个时间点,有多个任务(进程、线程)正在执行。多核心、多CPU变成
2、并发:在一个时间点,有多个任务同时发起。但是,在一个时间点,同时只能由一个任务正在运行。单核心、单CPU编程
(四)Q&A:CPU在多个任务之间来回切换,效率提高了还是降低了
1、对于一个任务,毋庸置疑是降低了
2、对于整个计算机系统,效率提高了,计算机硬件的运行效率是不同的:CPU 10^-9秒,内存 10^-6秒,硬盘 10^-3 秒
二、多线程的第一种实现方式
1、实现多线程的第一种方式:继承方式
在JDK中有一个类是线程类:Thread
步骤:1、自定义一个类型继承Thread
2、在自定义类型中重写Thread类中的run方法,方法体就是将来线程的任务
3、创建自定义类型对象,表示一条线程
4、调用start( )方法启动线程
public class Demo01_ThreadFirstWay {
public static void main(String[] args) {
//3.创建一个自定义类型的对象,表示一条线程
MyThread mt = new MyThread();
//4.调用继承了Thread类中的start()方法,启动线程
mt.start();
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====main");
}
}
}
//1.自定义一个类型继承Thread类
class MyThread extends Thread {
//2.重写Thread类中的run方法
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====MyThread");
}
}
}
(二)实现多线程的第二种方式:实现方式
1、在JDK中有一个接口:Runnable
2、步骤:1、自定义一个类,实现Runnable接口
2、在自定义类中重写Runnable接口的run方法
3、创建自定义类型的对象,表示一个任务
4、创建线程对象,将然乌添加到线程中
5、线程对象调用start方法,启动线程
public class Demo02_ThreadSecondWay {
public static void main(String[] args) {
//3.创建自定义类型的对象,表示一个任务对象
MyTask task = new MyTask();
//4.创建一个线程对象,将任务添加到线程中
Thread t = new Thread(task);
//5.线程对象调用start方法,启动线程
t.start();
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====main");
}
}
}
//1.自定义一个类型,实现Runnable接口
class MyTask implements Runnable {
//2.重写Runnable接口中的run方法
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====MyTask");
}
}
}
(三)匿名内部类简化两种线程的实现方式
Thread t = new Thread() {
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====MyThread");
}
}
};
t.start();
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 1; i <= 1000; i++) {
System.out.println(i + "====MyTask");
}
}
});
t.start();
两种方式的比较
1、代码复杂程度
1、继承简单 2、实现相对复杂
2、实现原理:
1、继承方式:创建好线程对象后,调用start( )方法启动线程,start()方法又调用start0()方法,start0()方法调用了run()方法,因为在自定义类型(子类中)重写了Thread类的run方法,所以直接执行子类中重写后的run,我们重写是定义好的业务逻辑就执行了
2、实现方式:创建接口中的实现类对象,这个对象是一个任务对象。创建线程对象,将任务对象封装到线程对象中,将任务对象当做参数传递给线程对象,线程对象在创建的过程中,连续调用2个init方法,之后,将任务对象设为线程对象的成员变量,赋值给线程。创建好线程对象后,调用start方法启动线程,start方法又调用了start0方法,start0方法调用run方法,因为在初始化的时候,已经将任务对象封装进了线程对象中,所以此时是线程对象在调用线程类中的run方法,此时run方法就要执行。判断任务对象部位null之后,任务对象调用接口中的run方法,因为接口的run方法已经被实现了,所以根据任务对象所属类型,调用任务对象所属类中重写过的run方法,执行重写后的方法。设计:
1、继承方式:自定义类型继承了Thread类型,就无法再去继承其他类型,代码扩展性较差
2、实现方式:一个类,实现多个接口的同时还能再继承一个类型,扩展性比较强灵活性:
1、继承的方式:将业务逻辑和线程对象绑定在了一起,耦合度搞,灵活性差
2、实现的方式:将任务对象和线程对象分离开,降低耦合度,;灵活性强。一个任务对象可以被对各线程执行,一个线程也可以执行不同的任务。并且将来还可以将任务对象,提交到线程池中;任务对象被不同的线程执行,也方便多线程之间的数据交互
三、Thread类中常用的方法
1、getName() 获取线程的名称
注意:
- [1 ] 当我们没有给线程起名时,线程具有默认名字【Thread-x】
- [2 ] 在继承的方式中,可以直接在run内调用getName(),因为继承方式的run和getName都是来源于父类Thread
- [3 ] 在实现的方式中,run方法里不能直接调用getName(),因为run方法是接口Runnable的方法,不在一个体系
2、setName(String name) 设置线程名称
构造方法:
- [1] Thread(String name)
- [2 ] Thread(Runnable target, String name)
3、static Thread currentThread()返回当前正在执行的这段代码所在线程的线程对象
System.out.println(Thread.currentThread().getName());
四、练习:
1、获取主方法所在的线程名称
2、获取垃圾回收线程的名称
/**
* 1、获取主方法所在的线程名称
2、获取垃圾回收线程的名称
*
* @author Zihuatanejo
*
*/
public class Demo06_Exercise {
public static void main(String[] args) {
//哪个线程执行了currentThread方法,就获取哪个线程对象
System.out.println(Thread.currentThread().getName());
new Garbage();
System.gc();
}
}
class Garbage {
@Override
protected void finalize() throws Throwable {
System.out.println(Thread.currentThread().getName());
}
}
(五)线程休眠
1、static sleep(long millis) 让当前线程休眠指定的毫秒数
3、注意事项:
(1)当在重写的方法中使用该方法时,如果重写前的方法没有声明异常,重写后的方法只能try…catch捕获
(2)该方法会出现InterruptedException,中断异常,当线程在休眠过程中被打断,会报此异常
(六)守护线程
1、守护线程:保证其他非守护线程能够正常执行的线程,为其他非守护线程提供良好的运行环境。守护线程死亡,非守护线程可以正常执行,非守护线程死亡,守护线程也就没有存在的意义了,片刻(或长或短的时间)之后,守护线程也会消亡。
2、isDaemon() 判断一个线程是否为守护线程
3、setDaemon(boolean bo) 将一个线程设置为守护线程
4、任何线程在创建出来的时候,都是一个非守护线程
5、别名:后台线程
(七)练习
//分别从作用上和代码上判断,垃圾回收线程是否为守护线程
public class Demo09_Exericse {
public static void main(String[] args) {
while(true) {
new Rubbish();
}
}
}
class Rubbish {
//重写finalize
@Override
protected void finalize() throws Throwable {
System.out.println(Thread.currentThread().isDaemon());
}
}
(八)线程的优先级
1、执行多线程的时候,每个线程都有优先级,优先级高的,在执行的整个周期内比较靠前执行,优先级低的,在执行的整个周期内比较靠后的执行
2、有三个线程优先级常量:
(1)MAX_PRIORITY 最高优先级:10
(2)MIN_PRIORITY 最低优先级:1
(3)NORM_PRIORITY 默认优先级:5
t.setPriority(1);
t1.setPriority(10);
四、多线程中的线程安全问题
1、同步代码块
使用一种格式,让某段代码执行的时候,CPU不会切换到影响这段代码执行的代码上去。这种格式既能保证CPU在执行A主线程的时候,不会影响A线程执行的其他线程上
2、格式:
synchronized(同步锁对象) {
需要保证执行完整性的代码
}3、使用同步代码块之后的效果:
(1)当CPU想要去执行同步代码块的时候,需要先获取到同步锁对象,获取之后就能执行同步代码块里面的内容;当CPU想要执行其他线程的时候,但是不会切换到具有相同通过不锁对象的代码上
(2)当CPU执行完了当前代码块中的代码,回释放同步锁对象,CPU就可以运行到其他的线程上,执行其他代码。