线程和进程的区别
程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘,网络等设备。进程就是用来加载指令,管理内存,管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行
一个进程之内可以分为一到多个线程
二者对比:
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行和并发的区别
单核CPU
- 单核CPU下线程实际还是串行执行的
- 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
- 总结为一句话就是:微观串行,宏观并行
- 一般会将这种线程轮流使用CPU的做法叫做并发(concurrent)
多核CPU
每个核(core)都可以调度运行线程,这时候线程可以是并行的
对比
并发(concurrent)是同一时间应对(dealing with)多件事情的能力
并行(parallel)是同一时间动手做(doing)多件事情的能力
创建线程的方式
共有四种方式可以创建线程,分别是:
- 继承Thread类
- 实现runnable接口
- 实现callable接口
- 线程池创建线程
继承Thread类
实现runnable接口
实现Callable接口
线程池创建线程
使用runnable和callable创建线程的区别
- Runnable接口run方法没有返回值
- callable接口call方法有个返回值,是个泛型,和Future,FutureTask配合可以用来获取异步执行的效果
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
线程的run()和srart()有什么区别
start()用来启动线程,通过该线程调用run方法执行run方法中定义的逻辑代码。start方法只能被调用一次。
run()封装了要被线程执行的代码,可以被调用多次
线程包括哪些状态,状态之间是如何变化的
线程的状态可以参考JDK中Thread类中的枚举State
线程包括哪些状态
- 新建(NEW)
- 可运行(RUNNABLE)
- 阻塞(BLOCKED)
- 等待(WAITING)
- 时间等待(TIMED_WAITING)
- 终止(TREMINATED)
线程之间的状态是如何变化的
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态
- 线程获取到了CPU的执行权,执行结束是终止状态
- 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep()方法,进入计时等待状态,到时间后可切换为可执行状态
新建T1,T2,T3三个线程,如何保证他们按顺序执行
可以使用线程中的join方法解决
notify和notifyAll有什么区别
- notifyAll:唤醒所有wait的线程
- notify:只随机唤醒一个wait线程
wait和sleep方法的不同
- 共同点:
- wait,wait(long),sleep(long)的效果都是让当前线程放弃CPU的使用权,进入阻塞状态
- 不同点:
- 方法归属不同
- sleep(long)是Thread的静态方法
- 而wait,wait(long)都是Object的成员方法,每个对象都有
- 醒来时机不同
- 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
- wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
- 他们都可以被打断唤醒
- 锁特性不同(重点)
- wait方法的调用必须先获取wait对象的锁,而sleep则无限制
- wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃cpu,但你们还可以用)
- 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃cpu,你们也用不了)
- 方法归属不同
如何停止一个正在运行的线程
有三种方式可以停止线程:
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程(sleep,wait,join)的线程,线程会抛出InterruptedException异常
- 打断正常的线程,可以根据打断标记来标记是否退出线程
synchronized关键字的底层原理
synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其他线程再想获取这个【对象锁】时就会阻塞住
Monitor
Monitor被翻译为监视器,是由jvm提供,c++语言实现
- Owner:存储当前获取锁的线程的,只有一个线程可以获取
- EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
- WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
Monitor实现的锁属于重量级锁,你了解过锁升级吗
- Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换,进程的上下文切换,成本较高,性能比较低
- 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,他们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题
对象的内存结构
在HotSpot虚拟机中,对象在内存中存储的布局可分为三块区域:对象头(Header),实例数据(Instance Data)和对齐填充
MarkWord
- hashcode:25位的对象表示Hahs码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- prt_to_heavtweight_monitor:重量级锁状态下,指向对象Monitor的指针,占30位
重量级锁的对象是怎么关联上Monitor的呢
每个Java对象都可以关联一个Monitor对象,如果使用synchronized给对象上锁(重量级)之后,该对象的MarkWord中就被设置指向Monitor对象的指针
轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁hi没必要的。因此JVM引入了轻量级锁的概念
加锁流程:
1. 在线程栈中创建一个Lock Record,将其obj字段指向锁对象
2. 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁
3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用
4. 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁
解锁过程:
1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record
2. 如果Lock Record的Markword为null,代表这是一次重入,将obj设置为null后continue
3. 如果Lock Record的MarkWord不为null,则利用CAS指令将对象头的markword恢复成无锁状态。如果失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争的时候,每次重入仍然需要执行CAS操作
引入偏向锁来做进一步优化:只有第一次使用CAS将线程ID设置到对象的MarkWord头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS,以后只要不发生竞争,这个对象就归该线程所有。
JMM
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程之间的交互需要通过主内存
CAS
CAS的全称是:Compare And Swap(比较再交换),他体现的是一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性
在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作
- AbstractQueuedSynchornizer(AQS框架)
- AtomicXXX类
CAS数据交换流程
一个当前内存值V,旧的预期值A,即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自选的方式等待并再次尝试,直到成功。
- 因为没有加锁,所以线程不会陷入阻塞,效率较高
- 如果竞争激烈,重试频繁发生,效率会受影响
CAS底层实现
CAS底层依赖一个Unsafe类来直接调用操作系统底层的CAS指令
乐观锁和悲观锁
- CAS 是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算失败了也没关系
- synchronized是基于悲观锁的思想:最悲观的估计,得放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会。
volatile
一旦一个共享变量(类的成员变量,类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证线程间的可见性
- 禁止进行指令重排序
保证线程间的可见性
用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见。
禁止进行指令重排序
用volatile修饰共享变量会在读,写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止指令重排序的效果。
volatile使用技巧
- 写变量让volatile修饰的变量在代码的最后位置
- 读变量让volatile修饰的变量在代码的最开始位置
什么是AQS
全称是AbstractQueuedSynchronizer,即抽象队列同步器。他是构建锁或者其他同步组件的基础框架
AQS和synchronized的区别
synchronized | AQS |
---|---|
关键字,C++实现 | java语言实现 |
悲观锁,自动释放锁 | 悲观锁,手动开启和关闭 |
锁竞争激烈都是重量级锁,性能差 | 锁竞争激烈的情况下,提供了多种解决方案 |
AQS的常见类
- ReentrantLock:阻塞式锁
- Semaphore:信号量
- CountDownLatch:倒计时锁
AQS-基本工作机制
AQS多个线程共同争抢资源是如何保证原子性的
使用CAS设置state状态,保证操作的原子性
AQS是公平锁还是非公平锁
- 新的线程与队列中的线程共同来抢资源,是非公平锁
- 新的线程到队列中等待,只让队列中的head线程获得锁,是公平锁
ReentrantLock
ReentrantLock翻译过来是可重入锁,相对于synchronized他具备以下特点:
- 可中断
- 可以设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与synchronized一样,都支持重入
实现原理
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail只想双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
synchronized和Lock有什么区别
- 语法层面
- synchronized是关键字,源码在jvm中,用c++实现
- Lock是接口,源码由jdk提供,用java语言实现
- 使用synchronized时,退出同步代码块锁会自动释放,而使用lock时,需要手动调用unlock方法释放锁
- 功能层面
- 两者均属于悲观锁,都具备基本的互斥,同步,锁重入功能
- Lock提供了许多synchronized不具备的功能,例如公平锁,可打断,可超时,多条件变量
- Lock有适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁)
- 性能层面
- 在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能不赖
- 在竞争激烈时,Lock的实现通常会提供更好的性能
死锁产生的条件
死锁:一个线程需要同时获取多把锁,这时就容易发生死锁
线程t1持有A的锁等待获取B锁,线程t2持有B的锁等待获取A的锁
如何进行死锁诊断
当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- stack:查看java进程内线程的堆栈信息
其他解决工具
- jconsole:用于对jvm的内存,线程,类的监控,是一个基于jmx的GUI性能监控工具
- VisualVM:故障处理工具,能够监控线程,内存情况,查看方法的CPU时间和内存中的对象,已被GC的对象,反向查看分配的堆栈。
ConcurrentHashMap
ConcurrentHashMap是一种线程安全的高效Map集合,使用数组+链表/红黑二叉树实现
采用CAS+synchronized来保证并发安全进行实现
- CAS控制数组节点的添加
- synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题,效率得到提升
导致并发程序出现问题的根本原因是什么
java并发编程中有三大特征
- 原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
- 可见性:
- 有序性
内存可见性:让一个线程对共享变量的修改对另一个线程可见
有序性:指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,他不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是他会保证程序最终执行结果和代码顺序的结果是一致的。
线程池的核心参数
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
- corePoolSize:核心线程数目
- maximumPoolSize:最大线程数目 = (核心线程 + 救急线程的最大数目)
- keepAliveTime生存时间-救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit:时间单位-救急线程的生存时间单位,如秒,毫秒等
- workQueue:当没有空闲和核心线程时,新的任务会加入到此队列排队,队列满会创建救急线程执行任务
- threadFactory:线程工厂-可以定制线程对象的创建,例如设置线程名字,是否是守护线程等
线程池的执行原理
线程池中有哪些常见的阻塞队列
workQueue-当没有空闲核心线程时,新来任务会假如到此队列排队,队列满会创建救急线程执行任务
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO
- LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO
- DelayedWrokQueue:是一个优先级队列,他可以保证每次出队的任务是当前队列中执行时间最靠前的
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移除操作
ArrayBlockingQueue | LinkedBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候自动添加数据 | 提前初始化Node数组 |
入队会生成新的Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
如何确定核心线程数
- IO密集型任务:一般来说:文件读写,DB读写,网络请求等=》核心线程数大小设置为2N+1
- CPU密集型任务:一般来说:计算型代码,Bitmap转换,Gson转换等:核心线程数大小设置为N+1
线程池的种类有哪些
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种
- 创建使用固定线程数的线程池
- 核心线程数与最大线程数一样,没有救急线程
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
适用于任务量已知,相对耗时的任务
- 单线程化的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
- 核心线程数和最大线程数都是1
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
适用于按照顺序执行的任务
- 可缓存的线程池
- 核心线程数为0
- 最大线程数为Integer.MAX_VALUE
- 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移除操作
适合任务数比较密集,但每个任务执行时间较短的情况
- 提供了延迟和周期执行功能的ThreadPoolExectutor
线程池的使用场景
CountDownLatch
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其他多个线程完成某件事情后才能执行)
- 其中构造参数用来初始化等待计数值
- await()用来等待技术归零
- countDown()用来让计数减一
多线程使用场景(es数据批量导入)
在项目上线之前需要将数据库的数据批量导入到es索引库中,数据量高达一千万,一次性读取数据肯定会导致OOM,于是可以使用线程池方式导入,利用countDownLatch来控制,避免一次性加载太多,防止内存溢出
如何控制某个方法允许并发访问线程的数量
Semaphore信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
使用场景:通常用于那些资源有明确访问数量限制的场景,常用于限流
Semaphore使用步骤
- 创建Semaphore对象,可以给一个容量
- semaphore。acquire():请求一个信号量,这时候的信号量个数-1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,知道其他线程释放了信号量)
- semaphore.release():释放一个信号量,此时信号量个数+1
对ThreadLocal的理解
ThreadLocal是多线程中对于解决线程安全的一个操作类,他会为诶个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享
ThreadLocal的基本使用
- set(value)设置值
- get():获取值
- remove()清除值
ThreadLocal的实现原理&源码解析
ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离
ThreadLocal内存泄漏问题
Java对象中的四种引用类型:强引用,软引用,弱引用,虚引用
- 强引用:最为普通的引用方式,表示一个对象处于有用且必须得状态,如果一个对象具有强引用,则GC并不会回收它。即是堆内存不足了,宁可出现OOM,也不会对其进行回收。
- 弱引用:表示一个对象处于可能有用且必须得状态。在GC线程扫描内存区域时,一旦发现弱引用,就会回收到弱引用相关联的对象。对于弱引用的回收,无关内存区域是否足够,一旦发现则会被回收。
每一个Thread维护一个ThreadLocalMap,在ThreadLocalMap中的Entry对象继承了WeakReference。其中key为使用弱引用的ThreadLocal实例,value为线程变量的副本
防止内存泄漏:必须remove