JAVA面试题--多线程

创建线程是几种方式

方式一:继承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之间关系。

  1. 当线程池中线程数小于corePoolSize时,新提交任务将创建一个新线程(使用核心)执行任务,即使此时线程池中存在空闲线程。

  2. 当线程池中线程数达到corePoolSize时(核心用完),新提交任务将被放入workQueue中,等待线程池中任务调度执行 。

  3. 当workQueue已满,且maximumPoolSize > corePoolSize时,新提交任务会创建新线程(非核心)执行任务。

  4. 当workQueue已满,且提交任务数超过maximumPoolSize(线程用完,队列已满),任务由RejectedExecutionHandler处理。

  5. 当线程池中线程数超过corePoolSize,且超过这部分的空闲时间达到keepAliveTime时,回收这些线程。

  6. 当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize范围内的线程空闲时间达到keepAliveTime也将回收。

线程池执行流程 : 核心线程 => 等待队列 => 非核心线程 => 拒绝策略

线程池构造器的7个参数
  • CorePoolSize:核心线程数,它是不会被销毁的

  • MaximumPoolSize :最大线程数,核心线程数+非核心线程数的总和

  • KeepAliveTime:非核心线程的最大空闲时间,到了这个空闲时间没被使用,非核心线程销毁

  • Unit:空闲时间单位

  • WorkQueue:是一个BlockingQueue阻塞队列,超过核心线程数的任务会进入队列排队

  • ThreadFactory:它是一个创建新线程的工厂

  • Handler:拒绝策略,任务超过最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler

线程池拒绝策略有几种

拒绝策略,当线程池任务超过 最大线程数+队列排队数 ,多出来的任务该如何处理取决于Handler

  1. AbortPolicy丢弃任务并抛出RejectedExecutionException异常;

  2. DiscardPolicy丢弃任务,但是不抛出异常;

  3. DiscardOldestPolicy丢弃队列最前面的任务,然后重新尝试执行任务;

  4. CallerRunsPolicy由调用线程处理该任务

可以定义和使用其他种类的RejectedExecutionHandler类来定义拒绝策略。

你知道ScheduledThreadPool使用场景吗

这是带定时任务的线程池,EurekaClient拉取注册表&心跳续约就是使用的这个线程池。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
java面试题真的很多,下面我来回答一个有关多线程的问题。 在Java中实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。这两种方式有何区别? 继承Thread类的方式是直接定义一个类继承Thread,并重写它的run()方法。然后创建该类的对象,并调用对象的start()方法来启动线程。这种方式简单直接,但因为Java是单继承的,所以如果某个类已经继承了其他类,就不能再直接继承Thread类实现多线程。 实现Runnable接口的方式是定义一个类实现Runnable接口,并实现其唯一的抽象方法run()。然后创建Thread类的对象,将实现了Runnable的对象作为参数传递给Thread类的构造方法。最后调用Thread对象的start()方法来启动线程。这种方式灵活性更大,因为Java允许一个类实现多个接口,所以即使某个类已经继承了其他类,仍然可以通过实现Runnable接口来实现多线程。 另一个区别在于资源共享的问题。继承Thread类的方式,不管是数据还是方法,都是线程自己拥有的,不存在共享的情况。而实现Runnable接口的方式,多个线程可以共享同一个对象的数据和方法,因为多个线程共同操作的是同一个Runnable对象。 总结来说,继承Thread类方式简单直接,但只能通过单继承来实现多线程;实现Runnable接口方式更灵活,可以解决单继承的限制,并且多个线程可以共享同一个Runnable对象的数据和方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值