今天你准备了吗——并发编程的面试题

161 阅读29分钟

并发编程的面试

1.并发编程的三要素?

原子性:指一个或多个操作要么全部执行,并且在执行过程中不能被其他线程打断,要么全部都不执行。

可见性:多个线程操作一个共享变量时,当其中一个线程操作该变量,其他线程可以立即看到修改之后的结果。

有序性:程序的执行是按照代码的顺序执行。

2.实现可见性的方法?

synchronized或者lock:保证同一时刻只有一个线程获取到资源的锁,加锁前会将本地内存中的值进行删除,锁释放之前会把最新的值刷新到内存中,实现可见性。synchronzied会导致线程的上下文切换并带来线程调度开销。

volatile:当一个变量通过volatile来修饰,那么在写入变量的时候会将数据直接写入到主内存中;当读这个变量的时候,会从主内存来获取该变量。

3.线程的创建的方式?

1.通过继承Thread类来创建线程

2.通过实现Runable接口 :实现run方法,任务执行完后不会有返回值,不可以抛出异常。

3.通过实现Callable接口:实现call方法,任务执行完后可以还有返回值,call方法可以抛出异常

4.通过线程池创建

4.什么是线程池?使用线程池的好处有哪些?线程池如何创建线 程?线程池的种类?

线程池就是提前创建若干线程,如果有任务处理,线程池中的线程就会去执行任务。处理完之后并不会进行销毁,而是等待下一个任务。考虑到创建和销毁都是需要消耗系统资源的,所以当频繁创建线程和销毁线程应该使用线程池。

线程池的好处:

1.降低资源消耗:可以重复利用已经创建的线程,线程的复用性。

2.提高响应速度:当任务到达时,可以直接使用线程池中线程,大大提高了响应速度。

3.提高线程的可管理性:将线程进行统一的管理、调优和监控。

线程池的执行原理:

线程池的状态分为:Running:运行状态, 正常接收任务;shutdown:不接受任务,阻塞队列中的任务正常进行执行。Stop:不接受新任务,阻塞队列中的任务也不会执行,并且会中断当前正在执行的任务;TIDING:线程池即将死去。TEMINATED:线程池终止

调用execute()方法:

提交一个任务的处理过程,当前线程池中的运行线程个数小于核心线程数,则直接创建线程。

当前线程池的线程个数大于核心线程数,则将线程放到阻塞队列中;

如果队列也满了,线程池判断是否运行的线程数是否小于最大线程数,如果小于则直接创建工作线程来执行任务。那就会执行拒绝策略,默认是abortPolicy,无法处理新任务抛出异常。

线程池的种类

newCacheThreadPool:可缓存的线程池。 (Cpu:100%)

构造函数里边有几个重要的参数:corePoolSize=0: 核心线程数 maxPoolSize=INTEGER.MAX:支持最大线程数 keepAliveTime:存活时间 SynchronousQueue:没有容量的队列,即不存储任何元素。

主线程调用execute()执行任务,将任务offer()提交到SynchronousQueued队列中

——》如果有空闲线程则执行poll方法。——》如果没有空闲线程,则会创建一个线程

——》执行完之后的线程,会在SynchronousQueued等待60s,如果没有任务执行,则会终止线程。

newFixedThreadPool:固定数量线程的线程池。 (OOM)

corePoolSize=n: 核心线程数 maxPoolSize=n:支持最大线程数 keepAliveTime:存活时间 LinkedBlockingQueue:阻塞队列。

1.如果运行的线程数小于核心线程数,对于新任务会创建新的线程来执行任务;

2.如果运行的线程数大于核心线程数,新任务会丢到阻塞队列中;

3.线程执行完1中的任务后循环从队列中取任务进行执行。

newSingleThreadExector:数量只允许有一个线程的线程池。

corePoolSize=1: 核心线程数 maxPoolSize=1:支持最大线程数 keepAliveTime=0:LinkedBlockingQueue

执行流程同newFixedThreadPool

newScheduledThreadPool:支持定时和周期任务执行的线程池。

corePoolSize=n: 核心线程数 maxPoolSize=INTEGER.MAX.VALUE:支持最大线程数 keepAliveTime=nacosTime:DelayedQueue

线程1从DelayedQueue获取即将到期的任务;执行完之后需要将时间修改为下次要执行的时间。

5.线程的生命周期或状态?

线程具有五种状态:

New:当线程对象创建后,即进入新建状态。

Runnable:当调用线程对象的start方法后,就会进入到就绪状态。但是进入到就绪状态的线程并没有执行,而是等待CPU调度执行。

Running:运行状态,当CPU调度刚刚处于就绪状态的线程,此线程才会真正的执行。

Blocked:阻塞状态。由于某种原因,暂时放弃对CPU的使用权,停止执行,一般调用synchronized中获取锁的时候会发生阻塞。

Waiting:等待状态:当调用Object.wait()/Object.join()或者LockSupport.park()方法会进入到等待状态。LockSupport是不可重入的,AQS源码中使用的是LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的。

Object.wait():调用wait方法必须先获得对象的监视器锁。同时唤醒线程使用的notify()方法或者notifyAll()方法。

Timed_Waiting:超时等待

Terminated:终止状态,当前线程已经执行完毕。

6.常用的并发工具类有哪些?

7.请解释下什么是AQS?

AQS是个抽象类,用于构建锁和同步器的框架,比如我们常使用ReentrantLock、Semaphore等都是基于AQS来实现的。基于模板设计模式,使用者需要继承同步器并重写其中的方法。最后,可以这么说AQS是JUC并发的基石

通过内置的FIFO队列,将暂时获取不到锁的线程加入到队列尾部,将线程封装成队列的Node节点,里面包括前驱节点、后继节点、node等待状态、线程等属性,并通过一个volatile修饰的int类型变量来表示持有锁的状态,通过CAS 自旋和LockSupport.lock() 方法来维护state的值。AQS通过内部类ConditionObject构建等待队列,当condition调用wait()方法,线程会加入到等待队列中,而当调用signal方法后,线程从从等待队列转移到同步队列中。

8.你对ReentrantLock了解多少?

概念:

ReentrantLock默认是可重入的独占锁,即在同一时刻只能有一个线程可以获取到锁,获取不到的会被阻塞并放到阻塞队列中。因为ReentrantLock基于AQS构建的,state状态表示持有锁的状态。state为0当前锁没有被任何线程持有,当一个线程第一次获取该锁则会通过CAS设置state=1;在该线程没有释放锁的前提下,再次获取该锁则会state=2,这就是可重入锁。

特性:

ReentrantLock分为公平锁和非公平锁。学生打饭例子来表示公平和非公平。

非公平锁:如果占用锁的线程刚刚释放,state设置为0,等待唤醒的线程还未唤醒,这时来了一个线程直接使用该锁。例如A线程去获取资源的锁时,发现当前锁还未被释放同时不是A线程占用的,所以就会被放到阻塞队列中,此时B线程直接去获取该锁,恰巧该锁被释放,B获取到该锁。默认的实现是按照非公平锁来实现的。

公平锁:会判断队列中是否有线程进行的等待,如果没有等待的线程,该线程直接获取到锁,如果有的话,放到队列中。

源码:

ReentrantLock里边提供了一个内部类,并以AQS为父类的Sync,非公平和公平锁实现都是继承Sync这个类并重写 lock等方法

9.ReentrantReadWriteLock是什么?

概念:

ReentrantReadWriteLock 读写锁,对于读多写少的场景比ReentrantLock更合适,多线程读时互不影响,但是当线程进行写访问时,其他的读和写线程都会阻塞。读写锁内部维护着读锁和写锁,依赖Sync实现具体功能。AQS内部维持一个state状态,高16位表示的是获取读锁的次数,低16位表示的写锁的线程的可重入次数

特性:

1.支持公平和非公平获取锁的方式

2.支持可重入锁。在获取读锁之后还可以再次获取读锁;同时在获取写锁之后可以再次获取写锁和读锁。

3.可以进行锁降级:写锁可以降级为读锁。当线程先获取到写锁,然后再去获取读锁时,接着再释放写锁。这个过程叫做锁降级

源码:

写锁的原理:写锁是独占锁, 某时只有一个线程可以获取该锁,如果当前没有线程获取到读锁和写锁,则当前线程可以获取到写锁直接返回;如果当前的线程已经获取到写锁或者读锁,那么这个线程直接挂起;但是当前线程已经获取到该锁,再次获取可以简单的把可重入次数进行加1后直接返回。

读锁的原理:如果当前没有线程获取到写锁,那么该线程就可以直接获取到读锁,并在state状态值高16位进行加1;如果当前线程已经获取到写锁,则当前线程直接进行阻塞。

10.请说下有哪些阻塞队列?

阻塞队列概念:

阻塞队列是支持两个附加操作的队列,分为插入元素和取出元素操作。当队列中没有数据,取出元素的线程就会阻塞,直到队列中不为空;当队列中的数据已经满时,插入元素的线程就会阻塞,直到队列不满才会进行插入数据。

阻塞队列的处理方式:

抛出异常的方法:add()和remove()方法;

有返回值得方法:offer()和poll()方法;

一直阻塞方法:put()和take()方法

说五种阻塞队列:

ArrayBlockingQueue:由数组构成的有界队列

LinkedBlockingQueue:由链表构成的有界阻塞队列,但是趋近于无界 PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列

DelayQueue:一个延迟的无界阻塞队列

SynchronousQueue:一个不存储元素的阻塞队列

详细解释下其中某个阻塞队列原理吗?

ArrayBlockingQueue:是一个数组实现的有界阻塞队列,按照先进先出的原则对元素进行排序,队尾插入元素,队首删除元素。同时可以指定公平和非公平策略。poll()和offer()方法是通过加锁的方式实现的;而put()和take()一直阻塞是通过条件变量进行实现的,当使用put()方法时,如果当队列中元素已经满时,调用notFull.await(),线程会一直阻塞,如果某个线程调用take去取元素,会调用notFull.signal()方法唤醒该线程;add()在队列元素满时,会抛出IlegalStateException,如果没满还是调用的offer方法;remove方法会抛出NoSuchElementException。

LinkedBlockingQueue:底层基于链表来实现的,不可以指定公平和非公平策略;内部维护这两把锁,一把是putLock,另一把是takeLock。基于两把锁的实现主要是因为:避免了读写时相互竞争锁的现象,因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,因此LinkedBlockingQueue在高并发读写操作都多的情况下,性能比ArrayBlockingQueue好很多。

PriorityBlockingQueue:支持优先级的无界阻塞队列,每次都返回优先级最高或者最低的元素。

DelayQueue:延时获取元素的无界阻塞队列。队列中每个元素都有一个过期时间,当从队列中获取元素时,只有过期元素才会出队列,队列头元素时最快要过期的元素。

SynchronousQueue:一个不存储元素的阻塞队列。某个线程在向队列中put元素后,如果当前没有线程进行消费该元素,即take元素,则该线程会一直进行阻塞。等待一个消费线程take操作,take操作会唤醒该线程,同时会获得元素。这称为一次配对过程。

出现问题:当只有一个元素的时候会出现并发问题,怎样解决的?

在队列中初始化的时候,会初始化一个为null的哨兵节点,所以出队操作不阻塞至少有两个元素,入队操作的是当前结点元素的next节点,而出队操作的是为当前节点元素的head节点,将哨兵节点next节点指向自己,回收哨兵节点,同时将当前节点赋值到头节点,并将值设置为null,就会变成哨兵节点。

11.线程同步器有哪些?

CountDownLatch: 类似于倒计时器:主线程开启多个线程去执行任务,主线程需要等到所有的线程完成之后才进行汇总的场景。就比如我们项目中的大屏项目,需要查询许多模块的数据,待所有模块的数据查完之后靠主线程来汇总这些数据统一返回。

原理: CountDownLatch 实现了一个内部类Sync并用它去继承AQS,这样就能使用AQS提供的大部分方法了,把计数器的值赋值给AQS状态的state的值。调用await方法会阻塞当前当前线程,直到state变为0就返回;

方法:

await():当前线程会被阻塞,调用对象的countDown方法, 会使计数器减1。当计数器的值为0时,被阻塞的线程才会被唤醒;

或者其他线程调用当前线程的Interrupt中断该线程。委托AQS中的acquireSharedInterruptibly方法,查看当前计数器是否为0,如果为0则直接返回,否则直接进入aqs等待队列。

CyclicBarrier: 让一组线程达到一个屏障时被阻塞,直到最后一个线程到达时,屏障才会开门,所有被屏障拦截的线程才会去干活。

原理:

基于Condition 来实现的,参数有线程拦截器( 条件队列trip),每次拦截的线程数parties,栅栏的当前代(方便循环等待).

当计数器值count为0,则调用trip.signAll() 唤醒所有线程并转化为下一代;如果不为0,则调用trip.await() 方法阻塞当前线程。

Semphore: 信号量,通过它实现控制同时访问特定资源的线程数量,合理保证资源的使用。通常用于那些资源有明确访问数量限制的场景,常用于限流,数据库连接池;

原理: 默认非公平策略;基于AQS,有两个内部类,FairSync与NonfairSync;把初始令牌数量赋值给同步队列的state状态state的值就代表当前所剩余的令牌数量。

acquire()方法:

1.当前线程会尝试去同步队列获取一个令牌,获取一个令牌把state修改为state=state-1。

2.当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程。

3.当计算出来的state>=0,则代表获取令牌成功

release():

1.线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程

2.释放令牌成功之后,同时会唤醒同步队列中的一个线程。

3.被唤醒的节点会重新尝试去修改state=state-1 的操作, 如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程。

CountLatchDown与CyclicBarrier区别?

前者是不可以重置的,所以无法重用。后者可以重复使用

CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值

前者是让一个线程等待其他N个线程达到条件后,自己再去做某件事;

后者是让N个多线程的互相等待,直到所有线程到达某个状态,然后这些线程在一起去执行任务。

12.请你解释下synchronized是什么?原理是什么?

概念: synchronized 可以保证方法或者代码块在运行时,同一时刻只有一个方法可以获取到锁;同时他还可以保证共享变量的内存可见性。获取锁后会清空锁块内本地内存的共享变量,在使用者使用共享变量直接从主内存进行获取;释放锁后将锁块内的本地内存刷新到主内存中。这样就实现了共享变量的可见性。

Java中的每个对象都可以作为锁,这是实现synchronized同步的基础。

synchronized修饰普通同步方法,锁的当前的实例对象;

synchronized修饰静态同步方法,锁的是当前的class对象;

synchronized 修饰同步方法块,锁的是括号里边的对象。

原理: 同步方法块使用的是monitorEnter(同步代码块开始的位置)锁计数器+1和monitorExit(同步代码块结束或者异常的位置)锁计数器-1;同步方法是依靠方法修饰符上的ACC_SYNCHRONIZED来完成。本质都是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器,没有获取到对象的监视器会被阻塞在同步代码块或者方法的入口处。

原理: java对象头和monitor是实现synchronized的基础。java对象头主要包括MarkWord(标记字段:GC分代年龄、锁状态、偏向线程ID)和Klass Pointer(类型指针),其中mark word是实现轻量级锁和偏向锁的关键。

请解释下锁升级:

java1.6版本之前都synchronized是重量级锁,依赖于操作系统,如果挂起或唤醒一个线程都需要操作系统的帮忙,而操作系统实现线程之间的切换需要从用户态切换为内核态,转换需要大量时间。所以就出现了偏向锁和轻量级锁。

锁主要分为四种状态:无锁状态——》偏向锁状态——》轻量级锁——》重量级锁。

偏向锁是当一个线程访问同步代码块获取锁时,会在对象头存储锁偏向的线程Id,以后该线程进入到同步代码块不需要进行CAS操作,而是直接就可以获取到锁;

轻量级锁:偏向锁被另一个线程访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的方式进行尝试获取锁,不会阻塞,提高性能;

重量级锁:如果自旋10次还是获取锁失败,此时可能有多个线程正在同时获取锁。则锁状态升级为重量级锁。

13.请你解释下volatile是什么?原理是什么?

原理: Volatile是轻量级synchronized,可以保证线程的可见性并提供一定的有序性,但是无法保证原子性。在JVM底层是通过内存屏障来实现的。

特性:

1: 保证线程可见性:对一个volatile变量的读,总是能看到(任意线程)对这个变量的写入。

2:禁止指令重排序:

3.无法保证复合操作的原子性:对于任意单个volatile变量的读、写具有原子性,但类似于volatile++这种复合操作不具有原子性。

14.什么是JMM?为啥会有JMM存在?什么是happens-before?什么是重排序?什么是内存屏障?

JMM:是一组规则,通过这组规则控制java程序各个变量在共享数据区域和私有数据区域的访问方式

主内存也就是存储的java对象,所有线程创建的实例对象都存放在主内存中,所以多个线程对同一变量进行非原子操作可能会发现线程安全问题。

工作内存主要存储的是当前方法的所有本地变量信息, 同时存在着主内存中变量副本拷贝,每个线程只能访问到自己的工作内存信息。

有两种情况:

1.假设A线程和B线程去自增同一个共享变量i=0,都进行+1操作,可能结果是两个线程同时从主内存中读取i值,并进行++操作,最后刷新到主内存中,i最后是等于1而不是2;

2.A线程去修改i值,B去读取i值,B可能读取到的值是0或者1.

happens-before: 如果一个操作的执行结果需要对另一个操作可见,那么两个操作之间必须存在happen-before关系。

1.程序顺序原则:同一线程中,前面的操作happen-before后续的操作;

2.锁规则监视器锁的解锁操作happen-before其后续的加锁操作(Synchronized规则);

3.volatile规则:对volatile变量的写操作happen-before后续的读操作

4.线程启动规则线程的start()方法happen-before该线程所有的后续操作;如果A在B的start()方法之前修改了共享变量,那么当线程B可以读取线程A修改的共享变量值。

5.线程传递原则:如果a happen-berofe b ,b happen-before c ,那么a happen-before c ;

6 .线程终止原则线程的所有操作先于线程的终结

7.线程中断原则:对线程interrupt()方法调用先行发生于被中断线程的代码检测到中断事件的发生。

8.对象终结原则: 对象的构造函数执行,结束于finalize()方法。

什么是重排序?

重排序: 为了效率是允许编译器和处理器对指令进行重排序,不会影响单线程的执行结果,但是对于多线程会有影响。

编译器的重排序:编译器在不改变单线程程序的语义情况下,可以重新安排语句的执行顺序;

处理器的重排序:如果不存在数据的依赖性,处理器可以改变语句对应机器指令的执行顺序。

Volatile重排序的规则: 第二个操作是volatile写操作时,不管第一个操作是什么,都不允许重排序;

第一个操作是volatile读操作时,不管第二个是什么操作,都不允许重排序;

第一个操作是volatile写操作时,第二个操作是读操作,不允许重排序;

内存屏障是是一组处理器指令,用于实现对内存操作的顺序限制,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

在每个volatile写操作前面插入一个StoreStore屏障;保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见

在每个volatile写操作后面插入一个StoreLoad屏障;防止volatile写与后面volatile读、写进行重排序。

在每个volatile读操作后面插入一个LoadLoad屏障;禁止处理器把上面的volatile读与下面的普通读重排序

在每个volatile读操作后面插入一个LoadStore屏障;禁止处理器把上面的volatile读与下面普通写重排序。

使用例子

DCL:双重锁锁定

public class Singleton{
    private static volatile Singleton instance;
    private Singleton{}
    public static Singleton getInstance(){
        if(instance==null){
            synchronized(Singleton.class){
                if(instance==null){
                    // 先在内存中为instance分配一块内存
                    // 进行instance初始化
                    // 将 instance 指向分配的内存地址
                    return new Singleton();
                }
            }
        }
        return instance;
    }
}

14.synchronized与volatile区别?

  1. 使用方式: 前者是既可以修饰方法、同步代码块和静态方法,后者只能修饰变量;
  2. 特性: volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性
  3. 后者每次读取变量的时候会从主存中读取数据,不会从工作内存中读取数据,前者表示只可以有一个线程可以获取对象的锁,执行代码,阻塞其他线程。
  4. 前者主要是实现多个线程之间的访问同步性,后者主要解决变量在多个线程之间的可见性

15.synchronized与Lock区别?

相同点:都实现了多线程同步和内存可见性的语义;都是可重入锁;

不同点:

同步实现机制不同:前者是通过java对象头锁标记和Monitor对象实现同步;后者是通过Cas、AQS和LockSupport实现同步;

锁的类型不同:前者支持公平锁,可重入即不可中断,后者可以支持共公平锁和非公平锁,默认使用的非公平锁,支持可重入和可中断。

使用方式不同:前者是个关键字同时可以修饰代码块、普通方法、静态方法,发生异常时会主动释放占有的锁,因此不会发生死锁的现象;后者是个接口,显示调用的tryLock(获取锁的状态)和lock方法,需要在finally进行释放锁。

16.ThreadLocal用途是什么?原理是什么?请解释下?

ThreadLocal类是JDK包提供的,主要用于数据隔离

它提供了线程本地变量,也就是如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这个是名为threadLocals的成员变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程的值。

set方法:获取当前线程,在根据该线程查询到相关联的threadLocals,如果没有就会去创建自己的threadLocals,如果查到了直接把value值设置到threadLocals中。

get方法:获取当前线程,在根据该线程查询到相关联的threadLocals,如果没有进行初始化当前线程threadLocals成员变量;如果查到了则返回本地变量的值。

怎样实现数据隔离:

每个线程Thread都维护了自己的threadLocals变量,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

出现内存泄漏的问题:

threadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。

所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,使用完 ThreadLocal方法后 最好手动调用remove()方法。

如何保持数据共享?

将ThreadLocal对象通过static修饰,这时这个变量针对一个线程内的所有操作都共享的。

使用案例:

数据库连接池

使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。ThreadLocal包装SimpleDataFormat

cookie,session等数据隔离都是通过ThreadLocal去做实现的。

17.你常用的原子类有哪些?

原子类基本都使用的Unsafe实现的包装类,在底层就是CAS操作。 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

  • AtomicInteger:整形原子类
  • AtomicLong:长整型原子类
  • AtomicBoolean:布尔型原子类
  • AtomicIntegerArray:整形数组原子类
  • AtomicLongArray:长整形数组原子类
  • AtomicReferenceArray:引用类型数组原子类

18. 请谈谈你对CAS的理解?CAS的问题?

CAS:比较替换,也是一种无锁算法。CAS操作无需用户态和内核态切换,直接就在用户态对内存进行操作。

CAS包括三个参数:期望值、内存值和要更改的新值。当且仅当期望值和内存值相同,才会将内存值更改为要改的新值。

例如:A线程和B线程都去更新主内存的值v=56,A线程做的事是v++,B线程做的事是v++;A和B线程首先都会把内存中的值复制到自己的工作内存中,这个值就是期望值56,A会拉取内存的值和期望值进行比较,都是56,将内存值修改为新值;B线程也会拉取内存中的值,此时值为57,不等于期望值,所以就会更新失败。

问题:

ABA问题: CAS需要在操作值得时候检查值有没有变化,但是当值由A_B_A,那么使用Cas检查时判定值没有发生变化,但是其实发生了变化。

解决方案:使用版本号》每次更新的时候在变量前边加上版本号

AtomicStampedReference来解决。

循环时间长开销大:自旋长时间不成功的线程,会给cpu带来非常大的时间开销。控制循环次数,当达到限制次数,就直接return;

19 线程池的关闭方式有几种?

可以通过shutdown或shutdownNow方法来关闭线程。他们的工作原理就是循环遍历线程池中的线程,然后逐个调用线程的interrupt方法来中断线程。

20.线程之间如何通信

1.使用volatile关键字修饰变量。使用的共享内存的思想,多个线程共同监听一个变量,当这个变量发生变化时,线程能够感知并执行相关业务。

2.使用Object类中的wait和notify方法,思想就是线程通信。

3.countDownLatch:基于AQS框架,维护一个线程之间共享变量state。

4.LockSupport实现线程之间的线程阻塞和唤醒

5.管道通信:PiepedWriter和PipedReader ,必须先建立链接。

6.Thread .join():如果一个线程A调用join方法,当前线程A等待thread线程终止后才从thread.join()返回。

7.ThreadLocal :通过static ThreadLocal修饰,这个资源就是共享的。

21.进程之间如何通信

1.管道

2.消息队列

3.socket

22.请解释下synchronized的锁升级的原理?

synchronized在jdk1.6之前使用的是重量级锁的方式来实现线程之间锁的竞争。重量级锁是应用程序调用系统方法时需要由用户态切换到内核态来执行,这个切换会带来性能的损耗。

在jdk1.6之后,synchronzied引入锁升级后,如果有线程去竞争锁,首先synchronized会尝试使用偏向锁的方式去竞争锁资源,如果能竞争到偏向锁就直接返回表示加锁成功;如果竞争失败,表明当前锁已经偏向了其他的线程,需要将锁升级到轻量级锁,在轻量级锁状态下,竞争锁的线程通过CAS自旋去尝试抢占所资源,如果在轻量级锁还没有竞争到资源,就只能升级到重量级锁,在重量级锁的情况下,没有竞争到锁的线程就会被阻塞,线程的状态就是Blocked状态。

总的来说,锁升级的设计思想就是性能和安全性的平衡,如何在不加锁的情况下能够保证线程安全性,就比如mysql中的使用乐观锁解决并发事务竞争锁的问题。

23.如何中断一个正在运行的线程?

首先,线程是系统级别的概念,在java实现的线程,最终执行和调度都是由操作系统来决定的。

我们在使用start启动一个线程的时候,只是告诉操作系统这个线程可以执行,但最终的执行权还是交给CPU来执行。

通过stop方法可以去中止一个正在执行的线程,但这种方式是不安全的,因为可能线程的任务还没有结束,导致出现运行结果不正确的问题。

安全的中断一个正在运行的线程,只能在线程内部埋下一个钩子,外部程序需要通过这个钩子来触发线程的中断命令。提供了interrupt方法,这个方法配isInterrupted()方法使用,就可以实现安全的中断机制。

24.fail-safe和fail-fast机制分别有什么作用?

都是多线程并发操作集合时的一种失败处理机制。

fail-fast:表示快速失败,在集合的遍历中,一旦发现容器中的数据修改,会立刻抛出concurrentModificationException异常,从而导致遍历失败。比如在hashmap中,使用iteratior迭代器进行数据遍历,在遍历的过程中,对集合做出变更,就会发生fail-fast,java.util包下的集合类都是快速失败机制的。

fail-safe:失败安全,在遍历时不直接在集合内容上访问的,而是先拷贝有原有的集合内容,在拷贝的新的集合内容上进行遍历。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用。

\