一、线程的概述
1.1 进程(Process)跟线程(Thread)
程序:说起进程,就不得不需要先说一下程序了。程序是指令和数据的有序集合,其本身没有什么含义,是一个静态的概念。
进程:进程是执行程序的一次过程,它是一个动态的概念。是系统资源分配的单位。
线程:通常再一个进程中可以包含若干个线程,一个进程中至少有一个线程,不然没有存在的意义。线程是CPU调度和执行的基本单位。
注意:很多时候,我们用的多线程都是模拟出来的,真正是多线程是指有多个cpu,也就是多核,如服务器。如果是模拟出来的多线程,就是在一个cpu的情况下,在同一个时间点,cpu只能执行一个代码,因为切换的很快,所以就有同时执行的错觉。
1.2 为什么要有线程
每个进程都有自己的地址空间,也就是进程空间,在网络或者多用户请求下,一个服务器通常要接受很多用户的并发请求,如果系统为每个请求都创建一个进程的话,那肯定是行不通的(系统开销大,用户请求效率低),因此操作系统中线程概念被引进了。
1.3 进程和线程的区别
1. 地址空间: 同一进程的所有线程共享本进程的地址空间,而不同的进程之间的地址空间是独立的。
2. 资源拥有: 同一进程的所有线程共享本进程的资源,如内存,CPU,IO等。进程之间的资源是独立的,无法共享。
3. 执行过程:每一个进程可以说就是一个可执行的应用程序,每一个独立的进程都有一个程序执行的入口,顺序执行序列。但是线程不能够独立执行,必须依存在应用程序中,由程序的多线程控制机制进行控制。
4. 健壮性: 因为同一进程的所以线程共享此线程的资源,因此当一个线程发生崩溃时,此进程也会发生崩溃。 但是各个进程之间的资源是独立的,因此当一个进程崩溃时,不会影响其他进程。因此进程比线程健壮。
线程执行开销小,但不利于资源的管理与保护。
进程的执行开销大,但可以进行资源的管理与保护。进程可以跨机器前移。
1.4 从JVM角度说一下进程和线程之间的区别
1.4.1 下面是进程和线程的图解
下图是Java内存区域,从这里我们从虚拟机角度可以分析线程和进程之间的关系
从上面这个图我们也可以看出:一个进程是包含多个线程的,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。
程序计数器为什么是私有的?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
- 虚拟机栈:每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈:和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
1.5 多线程和多进程
多进程:就是操作系统中同时运行的多个程序
多线程:也就是在同一个进程中运行的多个任务
举例:运用多线程下载文件,可以同时运行多个线程,但是通过程序运行的结果发现,每次运行的结果都不一样。这是因为读线程具有随机性。原因:cpu在瞬间不断切换区处理各个线程而导致的,可以理解成多个线程抢占cpu资源。
多线程可以提供cpu的使用率,如下图
多线程无法提高运行的速度,但是可以提高运行效率,让cpu的使用率更加高。但是如果多线程有安全问题或者频繁的进程上下文切换,运算速度可能反而降低。
二、线程的创建
2.1 创建线程有三种方式
1、继承Thread类创建线程类
1.定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务。因此把run()方法称为执行体。
2.创建Thread子类的实例,即创建了线程对象。
3.调用线程对象的start()方法来启动该线程。
示例代码:
//创建线程方式一:继承Thread类,重写run()方法,调用start开启线程
public class MyThread1 extends Thread{
@Override
public void run(){
//run方法线程体
for (int i = 0; i < 10; i++) {
System.out.println("我在run方法体里面"+i);
}
}
public static void main(String[] args) {
//main线程,主线程
//创建一个线程对象
MyThread1 myThread1 = new MyThread1();
//调用start方法开启线程
myThread1.start();
for (int i = 0; i < 500; i++) {
System.out.println("我在main函数里面"+i);
}
}
}
运行结果:
结论:通过这里可以看到,当调用start()方法的时候,run方法跟main方法是同时执行的
我们修改一下代码,将调用start()方法改成调用run方法
myThread1.run();
打印结果:
总结:线程开启后,不一定立即执行,由cpu调度执行。
多线程测试下载图片
//练习多线程下,同步下载图片
public class MyThread2 extends Thread {
private String url;
private String name;
public MyThread2(String url,String name){
this.url = url;
this.name = name;
}
//下载图片的执行体
@Override
public void run(){
WebDownloader w1 = new WebDownloader();
w1.downloader(url,name);
System.out.println("图片的名字:"+name);
}
public static void main(String[] args) {
MyThread2 myThread1 = new MyThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1599895042476&di=deddf983de1e69059171778dfbe45cad&imgtype=0&src=http%3A%2F%2Fimg3.doubanio.com%2Fview%2Fphoto%2Fl%2Fpublic%2Fp2530917266.jpg","朱一龙");
MyThread2 myThread2 = new MyThread2("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1599895107386&di=f40afb90e7c23415b1601dc2bcb7b8c1&imgtype=0&src=http%3A%2F%2Fb-ssl.duitang.com%2Fuploads%2Fitem%2F201812%2F29%2F20181229232103_KQwZC.jpeg","刘亦菲");
MyThread2 myThread3 = new MyThread2("https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1093572347,3506569978&fm=26&gp=0.jpg","李逍遥");
myThread1.start();
myThread2.start();
myThread3.start();
}
}
//下载器
class WebDownloader{
//下载方法
public void downloader(String url,String name){
try{
FileUtils.copyURLToFile(new URL(url),new File(name));
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
System.out.println("io异常,downloader方法出现问题");
}
}
}
测试结果:
图片的名字:李逍遥
图片的名字:朱一龙
图片的名字:刘亦菲
根据测试结果可以得出结论,线程是同步执行的,不是按照顺序,这是由cpu来调度的
2、通过Runnable接口创建线程类(一般用这个方法,比较灵活)
(1)定义runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
(2)创建 Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
(3)调用线程对象的start()方法来启动该线程。
我们根据上面的继承Thread类的代码拿过来做一个修改:
public class MyRunnale implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println("我在run方法里面:"+i);
}
}
public static void main(String[] args) {
//创建runnable接口的实现类对象
MyRunnale runnale = new MyRunnale();
new Thread(runnale).start();
for (int i = 0; i < 500; i++) {
System.out.println("我在main方法里面:"+i);
}
}
}
运行结果:
改成run方法
输出结果:
Thread源码追踪
实现Runnable接口模拟并发抢票
public class MyRunnable2 implements Runnable {
int ticketNums = 20;
@Override
public void run() {
while(true){
if(ticketNums<=0){
break;
}
//模拟一下延迟
try{
Thread.sleep(200);
}catch (InterruptedException e){
e.printStackTrace();
}
//测试输出
System.out.println(Thread.currentThread().getName()+"---->拿到了第"+ticketNums--+"票");
}
}
public static void main(String[] args) {
MyRunnable2 runnable2 = new MyRunnable2();
new Thread(runnable2,"张三").start();
new Thread(runnable2,"李四").start();
new Thread(runnable2,"小莫").start();
}
}
运行结果:
张三---->拿到了第20票
李四---->拿到了第19票
小莫---->拿到了第18票
张三---->拿到了第17票
李四---->拿到了第17票
小莫---->拿到了第16票
张三---->拿到了第15票
李四---->拿到了第15票
小莫---->拿到了第14票
李四---->拿到了第13票
张三---->拿到了第13票
小莫---->拿到了第12票
张三---->拿到了第11票
李四---->拿到了第11票
小莫---->拿到了第10票
李四---->拿到了第9票
张三---->拿到了第9票
小莫---->拿到了第8票
李四---->拿到了第7票
张三---->拿到了第7票
小莫---->拿到了第6票
李四---->拿到了第5票
张三---->拿到了第4票
小莫---->拿到了第3票
李四---->拿到了第2票
张三---->拿到了第1票
小莫---->拿到了第0票
Process finished with exit code 0
上面有什么问题?
可以看到有些人拿的票数都是一样的,例如17,13,9号票被重复拿了。这就有并发问题了。
对于怎么解决多线程并发,就需要加锁了。让线程排队
3、通过Callable和Future创建线程
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。(FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了Future和Runnable接口。)
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
//1)创建一个类实现 Callable 接口 ,重写 call 方法
//2) 创建目标对象
//3) 创建一个执行服务
//4)提交执行
//5)获取返回结果
//6)关闭执行服务
//实现一个类,重写call()方法,
public class TestCallable implements Callable<Boolean> {
@Override
public Boolean call() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "在上班-----" + i);
}
return Boolean.TRUE;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t1 = new TestCallable();
TestCallable t2 = new TestCallable();
TestCallable t3 = new TestCallable();
//创建执行服务
ExecutorService executorService = Executors.newFixedThreadPool(3);
//提交执行
Future<Boolean> result1 = executorService.submit(t1);
Future<Boolean> result2 = executorService.submit(t2);
Future<Boolean> result3 = executorService.submit(t3);
//获取结果
Boolean r1 = result1.get();
Boolean r2 = result2.get();
Boolean r3 = result3.get();
//关闭服务
executorService.shutdownNow();
}
}
4、 三种方式进行对比
1、采用实现Runnable、Callable接口的方式创建多线程时,
优势是:
线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
劣势是:
编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
2、使用继承Thread类的方式创建多线程时,
优势是:
编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
劣势是:
线程类已经继承了Thread类,所以不能再继承其他父类。
3、Runnable和Callable的区别
(1) Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。
(2) Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
(3) call方法可以抛出异常,run方法不可以。
(4) 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。