1. 什么是线程和进程?
进程:它表示运行中的程序。在操作系统中能够独立运行,并作为资源分配的基本单位。系统运行一个程序就是一个进程从创建、运行到消亡的过程。
线程:是比进程更小的执行单位,能够完成进程中的一个功能,也被称为轻量级进程。一个进程在其执行的过程中可以产生多个线程。
进程与线程的不同点:同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。
<br />**程序计数器的作用,为什么程序计数器是私有的:**<br />**作用:**<br />1.字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。<br />2.在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。<br />(如果执行的是native方法,程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。)<br />**私有的目的:**为了线程切换后能恢复到正确的执行位置。
虚拟机栈和本地方法栈为什么要私有:
虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。
私有的目的:为了保证线程中的局部变量不被别的线程访问到。
堆和方法区是所有线程共享的资源:
- 堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存)。
- 方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
2. Java可以开启线程吗?
开不了,只能通过native方法start0(),让底层的C++去操作硬件。
3. 并行与并发
并发指的是多个任务交替进行,并行则是指真正意义上的“同时进行”
并发编程的本质:充分利用CPU的资源
如果系统内只有一个CPU,使用多线程时,在真实系统环境下不能并行,只能通过切换时间片的方式交替进行,从而并发执行任务。真正的并行只能出现在拥有多个CPU的系统中。
*4. 线程的生命周期和状态
初始状态(被创建)、运行状态、阻塞状态、等待状态、超时等待状态、终止状态
序号 | 状态名称 | 说明 |
---|---|---|
1 | NEW | 初始状态,线程被创建,但还没有调用start()方法 |
2 | RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态统称为运行状态 |
3 | BLOCKED | 阻塞状态,表示线程阻塞于锁 |
4 | WAITING | 等待状态,表示当前线程需要等待其他线程做出一些特定的动作(通知或中断) |
5 | TIME_WAITING | 超时等待状态,它在指定时间后自行返回 |
6 | TERMINATED | 终止状态,表示当前线程已经执行完毕 |
PS:wait(long)同样也不抱锁。
*5. sleep和wait的异同
相同点:两者都可以暂停线程的执行,都会让线程进入等待状态。
不同点: | sleep | wait |
---|---|---|
1.来自不同类 | sleep()是来自Thread类的静态方法,作用于当前线程 | wait()是来自Object类的实例方法,作用于对象本身 |
2.锁的释放不同 | sleep时不会释放锁 | wait时会释放锁 |
3.使用的范围不同 | sleep可以在任何地方使用 | wait必须在同步代码块中使用 |
4.唤醒方式不同 | sleep可以通过超时或interrupt()方法唤醒 | wait需要调用notify()或notifyAll()方法唤醒 |
5.状态不同 | 处于超时等待状态 | 无参的wait方法调用后处于等待状态。 |
6. synchronized关键字
synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized关键字最主要的三种使用方式:修饰实例方法、修饰静态方法、修饰代码块。
- 对于普通同步方法,锁是当前实例对象。
- 对于静态同步方法,锁是当前类的Class对象。
- 对于同步代码块,锁是synchronized括号里配置的对象。
当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
synchronized在JVM里是怎么实现的?
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)
synchronized用的锁是存在哪里的?
synchronized用到的锁是存在Java对象头里的。
*7. synchronized 和 Lock 的区别
synchronized | Lock |
---|---|
是内置Java关键字 | 是一个Java类 |
无法判断获取锁的状态 | 可以判断是否获取到锁(tryLock()方法) |
会自动释放锁,出现异常的时候会自动释放锁 | 必须要手动释放锁,如果不释放会产生死锁,所以需要在finally中进行 lock.unlock() |
没有获得锁的线程进入等待,等待的线程会一直等待下去,不能响应中断 | 不一定会等待下去 |
可重入锁,不可以中断,非公平 | 可重入锁,可以判断锁,默认非公平(可以设置) |
适合锁少量的代码同步问题 | 适合锁大量的同步代码 |
Lock可以提高多个线程进行读操作的效率。 如果竞争不激烈,两者性能差不多;大量线程竞争时,Lock的性能要远优于synchronized。 |
ReentrantLock 是Lock接口的实现类。
ReentrantLock比 synchronized增加了一些功能:
1.等待可中断 lock.lockInterruptibly()
2.可实现公平锁 new ReentrantLock(true);
3.可实现选择性通知 lock.newCondition() condition.await() condition.signal condition.signalAll()
*8. 创建线程的几种方式?
1.定义类继承Thread类,重写run()方法。
2.定义类实现Runnable接口,重写run方法;将该类对象作为参数传入Thread有参构造。
3.定义类实现Callable接口,重写call方法;将该类对象作为参数传入FutureTask有参构造,将FutureTask对象作为参数传入Thread有参构造。
4.创建线程池:
4.1. 调用Executors.newXXXX()方法
4.2. 创建ThreadPoolExecutor对象TP,将实现了Runnable接口的对象作为参数传给TP.execute()方法。
9. 为什么要调用start()方法执行run()方法,而不是直接调用run()方法?
- new 一个 Thread,线程进入初始状态;
- 调用 start()方法,会调用底层native方法start0(),启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。
- start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
- 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结:调用start()方法可以启动线程,使线程进入就绪状态,而run()方法只是Thread的一个普通方法调用,是在主线程中执行的。
*10. 线程池
线程池:3大方法、7大参数、4种拒绝策略
池化技术:事先准备好一些资源,要用拿走,不用还回。
线程池的好处:
1.降低资源消耗;
2.提高响应速度;
3.方便管理
线程复用、可以控制最大并发数、管理线程
10.1. 线程池:三大方法
底层都是调用ThreadPoolExecutor()方法
在自己使用线程池的时候一定要通过ThreadPoolExecutor()方法创建,而不是直接调用这三个方法!
1 | newSingleThreadExecutor() | 创建只有一条线程的线程池 | 无界队列LinkedBlockingQueue |
---|---|---|---|
2 | newFixedThreadPool(n) | 创建固定数量n条线程的线程池 | 无界队列LinkedBlockingQueue |
3 | newCachedThreadPool() | 创建根据需要调整线程数量的线程池 | 无容量队列SynchronousQueue |
4 | ScheduleThreadPool(n) | 在给定的延迟之后执行任务,或定期执行任务 | 延迟队列DelayQueue |
10.2. 线程池:七大参数,ThreadPoolExecutor()方法的7个参数
1 | int corePoolSize | 核心线程池大小 |
---|---|---|
2 | int maximumPoolSize | 最大核心线程池大小 |
3 | long keepAliveTime | 超时未调用释放 |
4 | TimeUnit unit | 超时单位 |
5 | BlockingQueue workQueue | 阻塞队列 |
6 | ThreadFactor threadFactory | 线程工厂 |
7 | RejectedExecutionHandler handler | 拒绝策略 |
10.3. 线程池:四种拒绝策略
RejectedExecutionHandler 接口的四个实现类
1 | ThreadPoolExecutor.AbortPolicy | 原理:超过线程池的最大承载,不处理并且抛出异常 |
---|---|---|
使用场景:比较关键的业务使用这种拒绝策略,在系统不能承载更大并发量的时候可以及时通过异常发现 | ||
2 |
ThreadPoolExecutor.DiscardPolicy |
原理:超过线程池的最大承载,不处理,不抛出异常 |
使用场景:不关键的业务使用这种拒绝策略,即使并发量达到最大值,也不需要太关心。 | ||
3 | ThreadPoolExecutor.DiscardOldestPolicy | 原理:超过线程池的最大承载,丢弃队列最前面的任务,重新提交被拒绝的任务。 |
使用场景:根据实际业务是否允许丢弃老任务来衡量。 | ||
4 | ThreadPoolExecutor.CallerRunsPolicy | 哪个线程递交过来的,由哪个线程处理 |
10.4. 线程池:工作队列
- ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序。
- LinkedBlockingQueue:是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。
- SynchronousQueue:是一个不存储元素的阻塞队列。每个插入操作后必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态(一存一取),吞吐量通常要高于Linked-BlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
- PriorityBlockingQueue:一个具有优先级的无限阻塞队列
了解:IO密集型,CPU密集型(调优)
池的最大大小该如何设置:
1.CPU 密集型,CPU是几核就设置为几,可以保持CPU的效率最高(Runtime.getRuntime().availableProcessores())
2.IO 密集型 ,一般设置的线程数要大于程序中十分消耗IO的线程数量
11. JMM
什么是JMM?
JMM:java内存模型,只是个概念
关于JMM的一些同步的约定:
1.线程解锁前,必须把共享变量立刻刷回主存;
2.线程加锁前,必须读取主存中的最新值到工作内存中;
3.加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
- lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
- unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
- read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
- load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
- use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
- assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入工作内存的变量副本中
- store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
- write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现。即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock。多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
12. volatile
Volatile 是 Java虚拟机提供轻量级的同步机制
1、保证可见性——工作内存需要知道主内存中的值发生了变化
2、不保证原子性——线程执行任务的时候不能被打扰,不能被分割
3、禁止指令重排——使用内存屏障
12.1. volatile为什么不能实现原子性
以i++为例 实际上分为3步
- 读取i的值
- 对i的值进行+1
- 将新值写入到缓存中
线程A进行到第 1 步后阻塞,线程B对i进行操作,虽然改变了i的值,但在线程A中,已经读取过i的初值,所以读取这个原子操作已经结束了。
13. 各种锁
1.公平锁、非公平锁
公平锁:公平,先来后到,不可以插队
非公平锁:不公平,可以插队。(默认)
2.可重入锁
可重入锁(递归锁,好像所有锁都是可重入锁),Lock锁必须配对(lock和unlock成对)否则会出现死锁
3.自旋锁
spinlock:当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
4.活锁
活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。
5.乐观锁与悲观锁
悲观锁:假设最坏的情况,每次拿数据都认为别人会修改,所以在操作之前都会上锁,别人想拿到这个数据就会阻塞直到前一个操作释放锁。
行锁,表锁,读锁,写锁等,都是操作之前先上锁;
Java中的synchronized和ReentrantLock等独占锁都是悲观锁思想的体现
乐观锁:假设最好的情况,每次拿数据都认为别人不会修改,所以不会上锁,但在更新的时候会判断在此期间别人有没有更新这个数据,可以使用版本号机制和CAS算法实现
版本号机制:A、B两个人对服务器的数据进行修改,该数据表示为(value,version),具体为(100,1)。
A 拿到数据后进行 100-20 = 80;
B 拿到数据后进行 100/20 = 5;
A先提交数据80,此时服务器中该数据的版本号为1,与拿到时的版本号相同,故将数据改为(80,2);
B提交数据5,此时服务器中该数据的版本号为2,与拿到时的版本后不同,故不能修改数据。
CAS算法:涉及到3个操作数:
- 需要读写的内存值V
- 进行比较的值A
- 拟写入的新值B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试
线程冲突少的时候使用乐观锁(CAS算法),此时加锁会增加CPU消耗
线程冲突多的时候使用悲观锁(加锁),此时过多的自旋会消耗CPU
14. 死锁
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生:线程各自占有锁的同时去争抢对方占有的锁。
如何避免:
1.避免一个线程同时获取多个锁;
2.避免一个线程在锁内同时占用多个资源,尽量保证每个锁都只占用一个资源;
3.尝试使用定时锁lock.tryLock(timeout);
4.对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
如何排查:在Terminal中用 jps -l 查看进程;然后使用 jstack 进程号 来查看该进程是否有死锁。
出现问题怎么办?
1.看日志
2.看堆栈信息
15. 多线程开发带来的问题与解决方法
(一)线程安全问题
线程安全问题指的是在某一线程从开始访问到结束访问某一数据期间,该数据被其他的线程所修改,那么对于当前线程而言,该线程就发生了线程安全问题,表现形式为数据的缺失,数据不一致等。
线程安全问题发生的条件:
1)多线程环境下,即存在包括自己在内存在有多个线程。
2)多线程环境下存在共享资源,且多线程操作该共享资源。
3)多个线程必须对该共享资源有非原子性操作。
线程安全问题的解决思路:
1)尽量不使用共享变量,将不必要的共享变量变成局部变量来使用。
2)使用synchronized关键字同步代码块,或者使用jdk包中提供的Lock为操作进行加锁。
3)使用ThreadLocal为每一个线程建立一个变量的副本,各个线程间独立操作,互不影响。
(二)性能问题
线程的生命周期开销是非常大的,一个线程的创建到销毁都会占用大量的内存。同时如果不合理的创建了多个线程,cup的处理器数量小于了线程数量,那么将会有很多的线程被闲置,闲置的线程将会占用大量的内存,为垃圾回收带来很大压力,同时cup在分配线程时还会消耗其性能。
解决思路:
利用线程池,模拟一个池,预先创建有限合理个数的线程放入池中,当需要执行任务时从池中取出空闲的先去执行任务,执行完成后将线程归还到池中,这样就减少了线程的频繁创建和销毁,节省内存开销和减小了垃圾回收的压力。同时因为任务到来时本身线程已经存在,减少了创建线程时间,提高了执行效率,而且合理的创建线程池数量还会使各个线程都处于忙碌状态,提高任务执行效率,线程池还提供了拒绝策略,当任务数量到达某一临界区时,线程池将拒绝任务的进入,保持现有任务的顺利执行,减少池的压力。
(三)活跃性问题
1)死锁,假如线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。多个线程环形占用资源也是一样的会产生死锁问题。
解决方法:
- 避免一个线程同时获取多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用 lock.tryLock(timeout) 来代替使用内部锁机制。
想要避免死锁,可以使用无锁函数(cas)或者使用重入锁(ReentrantLock),通过重入锁使线程中断或限时等待可以有效的规避死锁问题。
2)饥饿,饥饿指的是某一线程或多个线程因为某些原因一直获取不到资源,导致程序一直无法执行。如某一线程优先级太低导致一直分配不到资源,或者是某一线程一直占着某种资源不放,导致该线程无法执行等。
解决方法:
与死锁相比,饥饿现象还是有可能在一段时间之后恢复执行的。可以设置合适的线程优先级来尽量避免饥饿的产生。
3)活锁,活锁体现了一种谦让的美德,每个线程都想把资源让给对方,但是由于机器“智商”不够,可能会产生一直将资源让来让去,导致资源在两个线程间跳动而无法使某一线程真正的到资源并执行,这就是活锁的问题。
(四)阻塞
阻塞是用来形容多线程的问题,几个线程之间共享临界区资源,那么当一个线程占用了临界区资源后,所有需要使用该资源的线程都需要进入该临界区等待,等待会导致线程挂起,一直不能工作,这种情况就是阻塞,如果某一线程一直都不释放资源,将会导致其他所有等待在这个临界区的线程都不能工作。当我们使用synchronized或重入锁时,我们得到的就是阻塞线程,如论是synchronized或者重入锁,都会在试图执行代码前,得到临界区的锁,如果得不到锁,线程将会被挂起等待,知道其他线程执行完成并释放锁且拿到锁为止。
解决方法:
可以通过减少锁持有时间,读写锁分离,减小锁的粒度,锁分离,锁粗化等方式来优化锁的性能。
16. ThreadLocal
ThreadLocal中填充的变量属于当前线程,该变量对其他线程是隔离的。ThreadLocal为每个变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
使用场景:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。
17. JUC包中有哪些常用类
JUC指java.util.concurrent包
常用的有:
- Callable
- ConcurrentHashMap
- BlockingQueue
- TimeUnit
- Executors
- ExecutorService
- ThreadPoolExecutor
- AtomicInteger
17.1 AtomicInteger的ABA问题
JUC包下Atomic开头的类都采用CAS乐观锁实现。
CAS 对于一个要更新的变量 V,我们提供一个它的旧值 A 和新值 B,如果变量 V 的值等于旧值 A,那么更新成功,否则更新失败,这个过程是原子性的。
什么是ABA问题:*
其中,这个过程存在 ABA 问题,即如果另一个线程修改 V 值假设原来是 A,先修改成 B,再修改回成 A。当前线程的 CAS 操作无法分辨当前 V 值是否发生过变化。举个例子,线程 1 查询 V 的值为 A 与旧值 A 比较,值相等。线程 2 查询 V 的值为 A 与旧值 A 比较,值相等,更新值为 B。线程 1 更新值为 A。这样, V 的值又被更新成 A 了。
18. JUC并发类容器
ConcurrentHashMap:CAS+Synchronized
CopyOnWriteArrayList:可重入锁,写的时候加锁,读的时候不加锁。写的时候复制一份要操作的数组(volatile),之后再设置回去。
CopyOnWriteArraySet
ConcurrentLinkedQueue
LinkedBlockingQueue