- 创建线程是几种方式
- Thread的start和run的区别
- sleep 和 wait的区别
- 线程的几种状态
- Synchronized 和 lock的区别
- 你知道AQS吗
- 悲观锁和乐观锁
- 你知道什么是CAS嘛
- Synchronized 加非静态和静态方法上的区别
- Synchronized(this) 和 Synchronized (User.class)的区别
- Synchronized 和 volatitle 关键字的区别
- synchronized 锁的原理
- synchronized 锁升级原理
- 乐观锁的使用场景(数据库,ES)
- AtomicInterger怎么保证并发安全性的
- 什么是重入锁,什么是自旋锁,什么是阻塞
- 你用过JUC中的类吗,说几个
- ThreadLocal的作用和原理
- 线程池的作用
- Executors创建四种线程池
- 线程池的执行流程
- 线程池构造器的7个参数
- 线程池拒绝策略有几种
- 你知道ScheduledThreadPool使用场景吗
创建线程是几种方式
方式一:继承Thread类,覆写run方法,创建实例对象,调用该对象的start方法启动线程
方式二:创建Runnable接口的实现类,类中覆写run方法,再将实例作为此参数传递给Thread类有参构造创建线程对象,调用start方法启动
方式三:创建Callable接口的实现类,类中覆写call方法,创建实例对象,将其作为参数传递给FutureTask类有参构造创建FutureTask对象,再将FutureTask对象传递给Thread类的有参构造创建线程对象,调用start方法启动
Thread有单继承的局限性,Runnable和Callable三避免了单继承的局限,使用更广泛。Runnable适用于无需返回值的场景,Callable使用于有返回值的场景
Thread的start和run的区别
start是开启新线程, 而调用run方法是一个普通方法调用,还是在主线程里执行。没人会直接调用run方法
sleep 和 wait的区别
第一,sleep方法是Thread类的静态方法,wait方法是Object类的方法
第二:sleep方法不会释放对象锁,wait方法会释放对象锁
第三:sleep方法必须捕获异常,wait方法不需要捕获异常
线程的几种状态
新建状态:线程刚创建,还没有调用start方法之前
就绪状态:也叫临时阻塞状态,当调用了start方法后,具备cpu的执行资格,等待cpu调度器轮询的状态
运行状态:就绪状态的线程,获得了cpu的时间片,真正运行的状态
冻结状态:也叫阻塞状态,指的是该线程因某种原因放弃了cpu的执行资格,暂时停止运行的状态,比如调用了wait,sleep方法
死亡状态:线程执行结束了,比如调用了stop方法
Synchronized 和 lock的区别
他们都是用来解决并发编程中的线程安全问题的,不同的是
- synchronized是一个关键字,依靠Jvm内置语言实现,底层是依靠指令码来实现;Lock是一个接口,它基于CAS乐观锁来实现的
- synchronized在线程发生异常时,会自动释放锁,不会发生异常死锁,Lock在异常时不会自动释放锁,我们需要在finally中释放锁
- synchronized是可重入,不可判断,非公平锁,Lock是可重入,可判断的,可手动指定公平锁或者非公平锁
你知道AQS吗
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制,它维护了一个volatile修饰的 int 类型的,state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
工作思想是如果被请求的资源空闲,也就是还没有线程获取锁,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果请求的资源被占用,就将获取不到锁的线程加入队列。
悲观锁和乐观锁
悲观锁和乐观锁,指的是看待并发同步问题的角度
- 悲观锁认为,对同一个数据的并发操作,一定是会被其他线程同时修改的。所以在每次操作数据的时候,都会上锁,这样别人就拿不到这个数据。如果不加锁,并发操作一定会出问题。用阳间的话说,就是总有刁民想害朕
- 乐观锁认为,对同一个数据的并发操作,是不会有其他线程同时修改的。它不会使用加锁的形式来操作数据,而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据
悲观锁一般用于并发小,对数据安全要求高的场景,乐观锁一般用于高并发,多读少写的场景,通常使用版本号控制,或者时间戳来解决.
你知道什么是CAS嘛
CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
Synchronized 加非静态和静态方法上的区别
实例方法上的锁,锁住的是这个对象实例,它不会被实例共享,也叫做对象锁
静态方法上的锁,锁住的是这个类的字节码对象,它会被所有实例共享,也叫做类锁
Synchronized(this) 和 Synchronized (User.class)的区别
Synchronized(this) 中,this代表的是该对象实例,不会被所有实例共享
Synchronized (User.class),代表的是对类加锁,会被所有实例共享
Synchronized 和 volatitle 关键字的区别
这两个关键字都是用来解决并发编程中的线程安全问题的,不同点主要有以下几点
第一:volatile的实现原理,是在每次使用变量时都必须重主存中加载,修改变量后都必须立马同步到主存;synchronized的实现原理,则是锁定当前变量,让其他线程处于阻塞状态
第二:volatile只能修饰变量,synchronized用在修饰方法和同步代码块中
第三:volatile修饰的变量,不会被编译器进行指令重排序,synchronized不会限制指令重排序
第四:volatile不会造成线程阻塞,高并发时性能更高,synchronized会造成线程阻塞,高并发效率低
第五:volatile不能保证操作的原子性,因此它不能保证线程的安全,synchronized能保证操作的原子性,保证线程的安全
synchronized 锁的原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实 现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖 底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,涉及到用户态到内核态的切换,会让整个程序性能变得很差。
因此在JDK1.6及以后的版本中,增加了锁升级的过程,依次为无锁,偏向锁,轻量级锁,重量级锁。而且还增加了锁粗化,锁消除等策略,这就节省了锁操作的开销,提高了性能
synchronized 锁升级原理
每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
- 偏向锁(无锁)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
- 轻量级锁(CAS):
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
- 重量级锁:
如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
乐观锁的使用场景(数据库,ES)
场景一:ES中对version的控制并发写。
场景二:数据库中使用version版本号控制来防止更新覆盖问题。
场景三:原子类中的CompareAndSwap操作
AtomicInterger怎么保证并发安全性的
通过CAS操作原理来实现的,就可见性和原子性两个方面来说
它的value值使用了volatile关键字修饰,也就保证了多线程操作时内存的可见性
Unsafe这个类是一个很神奇的类,而compareAndSwapInt这个方法可以直接操作内存,依靠的是C++来实现的,它调用的是Atomic类的cmpxchg函数。而这个函数的实现是跟操作系统有关的,比如在X86的实现就利用汇编语言的CPU指令lock cmpxchg,它在执行后面的指令时,会锁定一个北桥信号,最终来保证操作的原子性
什么是重入锁,什么是自旋锁,什么是阻塞
可重入锁是指允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作
自旋锁不是锁,而是一种状态,当一个线程尝试获取一把锁的时候,如果这个锁已经被占用了,该线程就处于等待状态,并间隔一段时间后再次尝试获取的状态,就叫自旋
阻塞,指的是当一个线程尝试获取锁失败了,线程就就进行阻塞,这是需要操作系统切换CPU状态的
你用过JUC中的类吗,说几个
Lock锁体系 ,ConcurrentHashMap ,Atomic原子类,如:AtomicInteger ;ThreadLoal ; ExecutorService
ThreadLocal的作用和原理
ThreadLocal,翻译成中国话,叫做线程本地变量,它是为了解决线程安全问题的,它通过为每个线程提供一个独立的变量副本,来解决并发访问冲突问题 - 简单理解它可以把一个变量绑定到当前线程中,达到线程间数据隔离目的。
原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它用来存储每个线程中的变量副本,key就是ThreadLocal变量,value就是变量副本。
当我们调用get方法是,就会在当前线程里的threadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本
它的使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求
线程池的作用
请求并发高的时候,如果没有线程池会出现线程频繁创建和销毁而浪费性能的情况,同时没办法控制请求数量,所以使用了线程池后有如下好处
- 主要作用是控制并发数量,线程池的队列可以缓冲请求
- 线程池可以实现线程的复用效果
- 使用线程池能管理线程的生命周期
Executors创建四种线程池
- CachedThreadPool:可缓存的线程池,它在创建的时候,没有核心线程,线程最大数量是Integer最大值,最大空闲时间是60S
- FixedThreadPool:固定长度的线程池,它的最大线程数等于核心线程数,此时没有最大空闲时长为0
- SingleThreadPool:单个线程的线程池,它的核心线程和最大线程数都是1,也就是说所有任务都串行的执行
- ScheduledThreadPool:可调度的线程池,它的最大线程数是Integer的最大值,默认最长等待时间是10S,它是一个由延迟执行和周期执行的线程池
线程池的执行流程
corePoolSize,maximumPoolSize,workQueue之间关系。
- 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。
- 当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
- 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。
- 当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。
- 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。
- 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。
线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略
线程池构造器的7个参数
- CorePoolSize:核心线程数,它是不会被销毁的
- MaximumPoolSize :最大线程数,核心线程数+非核心线程数的总和
- KeepAliveTime:非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁
- Unit:空闲时间单位
- WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队
- ThreadFactory:它是一个创建新线程的工厂
- Handler:拒绝策略,任务超过最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
线程池拒绝策略有几种
拒绝策略,当线程池任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler
- AbortPolicy丢弃任务并抛出RejectedExecutionException异常;
- DiscardPolicy丢弃任务,但是不抛出异常;
- DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;
- CallerRunsPolicy由调用线程处理该任务
可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。
你知道ScheduledThreadPool使用场景吗
这是带定时任务的线程池,EurekaClient拉取注册表&心跳续约就是使用的这个线程池。