多线程
1.线程和进程的定义
- 进程:是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
- 线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位
- 1.2.线程进程的区别体现在几个方面:
- 因为进程拥有独立的堆栈空间和数据段,所以每当启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这对于多进程来说十分“奢侈”,系统开销比较大,而线程不一样,线程拥有独立的堆栈空间,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。
- 体现在通信机制上面,正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制,而线程由于共享数据段所以通信机制很方便。
- 属于同一个进程的所有线程共享该进程的所有资源,包括文件描述符。而不同的进程相互独立。
- 线程又称为轻量级进程,进程有进程控制块,线程有线程控制块;
- 线程必定也只能属于一个进程,而进程可以拥有多个线程而且至少拥有一个线程;
- 简而言之:开启酷狗音乐只是一个进程,同时可以听歌和下载歌曲(2个线程)
1.3.我们的理解:
- 进程是指在系统中正在运行的一个应用程序;程序一旦运行就是进程,或者更专业化来说:进程是指程序执行时的一个实例。
- 线程是进程的一个实体。
- 进程——资源分配的最小单位,线程——程序执行的最小单位
2.多线程的2种或者4种实现方法
ps:网上说有4种实现方法,但是也有一部分人是持但对意见的,那么这4种方法究竟如何呢?
2.1.Java多线程实现的方式有四种
- 继承Thread类,重写run方法
- 实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target
- 通过Callable和Future/FutureTask创建多线程
- 通过线程池创建多线程
补充:
前面两种可以归结为一类:无返回值,原因很简单,通过重写run方法,run方式的返回值是void,所以没有办法返回结果。
后面两种可以归结成一类:有返回值,通过Callable接口,就要实现call方法,这个方法的返回值是Object,所以返回的结果可以放在Object对象中
3.多线程的创建之Thread的步骤
- 定义一个类继承Thread
- 重写run方法
- 创建子类对象,就是创建线程对象
- 调用start方法,开启多线程并执行,同时还会告诉jvm去调用run方法
package com.Li.xc01; /** * @Description:通过继承thread实现多线程 * @auther:Li Ya Hui * @Time:2021年4月20日下午7:39:10 */ public class JiSuanQi extends Thread { @Override public void run() { // System.out.println("计算器:run()"); // System.out.println("当前线程的名字:"+Thread.currentThread().getName()); for (int i = 0; i < 10; i++) { System.out.println("主函数的名字\t"+getName()); //Thread.currentThread().getName()可以缩写,但是为了语义化,一般都要写 } } //此构造期为更改线程名字 public JiSuanQi(String name) { super(name); } } /** * @Description: 测试类 通过继承thread类实现多线程 多线程 之 Thread的步骤 * @auther:Li Ya Hui * @Time:2021年4月20日下午7:17:50 */ public class Test { public static void main(String[] args) { for (int i = 0; i < 10; i++) { System.out.println("主函数的名字\t"+Thread.currentThread().getName()); } System.out.println("主函数001"); System.out.println("主函数002"); System.out.println("主函数003"); //主函数的线程名字:main System.out.println("当前线程的名字"+Thread.currentThread().getName()); //开启多线程,创建子类对象 JiSuanQi jiSuanQi = new JiSuanQi("计算器线程"); //当调用start方法的时候,jvm会自动执行run方法, //其他非主函数的线程的名字:Thread-0...... 但是可以通过有参数构造器进行修改 jiSuanQi.start(); } }
通过测试发现thread里的run方法不会因为main方法的结束而受到影响
4.通过实现runnable接口实现多线程
- 定义类实现Runnable接口
- 覆盖/实现接口中的run方法
- 创建Thread类的对象
- 将Runnable接口的实现类对象作为参数传递给Thread类的构造函数
- 调用Thread类的start方法开启线程
//Runable 接口 的实现类 /** * @Description: 通过Runable实现类 给Thread传参数实现 多线程 * @auther:Li Ya Hui * @Time:2021年4月20日下午8:34:21 */ public class Prims implements Runnable{ @Override public void run() { while (true) { try { //线程睡眠 1000毫秒 Thread.sleep(1000); System.out.println("当前线程的内容:"+Thread.currentThread().getName()); } catch (InterruptedException e) {//线程中断异常 //异常处理 System.out.println("当前"+Thread.currentThread().getName()+"线程终端异常"); } } } } //测试类 package com.Li.xc02; /** * @Description: 测试 runnable 给Thread传参数实现 多线程 * @auther:Li Ya Hui * @Time:2021年4月21日下午3:05:40 */ public class Test { public static void main(String[] args) throws InterruptedException { System.out.println("主线程执行的内容001"); //实例化runnable, Prims a = new Prims(); //参数形式传递给Thread Thread TH = new Thread(a); //主线程睡眠 Thread.sleep(2000); System.out.println("主线程执行的内容002"); //子线程开启 TH.start(); System.out.println("主线程执行的内容003"); } }
4.2.解释说明
Thread类中包括构造函数Thread(Runnable target)和Thread(Runnable target,String name),可以床底Runnable接口,说明构造函数支持传入一个Runnable接口的对象。
构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象。这样的好处是可以完全将Thread对象中的run()方法交由其他线程进行调用。
4.3.比较
如果一个类继承Thread,则不适合资源共享。但是如果实现了Runable接口的话,则很容易的实现资源共享。
main函数,实例化线程对象也有所不同,
extends Thread :t.start();
implements Runnable : new Thread(t).start();
使用Runnable,增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类
4.4.实现Runnable的原理
- 为什么需要定一个类去实现Runnable接口呢?继承Thread类和实现Runnable接口有啥区别呢?
- 实现Runnable接口,避免了继承Thread类的单继承局限性。覆盖/实现Runnable接口中的run方法,将线程任务代码定义到run方法中。
- 创建Thread类的对象,只有创建Thread类的对象才可以创建线程。线程任务已被封装到Runnable接口的run方法中,而这个run方法所属于Runnable接口的实现对象,所以将这个实现类对象作为参数传递给Thread的构造函数,这样,线程对象创建时就可以明确的得到要运行的线程的任务。
- 简而言之:实现Runable接口进而实现接口中的run方法,然后创建实例化Thread类,将线程任务所在的对象/类以参数的形式传递给Thread的构造函数最终实现多线程
4.5.实现Runnable的好处
- 第二种方式,实现Runnable接口避免了单继承的局限性,所以较为常用。实现Runnable接口的方式,更加的符合面向对象,线程分为两部分,一部分线程对象,一部分线程任务。继承Thread类,线程对象和线程任务耦合在一起。一旦创建Thread类的子类对象,既是线程对象,又有线程任务。实现runnable接口,将线程任务单独分离出来封装成对象,类型就是Runnable接口类型。Runnable接口对线程对象和线程任务进行解耦。
- 简而言之:避免了单继承的局限性,安全,耦合度低
5.利用匿名内部类+runnable接口实现多线程
package com.Li.xc02; /** * @Description: 利用匿名内部类+runnable接口实现多线程 * @auther:Li Ya Hui * @Time:2021年4月21日下午6:49:12 */ public class Test02 { public static void main(String[] args) { System.out.println("主函数线程打印:001"); Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("当前线程的名字为:"+Thread.currentThread().getName()); } }); thread.start(); System.out.println("主函数线程打印:002"); } }
优点:
- 线程任务类只是实现了Runnable接口,可以继续继承其他类,而且可以继续实现其他的功能
- 同一个线程任务对象可以被包装成多个线程对象
- 适合多个相同的程序代码的线程去共享同一个资源
- 实现解棍操作,代码可以被多个线程共享,线程任务代码和线程独立
6.Thread和Runnable的区别
6.1.通过继承Thread类模拟售票
package com.Li.xc03; /** * * @Description:买票类 测试通过继承Thread类模拟售票 * @auther:Li Ya Hui * @Time:2021年4月21日下午9:04:36 */ public class Mythread extends Thread{ public int tickets = 5; public void run() { for (int i = 1; i < 10; i++) { System.out.println("当前线程为:"+getName()+":"+(tickets--)); } } //线程添加名字 public Mythread(String name) { super(name); // TODO Auto-generated constructor stub } } package com.Li.xc03; /** * @Description: 测试类 测试通过继承Thread类模拟售票 * @auther:Li Ya Hui * @Time:2021年4月21日下午9:13:41 */ public class Test { public static void main(String[] args) { Mythread myA = new Mythread("A窗口"); Mythread myB = new Mythread("B窗口"); myA.start(); myB.start(); //通过结果可以看成是卖了10张票但是一共就5张票,所以此时只能说明这种方式实现的时候是没有共享资源的 } }
- 通过结果可以看成是卖了10张票但是一共就5张票,所以此时只能说明这种方式实现的时候是没有共享资源的 ( 没有共享tickets )
6.2.通过实现Runnable接口实现售票
可以实现共享资源的效果,但是会同时进行买票的问题,延伸出了互斥锁
synchronized (this)//同步锁 // mutex互斥 在此处是锁的效果,即互斥锁 { if (tickets > 0) { System.out.println("当前线程的名字:" + Thread.currentThread().getName() + ":" + (tickets--)); } }
互斥锁使得,多个线程不可同时访问一个资源
案例展示
package com.Li.xc04; /** * @Description: 通过实现 Runnable接口 实现多线程-模拟售票 * @auther:Li Ya Hui * @Time:2021年4月21日下午9:30:16 */ public class Mythread implements Runnable { //总票数 public int tickets = 500; @Override public void run() { for (int i = 0 ;i<10000 ; i++) { //this 指代当前类的意思 在此处表示为给当前的类加上互斥锁的效果 synchronized (this)//同步锁 // mutex互斥 在此处是锁的效果,即互斥锁 { if (tickets > 0) { System.out.println("当前线程的名字:" + Thread.currentThread().getName() + ":" + (tickets--)); } } } } } package com.Li.xc04; /** * @Description: 测试类 测试Runnable的实现类运用多个线程来共享资源去买票,不会有数据读脏的可能 * @auther:Li Ya Hui * @Time:2021年4月21日下午10:08:55 */ public class Test { public static void main(String[] args) { //实例化我的售票类 Mythread myRunThread1 = new Mythread(); //实例两个线程 用来分工 Thread thread1 = new Thread(myRunThread1, "1号窗口"); Thread thread2 = new Thread(myRunThread1, "线程二"); thread1.start(); thread2.start(); //通过结果我们发现,会有数据读脏的可能性 //所以要使用互斥锁来避免同时读取数据问题 } }
Runnable总结:
- 实现Runnable接口比继承Thread类所具有的优势:
- 适合多个相同的程序代码的线程去处理同一个资源
- 可以避免java中的单继承的限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立
- 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类