春招第一笔

题目分布

共15个选择题、6个问答、3个编程

image-20240310135821403

题目

问答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();
        }
    }
}

模拟成功截图: e签宝24春招笔试-2024-03-13-01-23-10

模拟实现加同步锁,一定安全代码:

按照上述方法加上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
  • 使用JUCAtomicReference
  • 使用JUCAtomicStampedReference
  • 使用JUCAtomicMarkableReference
  • 使用JUCAtomicReferenceArray

总结

总之,实现相对线程安全的单例模式方法有很多,但是,无论怎么实现,都必须保证只有一个实例,而且必须是线程安全的。

其实,为了保证线程安全,很多方法,都要写很多代码,代码极其臃肿。那么,有没有一种方法,可以保证线程安全,又可以保证代码精简呢?

其实,最简单的单例模式实现方式其实是枚举··· ···