1.创建线程是几种方式
方式一:继承Thread类,覆写run方法,创建实例对象,调用该对象的start方法启动线程方式二:创建Runnable接口的实现类,类中覆写run方法,再将实例作为此参数传递给Thread类有参构造创建线程对象,调用start方法启动
方式三:创建Callable接口的实现类,类中覆写call方法,创建实例对象,将其作为参数传递给FutureTask类有参构造创建FutureTask对象,再将FutureTask对象传递给Thread类的有参构造创建线程对象,调用start方法启动
Thread有单继承的局限性,Runnable和Callable三避免了单继承的局限,使用更广泛。Runnable适用于无需返回值的场景,Callable使用于有返回值的场景
2.Thread的start和run的区别
start是开启新线程, 而调用run方法是一个普通方法调用,还是在主线程里执行。没人会直接调用run方法
3.sleep 和 wait的区别
第一,sleep方法是Thread类的静态方法,wait方法是Object类的方法
第二:sleep方法不会释放资源,wait方法会释放资源
第三:wait方法在没有获取到锁时,会抛出异常
4.线程的几种状态
新建状态:线程刚创建,还没有调用start方法之前
就绪状态:也叫临时阻塞状态,当调用了start方法后,具备cpu的执行资格,等待cpu调度器轮询的状态
运行状态:就绪状态的线程,获得了cpu的时间片,真正运行的状态
冻结状态:也叫阻塞状态,指的是该线程因某种原因放弃了cpu的执行资格,暂时停止运行的状态,比如调用了wait,sleep方法
死亡状态:线程执行结束了,比如调用了stop方法
5.Synchronized 和 lock的区别
- synchronized是一个关键字,依靠Jvm内置语言实现,底层是依靠指令码来实现;Lock是一个接口,它基于CAS乐观锁来实现的
- synchronized在线程发生异常时,会自动释放锁,不会发生异常死锁,Lock在异常时不会自动释放锁,我们需要在finally中释放锁
- synchronized是可重入,不可判断,非公平锁,Lock是可重入,可判断的,可手动指定公平锁或者非公平锁
6.你知道AQS吗,是什么意思
AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制,它维护了一个volatile修饰的 int 类型的,state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。
工作思想是如果被请求的资源空闲,也就是还没有线程获取锁,将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果请求的资源被占用,就将获取不到锁的线程加入队列。
7.悲观锁和乐观锁
悲观锁和乐观锁,指的是看待并发同步问题的角度
- 悲观锁认为,对同一个数据的并发操作,一定是会被其他线程同时修改的。所以在每次操作数据的时候,都会上锁,这样别人就拿不到这个数据。如果不加锁,并发操作一定会出问题。
- 乐观锁认为,对同一个数据的并发操作,是不会有其他线程同时修改的。它不会使用加锁的形式来操作数据,而是在提交更新数据的时候,判断一下在操作期间有没有其他线程修改了这个数据
- 悲观锁一般用于并发小,对数据安全要求高的场景,乐观锁一般用于高并发,多读少写的场景,通常使用版本号控制,或者时间戳来解决.
8.你知道什么是CAS嘛
CAS,compare and swap的缩写,中文翻译成比较并交换。它是乐观锁的一种体现,CAS 操作包含三个操作数 ------ 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作
9.Synchronized 加非静态和静态方法上的区别
实例方法上的锁,锁住的是这个对象实例,它不会被实例共享,也叫做对象锁
静态方法上的锁,锁住的是这个类的字节码对象,它会被所有实例共享,也叫做类锁
10.Synchronized(this) 和 Synchronized (User.class)的区别
Synchronized(this) 中,this代表的是该对象实例,不会被所有实例共享
Synchronized (User.class),代表的是对类加锁,会被所有实例共享
11.Synchronized 和 volatitle 关键字的区别
这两个关键字都是用来解决并发编程中的线程安全问题的,不同点主要有以下几点
第一:volatile的实现原理,是在每次使用变量时都必须重主存中加载,修改变量后都必须立马同步到主存;synchronized的实现原理,则是锁定当前变量,让其他线程处于阻塞状态
第二:volatile只能修饰变量,synchronized用在修饰方法和同步代码块中
第三:volatile修饰的变量,不会被编译器进行指令重排序,synchronized不会限制指令重排序
第四:volatile不会造成线程阻塞,高并发时性能更高,synchronized会造成线程阻塞,高并发效率低
第五:volatile不能保证操作的原子性,因此它不能保证线程的安全,synchronized能保证操作的原子性,保证线程的安全
12.synchronized 锁的原理
synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实 现,基于进入与退出Monitor对象实现方法与代码块同步,监视器锁的实现依赖 底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低,涉及到用户态到内核态的切换,会让整个程序性能变得很差。
因此在JDK1.6及以后的版本中,增加了锁升级的过程,依次为无锁,偏向锁,轻量级锁,重量级锁。而且还增加了锁粗化,锁消除等策略,这就节省了锁操作的开销,提高了性能
Synchronized 核心组件
-
Wait Set:哪些调用 wait 方法被阻塞的线程被放置在这里;
-
Contention List:竞争队列,所有请求锁的线程首先被放在这个竞争队列中;
-
Entry List:Contention List 中那些有资格成为候选资源的线程被移动到 Entry List 中;
-
OnDeck:任意时刻,最多只有一个线程正在竞争锁资源,该线程被成为 OnDeck;
-
Owner:当前已经获取到所资源的线程被称为 Owner;
-
!Owner:当前释放锁的线程。
实现原理:
- JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将
一部分线程移动到 EntryList 中作为候选竞争线程。
- Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定
EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
- Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在
JVM 中,也把这种选择行为称之为"竞争切换"。
- OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList
中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify
或者 notifyAll 唤醒,会重新进去 EntryList 中。
- 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统
来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
- Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先
尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是
不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁
资源。
参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
- 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加
上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
- synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线
程加锁消耗的时间比有用操作消耗的时间更多。
- Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向
锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做
了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
-
锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
-
JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁
13.synchronized 锁升级原理
每个对象都拥有对象头,对象头由Mark World ,指向类的指针,以及数组长度三部分组成,锁升级主要依赖Mark Word中的锁标志位和释放偏向锁标识位。
偏向锁(无锁)
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程 获得锁之后(线程的id会记录在对象的Mark Word锁标志位中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。(第二次还是这个线程进来就不需要重复加锁,基本无开销),如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
轻量级锁(CAS):
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁自旋锁);没有抢到锁的线程将自旋,获取锁的操作。轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord锁标志位更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。
长时间的自旋操作是非常消耗资源的,一个线程持有锁,其他线程就只能在原地空耗CPU,执行不了任何有效的任务,这种现象叫做忙等(busy-waiting)
重量级锁:
如果锁竞争情况严重,某个达到最大自旋次数(10次默认)的线程,会将轻量级锁升级为重量级锁,重量级锁则直接将自己挂起,在JDK1.6之前,synchronized直接加重量级锁,很明显现在得到了很好的优化。
虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。
14.乐观锁的使用场景(数据库,ES)
场景一:ES中对version的控制并发写。
场景二:数据库中使用version版本号控制来防止更新覆盖问题。
场景三:原子类中的CompareAndSwap操作
- 读多写少:在并发环境下,数据被多个用户频繁读取,但更新操作相对较少。在这种情况下,乐观锁可以提高系统的并发性能,因为它在读取数据时不加锁,只有在更新时才进行冲突检查。
- 事务粒度较小:乐观锁适用于事务处理的数据范围相对较小的情况,例如单个记录或少量记录的更新。由于每次更新时才会进行版本校验,因此当数据集较大时,频繁校验版本会增加开销。
- 冲突概率较低:乐观锁假设并发冲突的概率较低,在这种前提下避免了不必要的锁定资源,从而提高了系统整体的吞吐量。
- 高可用要求:对于对响应时间敏感的服务,如电商、金融等领域的部分业务模块,乐观锁能够减少因为等待锁释放而造成的延迟,提高服务的响应速度和可用性。
具体使用场景举例:
- 商品库存管理:当一个商品有多个并发请求尝试购买时,每个请求先读取库存然后扣减,提交时检查库存是否已被其他请求修改过。
- 订单状态变更:多个操作可能同时尝试改变订单状态,乐观锁通过版本号或其他机制确保在更新前状态未被他人更改。
- 分布式环境下的数据一致性:在分布式系统中,乐观锁可以通过数据库中的version字段或者其他类似机制来保证不同节点间的数据同步时的一致性。
在实现上,乐观锁通常采用如下方式:
- 数据库层面:在表中添加一个版本字段(version),每次更新时都会比较当前版本与数据库中的版本,如果一致则更新并递增版本;如果不一致,则认为存在并发修改,更新失败。
- 内存级别:Java中通过AtomicInteger、AtomicLong等原子类,或者利用CAS(Compare And Swap)操作实现线程安全的无锁更新。
15.AtomicInterger怎么保证并发安全性的
通过CAS操作原理来实现的,就可见性和原子性两个方面来说
它的value值使用了volatile关键字修饰,也就保证了多线程操作时内存的可见性
Unsafe这个类是一个很神奇的类,而compareAndSwapInt这个方法可以直接操作内存,依靠的是C++来实现的,它调用的是Atomic类的cmpxchg函数。而这个函数的实现是跟操作系统有关的,比如在X86的实现就利用汇编语言的CPU指令lock cmpxchg,它在执行后面的指令时,会锁定一个北桥信号,最终来保证操作的原子性
16.什么是重入锁,什么是自旋锁,什么是阻塞
可重入锁是指允许同一个线程多次获取同一把锁,比如一个递归函数里有加锁操作
自旋锁不是锁,而是一种状态,当一个线程尝试获取一把锁的时候,如果这个锁已经被占用了,该线程就处于等待状态,并间隔一段时间后再次尝试获取的状态,就叫自旋
阻塞,指的是当一个线程尝试获取锁失败了,线程就就进行阻塞,这是需要操作系统切换CPU状态的
17.你用过JUC中的类吗,说几个
Lock锁体系 ,ConcurrentHashMap ,Atomic原子类,如:AtomicInteger ;ThreadLoal ; ExecutorService%%
显示锁类
ReentrantLook,ReadWriteLock
原子变量类
Atomic原子类
线程池相关
future定时相关,callable,Executor,拒绝策略常用的拒绝策略有
- 丢失任务抛出异常
- 丢弃任务不抛出异常
- 丢弃等待队列第一个,尝试执行任务
- 由调度线程执行任务
容器类
concurerentHashMap,
队列:双向队列,阻塞队列:常用的阻塞队列
synchronousQueue,不会阻塞,直接创建新的线程执行
ArrayBlockingQueue,基于数组的阻塞队列,需要给一个默认的长度
LinkedBlockingQueue,基于链表的阻塞队列,默认长度是int的最大值
工具类
信号量,计数器,栅栏,创建线程池的工具
- java.util.concurrent.locks.ReentrantLock:一个可重入的互斥锁,比传统的synchronized关键字提供了更高的灵活性,如尝试获取锁、定时获取锁、公平锁等特性。
- java.util.concurrent.Semaphore:信号量,用于控制同时访问特定资源的线程数量。
- java.util.concurrent.CountDownLatch:计数器门闩,用于多线程同步,允许一个或多个线程等待其他线程完成一系列操作后再执行。
- java.util.concurrent.CyclicBarrier:循环栅栏,让一组线程在到达某个屏障点时被阻塞,直到所有线程都到达后才继续执行。
- java.util.concurrent.ExecutorService 和 ThreadPoolExecutor:提供线程池服务,用于管理和调度任务执行。
- java.util.concurrent.Future<T> 和 FutureTask<T>:表示异步计算的结果,可以通过它来取消执行的任务或者检查计算是否完成并获取结果。
- java.util.concurrent.ConcurrentHashMap<K, V>:线程安全的哈希表,支持高并发环境下的读写操作。
- java.util.concurrent.atomic 包中的原子类:例如 AtomicInteger、AtomicLong、AtomicReference 等,它们提供了线程安全的原子操作。
- java.util.concurrent.BlockingQueue<E> 接口及其实现类:如 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue 等,用于线程间的数据交换。
- java.util.concurrent.Callable<V>:类似于 Runnable 接口,但它允许返回值,并且可以抛出异常。
这些JUC组件都是Java进行并发编程时的重要工具。
18.ThreadLocal的作用和原理
ThreadLocal,叫做线程本地变量,它是为了解决线程安全问题的,它通过为每个线程提供一个独立的变量副本,来解决并发访问冲突问题 - 简单理解它可以把一个变量绑定到当前线程中,达到线程间数据隔离目的。
原理:ThredLocal是和当前线程有关系的,每个线程内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它用来存储每个线程中的变量副本,key就是ThreadLocal变量,value就是变量副本。
当我们调用get方法是,就会在当前线程里的threadLocals中查找,它会以当前ThreadLocal变量为key获取当前线程的变量副本
它的使用场景比如在spring security中,我们使用SecurityContextHolder来获取SecurityContext,比如在springMVC中,我们通过RequestContextHolder来获取当前请求,比如在 zuul中,我们通过ContextHolder来获取当前请求
ThreadLocal内存泄漏的问题
threadLocal对象被回收了,但是threadlocalMap存在与当前线程中,如果当前线程还在工作没有被回收,那么这个map就是强引用,导致map中的entry无法被回收。可以手动remove移除,来消除掉entry的可达性,从而被GC回收。
19.线程池的作用
请求并发高的时候,如果没有线程池会出现线程频繁创建和销毁而浪费性能的情况,同时没办法控制请求数量,所以使用了线程池后有如下好处
- 主要作用是控制并发数量,线程池的队列可以缓冲请求
- 线程池可以实现线程的复用效果
- 使用线程池能管理线程的生命周期
20.Executors创建四种线程池
- CachedThreadPool:可缓存的线程池,它在创建的时候,没有核心线程,线程最大数量是Integer最大值,最大空闲时间是60S
- FixedThreadPool:固定长度的线程池,它的最大线程数等于核心线程数,此时没有最大空闲时长为0
- SingleThreadPool:单个线程的线程池,它的核心线程和最大线程数都是1,也就是说所有任务都串行的执行
- ScheduledThreadPool:可调度的线程池,它的最大线程数是Integer的最大值,默认最长等待时间是10S,它是一个由延迟执行和周期执行的线程池
21.线程池的核心参数
核心线程数:创建后不会被销毁'
最大线程数:核心线程数+非核心线程数
非核心线程最大等待时间:非核心线程超过这个时间没有被调用就会被回收
时间单位:
等待队列:常见的等待队列有synchronsQueue,ArrayBlockingQueue,LinkedBlockingQueue
拒绝策略:丢弃任务抛出异常,丢弃任务不抛出异常,丢弃队列第一个尝试执行当前任务,使用调度线程去执行任务
线程工厂:4个
22.线程池的执行流程
corePoolSize,maximumPoolSize,workQueue之间关系。
-
当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。
-
当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。
-
当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。
-
当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。
-
当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。
-
当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。
线程池执行流程: 核心线程=> 等待队列 => 非核心线程 => 拒绝策略