【java面试】多线程

并发和并行有什么区别?

并发强调的是同一时间间隔,并行强调的是同一时间发生。并发指的是同一台处理器在同一时间间隔下处理多个程序。而并行则是多个处理器同一时间处理多个程序

线程和进程有什么区别呢?

1.进程是运行中的一段程序。而进程中的执行的每个任务都可以作为一个线程。
2.进程拥有独立的内存单元。而线程共享内存资源。
3.线程的开销比进程的开销要小。
4.影响力不同,子进程无法影响父进程。但是子线程可以影响父线程。

创建线程有哪几种方式

继承 Thread 类创建线程类,调用线程对象的 start() 方法来启动该线程。
通过 Runnable 接口创建线程类。
通过 Callable 和 Future 创建线程,

线程的run()和start()有什么区别?

start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

当你调用 start()方法时你将创建新的线程,并且执行在 run()方法里的代码。

但是如果你直接调用 run()方法,它不会创建新的线程也不会执行调用线程的代码,

只会把 run 方法当作普通方法去执行

线程安全的解决方案?

• 数据不共享,单线程可见,比如 ThreadLocal 就是单线程可见的;
• 使用线程安全类,比如 StringBuffer 和 JUC(java.util.concurrent)下的安全类
• 使用同步代码或者锁。

守护线程是什么

守护线程(即 daemon thread),是个服务线程,准确地来说就是服务其他的线程。

线程有哪些状态???

新建,就绪,运行,阻塞,死亡。
新建:在生成线程对象,还没有调用该对象的 start 方法,这是属于创建状态
就绪:有资格分到 cpu 但还没有轮到
运行:分到 cpu,能真正执行线程内代码
阻塞:没资格分到 cpu 时间的
死亡:一个线程的 run 方法结束或者调用 stop 方法后,该线程就会死亡

导致线程阻塞的方法

sleep,suspend,wait 都可以使线程进入阻塞状态
sleep 是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用 sleep 不会释放对象锁。

wait 是 Object 类的方法,对此对象调用 wait 方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出 notify 方法(或 notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

当前线程调用 suspend() 后后立即挂起,线程状态还是 RUNNABLE, 不会释放锁资源,这就会很容导致死锁(这也是弃用它的原因);

请描述一下线程池?

简单来说就是创建一些线程放入一个池子中,来任务了有空闲线程就分配线程,如果没有的话任
务进入队列进行等待其它任务使用后释放线程,如果队列满了的话我们就在创建一个线程,如果
创建这个线程大于最大线程数限制那么就会触发拒绝策略!(这个过程体现复用的原理)

线程池的优点

  • 线程池可以实现少量线程复用执行大量任务,提高线程的利用率
  • 不用重复的创建销毁,提高程序的响应速度
  • 放在同一个池子中,方便统一管理
  • 可以控制最大并发数

线程池的参数

(1)corePoolSize:线程池中常驻核心线程数

(2)maximumPoolSize:线程池能够容纳同时执行的最大线程数

(3)keepAliveTime:多余的空闲线程存活时间

(4)unit:keepAliveTime的时间单位

(5)workQueue:任务队列,被提交但尚未执行的任务

(6)threadFactory:表示生成线程池中的工作线程的线程工厂

(7)handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何拒绝

谈一下悲观锁和乐观锁的区别

悲观锁的代表分别是 synchronized 和 Lock 锁
核心思想是线程占了锁才能去操作共享变量,每次只有一个线程占锁成功,获取锁失败的线程需要停下来等待线程从运行到阻塞,再从阻塞到唤醒涉及到上下文切换影响性能。实际上,线程在获取synchronized 锁和 Lock 锁时,如果锁已被占用,都会做几次重试操作,减少阻塞机会。

乐观锁的代表时 AtomicInteger,使用 cas 来保证原子性核心思想是无需加锁,每次只有一个线程能修改共享变量,其它失败线程不需要停止,不断重试直至成功。由于线程一直运行,不需要阻塞因此不涉及到线程的上下文切换但是它需要多核 cpu 的支持且线程数不应超过 cpu 核数。

lock 与 synchronized 的区别

语法不同:synchronized 是关键字,源码在 jvm 中,用 c++ 实现。Lock 是接口,源码由 jdk 提供,用 java 语言实现。使用 synchronized,退出同步代码块会自动释放,而使用 Lock 则需要手动调用 unlock 方法释放
功能方面: 两者都属于悲观锁,都具备基本的互斥,同步,锁重入功能。Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态,公平锁等等。
性能方面:在没有竞争的时候,synchronized 提供了很多优化比如偏向锁,轻量锁,性能不赖。在竞争激烈时,Lock 的实行通常会提供更好的性能

请描述一下 cas(cas 是一个什么样的同步机制)

CAS 即 compare and swap ,直译就是比较并交换。当要对变量进行修改时,先会将内存位置的值与预期的变量原值进行比较,如果一致则将内存位置更新为新值,否则不做操作,无论哪种情况都会返回内存位置当前的值!

compareAndSwap(V,A,B):(注意:三个参数也是个考点)
CAS 包含了三个参数:内存值 V,旧的预期值 A,要更新的值 B。当且仅当内存值 V 等于旧的预期值 A 时,才会将内存值 V 修改为新值 B,否则什么都不干;

cas 存在的缺陷

1.循环时间长,开销大:如果 cas 操作失败的话则要循环进行 cas 操作,如果长时间不成功的话则会造成 cpu 极大的开销。

2.只能保证一个共享变量的原子操作

3.ABA 问题:CAS 在检查值的时候,只会比较预期值 A 与内存位置的值是否相同,如果内存位置值经过若干次修改又变回了 A (A -> B -> A),CAS 检查依旧会通过,但是实际上这个值已经修改过了。

解决方案:解决的思路就是引入类似乐观锁的版本号控制,不止比较预期值和内存位置的值,还要比较版本号是否正确。

synchronized用法

  • 修饰代码块
  • 修饰方法

Synchronized 用过吗,其原理是什么?

(1)可重入性:就是一个线程不用释放,可以重复的获取一个锁n次,只是在释放的时候,也需要相应的释放n次。
(2)不可中断性:一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断;

为什么说 Synchronized 是非公平锁?

当锁被释放后,任何一个线程都有机会竞争得到锁,这样做的目的是提高效率,但缺点是可能产生线程饥饿现象。

多线程 Thread.yield 方法到底有什么用

yield 即 “谦让”,也是 Thread 类的方法。它让出当前线程 CPU 的时间片,使正在运行中的线程重新变成就绪状态,并重新竞争 CPU 的调度权。它可能会获取到,也有可能被其他线程获取到。

Reentrantlock

ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来对共享资源进行同步。Sync可以说是 ReentrantLock 的亲儿子,它寄托了全村的希望,完美的继承了 AbstractQueuedSynchronizer,是 ReentrantLock 的核心,后面的 NonfairSync 与 FairSync 都是基于 Sync 扩展出来的子类, 亦即通过二者实现了公平锁和非公平锁。new ReentrantLock() 默认创建的为非公平锁,如果要创建公平锁可以使用 new ReentrantLock(true)。

Reentrantlock 有哪些优势

  • ReentrantLock 具备非阻塞方式获取锁的特性,使用 tryLock() 方法。
  • 可以中断获得的锁,使用 lockInterruptibly() 方法当获取锁之后,如果所在的线程被中断,则会抛出异常并释放当前获得的锁(lock() 和 lockInterruptibly() 的区别在于获取线程的途中如果所在的线程中断,lock() 会忽略异常继续等待获取线程,而 lockInterruptibly() 则会抛出InterruptedException 异常)。
  • ReentrantLock 可以在指定时间范围内获取锁,使用 tryLock(long timeout,TimeUnit unit) 方法

ReentrantLock 是如何实现可重入性的?

(1)什么是可重入性

一个线程持有锁时,当其他线程尝试获取该锁时,会被阻塞;而这个线程尝试获取自己持有锁时,如果成功说明该锁是可重入的,反之则不可重入。

(2)synchronized是如何实现可重入性

synchronized关键字经过编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。每个锁对象内部维护一个计数器,该计数器初始值为0,表示任何线程都可以获取该锁并执行相应的方法。根据虚拟机规范要求,在执行monitorenter指令时,首先要尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了对象的锁,把锁的计数器+1,相应的在执行monitorexit指令后锁计数器-1,当计数器为0时,锁就被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。

(3)ReentrantLock如何实现可重入性

ReentrantLock使用内部类Sync来管理锁,所以真正的获取锁是由Sync的实现类控制的。Sync有两个实现,分别为NonfairSync(非公公平锁)和FairSync(公平锁)。Sync通过继承AQS实现,在AQS中维护了一个private volatile int state来计算重入次数,避免频繁的持有释放操作带来的线程问题。

ThreadLocal 是什么?有哪些使用场景?

ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session 管理 等问题。

请谈谈 ThreadLocal 是怎么解决并发安全的?

在java程序中,常用的有两种机制来解决多线程并发问题,一种是sychronized方式,通过锁机制,一个线程执行时,让另一个线程等待,是以时间换空间的方式来让多线程串行执行。而另外一种方式就是ThreadLocal方式,通过创建线程局部变量,以空间换时间的方式来让多线程并行执行。两种方式各有优劣,适用于不同的场景,要根据不同的业务场景来进行选择。

在spring的源码中,就使用了ThreadLocal来管理连接,在很多开源项目中,都经常使用ThreadLocal来控制多线程并发问题,因为它足够的简单,我们不需要关心是否有线程安全问题,因为变量是每个线程所特有的。

很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?

ThreadLocal 变量解决了多线程环境下单个线程中变量的共享问题,使用名为ThreadLocalMap的哈希表进行维护(key为ThreadLocal变量名,value为ThreadLocal变量的值);

使用时需要注意以下几点:

线程之间的threadLocal变量是互不影响的,
使用private final static进行修饰,防止多实例时内存的泄露问题
线程池环境下使用后将threadLocal变量remove掉或设置成一个初始值

synchronized 和 ReentrantLock 有什么区别?

•ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
•ReentrantLock 只适用于代码块锁,而 synchronized 可用于修饰方法、代码块等;
•ReentrantLock 性能略高于 synchronized。

AQS

AQS 是一个抽象类,它定义了一套多线程访问共享资源的同步器框架。通俗解释,AQS 就像是一个队列管理员,当多线程操作时,对这些线程进行排队管理。

AQS 主要通过维护了两个变量来实现同步机制的

  • state
    AQS 使用一个 volatile 修饰的私有变量来表示同步状态,当 state=0 表示释放了锁,当 state>0 表示获得锁
  • FIFO 同步队列
    AQS 通过内置的 FIFO 同步队列,来实现线程的排队工作。如果线程获取当前同步状态失败,AQS会将当前线程的信息封装成一个 Node 节点,加入同步队列中,并且阻塞该线程,当同步状态释放,则会将队列中的线程唤醒,重新尝试获取同步状态。

Volatile

volatile变量具备两种特性:一种是保证该变量对所有线程可见,在一个线程修改了变量的值后,新的值对于其它线程时可以立即获取的(可见性);一种是volatile禁止指令重排(前、后代码,各自里面的顺序性是无法保证的),即volatile变量不会被缓存在寄存器或者其它处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值,(有序性)。

请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?

volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。

volatile主要适用于一个变量被多个线程共享,多个线程均可针对这个变量执行赋值或者读取的操作。

在有多个线程对普通变量进行读取时,每个线程都首先须要将数据从内存中复制变量到CPU缓存中,如果计算机有个多CPU,则线程可能都在不同的CPU中被处理,这意味着每个线程都须要将同一个数据复制到不同的CPU cache中,这样每个线程都针对同一个变量的数据做了不同的处理后就可能存在数据不一致的情况。如果将变量声明为volatile,JVM就能保证每次读取变量时都直接从内存中读取,跳过CPU Cache 这一步,有效解决了多线程数据同步的问题.

乐观锁一定就是好的吗?

乐观锁认为对一个对象的操作不会引发冲突,所以每次操作都不进行加锁,只是在最后提交更改时验证是否发生冲突,如果冲突则再试一遍,直至成功为止,这个尝试的过程称为自旋。

乐观锁没有加锁,但乐观锁引入了ABA问题,此时一般采用版本号进行控制;
也可能产生自旋次数过多问题,此时并不能提高效率,反而不如直接加锁的效率高

公平锁和非公平锁

公平锁:每个线程获取锁的顺序是按照线程访问锁的先后顺序获取的,最前面的线程总是最先获取到锁。
非公平锁:每个线程获取锁的顺序是随机的,并不会遵循先来先得的规则,所有线程会竞争获取锁。

自旋锁,非自旋锁

自旋锁: 是指当一个线程在获取锁失败时将一直循环等待,不断重新获取锁,直到获取到锁才会退出循环, 自旋锁会让线程一直处于用户态, 不会发生上下文切换

非自旋锁: 获取锁失败会进入阻塞状态, 从而进入内核态

什么是自旋

很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核态切换的问题。既然 synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环,这就是自旋。如果做了多次循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。

你可能感兴趣的:(java面试,java,面试,jvm)