JAVA基础篇面试题
文章目录
1. 什么是JMM
JMM(Java Memory Model)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规定定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式;
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回在主内存;
- 线程加锁前,必须读取主内存的最新值到自己的工作内存;
- 加锁解锁是同一把锁;
2. 介绍一下violated
定义:是java虚拟机提供的轻量级的同步机制;
特征:
-
保证可见性;
JVM保证每次写后会立即同步回主内存;
-
不保证原子性;
-
禁止指令重排序;
JVM保证在violate变量写入后的代码肯定不会出现在写入前执行; 有volatile修饰的变量赋值后,字节码多了一个
lock add$0x0, (%esp)
(空操作),该操作相当于内存屏障,重排序时不能将后面的指令重排序到内存屏障中前的位置。多的指令由于IA32规范中规定lock前缀不允许使用nop指令,该指令的作用是将本处理器缓存写入内存,该动作引起别的处理器或者别的内核无效化其缓存,这种操作相当于对缓存中的变量做了一次类似store和write操作,可以使volatile变量的修改对其他处理器立即可见。
3. 写一个单例模式
DCL双端检锁机制不一定线程安全,可能出现指令重排序,加入violate可以避免重排序。例如单例下new一个对象需要进行以下3步:
-
分配对象内存空间;
-
初始化对象;
- 将引用指向内存地址;
步骤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的问题
-
长时间不成功,会造成自旋导致CPU带来很大的开销;
-
只能保证一个共享变量的操作;
-
出现ABA问题;
如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,不能说明它的值没有被其他线程改变过。因为可能他在这段期间被改为了B后来又改回A。如果遇到此问题,使用互斥同步来处理或者
AtomicStampedReference
原子更新引用;
6. ArrayList线程不安全的替换方案
-
使用Vector
-
使用集合类方法
Collections.synchronizedList()
1.SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类;
2.使用SynchronizedList的时候,进行遍历时要手动进行同步处;
3.SynchronizedList可以指定锁定的对象;
-
使用CopyOnWriteList,写时复制容器;
在往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先加锁将数组复制一份(len+1),往新的数组中增加一个,然后再将原容器引用指向新容器,解锁。这样做的好处是可以并发的读,读写可以分离的思想,读和写是对不同的数组进行操作。
7. 什么是公平锁
概念:在并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁;
公平锁:在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果空,或者是队列首个就占有锁,否则就加入等待队列中,按照FIFO的规则获取锁;
非公平锁:先抢先得,否则就排队等待。 优点吞吐量大。synchronized
也是非公平的。
8. 什么是可重入锁
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。加几次锁需要释放几次锁,否则死锁。synchronized, ReentrantLock都是可重入锁。
synchronized
:java中最基本的互斥同步手段。javac
编译后,会在同步块前后形成monitorenter
和monitorexit
这两个字节码指令;如果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:由链表结构组成的双向阻塞队列
核心方法:
抛出异常 | 当阻塞队列满时:在往队列中add插入元素会抛出 IIIegalStateException:Queue full 当阻塞队列空时:再往队列中remove移除元素,会抛出NoSuchException |
---|---|
特殊性 | 插入方法,成功true,失败false 移除方法:成功返回出队列元素,队列没有就返回空 |
一直阻塞 | 当阻塞队列满时,生产者继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出, 当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用。 |
超时退出 | 当阻塞队列满时,队里会阻塞生产者线程一定时间,超过限时后生产者线程会退出 |