Java面试—并发编程篇

1、什么是并行?什么是并发?

从操作系统来看,线程是CPU分配的最小单位

  • 并行:就是同一时刻,两个线程都在执行,这就要求有两个CPU去分别执行了,如果只有一个CPU的话,执行完一个线程,才能去执行另一个
  • 并发:就是同一时刻,只有一个执行,但是一个时间段,两个线程都执行了,并发的实现依赖于CPU的切换,切换的时间特别短,基本来说对用户是无感知的

2、什么是进程?什么是线程?

  • 进程:进程是代码在数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位
  • 线程:线程是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源,是CPU分配的基本单位

比如在Java中,启动一个main函数就是启动了一个JVM进程,而这个main就是这个进程中的一个线程(主线程),一个进程中可以有多个线程,他们共享进程的堆和方法区资源,但是每个线程都有自己的程序计数器和栈。

3、线程生命周期有哪些?状态切换的过程?

  • 新生(NEW)
  • 运行(RUNABLE)
  • 阻塞(BLOCKED)
  • 等待(WAITING)
  • 超时等待(TIMED_WAITING)
  • 终止(TERMINATED)

image.png

4、什么是死锁?死锁产生的条件?如何避免死锁?

       比如线程A持有着资源1,线程B持有着资源2,他们都没有释放各自的资源,而且想要获取对方的资源,所以这两个线程就会互相等待而造成的状态,就称之为死锁。理解如下代码。

public class DeadLockDemo {
    
    
    private static Object resource1 = new Object();//资源 1
    private static Object resource2 = new Object();//资源 2

    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            synchronized (resource1) {
    
    
                System.out.println(Thread.currentThread() + "get resource1");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
    
    
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "线程 1").start();

        new Thread(() -> {
    
    
            synchronized (resource2) {
    
    
                System.out.println(Thread.currentThread() + "get resource2");
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
    
    
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "线程 2").start();
    }
}

## 输出
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1

死锁产生的条件:

  • 互斥:该资源任意一个时刻只由一个线程占用
  • 请求并持有条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

如何避免死锁:

至少要破坏死锁发生的一个条件

  • 破坏请求并持有条件:可以一次性请求所有的资源
  • 破坏不可剥夺条件:占用部分资源的线程申请其他的资源,如果申请不到,可以主动释放它占有的资源
  • 破坏循环等待条件:可以在申请资源的时候,按序申请先申请序号小的,再申请序号大的

5、synchronized锁住的是什么?

      synchronized本身并不是锁,锁本身是一个对象,synchronized最多相当于“加锁”操作,所以synchronized并不是锁住代码块。用在实例方法上锁的是调用该方法的对象,用在静态方法上,锁的是当前类的所有对象,用在代码块上如果修饰的是对象则锁的是对象,如果修饰的是类,那么锁的是该类的所有对象。

6、synchronized底层?

synchronized 关键字底层原理属于 JVM 层面的东西。

  1. synchronized同步语句块的情况

      synchronized同步语句块的实现是使用的是monitorentermonitorexit指令,其中monitorenter指向的是同步代码块开始的位置,也就是执行它的时候,如果锁的计数器为0则表示可以被获取,那么锁的计数器就会加1,其他想要获取锁的看到为1就等待;monitorexit指向的同步代码块结束的位置,执行它就会释放刚才获取的锁,将锁的计数器减1。

  1. synchronized修饰方法的情况

      修饰方法的时候,使用的是ACC_SYNCHRONIZED标识,指明了该方法是一个同步方法,JVM通过该标识来辨别一个方法是否声明为同步方法,从而执行响应的同步调用。**

7、synchronized锁升级?

      Java对象头里,有一块结构,叫Mark Word标记字段,这块结构会随着锁的状态变化而变化。首先要了解一下锁的四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,它们会随着竞争的激烈而逐渐升级,锁可以升级但是不能降级,这种策略是为了提高获得锁和释放锁的效率。
image.png

jdk1.6之前,synchronized的实现直接调用了ObjectMonitor的enter和exit,这种锁被称之为重量级锁,为了对所进行优化,jdk1.6对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销

8、volatile关键字的作用?什么是内存可见性?什么是指令重排?volatile禁止指令重排原理?

  1. 被volatile修饰的共享变量,主要具有了可见性、有序性和不保证原子性。
  2. 内存可见性:这里要谈到Java的内存模型(JMM),每个线程都有格子的工作内存,当修改一些变量的时候,会将新值同步到主内存中,可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
  3. 什么是指令重排,看下面的例子:
public class test1 {
    
    
    static int x = 0;
    static int b = 0;
    public static void main(String[] args) {
    
    
        new Thread(() -> {
    
    
            x = b;
            b = 1;
            System.out.println("x = " + x + " b = " + b);
        }).start();

        new Thread(() -> {
    
    
            x = b;
            b = 1;
            System.out.println("x = " + x + " b = " + b);
        }).start();
    }
}

# 结果(多线程情况下)
x = 0 b = 1
x = 1 b = 1

      正经的定义:CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。但代码逻辑之间是存在一定的先后顺序,并发执行时按照不同的执行逻辑会得到不同的结果。

  1. volatile禁止指令重排原理,通过施加内存屏障,禁止指令重排的操作

image.png

9、Volatile和Synchronized有什么区别?

  1. volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好,但是volatile只能修饰变量,而synchronized可以修饰方法和代码块。
  2. `volatile关键字能保证数据的可见性,但不能保证数据的原子性,synchronized两者都能保证。**
  3. volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized解决的是多个线程访问资源的同步性。

10、ReentrantLock是什么?

  1. ReentrantLock实现了Lock接口,是一个可重入且独占式的锁,和synchronized关键字类似,不过,ReentrantLock更加灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。
  2. ReentrantLock里面有一个内部类SyncSync继承AQS,添加锁和释放锁的大部分操作实际上都是在Sync中实现的。Sync有公平锁FairSync和非公平锁NonFairSync两个子类。
  3. 默认是使用的非公平锁,也可以通过构造器还指定使用公平锁。

11、ReentrantLock和Synchronized有什么区别?

区别 synchronized reentrantlock
锁实现机制 对象头监视器模式 依赖AQS
灵活性 不灵活 支持响应中断、超时、尝试获取锁
释放锁形式 自动释放锁 显示调用unlock()
支持锁类型 非公平锁 公平锁&非公平锁
条件队列 单条件队列 多条件队列
可重入支持 支持 支持

12、线程池是什么?应用场景?

       就是一个管理线程的池子,使用了池化技术的思想,可以降低资源的消耗提高响应的速度重复利用方便管理,这里主要要了解的就是三大方法、七大参数、四种拒绝策略。
       应用场景有并行任务、定时任务等。

13、线程池的三大方法、七大参数、四大拒绝策略?五大队列?

三大方法:

方法 作用
Executors.newSingleThreadExecutor() 单个线程的线程池
Executors.newFixedThreadPool(5) 固定大小的线程池
Executors.newCachedThreadPool() 遇强则强,遇弱则弱的线程池

ps:这三大方法基本用不上,了解即可,《阿里巴巴Java开发手册》中不允许使用Excutors去创建,要使用ThreadPoolExcutors去创建,这样可以更加明确线程池的运行规则,规避资源耗尽的风险。

七大参数:

public ThreadPoolExecutor(int corePoolSize, // 核心线程大小
                          int maximumPoolSize, // 最大核心线程池大小
                          long keepAliveTime, // 超时了,没有人调用就会释放
                          TimeUnit unit, // 超时的单位
                          BlockingQueue<Runnable> workQueue, // 阻塞队列
                          ThreadFactory threadFactory, // 创建线程的工厂
                          RejectedExecutionHandler handler // 拒绝策略) {
    
    
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}
参数 作用
corePoolSize 初始化线程池中核心线程数
maximumPoolSize 表示允许的最大线程数
keepAliveTime 没人调用多长时间就会释放
unit 存活时间的单位
workQueue 线程池等待队列,维护着等待执行的Runnable对象
threadFactory 创建新线程使用的工厂
handler 拒绝策略(参考下方)

四大拒绝策略:

策略 作用
AbortPolicy 直接抛出异常,默认使用此策略
CallerRunsPolicy 用调用者所在的线程来执行任务
DiscardOldestPolicy 丢弃阻塞队列中最老的任务,也就是队列靠前的任务
DiscardPolicy 当前任务直接丢弃

五大队列:

队列 作用
ArrayBlockingQueue(有界队列) 是一个用数组实现的有界阻塞队列,按FIFO排序量
LinkedBlockingQueue(可设置容量队列) 基于链表的阻塞队列,容量可以设置,不设置的话就是个无边界的阻塞队列
DelayQueue(延迟队列) 是一个任务定时周期的延迟执行的队列。根据指定的的执行时间从小到大排序,否则根据插入到队列的先后顺序
PriorityBlockingQueue(优先级队列) 是具有优先级的无界阻塞队列
SynchronousQueue(同步队列) 是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则一直阻塞

14、简述一下线程池的工作流程?

image.png

15、如何配置线程池的核心线程数?

在高并发的情况下采用线程池,可以有效降低创建线程释放的时间花销及资源开销,如不使用线程池,有可能操场系统大量线程而导致消耗完系统内存以及“过度切换”,我们希望尽可能多的创建任务,但由于资源所限我们又不能创建过多的线程,那么在高并发的情况下,怎么选择最优的线程数量?

CPU密集型:核心线程数 = CPU核数 + 1
IO密集型:核心线程数 = CPU核数 * 2

CPU核数:Runtime.getRuntime().availableProcessors()
CPU密集型:当线程CPU时间所占比例越高,需要越少的线程
IO密集型:当线程等待时间所占比例越高,需要越多线程 ,启用其他线程继续使用CPU,以此提高CPU的利用率

16、ThreadLocal是什么?使用场景?内存泄露了解吗?为什么key设计成弱引用?

image.png

  1. 可以叫做线程局部变量,ThreadLocal类主要解决的就是让每个线程绑定自己的值,每个线程都有自己的私有数据。
  2. 使用场景有每个线程需要一个独享的对象、当前用户信息需要被线程内所有方法共享等。
  3. ThreadLocal中的key是弱引用,value是强引用,弱引用有这么一个特性,不管jvm内存空间是否充足,都会回收该对象占用的内存,如果此时key被回收了,value还在就会造成内存泄露了。防止内存泄露可以在使用完ThreadLocal及时调用remove方法释放空间。
  4. 假如key是强引用,如果ThreadLocal Reference被销毁,此时它指向ThreaLocal强引用就没有了,就应该被回收掉了,但是key还是强引用指向的它,导致他不能被回收,也会发生内存泄露的问题。和设计成弱引用来比较的话,就是弱引用还有补救内存泄露的方法,强引用则没有。

17、CAS是什么?

       CAS叫做CompareAndSwap,比较并变换,主要是通过处理器的指令来保证操作的原子性,CAS指令包含三个参数:共享变量的内存地址A、预期的值B和共享变量的新值C,只有内存地址A中的值等于B时,才会将内存中A的值更新为C。作为一条CPU指令,CAS指令本身是可以保证原子性的。

18、CAS有什么问题?该怎么解决?

ABA问题:
       并发条件下,假设初始条件是A,去修改数据时,发现是A就会执行修改,但是A可能已经之前变成了B,又变成了A,此时就可能会产生一些问题。
       解决办法:加版本号,每次修改变量,就要给这个变量的版本号加1,在执行CAS的时候,去判断当前版本号和预期的版本号是否一致(一次校验),可以使用AtomicStampReference类,通过它的compareAndSet方法首先检查当前的对象引用值是否等于预期的引用,并且当前印戳标志是否等于预期标志,如果全部相等,则以原子方式将引用值和印戳标志的值更新为给定的更新值。

public class CASDemo {
    
    
    // AtomicStampedReference注意,如果泛型是包装类,注意对象的引用问题
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(1, 1);
    public static void main(String[] args) {
    
    
        new Thread(()->{
    
    
            int stamp = atomicStampedReference.getStamp();
            System.out.println("a1 => " + stamp);

            try {
    
    
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 2, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a2 => " + atomicStampedReference.getStamp());

            System.out.println(atomicStampedReference.compareAndSet(2, 1, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1));

            System.out.println("a3 => " + atomicStampedReference.getStamp());
        }, "a").start();

        new Thread(()->{
    
    
            int stamp = atomicStampedReference.getStamp();
            System.out.println("b1 => " + stamp);

            try {
    
    
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }

            System.out.println(atomicStampedReference.compareAndSet(1, 3, stamp, stamp + 1));
            System.out.println("b2 => " + atomicStampedReference.getStamp());

            }, "b").start();
    }
}
# 结果
a1 => 1
b1 => 1
true
a2 => 2
true
a3 => 3
false
b2 => 3

循环性能开销
       自旋CAS,如果一直循环执行,一直不成功,会给CPU带来非常大的执行开销。
       解决方法:在Java中,很多使用自旋CAS的地方,会有一个自旋次数的限制,超过一定次数,就会停止。

只接受一个变量的原子操作:
       CAS保证的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS无法保证操作的原子性
       解决办法:可以考虑改用锁来保证操作的原子性,可以合并多个变量,封装成一个对象,通过AtomicReference来保证原子性。

19、wait()和sleep()相同点和不同点?

相同点:它们的调用都会暂停当前线程并让出CPU

不同点:

wait() sleep()
定义的位置 是Object的方法 是Thread的方法
调用的地方 只能在代码块或同步方法中 可以在任何地方使用
锁资源释放方式 让当前线程暂时释放了锁,当调用了notify/notifyAll方法才会解除wait状态,去争夺锁,进而执行 让出了CPU,但是没有释放锁
恢复方式不同 进入了wait状态,放弃了锁,调用了notify/notifyAll后,才会去争夺锁,才能进入运行状态 停止运行期间,仍然持有锁,当超时时间一到,就会继续执行
异常捕获 不需要 需要捕获或抛出异常

20、线程有几种创建的方式?

有三种创建线程的方法:继承Thread类、实现Runnable接口、实现Callable接口

  1. 继承Thread类:重写run()方法,调用start()方法启动线程。
  2. 实现Runnable接口:重写run()方法,调用start()方法启动线程。
  3. 实现Callable接口:上面两种方法都是没有返回值的,但是这种可以获取到返回值,可以抛出异常,并且重写的方法是call()

image.png

public class test1 {
    
    
    public static void main(String[] args) {
    
    
        FutureTask<String> stringFutureTask = new FutureTask<>(new MyThread());
        new Thread(stringFutureTask).start();
        try {
    
    
            String s = stringFutureTask.get();
            System.out.println("call = "  + s);

        } catch (InterruptedException e) {
    
    
            throw new RuntimeException(e);
        } catch (ExecutionException e) {
    
    
            throw new RuntimeException(e);
        }
    }
}

class MyThread implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        System.out.println("call()");
        return "1024";
    }
}
# 结果
call()
call = 1024

21、线程间的通讯方式和区别?

方式 解释
volatile和synchronized volatile修饰的变量能保证所有线程对变量访问的可见性;synchronized能确保多个线程在同一时刻,只能有一个线程处于方法或者同步代码块中,保证线程对变量访问的可见性和排它性。
等待/通知机制 通过(wait()、notify())实现一个线程修改一个对象的值,而另一个线程感知到了变化,然后做出响应的操作
管道输入/输出流 管道输入/出流和普通的文件输入/出流或者网络输入/出流不同之处,它主要用于线程之间的数据传输,而传输的媒介为内存。具体有面向字节:PipedOutputStream、PipedInputStream,面向字符:PipedReader、PipedWriter。
使用Thread.join() 如果一个线程A执行了threa.join()语句,含义:当前线程A等待thread线程终止之后才从thread.join()返回。还提供了join(long millis)和join(long millis,int nanos)两个具备超时特性的方法
使用ThreadLocal 线程变量,是一个以ThreaLocal对象为key,任意对象为value的结构,它被附带在线程上,可以通过ThreadLocal对象来查询到绑定到ThreadLocal上的值(通过set()、get()方法)

猜你喜欢

转载自blog.csdn.net/weixin_52487106/article/details/130954632