春招第一笔
题目分布
共15个选择题、6个问答、3个编程
题目
问答Q1:
首先,这三个特性都是线程安全的三个提现。 其次,原子性就是线程之间互斥访问,同一时间,只能有一个线程在进行操作,比如锁、synchronized、CAS算法; 可见性就是如果同一内存中一个线程做出了修改操作,那么对于其他线程来说也都是同步可见的; 有序性就是Hapens-before算法,即我们不需要关注顺序,因为 JVM 内部会进行顺序打乱,所以无需关注顺序。
问答Q2:
BIO(Blocking IO):同步阻塞IO NIO(NonBlockingIO):同步不阻塞IO AIO(asynchronousIO):异步不阻塞IO
BIO就相当于一个链接一个线程,发起请求会一直阻塞,可以通过连接池来改善; NIO就相当于多个连接复用一个线程,一个请求一个线程(我的理解是BIO+连接池); AIO就是一个有效请求一个线程,IO发起请求,立马响应,操作结束后,异步回调通知结果。
问答Q3:
当空间不足的时候会触发。 1,旧生代空间不足 2,Minor GC 空间不足,低于旧生代空间内存 3,perm gen空间存放 class 信息满
问答Q4:
显而易见,此表中并没有合适的字段作为索引。因为性别、省份区分度不大,不适合索引,然而身份证号长度太长,作为索引效率不高,没有必要。
我的索引优化思路: 新增一个字段,比如可以截取身份证后几位,或者通过一些哈希算法对idcard进行运算,得到一个新字段,然后把这个不易重复的字段作为唯一索引。
问答Q5:
隔离级别: ISOLATION_DEFAULT默认隔离级别 ISOLATION_READ_UNCOMMITED修改时可读(出现幻读) ISOLATION_READ_COMMITED修改后才可读(避免幻读) ISOLATION_REPEATABLE_READ防止脏读 ISOLATION_SERIALIZABLE(完美隔离级别)
事务传播行为: 默认为propagation.REQUIRED,如果当前存在事务,就加入,不存在就创建
问答Q6:
自动装箱与自动拆箱是 Java 的语法糖之一。 简单来说,自动装箱就是将 Java 的基本数据类型转换为对应的对象,比如 int 转换为 Integer,float 转换为 Float;反之,自动拆箱就是讲对应类型的对象重新转换为基本类型。 其中,通过我的研究,发现反编译之后的代码,自动装箱的原理是这样的: int i = 5; Integer n = Integer.valueOf(i); 自动拆箱原理: Integer i = 5; int n = i.intValue();
编程Q1:
几种常见的单例模式:
// 懒汉模式————先不创建单例,用的时候再创建
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
// 饿汉模式————立即创建单例,因此线程安全
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
public class Singleton {
private static Singleton instance = null;
static {
instance = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return this.instance;
}
}
// 静态内部类
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTAMCE;
}
}
// 枚举
public enum Singleton {
INSTANCE;
public void whateverMethod() {
···
}
}
简单实现一下单例模式的调用:
package eqianbao;
/**
* 模拟一下单例模式的调用(以懒汉模式为例)
* @Author <a href="https://github.com/wl2o2o">程序员CSGUIDER</a>
* @From <a href="https://wl2o2o.github.io">CSGUIDER博客</a>
* @CreateTime 2024/3/10
*/
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy(){}
// 注意看这里用了同步锁
public static synchronized SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
public void say() {
System.out.println("i'm lazyPeople!");
}
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
instance.say();
}
}
懒汉单例模式其实是线程不安全的,因为懒汉模式没有事先创建单例,所以我在创建单例的时候加上了同步锁,通过同步锁的同步互斥访问来达到线程安全的目的。更常用的方法还有双重检查锁(Double-Check-Lock),代码如下:
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy(){}
public static SingletonLazy getInstance() {
// 双重校验锁
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
}
如果我放任不管呢?是如何线程不安全的?诶我就不加锁,就是玩!就是玩!就是玩!show me your code!
public class SingletonLazy {
private static SingletonLazy instance;
private SingletonLazy(){}
// 注意看!这里没有!!!同步锁!
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
public void say() {
System.out.println("i'm lazyPeople!就是玩!");
}
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
instance.say();
}
}
最终经过多种情况尝试,发现不加同步锁的懒汉单例模式并没有创建多个单例,可能因为现代 JVM 的内存模型、内部优化 和 CPU 之间的通信方式,通过短时间循环和简单线程池模拟并不能体现出创建了多个单例,线程调度的不确定性仍然存在。
所以,我们需要了解这句话:非线程安全单例模式在某些情况下可能导致多个实例。因此总结一下,没有同步锁,不一定不安全;有同步锁,一定安全。
下面的代码是我想复现一下懒汉单例模式如果不加同步锁的情况下,多线程会创建多个instance
,这样就违背单例的概念了,但是模拟代码均未实现创建多例的情况,具体原因如上述描述↑
模拟实现不加同步锁,会创建多例的情况代码:
// 创建多个线程
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
UnsafeLazySingleton instance = UnsafeLazySingleton.getInstance();
instance.say();
System.out.println(instance.hashCode());
}).start();
}
}
// 增加循环次数,并让线程休息一会,模拟更加真实的场景
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 900; i++) {
new Thread(() -> {
UnsafeLazySingleton instance = UnsafeLazySingleton.getInstance();
instance.say();
System.out.println(instance.hashCode());
}).start();
Thread.sleep(1000);
}
}
// 上述发现进程执行的太快,睡一睡发现还是获取的单例哈希值,继续优化,加上了 thread.join()
public static void main(String[] args) throws InterruptedException {
final int LOOP_COUNT = 100; // 设置循环次数
for (int i = 0; i < LOOP_COUNT; i++) {
Thread thread = new Thread(() -> {
UnsafeLazySingleton instance = UnsafeLazySingleton.getInstance();
instance.say();
System.out.println("Thread ID: " + Thread.currentThread().getId());
System.out.println("Hash Code: " + instance.hashCode());
});
thread.start();
thread.join(); // 等待每个线程执行完毕,确保输出顺序清晰
}
}
// 不行还是不行,使用线程池试一下
public static void main(String[] args) throws InterruptedException, ExecutionException {
final int LOOP_COUNT = 1000; // 设置循环次数
ExecutorService executor = Executors.newFixedThreadPool(10); // 创建一个固定大小的线程池
// Future<Integer>:executor.submit()通过 lambda 表达式提交 instance 的哈希值,存放于 Future<Integer> 中
List<Future<Integer>> futures = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < LOOP_COUNT; i++) {
Future<Integer> future = executor.submit(() -> {
UnsafeLazySingleton instance = UnsafeLazySingleton.getInstance();
instance.say();
return instance.hashCode();
});
futures.add(future);
// 在这里添加一些随机延时以增加并发冲突的可能性
Thread.sleep(random.nextInt(10)); // 随机等待时间(单位:毫秒)
}
// 遍历取出哈希值
for (Future<Integer> future : futures) {
System.out.println("Hash Code: " + future.get());
}
// 关闭线程池
executor.shutdown();
}
等等,绝对不可能,今天的我发现了昨天很傻的我,我发现了问题所在!用
STAR
原则来分析一下:
- Situation(情景):懒汉单例模式不是线程安全的
- Task(任务):模拟实现(证明)线程不安全
- Action(行动):通过拆分分析代码,只有当多个线程同时发现当前实例为空时,才会
new
新的实例,因此增加线程冲突率,减少循环次数(多次循环会堵塞) - Result(结果):去掉线程间睡眠时间、减少为两个循环,以此增加碰撞率,提高了创建多例可能性
package singletondesignpattern;
/**
* @Author <a href="https://github.com/wl2o2o">程序员CSGUIDER</a>
* @From <a href="https://wl2o2o.github.io">CSGUIDER博客</a>
* @CreateTime 2024/3/12
*/
public class NonThreadSafeSingleton {
private static NonThreadSafeSingleton instance;
private NonThreadSafeSingleton() {}
// 注意:这里没有加锁,所以是线程不安全的
public static NonThreadSafeSingleton getInstance() {
if (instance == null) {
// 在多线程环境下,两个线程可能同时发现 instance 为 null,并各自创建一个实例
instance = new NonThreadSafeSingleton();
}
return instance;
}
@Override
public String toString() {
return "NonThreadSafeSingleton@" + Integer.toHexString(hashCode());
}
public static void main(String[] args) {
// 创建两个并发线程来获取单例
for (int i = 0; i < 2; i++) {
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + ": " + NonThreadSafeSingleton.getInstance());
}).start();
}
}
}
模拟成功截图:
模拟实现加同步锁,一定安全代码:
按照上述方法加上synchronized
同步锁关键字,或者使用双重校验锁,也或者可以使用ConcurrentHashMap
,都可以解决线程安全问题。
以ConcurrentHashMap
为例:
public class ConcurrentSingleton {
private static ConcurrentHashMap<String, ConcurrentSingleton> map = new ConcurrentHashMap<>();
private static final String KEY = "key";
private ConcurrentSingleton() {}
public static ConcurrentSingleton getInstance() {
return map.computeIfAbsent(KEY, k -> {
ConcurrentSingleton instance = new ConcurrentSingleton();
return instance;
});
}
@Override
public String toString() {
return "ConcurrentSingleton@" + Integer.toHexString(hashCode());
}
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
System.out.println("Thread " + Thread.currentThread().getName() + ": " + ConcurrentSingleton.getInstance());
}).start();
}
}
}
实现线程安全的单例模式还有很多种方式,比如:
- 使用饿汉式单例模式,
- 使用静态内部类,
- 使用双重校验锁,
- 使用枚举类,
- 使用
synchronized
关键字, - 使用
ConcurrentHashMap
, - 使用
ThreadLocal
, - 使用
volatile
关键字, - 使用
AtomicReference
, - 使用
Unsafe
, - 使用
JUC
的AtomicReference
, - 使用
JUC
的AtomicStampedReference
, - 使用
JUC
的AtomicMarkableReference
, - 使用
JUC
的AtomicReferenceArray
,
总结
总之,实现相对线程安全的单例模式方法有很多,但是,无论怎么实现,都必须保证只有一个实例,而且必须是线程安全的。
其实,为了保证线程安全,很多方法,都要写很多代码,代码极其臃肿。那么,有没有一种方法,可以保证线程安全,又可以保证代码精简呢?
其实,最简单的单例模式实现方式其实是枚举··· ···