关键词:
前言
多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理或同时多线程处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理” 。
多线程一直也是我们面试必考点,我个人珍藏了80道多线程面试题,从基础到高级,由于篇幅问题,我今天只分享一下多线程与并发基础的面试题,如需全部面试题,可戳此处提前获取!
一、进程与线程
进程:是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的 资源。
虽然系统是把资源分给进程,但是CPU很特殊,是被分配到线程的,所以线程是CPU分配的基本单位。
二者关系:
一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
方法区:则用来存放 NM 加载的类、常量及静态变量等信息,也是线程共享的 。
二者区别:
进程:有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响。
线程:是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
1) 简而言之,一个程序至少有一个进程,一个进程至少有一个线程.
2) 线程的划分尺度小于进程,使得多线程程序的并发性高。
3) 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
4) 每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
5) 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别
二、并发与并行
并发:是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。
并行:是说在单位时间内多个任务同时在执行 。
在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
并发过程中常见的问题:
1、线程安全问题:
多个线程同时操作共享变量1时,会出现线程1更新共享变量1的值,但是其他线程获取到的是共享变量没有被更新之前的值。就会导致数据不准确问题。
2、共享内存不可见性问题
Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量 。(如上图所示)
上图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。 那么Java内存模型里面的工作内存,就对应这里的 Ll或者 L2 缓存或者 CPU 的寄存器
1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。
2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
synchronized 的内存语义:
这个内存语义就可以解决共享变量内存可见性问题。进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性
Volatile的理解:
该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性
三、创建线程
1、继承Thread类
重写run方法:使用继承方式的好处是,在run()方法内获取当前线程直接使用this就可以了,无须使用Thread.currentThread()方法;不好的地方是Java不支持多继承,如果继承了Thread类,那么就不能再继承其他类。另外任务与代码没有分离,当多个线程执行一样的任务时需要多份任务代码。
public class ThreadRuning extends Thread
public ThreadRuning(String name)
//重写构造,可以对线程添加名字
super(name);
@Override
public void run()
while(true)
System.out.println("good time");
//在run方法里,this代表当前线程
System.out.println(this);
public static void main(String[] args)
ThreadRuning threadRuning = new ThreadRuning("1111");
threadRuning.start();
2、实现Runable接口
实现run方法:解决继承Thread的缺点,没有返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
3、实现Callable接口
实现call方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
使用继承方式的好处是方便传参,你可以在子类里面添加成员变量,通过set方法设置参数或者通过构造函数进行传递,而如果使用Runnable方式,则只能使用主线程里面被声明为final的变量。不好的地方是Java不支持多继承,如果继承了Thread类,那么子类不能再继承其他类,而Runable则没有这个限制。前两种方式都没办法拿到任务的返回结果,但是Callable方式可以
四、Thread类详解
线程特性:
1、线程能被标记为守护线程,也可以是用户线程
2、每个线程均分配一个name,默认为(Thread-自增数字)的组合
3、每个线程都有优先级.高优先级线程优先于低优先级线程执行. 1-10,默认为5
4、main所在的线程组为main,构造线程的时候没有现实的指定线程组,线程组默认和父线程一样
5、当线程中的run()方法代码里面又创建了一个新的线程对象时,新创建的线程优先级和父线程优先级一样.
6、当且仅当父线程为守护线程时,新创建的线程才会是守护线程.
7、当JVM启动时,通常会有唯一的一个非守护线程(这一线程用于调用指定类的main()方法)
JVM会持续执行线程直到下面情况某一个发生为止:
1)类运行时exit()方法被调用 且 安全机制允许此exit()方法的调用.
2)所有非守护类型的线程均已经终止,or run()方法调用返回or在run()方法外部抛出了一些可传播性的异常.
Init方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
|
构造方法:所有的构造方法都是调用init()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
线程状态:
1 2 3 4 5 6 7 8 |
|
NEW:状态是指线程刚创建, 尚未启动
RUNNABLE:状态是线程正在正常运行中, 当然可能会有某种耗时计算/IO等待的操作/CPU时间片切换等, 这个状态下发生的等待一般是其他系统资源, 而不是锁, Sleep等
BLOCKED:这个状态下, 是在多个线程有同步操作的场景, 比如正在等待另一个线程的synchronized 块的执行释放, 或者可重入的 synchronized块里别人调用wait() 方法, 也就是这里是线程在等待进入临界区
WAITING:这个状态下是指线程拥有了某个锁之后, 调用了他的wait方法, 等待其他线程/锁拥有者调用 notify / notifyAll 一遍该线程可以继续下一步操作, 这里要区分 BLOCKED 和 WATING 的区别, 一个是在临界点外面等待进入, 一个是在理解点里面wait等待别人notify, 线程调用了join方法 join了另外的线程的时候, 也会进入WAITING状态, 等待被他join的线程执行结束
TIMED_WAITING:这个状态就是有限的(时间限制)的WAITING, 一般出现在调用wait(long), join(long)等情况下, 另外一个线程sleep后, 也会进入TIMED_WAITING状态
TERMINATED: 这个状态下表示 该线程的run方法已经执行完毕了, 基本上就等于死亡了(当时如果线程被持久持有, 可能不会被回收)
(在很多文章中都写了running状态,其实源码里面只有六种的,当自己写一个线程通过while一直保持执行状态,然后使用jconsole工具去查看线程的状态,确实是Runable状态)
Api文档是这么说的:
其实我们可以理解为两种状态,一个是running,表示正在执行,一个是runable,表示准备就绪了,只是在等待其他的系统资源。然后我们就可以理解如下图
Start方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
yield方法:
1 |
|
是一个本地方法,提示线程调度器当前线程愿意放弃当前CPU的使用。如果当前资源不紧张,调度器可以忽略这个提示。本质上线程状态一直是RUNNABLE,但是我可以理解为RUNNABLE到RUNNING的转换
sleep方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
sleep方法,有一个重载方法,sleep方法会释放cpu的时间片,但是不会释放锁,调用sleep()之后从RUNNABLE状态转为TIMED_WAITING状态
join方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
join某个线程A,会使得线程B进入等待,知道线程A结束,或者到达给定的时间,那么期间线程B处于BLOCKED的状态,而不是线程A
五、其他方法
接下来聊一下Object类的wait,notify和notifyAll方法
wait方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
可见wait()和wait(long timeout, int nanos)都在在内部调用了wait(long timeout)方法。
下面主要是说说wait(long timeout)方法
wait方法会引起当前线程阻塞,直到另外一个线程在对应的对象上调用notify或者notifyAll()方法,或者达到了方法参数中指定的时间。
调用wait方法的当前线程一定要拥有对象的监视器锁。
wait方法会把当前线程T放置在对应的object上的等待队列中,在这个对象上的所有同步请求都不会得到响应。线程调度将不会调用线程T,在以下四件事发生之前,线程T会被唤醒(线程T是在其代码中调用wait方法的那个线程)
1、当其他的线程在对应的对象上调用notify方法,而在此对象的对应的等待队列中将会任意选择一个线程进行唤醒。
2、其他的线程在此对象上调用了notifyAll方法
3、其他的线程调用了interrupt方法来中断线程T
4、等待的时间已经超过了wait中指定的时间。如果参数timeout的值为0,不是指真实的等待时间是0,而是线程等待直到被另外一个线程唤醒为止。
被唤醒的线程T会被从对象的等待队列中移除并且重新能够被线程调度器调度。之后,线程T会像平常一样跟其他的线程竞争获取对象上的锁;一旦线程T获得了此对象上的锁,那么在此对象上的所有同步请求都会恢复到之前的状态,也就是恢复到wait被调用的情况下。然后线程T从wait方法的调用中返回。因此,当从wait方法返回时,对象的状态以及线程T的状态跟wait方法被调用的时候一样。
线程在没有被唤醒,中断或者时间耗尽的情况下仍然能够被唤醒,这叫做伪唤醒。虽然在实际中,这种情况很少发生,但是程序一定要测试这个能够唤醒线程的条件,并且在条件不满足时,线程继续等待。换言之,wait操作总是出现在循环中,就像下面这样:
1 2 3 4 5 6 |
|
如果当前的线程被其他的线程在当前线程等待之前或者正在等待时调用了interrupt()中断了,那么会抛出InterruptedExcaption异常。直到这个对象上面的锁状态恢复到上面描述的状态以前,这个异常是不会抛出的。
要注意的是,wait方法把当前线程放置到这个对象的等待队列中,解锁也仅仅是在这个对象上;当前线程在其他对象上面上的锁在当前线程等待的过程中仍然持有其他对象的锁。
这个方法应该仅仅被持有对象监视器的线程调用。
wait(long timeout, int nanos)方法的实现中只要nanos大于0,那么timeout时间就加上一毫秒,主要是更精确的控制时间,其他的跟wait(long timeout)一样
notify方法
1 |
|
通知可能等待该对象的对象锁的其他线程。由JVM(与优先级无关)随机挑选一个处于wait状态的线程。
在调用notify()之前,线程必须获得该对象的对象级别锁
执行完notify()方法后,不会马上释放锁,要直到退出synchronized代码块,当前线程才会释放锁
notify()一次只随机通知一个线程进行唤醒
notifyAll()方法
1 |
|
和notify()差不多,只不过是使所有正在等待池中等待同一共享资源的全部线程从等待状态退出,进入可运行状态
让它们竞争对象的锁,只有获得锁的线程才能进入就绪状态
每个锁对象有两个队列:就绪队列和阻塞队列
- 就绪队列:存储将要获得锁的线程
- 阻塞队列:存储被阻塞的线程
六、实例
1、sleep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
2、join和中断(推荐用标记中断)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
|
3、优先级和守护进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
|
4、生产者与消费者(忘记在哪篇文章看到的了。。抱歉)
定义一个接口:
1 2 3 4 5 6 |
金九银十,史上最强java面试题整理。 ...,会从以下几个方面汇总,本文会长期更新。Java面试篇史上最全Java面试题,带全部答案史上最全69道Spring面试题和答案史上最全Java多线程面试题及答案史上最全50道Redis面试题及答案史上最全 55道MySQL面试题总结i++是线程安... 查看详情 2019年全网最热门的123个java并发面试题总结...辑清晰、思维缜密,这样才能写出高效、安全、可靠的多线程并发程序。 关于Java并发编程的知识总结了个思维导图,分享给大家 整理了网络上热门的123道Java并发面试题,看看你都遇到过哪些? Java并发编程(一)1... 查看详情 java并发面试题题目参考自并发编程网:http://ifeve.com多线程java中有几种方法可以实现一个线程?继承Thread类;实现Runnable接口;实现Callable接口通过FutureTask包装器来创建Thread线程;使用ExecutorService、Callable、Future实现有返回结果的多线... 查看详情 java并发面试题一、什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个... 查看详情 java面试:投行的15个多线程和并发面试题(代码片段)多线程和并发问题已成为各种Java面试中必不可少的一部分。如果你准备参加投行的Java开发岗位面试,比如巴克莱银行(Barclays)、花旗银行(Citibank)、摩根史坦利投资公司(MorganStanley),你会遇到很多有关多线程的面试题。... 查看详情 java面试:投行的15个多线程和并发面试题(代码片段)多线程和并发问题已成为各种Java面试中必不可少的一部分。如果你准备参加投行的Java开发岗位面试,比如巴克莱银行(Barclays)、花旗银行(Citibank)、摩根史坦利投资公司(MorganStanley),你会遇到很多有关多线程的面试题。... 查看详情 java并发面试题精选1,什么是线程?线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,你可以使用多线程对运算密集型任务提速。比如,如果一个线程完成一个... 查看详情 史上最全高级java教程总结版(强烈建议收藏)本篇文章主要用于分享java相关的视频,课程等,涉及面比较广,建议先收藏再慢慢观看,信息量比较大。适合新手,老手各阶段Java程序员。订阅本专栏《高级java教程系列》的福利!目录java知识体系列表面试题库 java各知识... 查看详情 史上最全高级java教程总结版(强烈建议收藏)本篇文章主要用于分享java相关的视频,课程等,涉及面比较广,建议先收藏再慢慢观看,信息量比较大。适合新手,老手各阶段Java程序员。订阅本专栏《高级java教程系列》的福利!目录java知识体系列表面试题库 java各知识... 查看详情 java基础16:java多线程基础最全总结Java基础16:Java多线程基础最全总结Java中的线程Java之父对线程的定义是:线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)... 查看详情 个人珍藏的80道多线程并发面试题(1-10答案解析)前言个人珍藏的80道Java多线程/并发经典面试题,因为篇幅太长,现在先给出1-10的答案解析哈,后面一起完善,并且上传github哈~?https://github.com/whx123/JavaHome?「公众号:捡田螺的小男孩」1.synchronized的实现原理以及锁优化?synchroniz... 查看详情 java并发面试题(代码片段)多线程java中有几种方法可以实现一个线程?继承Thread类;实现Runnable接口;实现Callable接口通过FutureTask包装器来创建Thread线程;使用ExecutorService、Callable、Future实现有返回结果的多线程(也就是使用了ExecutorService来管理前面的三... 查看详情 安全经典面试题总结-史上最全面试题思维导图总结(2022最新版)(代码片段)写在前面🍁个人主页:HNUJSY✨本期专栏:《史上最全经典面试题总结》欢迎订阅学习~📌Xmind文件获取:GitHub持续更新中,别忘了star喔~目录写在前面思维导图(png格式可下载放大)安全跨站脚本漏... 查看详情 012期javase面试题:多线程(代码片段)开篇介绍大家好,我是Java最全面试题库的提裤姐,今天这篇是JavaSE系列的第十二篇,主要总结了Java中的多线程问题,多线程分为三篇来讲,这篇是第二篇,在后续,会沿着第一篇开篇的知识线路一直总结下去,做到日更!如果... 查看详情 两万字长文,史上最全c++年度总结!【编者按】C++四十年,历久弥新长盛不衰。几日前CSDN组织了一次C++直播对话,在非常短的时间内就吸引了两万多开发者观看,足以说明C++在开发者中的影响力。本文的四位作者联合撰文,写下了... 查看详情 两万字长文,史上最全c++年度总结!【编者按】C++四十年,历久弥新长盛不衰。几日前CSDN组织了一次C++直播对话,在非常短的时间内就吸引了两万多开发者观看,足以说明C++在开发者中的影响力。本文的四位作者联合撰文,写下了... 查看详情 史上最全的并发编程学习目录目录一:线程基础知识1.并发编程的基本概念2.线程的基本操作3.线程之间的通信wait和notify4.join和yield以及sleep详解5.synchronized关键字讲解6.volatile原理7.线程组8.线程优先级9.守护线程10.ThreadLocal二:JDK并发包1.jdk并发工具类2.jdk并发... 查看详情 013期javase面试题(十三):多线程(代码片段)开篇介绍大家好,我是Java最全面试题库的提裤姐,今天这篇是JavaSE系列的第十三篇,主要总结了Java中的多线程问题,多线程分为三篇来讲,这篇是第三篇,在后续,会沿着第一篇开篇的知识线路一直总结下去,做到日更!如果... 查看详情 |