线程与进程区别
进程是正在运行的程序的实例,进程中包含了线程,每个线程执行不同的任务
不同进程使用不同的内存空间,在当前进程下所有线程可以共享内存空间
线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指从一个线程切换到另一个线程)
并行和并发有什么区别
在多核CPU下,
- 并发指同一时间,多个线程轮流使用一个或多个CPU
- 并行是指在同一时间内,多个CPU同时执行多个不同的线程
线程创建方式
继承Thread类
public class MyThread extends Thread{
@Override
public void run(){
System.out.println("MyThread...run...");
}
public static void main(String[] args){
// 创建MyThread对象
Mythread t1 = new MyThread();
t1.start();
}
}
实现runnable接口
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("Runing");
}
public static void main(String[] args){
MyRunnable mr = new MyRunnable();
Thread t1 = new Thread(mr);
Thread t2 = new Thread(mr);
t1.start();
t2.start();
}
}
实现Callable接口
线程池创建线程
Runnable和Callable区别
- Runnable接口的run方法没有返回值
- Callable接口call方法有返回值,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部笑话,不能继续抛出
在启动线程时,可以使用run()方法吗?run()方法与start()有什么区别?
- start():用来启动线程,通过该线程调用run方法中定义的代码逻辑。start方法只能被调用一次
- run():封装了要被线程执行的代码,可以被调用多次。
线程的状态之间如何变化
- 创建线程对象是新建状态
- 调用了start()方法转变为可执行状态
- 线程获取了CPU的执行权,执行结束是终止状态
- 可执行状态的过程中,如果没有获取CPU的执行权,可能回切换为其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获得锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
notify()和notifyAll()有什么区别
notifyAll:唤醒所有wait的线程
notify: 只随即唤醒一个wait线程
在java中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方法执行后回释放对象锁,允许其他线程获得该对象锁;而sleep如果在synchronized代码块中执行,并不会释放对象锁
如何停止一个正在运行的线程
有三种方式可以停止线程
- 使用退出标志,使线程正常退出,即当run方法完成后线程终止
- 使用stop方法强行终止(不推荐)
- 使用interrupt方法中断线程。打断阻塞的线程(sleep,wait,join),线程会抛出InterruptedException异常;打断正常的线程,可以根据打断状态来标记是否退出线程。
synronized关键字的底层原理
- Synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有对象锁
- 它的底层由monitor实现,monitor是jvm级别的对象,线程获得锁需要使用对象关联monitor
- monitor内部有三个属性,分别是owner、entrylist、waitset。其中owner是关联的获得锁的线程,且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程;
Monitor实现的锁属于重量级锁,你了解过锁升级吗?
Java的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只能被一个线程持有、不同线程交替持有和多线程竞争锁三种情况。一旦锁发生了竞争,都会升级为重量级锁。
锁的类型 | 描述 |
---|---|
重量级锁 | 底层使用的Monitor实现,涉及用户态和内核态的切换、进程的上下文切换,成本较高,性能较低 |
轻量级锁 | 线程加锁的时间是错开的,也就是没有竞争,可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁的性能提升了很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 当一段很长的时间内都只被一个线程使用时,可用使用偏向锁。在第一次获得锁时,会有一个CAS操作,之后改线程在获得锁只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令。 |
谈谈JMM(Java内存模型)
- JMM,又称Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程和线程之间是相互隔离的,线程和线程交互需要通过主内存
CAS是什么
- 全称是Compare And Swap,体现的是一种乐观锁的思想,在无锁状态下保证线程操作共享数据的原子性
- CAS使用到的地方很多:AQS框架、AtomicXXX类
- 在操作系统共享变量的时候使用的自旋锁,效率上更高一点
- 底层是调用的Unsafe类中的方法,都是操作系统提供的。
乐观锁和悲观锁的区别
- 乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了,具体方法可以使用版本号机制或 CAS 算法。乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量。
- 悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。
对volatile关键字的理解
一但一个共享变量(类成员变量、静态成员变量)被volitile修饰,其具有了两层含义
- 保证线程间的可见性: 用volatile修饰共享变量,能够防止编译器等优化发生让一个变量的修改对另一个线程可见
- 禁止进行指令重排序:volatile修饰共享变量会在读写共享变量时加入不同的屏障,阻止其它读写操作越过屏障,从而达到阻止重新排序的效果。
- 写操作加的屏障是阻止其上方任何写操作排到volatile变量之下
- 读操作阻止下方其它读操作越过屏障拍到volatile变量读之上
synchronized 和 volatile 有什么区别?
- volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
- volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
- volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
什么是AQS
- AQS是多线程的队列同步器,在基础框架中使用的锁机制
- AQS内部维护了一个先进先出的双向队列,队列中存储的是排队的进程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认为0(无锁状态),如果队列中的有一个线程成功将state修改为1,则视为改线程获取了资源。
- 对state修改的时候使用了cas操作,保证多个线程修改的情况下的原子性。
ReentrantLock的实现原理
ReentrantLock,可重入锁,相对于synchronized具备以下特点
- 可中断
- 可设置超时时间
- 可以设置公平锁
- 支持多个条件变量
- 与sunchronized一样,支持重入
ReentranLock主要利用CAS+AQS队列来实现,支持公平锁和非公平锁,两者的实现类似
构造方法接收一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁。
非公平锁
- 线程抢锁后使用cas的方式修改state状态,修改状态成功为1,让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 如果修改失败,将进入双向队列等待
- 当exclusiveOwnerThread属性为null时,会唤醒双向队列中等待的线程
- 公平锁按先后顺序获取锁,非公平锁则不再排队中的线程也可以参与抢锁。
synchronized与Lock有什么区别
- 语法层面
synchronized是关键字,由c++实现
Lock是接口,由java语言实现
使用synchronized时,退出同步代码块锁会自动释放;而使用Lock时,需要调用unlock手动释放锁
- 功能层面
二者均属于悲观锁,都具备基本的互斥、同步、锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,例如ReentrantLock(可重入锁)
- 性能层面
在竞争较小时,synchronized做了大量优化,如偏向锁、轻量级锁等
在竞争激烈时,Lock通常性能更好
死锁产生的条件
当一个线程需要同时获得多把锁时,就容易发生死锁。假设存在两把锁LockA和LockB,线程t1和t2都需要这两把锁,但是t1获取了LockA,t2获取了LockB,这时就产生了死锁。
ConcurrentHashMap
线程安全的HashMap。
- 底层数据结构
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑树
- 加锁的方式
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑树的首节点,相对Segment分段锁粒度更细,性能更好
JDK1.7 ConcurrentHashMap
首先将数据分为一段一段(这个“段”就是 Segment
)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。Segment继承了ReentrantLock
,承担可重入锁的功能,HashEntry
用于存储键值对数据。
线程池
线程池的核心参数
public ThreadPoolExecutor(int coolPoolSize,
int maximumPoolSize,
long keepAliveTime,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize 核心线程数目
- maximumPoolSize 最大线程数目=(核心线程数目+临时线程数目)
- keepAliveTime 生存时间(指临时线程的生存时间,生存时间内没有新任务,此线程资源会释放)
- unit 时间单位 (临时线程的生存时间单位,如秒、毫秒等)
- workQueue 当没有空闲核心线程时,新任务会加入到此排队队列,队列满会创建救急线程执行任务
- threadFactory 线程工厂(可以定制线程对象的创建,如设置线程名字、是否为守护线程)
- handler 拒绝策略(当所有线程在忙,workQueue也满时,会按照策略拒绝新线程)
线程池的执行原理
线程池中有哪些常见的阻塞队列
线程池中常见的阻塞队列有ArrayBlockQueue和LinkedBlockingQueue
LinkedBlockingQueue | ArrayBlockQueue |
---|---|
默认无界,支持有界 | 有界 |
底层是链表 | 底层是数组 |
懒惰的,创建节点时添加数据 | 提前初始化Node数组 |
入队会产生新的Node | Node需要提前创建好 |
两把锁 | 一把锁 |
如何确定核心线程数
- 对于高并发、任务执行时间短的任务 -->(CPU核数+1),减少线程上下文切换
- 并发不高、任务执行时间长
- IO密集型的任务 -->(CPU核数*2+1)
- 计算密集型的任务 --> (CPU核数+1)
- 并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体框架的设计,首先看这些业务中的数据能否进行缓存,其次增加服务器
线程池的种类
常见的有四种
- 创建使用固定线程数的线程池
FixedThreadPoolExecutor
- 核心线程数与最大线程数一样,没有救急线程
- 阻塞队列是
LinkedBlockingQueue
,最大容量为Integer.MAX_VALUE
适用于任务量
- 单线程化的线程池
SingleThreadPoolExecutor
只会用唯一的工作线程执行任务,保证所有任务按照指定顺序(FIFO)执行 SingleThreadPoolExecutor- 核心线程数和最大线程数都是1
- 阻塞队列是
LinkedBlockingQueue
,最大容量为Integer.MAX_VALUE
适用于按照顺序执行的任务要求
- 可缓存线程池
CatchedThreadPoolExecutor
创建一个可缓存的线程池,如果线程池长度超过处理需求,可回收空闲线程,若无可回收,则新建线程- 核心线程数为0
- 最大线程数是Integer.MAX_VALUE
- 组赛队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作必须等待一个移出操作
适用于任务数比较密集,但每个任务执行时间较短的情况
- 提供了延迟和周期执行功能的线程池
ScheduledThreadPoolExecutor
为什么不建议用Executors创建线程池
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这种处理方式更加明确线程池的运行规则,避免资源耗尽风险
- FixedThreadPool和SingleThreadPool: 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
- CachedThreadPool: 允许的船舰线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM
使用场景
CountDownLatch
CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或多个线程,等待其它多个线程完成某件事后才能执行)
- 构造参数用来初始化等待计数值
- await() 用来等待计数清零
- countDown() 用来让计数减一
异步线程 ⭐️
为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间。
如何控制某个方法允许并发访问线程的数量
可以通过信号量(semaphore)进行限制,通常用于有明确访问数量限制的场景。
使用步骤
- 创建Semophore对象,可以指定容量
- semaphore.acquire():请求一个信号量,这时候的信号量个数-1(一旦信号量个数变为附属,再次请求的时候就会阻塞,直到其他线程释放信号量)
- semaphore.release():释放一个信号量,此时信号量个数+1
谈谈你对ThreadLocal的了解
ThreadLocal是多线程中解决线程安全的一个操作类,会为每个线程都分配一个独立的线程副本,从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程的资源共享。
ThreadLocal类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
** 基本使用方法**
- set() 设置值
- get() 获取值
- remove() 清除值