多线程的一些基本概念与使用

一、线程的概述

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 之后的元空间)资源,但是每个线程有自己的程序计数器虚拟机栈 和 本地方法栈

程序计数器为什么是私有的?

程序计数器主要有下面两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

需要注意的是,如果执行的是 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对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值