并发编程高级面试专栏

在这里插入图片描述
1、Synchronized用过吗,其原理是什么?
这是一道Java面试中几乎百分百会问到的问题,因为没有任何写过并发程 序的开发者会没听说或者没接触过Synchronizedo Synchronized是由JVM 实现的一种实现互斥同步的一种方式,如果你查看被Synchronized修饰过 的程序块编译后的字节码,会发现,被Synchronized修饰过的程序块,在 编译前后被编译器生成了 monitorenter和monitorexit两个字节码指令。这两 个指令是什么意思呢?在虚拟机执行到monitorenter指令时,首先要尝试 获取对象的锁:如果这个对象没有锁定,或者当前线程已经拥有了这个对 象的锁,把锁的计数器+ 1 ;当执行monitorexit指令时将锁计数器-1 ;当计 数器为0时,锁就被释放了。如果获取对象失败了,那当前线程就要阻塞 等待,直到对象锁被另外一个线程释放为止。Java中Synchronize通过在对 象头设置标记,达到了获取锁和释放锁的目的。
2、你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的 锁?
"锁"的本质其实是monitorenter和monitorexit字节码指令的一个Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用Synchronized可以 修饰不同的对象,因此,对应的对象锁可以这么确定。

  1. 如果Synchronized明确指定了锁对象,比如Synchronized(变量名)、 Synchronized(this)等,说明加解锁对象为该对象。
  2. 如果没有明确指定:
    若Synchronized修饰的方法为非静态方法,表示此方法对应的对象为锁 对象
    若Synchronized修饰的方法为静态方法,则表示此方法对应的类对象为 锁对象。
    注意,当一个对象被锁住时,对象里面所有用Synchronized修饰的方法 都将产生堵塞,而对象里非Synchronized修饰的方法可正常被调用,不 受锁影响。
    3、 什么是可重入性,为什么说Synchronized是可重入锁?
    可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。 比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如 Synchronized不支持重入,进入method2方法时当前线程获得锁, method2方法里面执行method1时当前线程又要去尝试获取锁,这时如 果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。 对Synchronized来说,可重入性是显而易见的,刚才提到,在执行 monitorenter指令时,如果这个对象没有锁定,或者当前线程已经拥有了 这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器+1, 其实本质上就通过这种方式实现了可重入性。
    4、 JVM对Java的原生锁做了哪些优化?
    在Java 6之前,Monitor的实现完全依赖底层操作系统的互斥锁来实现, 也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
    由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个 线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内 核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代JDK中做 了大量的优化。一种优化是使用自旋锁,即在把线程进行阻塞操作之前先 让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无 需再让线程执行阻塞操作,避免了用户态到内核态的切换。
    现代JDK中还提供了三种不同的Monitor实现,也就是三种不同的锁:
    •偏向锁(Biased Locking)
    •轻量级锁
    •重量级锁
    这三种锁使得JDK得以优化Synchronized的运行,当JVM检测到不同 的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。
    •当没有竞争出现时,默认会使用偏向锁。
    JVM会利用CAS操作,在对象头上的Mark Word部分设置线程ID,以表 示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应 用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可 以降低无竞争开销。
    •如果有另一线程试图锁定某个被偏斜过的对象,JVM就撤销偏斜锁,切 换到轻量级锁实现。
    •轻量级锁依赖CAS操作Mark Word来试图获取锁,如果重试成功,就 使用普通的轻量级锁;否则,进一步升级为重量级锁。
    5、 为什么说Synchronized是非公平锁?
    非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待 线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样 做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
    6、 什么是锁消除和锁粗化?
    •锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检 测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
    程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程 序员自己加入的。
    •锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操 作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进 行互斥同步操作也会导致不必要的性能损耗。
    锁粗化就是增大锁的作用域。
    7、 为什么说Synchronized是一个悲观锁?乐观锁的实现原理又是什么?
    什么是CAS,它有什么特性?
    Synchronized显然是一个悲观锁,因为它的并发策略是悲观的:不管是否 会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁 计数器和检查是否有被阻塞的线程需要被唤醒等操作。随着硬件指令集的 发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没 有其他线程征用数据,那操作就成功了;如果共享数据有征用,产生了冲 突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要 线程挂起,所以被称为非阻塞同步。乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预 期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。 这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样, 如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否 则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。 CAS具有原子性,它的原子性由CPU硬件指令实现保证,即使用JNI调 用Native方法调用由C++编写的硬件级别指令,JDK中提供了 Unsafe 类执行这些操作。
    8、 乐观锁一定就是好的吗?
    乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有 缺点:
  3. 乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐 观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗 粒度大小。
  4. 长时间自旋可能导致开销大。假如CAS长时间不成功而一直自旋,会 给CPU带来很大的开销。
  5. ABA问题。CAS的核心思想是通过比对内存值与预期值是否一样而判 断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是A,后 来被一条线程改为B,最后又被改成了 A,则CAS认为此内存值并没有发 生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景 的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本 号加一。
    9、 跟Synchronized相比,可重入锁ReentrantLock其实现原理有什 么不同?
    其实,锁的实现原理基本是为了达到一个目的:让所有的线程都能看到某 种标记。
    Synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原 生的锁实现方式,而ReentrantLock以及所有的基于Lock接口的实现 类,都是通过用一个volitile修饰的int型变量,并保证每个线程都能拥有 对该int的可见性和原子修改,其本质是基于所谓的AQS框架。
    10、那么请谈谈AQS框架是怎么回事儿? AQS(AbstractQueuedSynchronizer类)是一个用来构建锁和同步器的框架, 各种Lock包中的锁(常用的有ReentrantLock、ReadWriteLock),以及其 他如 Semaphorex CountDownLatch,甚至是早期的 FutureTask 等,都是 基于AQS来构建。
  6. AQS在内部定义了一个volatile int state变量,表示同步状态:当线程调 用lock方法时,如果state=O,说明没有任何线程占有共享资源的 锁,可以获得锁并将state=1;如果state=1,则说明有线程目前正在使 用共享变量,其他线程必须加入同步队列进行等待。
  7. AQS通过Node内部类构成的一个双向链表结构的同步队列,来完成线 程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
    • Node类是对要访问同步代码的线程的封装,包含了线程本身及其状 态叫
    waitStatus(有五种不同取值,分别表駅是否被阻塞,是否等待唤醒,是 否已经被取消等),每个Node结点关联其prev结点和next结点,方便 线程释放锁后快速唤醒下一个在等待的线程,是一个FIFO的过程。
    • Node类有两个常量,SHARED和EXCLUSIVE,分别代表共享模式和 独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore就是基于AQS的共享模式实现的),独占模式是同一个时 间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等 待(如 ReentranLock)。
  8. AQS通过内部类ConditionObject构建等待队列(可有多个),当 Condition调用wait。方法后,线程将会加入等待队列中,而当 Condition调用signal。方法后,线程将从等待队列转移动同步队列中进 行锁竞争。
  9. AQS和Condition各自维护了不同的队列,在使用Lock和Condition 的时候,其实就是两个队列的互相移动。
    11、请尽可能详尽地对比下Synchronized和ReentrantLock的异同。 ReentrantLock是Lock的实现类,是一个互斥的同步锁。从功能角度, ReentrantLock比Synchronized的同步操作更精细(因为可以像普通对象一 样使用),甚至实现Synchronized没有的高级功能,如:
    •等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以 选择放弃等待,对处理执行时间非常长的同步块很有用。
    •带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无 法获取则返回。
    •可以判断是否有线程在排队等待获取锁。
    •可以响应中断请求:与Synchronized不同,当获取到锁的线程被中断
    时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
    •可以实现公平锁。
    从锁释放角度,Synchronized在JVM层面上实现的,不但可以通过一些 监控工具监控Synchronized的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用Lock则不行,Lock是通过代码实现的,要保证 锁定一定会被释放,就必须将unLock。放到finally。中。
    从性能角度,Synchronized早期实现比较低效,对比ReentrantLock,大 多数场景性能都相差较大。
    但是在Java 6中对其进行了非常多的改进,在竞争不激烈时, Synchronized的性能要优于ReetrantLock;在高竞争情况下, Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常 态。
    12、 ReentrantLock是如何实现可重入性的?
    ReentrantLock内部自定义了同步器Sync(Sync既实现了 AQS,又实现了 AOS,而AOS提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下 当前维护的那个线程ID和当前请求的线程ID是否一样,一样就可重入 了。
    13、 除了 ReetrantLock,你还接触过JUC中的哪些并发工具?
    通常所说的并发包(JUC)也就是java.util.concurrent及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:
    •提供了 CountDownLatch、CyclicBarrier、Semaphore等,比 Synchronized更加高级,可以实现更加丰富多线程操作的同步结构。

•提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通 过类似快照机制实现线程安全的动态数组CopyOnWriteArrayList等各种线

程安全的容器。
•提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue等,各种并发队列实现。
•强大的Executor框架,可以创建各种不同类型的线程池,调度任务运行 等。
14、请谈谈 ReadWriteLock 和 StampedLocko
虽然ReentrantLock和Synchronized简单实用,但是行为上有一定局限 性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写 操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java提 供了读写锁。读写锁基于的原理是多个读操作不需要互斥,如果读锁试图 锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作 结束,这样就可以自动保证不会读取到有争议的数据。
ReadWriteLock代表了一对锁,下面是一个基于读写锁实现的数据结构, 当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优 势:

return m.get(key); } finally {
r.unlock();
public String put(String keyt String entry)《 w.lock();
System.out.prlntln(M 写.・定! **); try { return m.put(keyt entry);
} finally { w.unlock();
读写锁看起来比Synchronized的粒度似乎细一些,但在实际应用中,其 表现也并不尽如人意,主要还是因为相对比较大的开销。所以,JDK在后 期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。 优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先 试着修改,然后通过validate方法确认是否进入了写模式,如果没有进 入,就成功避免了开销;如果进入,则尝试获取读锁。
public class StampedSaiBple { private final StanpedLock el ■ new StmnpedLock();
void nutateO {
long stamp ・ sl.writeLock(); try < writ«();
} finally {
sl.unlockWrite(staMp):
Data access() {
long staap ・ sl.tryOpti«isticRead(); Data data ■ read();
if (!al.validate^stamp)) ( stamp ・ tl.readLock(); try { data ・ r«ad();
} finally ( sl.unlockRead(stamp);
return data;
15、如何让Java的线程彼此同步?你了解过哪些同步器?请分别介绍下。 JUC中的同步器三个主要的成员:CountDownLatch、CyclicBarrier和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。
CountDownLatch叫倒计数,允许一个或多个线程等待某些操作完成。看 几个场景:
•跑步比赛,裁判需要等到所有的运动员(“其他线程")都跑到终点(达到目 标),才能去算排名和颁奖。
•模拟并发,我需要启动100个线程去同时访问某一个地址,我希望它们 能同时并发,而不是一个一个的去执行。
用法:CountDownLatch构造方法指明计数数量,被等待线程调用 countDown将计数器减1,等待线程使用await进行线程等待。一个简单 的例子:

public class TestCountDounLitch < private CountDownOtch countDownLitch . new CountDownLatch(4)}
public static void MinfStringl) args) { TestCounttkMnLstch testCountDownL^tch « new TestCountOownLatcti(); t es t CountfiownLat ch . beg in()|

private class Runner inplenents Runnable 《 private int result; public Runnerdnt result) ( this.result ■ result,
(Override public void run()《 try <
ThreM. slecp( result • !••)] countOownLatch. countDownO) } catch (InterruptedException e) { c.prlntStackTrtce()|
private void be^lnO {
Systen.out.printlnCVBHM
’):
Random r>ndoa ■ new Rando«<Systei.curr«ntTlMMillls()): for (int 1 ■ 1 < 4> ix) {
int result • rBndoa.nextlntO) ♦ 1)
new ThrMd(ncw Runn«r(result))«itart();

try < countOownLAtch ・ wa it()|
} catch (InterruptedException e) (
••printStackTractOf

Systwi.out.println(-/WW人了. ■邦升
CyclicBarrier叫循环栅栏,它实现让一组线程等待至某个状态之后再全部 同时执行,而且当所有等待线程被释放后,CyclicBarrier可以被重复使 用。CyclicBarrier的典型应用场景是用来等待并发线程结束。CyclicBarrier 的主要方法是await。,await。每被调用一次,计数便会减少1,并阻塞住 当前线程。当计数减至0时,阻塞解除,所有在此CyclicBarrier上面阻塞 的线程开始运行。
在这之后,如果再次调用await。,计数就又会变成N-1,新一轮重新开 始,这便是Cyclic的含义所在。CyclicBarrier.await。带有返回值,用来表 示当前线程是第几个到达这个Barrier的线程。
举例说明如下:

public class TestCycllcBarrier {

private CycllcBarrier cyclicBarrier ■ new CycllcBarri«r(5);

public static void Min(String() args) ( new TestCyclicBarrier().begln();

public void begin() {

for (Int 1 ■ 0; 1 < 5; !*♦) { new Thread(n«w Student()).start();

private class Student iBplementt Runnable ( •Override public void run() ( try (

Thread.sleep(2B60): cyclicBarrier.await();

} catch (Exception •) (

t.prIntStackT race();

Semaphore, Java版本的信号量实现,用于控制同时访问的线程个数,来 达到限制通用资源访问的目的,其原理是通过acquire。获取一个许可,如 果没有就寺侍,而release。释放一个许可。

public class Test (

public static void main(Strlng(] args)(

Semaphore semaphore « new Semaphore。); for(int i « 6; 1 < 8; !♦♦)

new Worker(leseiMphore).start();

static class Worker extends Thread{ private int nun; private Semphore semaphore;

public Norker(int nun,SeBaphore semaphore)( this.nun ■ nun;

this.seaaphore ■ semaphore;

•Override

public void run() ( try {

senaphore.acqulreO ; Thread.sleep(2B68); semaphore.release();

)catch (InterruptedException e) ( •.printStickTrace();

如果Semaphore的数值被初始化为1,那么一个线程就可以通过acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比 如互斥锁是有持有者的,而对于Semaphore这种计数器结构,虽然有类 似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。

16、 CyclicBarrier和CountDownLatch看起来很相似,请对比下呢?

它们的行为有一定相似度,区别主要在于:

• CountDownLatch是不可以重置的,所以无法重用,CyclicBarrier没有这 种限制,可以重用。

• CountDownLatch的基本操作组合是countDown/await,调用await的线 程阻塞等待countDown足够的次数,不管你是在一个线程还是多个线程 里countDown,只要次数足够即可。CyclicBarrier的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重 置。

CountDownLatch目的是让一个线程等待其他N个线程达到某个条件后, 自己再去做某个事(通过CyclicBarrier的第二个构造方法public

CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达 到同样的效果)。而CyclicBarrier的目的是让N多线程互相等待直到所有 的都达到某个状态,然后这N个线程再继续执行各自后续(通过 CountDownLatch在某些场合也能完成类似的效果)。

17、 Java中的线程池是如何实现的?

•在Java中,所谓的线程池中的“线程",其实是被抽象为了一个静态内部 类Worker,它基于AQS实现,存放在线程池的

HashSet workers成员变量中;

•而需要执行的任务则存放在成员变量workQueue(BlockingQueue workQueue)中。

这样,整个线程池实现的基本思想就是:从workQueue中不断取出需要执 行的任务,放在Workers中进行处理。

18、 创建线程池的几个核心构造参数?

Java中的线程池的创建其实非常灵活,我们可以通过配置不同的参数, 创建出行为不同的线程池,这几个参数包括:

• corePoolSize:线程池的核心线程数。

• maximumPoolSize:线程池允许的最大线程数。

• keepAliveTime:超过核心线程数时闲置线程的存活时间。

• workQueue :任务执行前保存任务的队列,保存由execute方法提交的 Runnable 任务。

19、 线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好 的吗?

显然不是的。线程池默认初始化后不启动Worker,等待有请求时才启 动。

每当我们调用execute()方法添加一个任务时,线程池会做如下判断: •如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这 个任务

•如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放 入队列;

•如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务

•如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException o 当一个线程完成任务时,它会从队列中取下一个任务来执行。当一个线程 无事可做,超过一定的时间(keepAliveTime )时,线程池会判断。

如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。所以 线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

20、 既然提到可以通过配置不同参数创建出不同的线程池,那么Java中 默认实现好的线程池又有哪些呢?请比较它们的异同。

  1. SingleThreadExecutor 线程池

这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有 任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代 它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

• corePoolSize:'只有一个核心线程在工作。

• maximumPoolSize: 1。

• keepAliveTime: 0L。

• workQueue:new LinkedBlockingQueue(),其缓冲队列 是无界的。

  1. FixedThreadPool 线程池

FixedThreadPool是固定大小的线程池,只有核心线程。每次提交一个任 务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦 达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程 池会补充一个新线程。

FixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服

务器。

• corePoolSize: nThreads

• maximumPoolSize: nThreads

• keepAliveTime: 0L

• workQueue:new LinkedBlockingQueue(),其缓冲队列 是无界的。

  1. CachedThreadPool 线程池

CachedThreadPool是无界线程池,如果线程池的大小超过了处理任务所 需要的线程,那么就会回收部分空闲(60秒不执行任务)线程,当任务数增 加时,此线程池又可以智能的添加新线程来处理任务。线程池大小完全依 赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为1的阻塞队列。缓存型池子通常用于执行一些生存期很 短的异步型任务,因此在一些面向连接的daemon型SERVER中用得不 多。但对于生存期短的异步任务,它是Executor的首选。

• corePoolSize: 0

• maximumPoolSize: Integer.MAX_VALUE

• keepAliveTime: 60L

• workQueue:new SynchronousQueue(), —个是缓冲区为 1的阻塞队列。

  1. ScheduledThreadPool 线程池

ScheduledThreadPool :核心线程池固定,大小无限的线程池。此线程池 支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程 池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内 回收。

• corePoolSize: corePoolSize

• maximumPoolSize: Integer.MAX_VALUE

• keepAliveTime: DEFAULT_KEEPALIVE_MILLIS

• workQueue:new DelayedWorkQueue()

21、如何在Java线程池中提交线程?

线程池最常用的提交任务的方法有两种:

  1. execute。: ExecutorService.execute 方法接收一个例,它用来执行一个 任务:

utorService.execut«(Runnable runable)

  1. submit。: ExecutorService.submit。方法返回的是 Future 对象。可以用 isDone()来查询Future是否已经完成,当任务完成时,它具有一个结果, 可以调用get。来获取结果。也可以不用isDone。进行检查就直接调用 get。,在这种情况下,get。将阻塞,直至结果准备就绪。

FutureTask task « ExecutorService.subult(Runnable runnable); FutureTask task ■ ExecutorService.submit(Runnable runnable,T Result); FutureTask task ■ ExecutorService•subait(Callable callable);

22、 什么是Java的内存模型,Java中各个线程是怎么彼此看到对方的 变量的?

Java的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变 量存储到内存和从内存中取出这样的底层细节。此处的变量包括实例字 段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数, 因为这些是线程私有的,不会被共享,所以不存在竞争问题。

Java中各个线程是怎么彼此看到对方的变量的呢?Java中定义了主内存与 工作内存的概念:

所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该 线程使用到的变量的主内存副本拷贝。

线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接 读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变 量,线程间变量值的传递需要通过主内存。

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

关键字volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被 定义成volatile之后,具备两种特性:

  1. 保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新 值对于其他线程是可以立即得知的。而普通变量做不到这一点。

  2. 禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到 正确结果,但是不保证程序代码的执行顺序。

Java的内存模型定义了 8种内存间操作:

lock 和 unlock

•把一个变量标识为一条线程独占的状态。

•把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程 锁定。

read 和 write

•把一个变量值从主内存传输到线程的工作内存,以便load。

•把store操作从工作内存得到的变量的值,放入主内存的变量中。

load 和 store

•把read操作从主内存得到的变量值放入工作内存的变量副本中。•把工 作内存的变量值传送到主内存,以便write。

use 和 assgin

•把工作内存变量值传递给执行引擎。

•将执行引擎值传递给工作内存变量值。

volatile的实现基于这8种内存间操作,保证了一个线程对某个volatile变 量的修改,一定会被另一个线程看见,即保证了可见性。

24、 既然volatile能够保证线程间的变量可见性,是不是就意味着基于 volatile变量的运算就是并发安全的?

显然不是的。基于volatile变量的运算在并发下不一定是安全的。volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中 volatile变量,每次使用前都要刷新到主内存)。

但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一 样是不安全的。

25、 请对比下volatile对比Synchronized的异同。

Synchronized既能保证可见性,又能保证原子性,而volatile只能保证可 见性,无法保证原子性。

ThreadLoca l和Synchonized都用于解决多线程并发访问,防止任务在共 享资源上产生冲突。但是ThreadLocal与Synchronized有本质的区别。 Synchronized用于实现同步机制,是利用锁的机制使变量或代码块在某一 时该只能被一个线程访问,是一种“以时间换空间’'的方式。

而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一 时间访问到的并不是同一个对象,根除了对变量的共享,是一种“以空间 换时间”的方式。

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

ThreadLocal这是Java提供的一种保存线程私有信息的机制,因为其在 整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模 块之间传递信息,比如事务ID、Cookie等上下文相关信息。

ThreadLocal为每一个线程维护变量的副本,把共享数据的可见范围限制 在同一个线程之内,其实现原理是,在ThreadLocal类中有一个Map,用 于存储每一个线程的变量的副本。

27、很多人都说要慎用ThreadLocal,谈谈你的理解,使用

ThreadLocal需要注意些什么?

使用 ThreadLocal 要注意 remove!

ThreadLocal的实现是基于一个所谓的ThreadLocalMap,在 ThreadLocalMap中,它的key是一个弱引用。

通常弱引用都会和引用队列配合清理机制使用,但是ThreadLocal是个例 外,它并没有这么做。

这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束, 进而回收相应ThreadLocalMap!这就是很多00M的来源,所以通常都会 建议,应用一定要自己负责remove,并且不要和线程池配合,因为 worker线程往往是不会退出的。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值