多线程你了解多少?看完你就全明白了!

什么是程序、进程、线程?

什么是程序?

程序是指令和数据的有序集合,其本身没有任何的运行含义,是一个静态的概念。就像我们写的代码就是程序。

什么是进程(Process)?

进程是操作系统结构的基础,是一次程序的执行,资源分配的基本单位。它是一个动态的概念。我们可以这样理解,一个正在操作系统中运行的exe程序可以理解为一个进程,进程是收操作系统管理的基本运行单元。就像我现在运行的谷歌浏览器、印象笔记他们就是一个进程。

windows中的进程图:

多线程你了解多少?看完你就全明白了!_第1张图片

linux中的进程图:

多线程你了解多少?看完你就全明白了!_第2张图片

什么是线程(Thread)?

线程是运行的基本的单位,是调度基本单位,也就是一系列的指令。那我们如何理解线程呢,就是一个进程中包含多个线程,每个线程之间互不影响,就比如我们在看电影的时候,可以看到图像,又能听到声音,还能看弹幕等等,这其中的每一项都是线程。

线程的两种模型(了解一下)

用户级线程(ULT)  用户线程实现,不依赖操作系统核心,应用提供创建、同步、调度和管理线程的函数来控制用户线程,不需要用户态、核心态切换,速度快,内核对UTL而无感知,线程阻塞则进程(包括他所有的线程)阻塞。

内核级线程  (KLT )   系统内核管理线程(KLT)内核保存线程的状态和上下文信息,线程阻塞不会引起进程阻塞进程阻塞,在多c处理器系统上,多线程在处理器上并行运行。线程的创建、调度和管理由内核完成,效率比ULT要慢,比进程操作快。

我们java虚拟机就是使用的内核级线程 。

java线程与系统内核线程的关系?

java线程创建是依赖于系统内核,通过jvm调用系统库创建内核线程,内核线程与Java-Thread是1:1的映射关系。jvm有一个线程,操作系统相应的也有该线程 。jvm将资源抢夺,线程同步,上锁,争抢锁,等复杂的操作都给了操作系统,让操作系统来操纵。

多线程你了解多少?看完你就全明白了!_第3张图片

单线程和多线程有什么不同呢?

从下面的图我们可以看出,比如单线程中的主线程先调用一个方法,等该方法调用执行完才可以继续往下执行,这样看来效率就特别的低。而我们多线程的话主线程执行主线程的方法,子线程执行子线程的方法,两者并行交替执行,互不影响。效率就会高很多。

多线程你了解多少?看完你就全明白了!_第4张图片

我们需要了解的核心概念

1、线程就是独立执行路径
2、在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程(也是我们的main方法),GC线程。
3、main()称之为主线程,为系统的入口,用于执行整个程序。
4、在一个进程中,如果开辟了多个线程,线程的运行由调度器(也就是cpu)安排调度,调度器是与操作系统紧密相关的,先后顺序是不能人为干预的。
5、对同一份资源操作时,会存在资源抢夺的问题,每个线程会将这个值放到自己的线程中(在自己的工作内存交互),进行操作,在此期间,可能其他线程对此值做了改变,所以说多线程很难保证一致性,这时就需要加入并发控制。
6、线程会带来额外的开销,如cpu调度时间,并发控制开销,多次的线程切换。所以说并不是线程越多效率越高。

线程创建的三种方法

多线程你了解多少?看完你就全明白了!_第5张图片

1、创建线程方式一:继承Thread类

package com.example.boot.test; 
//创建线程方式一:继承Thread类,重新run()方法,调用start方法开启线程
public class TestThread extends Thread{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("我是run方法----"+i);
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        TestThread t =new TestThread();
        //调用start方法开启线程
        t.start();
        //主线程
        for (int i = 0; i < 2000; i++) {
            System.out.println("我是main方法----"+i);
        }
    }
}

部分输出结果:

多线程你了解多少?看完你就全明白了!_第6张图片

由此我们可以看出:

主线程main和子线程run方法交替执行, 是随机输出的,原因就是cpu将时间片分给不同的线程,线程获得时间片后就执行任务,所以这些线程在交替的执行输出,导致输出呈现乱序的效果。线程开启不一定立即执行,是由cpu调度执行的。

Thread.java类中的start()方法通知“线程规划器”,此线程已经准备就绪,准备调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,即让线程执行具体的任务,具有随机顺序执行的效果。

如果调用run()方法,而不是start(),其实就不是异步执行了,而是同步执行,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完毕后才可以执行后面的代码。如下图:

package com.example.boot.test;
//创建线程方式一:继承Thread类,重新run()方法,调用start方法开启线程
public class TestThread extends Thread{

    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我是run方法----"+i);
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        TestThread t =new TestThread();
        //调用run方法开启线程
        t.run();
        //主线程
        for (int i = 0; i < 2000; i++) {
            System.out.println("我是main方法----"+i);
        }
    }
}

部分输出结果:

多线程你了解多少?看完你就全明白了!_第7张图片

2、创建线程方式一:实现Runnable接口

package com.example.boot.test;
/**
 * 创建线程方式二:实现Runable接口,
 * 重新run()方法,
 * 执行线程需要丢入runable接口的实现类
 * 调用start方法启动线程
 * */

public class TestRunable implements Runnable{
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 20; i++) {
            System.out.println("我是run方法----"+i);
        }
    }

    public static void main(String[] args) {
        //创建runable接口的实现类
        TestRunable runable = new TestRunable();
        //创建一个线程对象,通过线程对象来开启我们的线程,静态代理,将runable接口的实现类
        Thread t =new Thread(runable);
        //调用start方法开启线程
        t.start();      
        //主线程
        for (int i = 0; i < 2000; i++) {
            System.out.println("我是main方法----"+i);
        }
    }
}

部分输出结果:


多线程你了解多少?看完你就全明白了!_第8张图片

由此我们可以看出:

实现Runnable接口的输入结果和继承Thread类一样都是交替运行的,但是我们知道java是单继承的,就比如我们一个类已经继承了其他的类,就不能使用继承Thread类来创建线程了。但是我们可以使用实现Runnable接口来创建线程。

实现Runnable接口与继承Thread类内部流程复杂度比较:

我们打Runable的源码可以看到里边只有一个run的方法,所以我们每次实现都有重写这个方法。看起来是非常的简单。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface Runnable is used
     * to create a thread, starting the thread causes the object's
     * run method to be called in that separately executing
     * thread.
     * 

* The general contract of the method run is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run(); }

那我们看一下Thred类的源码:

多线程你了解多少?看完你就全明白了!_第9张图片

我们可以看出Thread类是实现了Runable接口,着就意味着构造函数Thread(Runable target)不仅可以传入Runable接口的对象,而且可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他线程进行调用。

但是我们知道实现Runable接口和继承Thred类启动一个线程的执行过程是不一样的,如下图:

 

JVM直接调用的是Thread.java类的run()方法,该方法源代码如下:

多线程你了解多少?看完你就全明白了!_第10张图片

在方法中判断target变量是否为null,不为null就执行target对象的run()方法,target存储的对象就是前面声明的TestRunable run对象,对Thread构造方法传入Runable对象,再结合if判断就可以执行Runable对象的run()方法了。变量target是在init()方法中进行赋值初始化的,核心源码如下:

多线程你了解多少?看完你就全明白了!_第11张图片

而方法init()是在Thread.java构造方法中被调用的,源代码如下:

通过分析JDK源代码可以发现,实现Runale接口法在执行过程上比继承Thread类法稍微复杂一些。

3、实现Callable接口

Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的,方法可以有返回值,并且可以抛出异常。但是Runnable不行。Callable需要依赖FutureTask,用于接收运算结果。一个产生结果,一个拿到结果。FutureTask是Future接口的实现类,也可以用作闭锁。

package com.example.boot.test;

import java.util.concurrent.*;

/**
 * 创建线程方式三:实现Callable接口,
 * 重新call()方法,
 * 执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
 * 调用start方法启动线程
 * 得到返回值
 * */
public class TestCallable implements Callable{
    @Override
    public Integer call() {
        //run方法线程体
        int sum =0;
        for (int i = 0; i < 20; i++) {
            System.out.println("当前是第"+i+"次运算"+",值为"+sum);
            sum += i;
        }
        return sum;
    }
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建callable对象
        TestCallable call =new TestCallable();
        //执行Callable方式,需要FutureTask实现类的支持,用于接收运算结果
        FutureTask fu=new FutureTask(call);
        //启动线程
       new Thread(fu).start();
        //主线程
        for (int i = 0; i < 2000; i++) {
            System.out.println("我是main方法----"+i);
        }
        //2.接收线程运算后的结果
        try {
            Integer sum = fu.get(); //FutureTask 可用于闭锁
            System.out.println(sum);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        //得到返回值
        try {
            System.out.println("返回值是:" + fu.get());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

输出结果:

多线程你了解多少?看完你就全明白了!_第12张图片

由此我们可以看出,线程之间还是随机交替执行的。

三种创建线程方式的比较:

1、实现Runnable接口可以避免Java单继承特性而带来的局限;增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的;适合多个相同程序代码的线程去处理同一资源的情况。

2、继承Thread类和实现Runnable方法启动线程都是使用start方法,然后JVM虚拟机将此线程放到就绪队列中,如果有处理机可用,则执行run方法。

3、实现Callable接口要实现call方法,并且线程执行完毕后会有返回值。其他的两种都是重写run方法,没有返回值。

常问面试题总结:

1、并发和并行的区别

并发:多个线程同是争夺同一资源

并发:多个线程同是执行多个资源

2、wait和sleep的区别

1)来自不同的类

wait是Object类

sleep是Theard类

2)关于锁的释放

wait会释放锁

sleep会释放锁

3)使用的范围不同

wait必须在同步代码块中

sleep可以在任意地方

3、sychronized和locak的区别

1)sychronized是一个java的关键字,lock是一个java类

2)sychronized无法判断锁的状态,lock可以判断锁的状态

3)sychronized自动关闭锁,lock需要手动手动释放锁

4)sychronized会使线程阻塞,lock不一定会阻塞线程

5)可重入锁,不可以中断的,非公平;lock ,可重入锁,可以 判断锁,非公平(可以 自己设置)

6)synchronized 适合锁少量的代码同步问题,lock 适合锁大量的同步代码

4、volation和sychronized的区别

5、创建线程池的五种方法

Executors目前提供了5种不同的线程池创建配置:

1、newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
2、newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
3、newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
4、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
5、newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。

6、线程池的七大参数

1、corePoolSize 线程池核心线程数
创建线程池后,当有请求任务来之后,就会安排池中线程去执行请求任务,近似理解为今日当值线程。
当线程池中的线程数目达到了corePoolSize后,就会把任务放到缓存队列中;
2、maxmumPoolSize:
线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余空闲线程的存活时间,超时了没有人调用就会释放 。
当前线程池的数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
4、unit:keepAliveTime的时间单位
5、workQueue:任务队列,被提交但未被执行的任务
6、threadFactory:表示生成线程池中线程的线程工厂,用于创建线程,一般用默认的即可
7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)

7、线程池的四大拒绝策略

1、new ThreadPoolExecutor.AbortPolicy()   线程池的默认拒绝策略为AbortPolicy,即丢弃任务并抛出RejectedExecutionException异常
2、 new ThreadPoolExecutor.CallerRunsPolicy() 由调用线程处理该任务,如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务

3、new ThreadPoolExecutor.DiscardPolicy() 丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
4、new ThreadPoolExecutor.DiscardOldestPolicy() 丢弃队列最前面的任务,然后重新提交被拒绝的任务。

8、线程的几种状态
1. 新建(NEW):新创建了一个线程对象。
2. 可运行(RUNNABLE):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu 的使用权 。
3. 运行(RUNNING):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4. 阻塞(BLOCKED):阻塞状态是指线程因为某种原因放弃了cpu 使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice 转到运行(running)状态。
5. 死亡(DEAD):线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。

你可能感兴趣的:(玩转并发编程,多线程,java,面试)