JUC面试题

JUC

level_1

1.并发与并行, 线程与进程的概念

  • 并行:指两个或多个事件在同一时刻发生(同时执行)
  • 并发:指两个或多个事件在同一个时间段内发生(交替执行)
  • 进程:是指一个内存中运行的应用程序 , 每个进程都有一个独立的内存空间 , 一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程 , 是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程. (有独立的内存空间 , 进程中的数据存放空间(堆空间和栈空间)是独立的 , 至少有一个线程. )
  • 线程:是操作系统能够进行运算调度的最小单位. 是进程中的实际运作单元 , 负责当前进程中程序的执行 , 一个进程中至少有一个线程. 一个进程中是可以有多个线程的 , 这个应用程序也可以称之为多线程程序. (堆空间是共享的, 栈空间是独立的 , 线程消耗的资源比进程小的多. )

2.Java中线程类(Thread)及其常用API

java.lang.Thread

	public void run() //核心方法, 线程任务
	public void start() //启动线程
	public static Thread currentThread() //获取当前线程对象的引用
	public static void sleep(long millis, int nanos)// 暂停当前线程一段时间
	public static native void yield(); // 暂停当前正在执行的线程对象, 并执行其他线程 
	public final synchronized void join(long millis, int nanos) // 等待指定时间后, 线程终止, 被打断将抛异常

3.Thread类和Runable接口

  • Thread是Runable的实现类

  • 继承Thread和实现Runable都可以创建线程对象, 两种方式对比如下

    • 继承Thread可以使用Thread的内部方法, Runable中只有run方法

    • 实现Runable避免单继承的局限性

    • 解耦, 增强了程序的健壮性

    • 线程池只能放入Runable或Callable类, 不能放入继承Thread的类

      public interface Executor {
          void execute(Runnable command);
      }
      

4.java中使用线程池的基本API

java.util.concurrent.Executors

public static ExecutorService newFixedThreadPool(int nThreads) // 创建指定容量的线程池
public interface ExecutorService extends Executor

<T> Future<T> submit(Callable<T> task); // 提交线程任务并执行
Future<?> submit(Runnable task); //提交一个 Runnable 任务用于执行, 并返回一个表示该任务的 Future. 
void shutdown(); // 启动一次顺序关闭, 执行以前提交的任务, 不接受新任务
Callable<Double> callable = new Callable<Double>() {
    @Override
    public Double call() throws Exception {
        //TODO
    }
};

5.Object.wait()与Thread.sleep(long)的区别

  • Thread 中的sleep(long)方法:不释放锁, 休眠结束之后继续执行
  • Object 中的wait(long)方法:调用即释放锁 (notify() | notifyAll()唤醒)

level_2

1.线程的生命周期: Thread.State 线程状态

// Thread内部类
public enum State {
    NEW, // 新建, 未调用start()

    RUNNABLE, // 可运行 已调用start() 具体分为ready跟running, 当线程被挂起或者调用Thread.yield()的时候为ready

    BLOCKED, // 阻塞, 等待有锁可用 获取锁后变为RUNNABLE

    WAITING, // 等待, 不能自动唤醒 Object.wait() | Thread.join()

    TIMED_WAITING, // 计时等待, 到超时期满, 或者被其他线程唤醒 Thread.sleep(long) | Object.wait(long) | Thread.join(long) | LockSupport.parkNanos(long) | LockSupport.parkUntil(long)

    TERMINATED; // 被终止. 异常或执行完任务, 不能再执行 start()
}

2.JVM中多线程的运行机制

每个线程有自己独立的栈区域, 共用一个堆区域, 操作共享变量时会先拷贝一个变量副本到自己的工作内存(里面有线程自己的局部变量等), 执行共享变量操作时 操作的是这个变量副本, 所以高并发情况下可能会造成可见性的问题

[线程对变量的所有的操作(读, 取)都必须在工作内存中完成, 而不能直接读写主内存中的变量, 不同线程之间也不能直接访问, 对工作内存中的变量, 线程间变量的值的传递需要通过主内存完成]

3.volatile和synchronized关键字的区别

  1. volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的 , 需要从主存中读取synchronized则是锁定当前变量 , 只有当前线程可以访问该变量 , 其他线程被阻塞住
  2. volatile仅能使用在变量级别; synchronized则可以使用在变量/方法级别的
  3. volatile仅能实现变量的修改可见性, 不能保证原子性; 而synchronized则可以保证变量的修改可见性和原子性
  4. volatile不会造成线程的阻塞; synchronized可能会造成线程的阻塞.
  5. volatile标记的变量不会被编译器优化; synchronized标记的变量可以被编译器优化

4.java.util.concurrent.atomic包下常用的类(原子类)及其底层原理(CAS)

sun.misc.Unsafe

private static final Unsafe unsafe = Unsafe.getUnsafe();
// 原子操作, cpu命令
// 仅当预期值与内存中的值相同时, 才执行值替换操作
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

ABA问题(狸猫换太子问题)

java.util.concurrent.atomic.AtomicStampedReference

//底层使用了时间戳, 就和我们添加一个version字段一样的效果
public boolean compareAndSet(V expectedReference,
                              V newReference,
                              int expectedStamp,
                              int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference &&
        expectedStamp == current.stamp &&
        ((newReference == current.reference &&
          newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

5.java.util.concurrent.locks.Lock

Lock lock = new ReentrantLock();
new ReentrantLock(true) 表示公平锁, 不带参数默认为false, 非公平锁

ReentrantLock可以替代synchronized
但是ReentrantLock必须手动开启锁/关闭锁, synchronized遇到异常会自动释放锁, ReentrantLock需要手动关闭, 一般都是放在finally中关闭
	定义锁 Lock lock = new ReentrantLock();
	开启 lock.lock();
	关闭 lock.unlock();

使用Reentrantlock可以进行“尝试锁定”tryLock,这样无法锁定, 或者在指定时间内无法锁定, 线程可以决定是否继续等待, 使用tryLock进行尝试锁定, 不管锁定与否, 方法都将继续执行, 可以根据tryLock的返回值来判定是否锁定, 也可以指定tryLock的时间, 由于tryLock(time)抛出异常, 所以要注意unclock的处理, 必须放到finally, 如果tryLock未锁定, 则不需要unlock

6.JUC下常用并发辅助类

  • CountDownLatch

    • 底层是通过一个计数器来控制的, 每当一个线程完成了自己的任务后 , 可以调用countDown()方法让
      计数器-1,当计数器到达0时, await()方法的线程阻塞状态解除, 线程继续执行
    • 与join区别, 使用join的线程将被阻塞, 使用countDown的线程不受影响, 只有调用await()的时候才会阻塞
    public CountDownLatch(int count)// 初始化一个指定计数器的CountDownLatch对象
        
    public void await() throws InterruptedException// 让当前线程等待
    public void countDown() // 计数器进行减1
    
  • CyclicBarrier

    • 让一组线程到达一个屏障(也可以叫同步点)时被阻塞, 直到最后一个线程到达屏障时, 屏障才会打开, 所有被屏障拦截的线程才会继续运行 (使用场景:可以用于多线程计算数据, 最后合并计算结果)
    public CyclicBarrier(int parties, Runnable barrierAction)// 用于在线程到达屏障时, 优先执行barrierAction,方便处理更复杂的业务场景
        
    public int await()// 每个线程调用await方法告诉CyclicBarrier我已经到达了屏障, 然后当前线程被阻塞
    
  • Semaphore

    • 控制线程的并发数量
    • 对于Semaphore来说, 它要保证的是资源的互斥而不是资源的同步, 在同一时刻是无法保证同步的, 但是却可以保证资源的互斥. 只是限制了访问某些资源的线程数, 其实并没有实现同步.
    public Semaphore(int permits, boolean fair) //fair 表示公平性, 如果这个设为 true 的话, 下次执行的线程会是等待最久的线程, 默认为false
        
    public void acquire() throws InterruptedException 表示获取许可
    public void release() release() 表示释放许可
    
  • Exchanger

    • 两个线程间的数据交换
    public V exchange(V x) //等待另一个线程到达此交换点(除非当前线程被中断), 然后将给定的对象传送给该线程, 并接收该线程的对象
    
    V exchange(V v, long timeout, TimeUnit unit) //等待另一个线程到达此交换点(除非当前线程被中断或超出了指定的等待时间), 然后将给定的对象传送给该线程, 并接收该线程的对象 
    

7.悲观锁/乐观锁

一种思想, java中JUC.atomic下的类, 底层CAS就是乐观锁的一种实现, 默认数据不会改变, 心比较乐观;
synchronized | ReentrantLock 就是悲观锁的一种实现, 比较悲观, 每次都认为其他人会改变数据, 所以加锁不让其他人操作, 整个数据处理过程只能自己操作

乐观锁比较适用于读多写少的情况(多读场景)
悲观锁比较适用于写多读少的情况(多写场景)

level_3

1.ConcurrentHashMap与HashTable区别(效率相比)

HashTable底层使用synchronized对整个数组进行锁定, 而ConcurrentHashMap使用CAS+局部锁定(分段锁)

// 常用并发容器
CopyOnWriteArrayList
CopyOnWriteArraySet
ConcurrentHashMap

分段锁

分段锁其实是一种锁的设计, 并不是具体的一种锁, ConcurrentHashMap并发的实现就是通过分段锁的形式来实现高效的并发操作 

ConcurrentHashMap中的分段锁称为Segment, 它类似于HashMap(JDK7与JDK8中HashMap的实现)的结构, 即内部拥有一个Entry数组, 数组中的每个元素又是一个链表; 同时又是一个ReentrantLock(Segment继承了ReentrantLock). 当需要put元素的时候, 并不是对整个hashmap进行加锁, 而是先通过hashcode来知道他要放在哪一个分段中, 然后对这个分段进行加锁, 所以当多线程put的时候, 只要不是放在一个分段中, 就实现了真正的并行的插入 
但是, 在统计size的时候, 就是获取hashmap全局信息的时候, 就需要获取所有的分段锁才能统计 

分段锁的设计目的是细化锁的粒度, 当操作不需要更新整个数组的时候, 就仅仅针对数组中的一项进行加锁操作

2.公平锁 | 非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁; 
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序, 有可能后申请的线程比先申请的线程优先获取锁; 有可能会造成优先级反转或者饥饿现象, 非公平锁的优点在于吞吐量比公平锁大

对于Java ReentrantLock而言, 通过构造函数指定该锁是否是公平锁, 默认是非公平锁 

对于Synchronized而言, 也是一种非公平锁, 由于其并不像ReentrantLock是通过AQS的来实现线程调度, 所以并没有任何办法使其变成公平锁 

3.可重入锁(递归锁)

是指在同一个线程在外层方法获取锁的时候, 在进入内层方法会自动获取锁

ReentrantLock, Synchronized都是可重入锁 
可重入锁的一个好处是可一定程度避免死锁

比如说A类中有个methodA1()
	public synchronized methodA1(){
		methodA2();
	}
	
	public synchronized methodA2(){
	    //TODO
	}
当线程调用methodA1时, 获取到锁之后, 执行到内部methodA2, 就能再次获取本对象的锁, 一定程度上避免了死锁

4.独享锁/共享锁 | 互斥锁/读写锁

独享锁是指该锁一次只能被一个线程所持有; 
共享锁是指该锁可被多个线程所持有 

ReentrantLock和Synchronized都是独享锁 
	但是对于Lock的另一个实现类ReadWriteLock, 其读锁是共享锁, 其写锁是独享锁 
	读锁的共享锁可保证并发读是非常高效的, 读写 | 写读 | 写写的过程是互斥的 

独享锁/共享锁是一种广义的说法, 互斥锁/读写锁就是具体的实现 
	互斥锁在Java中的具体实现 ReentrantLock
	读写锁在Java中的具体实现 ReadWriteLock

5.偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态, 并且是针对Synchronized 
在Java 5通过引入 锁升级 的机制来实现高效Synchronized 
这三种锁的状态是通过对象监视器在对象头中的字段来表明的

偏向锁(偏隙锁)
	是指一段同步代码一直被一个线程所访问, 那么该线程会自动获取锁, 降低获取锁的代价 

轻量级锁(其他线程自旋)
	是指当锁是偏向锁的时候, 被另一个线程所访问, 偏向锁就会升级为轻量级锁, 其他线程会通过自旋的形式尝试获取锁, 不会阻塞, 提高性能 

重量级锁(其他线程阻塞)
	是指当锁为轻量级锁的时候, 另一个线程虽然是自旋, 但自旋不会一直持续下去, 当自旋一定次数的时候, 还没有获取到锁, 就会进入阻塞, 轻量级的锁就膨胀为重量级锁. 重量级锁会让其他申请的线程进入阻塞, 性能降低 

6.自旋锁

	如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会一上来就被挂起(阻塞), 而是采用循环的方式去尝试获取锁, 这样的好处是减少线程切换的消耗, 缺点是如果锁持有者持有时间过长, 长时间的自旋会消耗CPU 
	自旋锁比较适用于锁使用者保持锁时间比较短的情况, 这种情况下自旋锁的效率要远高于互斥锁

7.AQS(底层CLH)

java.util.concurrent.locks.AbstractQueuedSynchronizer

参见:

  • https://www.cnblogs.com/waterystone/p/4920797.html
  • https://blog.csdn.net/mulinsen77/article/details/84583716
	AQS是JDK下提供的一套用于实现基于FIFO等待队列的阻塞锁和相关的同步器的一个同步框架. 这个抽象类被设计为作为一些可用原子int值来表示状态的同步器的基类. 比如 CountDownLatch 类, 其内部有一个继承了  AbstractQueuedSynchronizer 的内部类 Sync. 可见 CountDownLatch 是基于AQS框架来实现的一个同步器. 类似的同步器在JUC下还有不少(如 Semaphore)

	AQS的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中.
	CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系.CLH锁是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性; 也是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋.
	AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配.
	简单来说, AQS就是基于CLH队列,用volatile修饰共享变量state,线程通过CAS去改变状态符,成功则获取锁成功,失败则进入等待队列,等待被唤醒.

	以ReentrantLock为例, state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁.A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念.
注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的.

	以CountDownLatch为例,任务分N个子线程去执行,state就初始化为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一.当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作.
	
AQS 定义了两种资源共享方式:
	1.Exclusive: 独占, 只有一个线程能执行, 如ReentrantLock
	2.Share: 共享, 多个线程可以同时执行, 如Semaphore, CountDownLatch, ReadWriteLock, CyclicBarrie

实现不同的方法, 以决定锁策略:
	tryAcquire(int):独占方式.尝试获取资源,成功则返回true,失败则返回false.
	tryRelease(int):独占方式.尝试释放资源,成功则返回true,失败则返回false.
	tryAcquireShared(int):共享方式.尝试获取资源.负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源.
	tryReleaseShared(int):共享方式.尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false.
	
	一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire/tryRelease || tryAcquireShared/tryReleaseShared中的一种即可.但AQS也支持自定义同步器同时实现独占和共享两种方式, 如ReentrantReadWriteLock.	
	
	... 自己去看上面两个链接吧 ...

你可能感兴趣的:(笔记,多线程,面试)