Java面试题集锦(好文收藏+总结,持续更新...)

前言

从各种方面收集整理加一点总结(甚少),方便自己理解,好多出处不是很清楚,如果侵权问题请马上联系我,会立即改正注明出处哒,谢谢前辈们整理的资料,膜拜大神们~


B站面试视频相关笔记

一、尚硅谷Java大厂面试题第2季,面试必刷,跳槽大厂神器

并发

一、volatile关键字解析

volatile保证可见性,不保证原子性,禁止指令重排序。
多线程下指令重排序会导致结果错误:
单线程环境里面,指令重排序最终执行结果和代码顺序执行的结果一致,因为处理器在进行重排序时考虑了指令之间的数据依赖性,但是如果两个线程之间有数据依赖性,指令重排序会将这种依赖性打乱,导致最终结果与期望结果不一致。
volatile禁止指令重排序:

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

二、CAS

1.什么是CAS
CAS (compare and swap),比较并交换,将主内存中待修改的数据拷贝一份到当前线程的工作内存,比较当前工作内存中的值(期待值)与主内存中的值是否一致,如果一致,将拷贝的值修改为期待值,并将修改后的结果刷新回主内存。由于整个过程涉及到对内存的操作,所以借助了CAS的核心类Unsafe,Unsafe中的方法用关键字native进行修饰,为本地方法,可以直接操作内存,每个方法都是原子的,不能中断。
2.CAS的存在的问题

  • ABA问题
    例如说:
    一. 线程1查询内存中的值和预期值是否相等,且是否为A,线程1挂起
    二. 线程2获取CPU的使用权,并查询值是否为A
    三. 线程2使用CAS将值更新为B
    四. 线程2查询值是否为B
    五. 线程2使用CAS将值更新为A,线程2挂起
    六. 线程1获取CPU的使用权,使用CAS将值更新为C
    线程一线程二交替执行。第二步到第五步,线程二将值由A更新为B再更新为A,但线程一并没有察觉,因此线程一还是可以继续执行。我们称这种现象为ABA问题。
    解决方法:
    使用版本号 (时间戳),在每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号加一,否则就执行失败。例如AtomicStampedReference就是通过对值加一个戳(stamp)来解决“ABA”问题的。
package concurrence.cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo {
    /**
     * 普通的原子引用包装类
     */
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);

    // 传递两个值,一个是初始值,一个是初始版本号
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);

    public static void main(String[] args) {

        System.out.println("============以下是ABA问题的产生==========");

        new Thread(() -> {
            // 把100 改成 101 然后在改成100,也就是ABA
            atomicReference.compareAndSet(100, 101);
            atomicReference.compareAndSet(101, 100);
        }, "t1").start();

        new Thread(() -> {
            try {
                // 睡眠一秒,保证t1线程,完成了ABA操作
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 把100改成2019
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());

        }, "t2").start();


        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
            e.printStackTrace();
        }



        System.out.println("============以下是ABA问题的解决==========");

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t3一秒钟
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 传入4个值,期望值,更新值,期望版本号,更新版本号
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);

            System.out.println(Thread.currentThread().getName() + "\t 第二次版本号" + atomicStampedReference.getStamp());

            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(),
                    atomicStampedReference.getStamp() + 1);

            System.out.println(Thread.currentThread().getName() + "\t 第三次版本号" + atomicStampedReference.getStamp());

        }, "t3").start();

        new Thread(() -> {

            // 获取版本号
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t 第一次版本号" + stamp);

            // 暂停t4 3秒钟,保证t3线程也进行一次ABA问题
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);

            System.out.println(Thread.currentThread().getName() + "\t 修改成功否:" + result + "\t 当前最新实际版本号:"
                    + atomicStampedReference.getStamp());

            System.out.println(Thread.currentThread().getName() + "\t 当前实际最新值" + atomicStampedReference.getReference());

        }, "t4").start();

    }
}

  • 循环开销过大
    CAS操作不成功的话,会导致一直自旋,CPU的压力会很大。例如说Unsafe下的getAndAddInt方法会一直循环,直到成功才会返回。
  • 只能保证一个变量的原子操作

3.CAS与synchronized之间的区别
回答思路:
什么是CAS(参考上文)
什么是synchronized:

  • 对于普通同步方法,锁是当前实例对象
public class SynchronizedTest {
    // ......
    public synchronized void test1(){
    }
}
  • 对于静态同步方法,锁是当前类的Class对象
public class SynchronizedTest { 
     // ......
    public static synchronized void test2(){
    }
}
  • 对于同步方法块,锁是Synchonized括号里配置的对象
public class SynchronizedTest {   
    // ......
    Object lock = new Object() ;
    public void test3(){
        synchronized (lock){
        }
    }
}

synchronized是重量级锁(悲观锁),依赖于底层操作系统的互斥锁进行实现,线程的阻塞挂起以及获取CPU使用权,需要进行内核态和用户态之间的转换,消耗大量的系统资源;CAS(乐观锁),利用操作系统指令原子执行操作并配合自旋(do…while)实现,大量忙等时间导致CPU的开销也很大。
4.Synchronized的优化
锁的四种状态以及锁升级的过程
在这里插入图片描述


设计模式

一、单例模式创建方式

以下截图来自java专栏公众号

1.恶汉式

2.懒汉式

懒汉式分析:如果两个线程同时进入getInstance()方法,进行了demo2==null的判断,一个线程创建了Demo2的实例对象结束后另一个线程不会再次进行判断,直接又实例化一个对象,造成单例模式的不安全,所以getInstance()方法必须加synchronized关键字,保证线程安全。

3.静态内部类

为什么静态内部类 SingletonClassInstance()方法是线程安全的?
JVM类生命周期概述:加载时机与加载过程
深入理解Java对象的创建过程:类的初始化与实例化

实例对象创建之前必须确保已经进行了初始化,初始化又可以细分为加载、链接、初始化三个阶段。
类的加载:
将 .java文件经过前端编译器转换成.class文件,类加载器将二进制字节流转换成运行时数据区中的对应数据;
类的链接:

  • 验证:验证字节码文件格式等是否正确
  • 准备:类的静态变量分配内存,并设置默认值
  • 解析:符号引用转换为直接引用

类的初始化
初始化阶段是执行类构造器< clinit >()方法的过程。< clinit >()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
虚拟机会保证一个类的类构造器< clinit >()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器< clinit >(),其他线程都需要阻塞等待,直到活动线程执行< clinit >()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行< clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行< clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。
基于 以上分析可知静态内部类中的静态类对象的创建过程实际是在类初始化阶段就已经完成,而初始化阶段执行类构造器方法,JVM会保证类只有一个线程去执行这个类构造器方法,也就是类对象的创建过程只有一个线程能进入并执行,所以静态内部类的单例模式是线程安全的。

4.枚举单例模式

5.doublecheck

public class SingletonDemo{

	private volatile static SingletonDemo instance = null;
	//构造器私有,外部不能new对象
	private SingletonDemo(){}  
    
    public static SingletonDemo getInstance() {
        if(instance == null) {//判断1
            synchronized(SingletonDemo.class){
                if(instance == null){//判断2
                    instance = new SingletonDemo();       
                }
            }
        }
        return instance;
    }
}

双重检测:第一次判断是为了提高程序执行效率,一个线程创建了单例对象,其他线程再进来的时候判断不为null就直接返回;假设两个线程进来之后都进行了第一层判断,接下来创建对象,为了保证单例仅仅加锁是不够的,还需要进行一层判断,因为最开始两个线程都通过了第一层判断,其中一个线程创建对象之后,另一个线程会紧接着创建第二个对象,所以这里会再进行一次判断,如果第一个线程进去之后创建对象完成,第二个线程进入再次判断对象是否存在,存在的话就不需要再次创建。

volatile关键字的作用:线程A进入判断2,执行instance = new SingletonDemo();语句,该语句的执行实际上分为3步:

  • 1-给singleton分配内存;
  • 2-调用 Singleton 的构造函数来初始化成员变量;
  • 3-将给singleton对象指向分配的内存空间(此时singleton才不为null);
    虚拟机指令重排序可能会使得执行顺序变成1-3-2,当程序执行到第二步也就是3的时候,假如此时线程B来到了判断1处,这时由于执行了3,线程B判断singletion!=null,会直接返回,但是直接返回的结果并没有进行初始化,有可能导致空指针异常,instance加了volatile关键字之后,就可以禁止指令重排序,避免出现上面的情况。

多线程

一、锁

1、公平锁和非公平锁

  • 公平锁,是指多个线程按照申请锁的顺序来获取锁,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
  • 非公平锁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
  • ReentrantLock,可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁,下边是ReentrantLock的源码:
/**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

2.可重入锁(递归锁)
可重入锁详解(什么是可重入)

  • 同一个线程在外层函数获取锁之后,内层函数依然可以获取锁,而不会发生死锁
  • ReentrantLock/synchronized就是一个典型的可重入锁,ReentrantLock有多少个lock,对应就有多少个unlock,只加锁而忘记解锁,当其他线程想要获取锁的时候,会发生死锁

3.自旋锁
多个线程竞争同一把锁,会发生线程的阻塞挂起,以及唤醒,重量级锁的设计思路是利用到了操作系统的互斥锁,涉及到CPU用户态以及内核态切换,十分占用资源。当锁是自旋锁的时候,线程竞争锁失败并不会直接挂起,而是通过自旋的方式循环判断竞争的锁是否被其他线程释放,自旋的时候也是占用CPU资源的,自旋时间不宜过长。

package concurrence.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 自旋锁
 */
public class SpinLockDemo {
    // 现在的泛型装的是Thread,原子引用线程
    AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public void myLock() throws InterruptedException {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();


        // 开始自旋,期望值是null,更新值是当前线程,如果是null,则更新为当前线程,否者自旋
        while(!atomicReference.compareAndSet(null, thread)) {
            //摸鱼
            System.out.println(Thread.currentThread().getName()+"  等待...");
            Thread.sleep(1000);
        }
        System.out.println(Thread.currentThread().getName() + "\t come in ");
    }

    public void myUnLock() {
        // 获取当前进来的线程
        Thread thread = Thread.currentThread();

        // 自己用完了后,把atomicReference变成null
        atomicReference.compareAndSet(thread, null);

        System.out.println(Thread.currentThread().getName() + "\t invoked myUnlock()");
    }

    public static void main(String[] args) {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        // 启动t1线程,开始操作
        new Thread(() -> {

            // 开始占有锁
            try {
                spinLockDemo.myLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t1").start();


        // 让main线程暂停1秒,使得t1线程先执行
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 1秒后,启动t2线程,开始占用这个锁
        new Thread(() -> {

            // 开始占有锁
            try {
                spinLockDemo.myLock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 开始释放锁
            spinLockDemo.myUnLock();

        }, "t2").start();
    }
}

4.读写锁
ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。多个线程想要读取某个共享资源类,可以同时进行,但是,一旦一个线程想要对其进行修改,其他线程都不能都其进行读或者写。可以读读,读写,写读,写写都是互斥的。

二、JUC常用辅助类

1.CountDownLatch

import java.util.concurrent.CountDownLatch;

public class CountDownLatchDemo {
    public static void main(String[] args) throws InterruptedException {

        // 计数器
        CountDownLatch countDownLatch = new CountDownLatch(6);

        for (int i = 0; i <= 6; i++) {
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 上完自习,离开教室");
                countDownLatch.countDown();
            }, String.valueOf(i)).start();
        }

        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "\t 班长最后关门");
    }
}

CountDownLatch有两个重要方法,await()以及countDown(),调用await()方法的线程被阻塞,调用countDown()方法的线程倒计时直到数值==0时,另一个阻塞线程被唤醒。

2.CyclicBarrier

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class SummonTheDragonDemo {
    public static void main(String[] args) {
        /**
         * 定义一个循环屏障,参数1:需要累加的值,参数2 需要执行的方法
         */
        CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> {
            System.out.println("召唤神龙");
        });

        for (int i = 1; i <= 7; i++) {
            final Integer tempInt = i;
            new Thread(() -> {
                System.out.println(Thread.currentThread().getName() + "\t 收集到 第" + tempInt + "颗龙珠");

                try {
                    // 先到的被阻塞,等全部线程完成后,才能执行方法
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, String.valueOf(i)).start();
        }
    }
}

CyclicBarrier,调用cyclicBarrier.await()方法的线程使得计数加1,如果加1之后的值不等于构造方法中的第一个参数7,则该线程阻塞,其他线程继续调用await()方法,直到计数器的值到达7,则执行构造函数中的第二个参数(Runnable接口中的run方法),计数器可reset循环使用。
CyclicBarrier和CountDownLatch的区别

3.Semaphore

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {

        /**
         * 初始化一个信号量为3,默认是false 非公平锁, 模拟3个停车位
         */
        Semaphore semaphore = new Semaphore(3, false);

        // 模拟6部车
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                try {
                    // 代表一辆车,已经占用了该车位
                    semaphore.acquire(); // 抢占

                    System.out.println(Thread.currentThread().getName() + "\t 抢到车位");

                    // 每个车停3秒
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + "\t 离开车位");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 释放停车位
                    semaphore.release();
                }
            }, String.valueOf(i)).start();
        }
    }
}

6个车抢占3个车位,信号量semaphore创建的时候就定义好共享资源的数量,semaphore.acquire() 获取共享资源,如果被占用,则阻塞,直到其他线程释放资源,信号量+1,等待的线程会被唤醒。
作用:

  • 多个共享资源互斥的使用(多个共享资源,每个资源每次只能被一个线程占用)
  • 并发限流,控制最大的线程数
三、阻塞队列

JAVA中阻塞队列的类别和区别
在多线程的情况下,阻塞队列BlockingQueue可以帮助我们决定什么时候阻塞线程,什么时候唤醒线程。

包含四种插入移除方式

  • 抛出异常(add/remove)
  • 返回特殊值(offer/poll)
  • 阻塞(put/take)
  • 超时退出(offer/poll+time)

七种阻塞队列

  • ArrayBlockingQueue:由数组结构组成的有界阻塞队列。
  • LinkedBlockingQueue:由链表结构组成的有界(但大小默认
  • 值为Integer.MAX_VALUE)阻塞队列。
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列。
  • DelayQueue:使用优先级队列实现妁延迟无界阻塞队列。
  • SynchronousQueue:不存储元素的阻塞队列。
  • LinkedTransferQueue:由链表结构绒成的无界阻塞队列。
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列。
四、生产者消费者问题

在这里插入图片描述
Synchronized和Lock有什么区别

  • Synchronized 是java的关键字;Lock是接口
  • Synchronized 不需要手动释放锁;Lock需要手动释放锁,否则会造成死锁
  • Synchronized 可重入,非公平,不可中断;Lock 可重入,可设置公平/非公平,可以中断
  • Synchronized 要么随机唤醒线程,要么全部唤醒;Lock,可以全部唤醒,也可以精准唤醒,通过定义多个Condition实现
  • Synchronized 适合锁少量的代码同步问题, Lock 适合锁大量的同步代码
  • Synchronized ,线程1获得锁,线程2阻塞,一直等待;Lock,线程1获得锁,线程2可以通过tryLock()判断锁的状态,或者加上时间限制,在一段时间内等待拿锁,如果不成功,直接退出
    Lock锁——tryLock()方法

集合

一、集合类不安全解决方法

1、List集合不安全

  • Vector(老的类,不推荐使用)
  • Collections.synchronizedList()
  • CopyOnWriteArrayList(读写分离,读不加锁,写加锁,写时复制)

2、Set集合不安全

  • Collections.synchronizedSet(new HashSet<>())
  • CopyOnWriteArraySet<>()(推荐)

3、Map集合不安全

  • HashTable(不推荐)
  • Collections.synchronizedMap(new HashMap<>())
  • ConcurrencyMap<>()(推荐)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值