JAVA面试题|JAVA锁相关面试题总结(一)

JAVA基础篇面试题


1. 什么是JMM

JMM(Java Memory Model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规定定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式;

JMM关于同步的规定:

  1. 线程解锁前,必须把共享变量的值刷新回在主内存;
  2. 线程加锁前,必须读取主内存的最新值到自己的工作内存;
  3. 加锁解锁是同一把锁;

2. 介绍一下violated

定义:是java虚拟机提供的轻量级的同步机制;

特征:

  1. 保证可见性;

    JVM保证每次写后会立即同步回主内存;

  2. 不保证原子性;

  3. 禁止指令重排序;

    JVM保证在violate变量写入后的代码肯定不会出现在写入前执行; 有volatile修饰的变量赋值后,字节码多了一个lock add$0x0, (%esp)(空操作),该操作相当于内存屏障,重排序时不能将后面的指令重排序到内存屏障中前的位置。多的指令由于IA32规范中规定lock前缀不允许使用nop指令,该指令的作用是将本处理器缓存写入内存,该动作引起别的处理器或者别的内核无效化其缓存,这种操作相当于对缓存中的变量做了一次类似store和write操作,可以使volatile变量的修改对其他处理器立即可见。

3. 写一个单例模式

DCL双端检锁机制不一定线程安全,可能出现指令重排序,加入violate可以避免重排序。例如单例下new一个对象需要进行以下3步:

  1. 分配对象内存空间;

  2. 初始化对象;

    1. 将引用指向内存地址;

    步骤2和步骤3不存在依赖关系,可以重排序;

    因此DCL应该如此实现:

    class SingletonDemo {
        private static violate SingletonDemo instance = null;
        public static SingletonDemo getInstance() {
            if (instance == null) {
                synchronized(SingletonDemo.class) {
                    if (instance == null) {
                        instance = new SingletonDemo();
                    }
                }
            }
            return instance;
        }
    }
    

4. 介绍一下CAS

概念:compare and set。需要3个操作数,分别是内存位置V,旧的预期值A,准备设置的新值B。CAS执行时,当地址V对应的旧值是A时,处理器才会将V对应的值更新为B,否则就不执行更新。该操作为原子操作,不会被其他线程中断;

Java的实现:引入Unsafe类,其通过本地native方法直接操作特定的内存数据。通过对内存的偏移地址去获取值和循环修改数据直至成功。JVM会编译成CAS的字节码指令,通过硬件功能保证指令执行过程中是连续的,原子性的。

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // 获取对象值的内存偏移量
    private static final long valueOffset; 
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 此处要设置为violatile
    private volatile int value;
    // ......
}

5. CAS的问题

  1. 长时间不成功,会造成自旋导致CPU带来很大的开销;

  2. 只能保证一个共享变量的操作;

  3. 出现ABA问题;

    如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,不能说明它的值没有被其他线程改变过。因为可能他在这段期间被改为了B后来又改回A。如果遇到此问题,使用互斥同步来处理或者AtomicStampedReference原子更新引用;

6. ArrayList线程不安全的替换方案

  1. 使用Vector

  2. 使用集合类方法Collections.synchronizedList()

    1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类;

    2.使用SynchronizedList的时候,进行遍历时要手动进行同步处;

    3.SynchronizedList可以指定锁定的对象;

  3. 使用CopyOnWriteList,写时复制容器;

    在往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先加锁将数组复制一份(len+1),往新的数组中增加一个,然后再将原容器引用指向新容器,解锁。这样做的好处是可以并发的读,读写可以分离的思想,读和写是对不同的数组进行操作。

7. 什么是公平锁

概念:在并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁;

公平锁:在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果空,或者是队列首个就占有锁,否则就加入等待队列中,按照FIFO的规则获取锁;

非公平锁:先抢先得,否则就排队等待。 优点吞吐量大。synchronized也是非公平的。

8. 什么是可重入锁

指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。加几次锁需要释放几次锁,否则死锁。synchronized, ReentrantLock都是可重入锁。

synchronized:java中最基本的互斥同步手段。javac编译后,会在同步块前后形成monitorentermonitorexit这两个字节码指令;如果synchronized指明了对象参数,那就锁定这个对象;如果未指定对象参数,则根据其修饰的方法类型(实例方法,或类方法)来决定是取代码所在的对象实例还是取类型对应的Class对象来作为线程要持有的锁。

具体的执行过程需要看[java基础知识-java内部的锁]文章,简述一下加锁解锁原理:

实现:每个锁对象头有一个锁的计数器,和一个指向持有锁的线程的指针;

原理:目标锁对象的计数器为0时,线程monitorenter将其占有,并计数器+1,每次加锁(重入时)计数器+1,当monitorexit退出时,计数器-1。当计数器为0时,表示锁已释放;

9. 什么是自旋锁

是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

10. 什么是独占/共享/互斥锁

多个线程同时读一个资源类没有任何问题,所以为了满足开发量,读取共享资源应该可以同时进行,但是,如果有一个线程写共享资源,其他线程就无法读写。

class Cache {
    private volatile Map<String, Object> map = new HashMap<>();

    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public Object get(String key) {
        lock.readLock().lock();
        System.out.println(String.format("线程%s 读取开始", Thread.currentThread().getName()));
        try {
            Object o = map.get(key);
            return o;
        }finally {
            System.out.println(String.format("线程%s 读取结束", Thread.currentThread().getName()));
            lock.readLock().unlock();
        }
    }

    public void set(String key, Object v) {
        lock.writeLock().lock();
        System.out.println(String.format("线程%s 写开始", Thread.currentThread().getName()));
        try {
            map.put(key, v);
        } finally {
            System.out.println(String.format("线程%s 写结束", Thread.currentThread().getName()));
            lock.writeLock().unlock();
        }
    }
}

11. CountDownLatch,CyclicBarrier,Semaphore

CountDownLatch递减,直到为0才不阻塞;

CyclicBarrier递增,直到预期值才不阻塞;

Semaphore信号量,同时可进入临界区线程数量;

Semaphore s = new Semaphore(3);// 同时允许三个线程访问
s.acquire();
s.release();

12. 什么是阻塞队列

特征:BlockingQueue 阻塞队列,当阻塞队列是空时,从队列中获取元素的操作将会被阻塞,当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。

优势:在多线程环境下,我们必须自己取控制这些资源管理的细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度。

实现类:

  • ArrayBlockQueue:由数组结构组成的有界阻塞队列
  • LinkedBlockingQueue:由链表结构组成的有界(但是默认大小 Integer.MAX_VALUE)的阻塞队列
    • 有界,但是界限非常大,相当于无界,可以当成无界
  • PriorityBlockQueue:支持优先级排序的无界阻塞队列
  • DelayQueue:使用优先级队列实现的延迟无界阻塞队列
  • SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列
    • 生产一个,消费一个,不存储元素,不消费不生产
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列
  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

核心方法:

JAVA面试题|JAVA锁相关面试题总结(一)

抛出异常 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException
特殊性 插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空
一直阻塞 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出, 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。
超时退出 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出
版权声明:程序员胖胖胖虎阿 发表于 2022年11月22日 下午8:40。
转载请注明:JAVA面试题|JAVA锁相关面试题总结(一) | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...