java面试题每日一背(1)-并发、redis

java面试题每日一背(1)

并发

1.线程池核心参数?

核心线程数:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁。
最大线程数:当前线程数达到核心线程数后,如果继续有任务被提交到线程池,会将任务缓存到工作队列中。如果队列也已满,则会去创建一个新线程来处理这个任务。
空闲线程存活时间:一个线程如果处于空闲状态,并且当前的线程数量大于核心线程数,那么在指定时间后,这个空闲线程会被销毁。
空闲线程存活时间单位:空闲线程存活时间的单位。
工作队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。
- 数据结构有:
* 基于数组的有界队列(FIFO)
* 基于链表的无界队列(FIFO,可以设置容量,如果不设置的话最大长度是Integer.MAX_VALUE,newFixedThreadPool)
* 优先级无界队列(参数设定)
* 不缓存任务的阻塞队列(不存储元素的无界队列,每个插入操作必须等到另一个线程调用移除操作,适用于消费者充足,newCachedThreadPool)
* 延迟队列(根据指定执行时间从小到大排序,否则根据插入到队列的先后排序)
拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来处理的策略。
- 四种:
* 调用线程直接处理;
* 丢弃并抛出异常(默认);
* 丢弃且什么都不做;
* 工作队列第一个任务丢弃,新任务放队尾。
执行原理(顺序):核心线程数→工作队列→最大线程数
问题深入:
1). 核心线程是创建了线程池之后就有的吗?
不是,核心线程是根据线程池的需求动态创建的,‌而不是在创建线程池时立即全部创建。‌
2). 核心线程数可以设置为0吗?
可以。当核心线程数为 0 且有任务被提交时,会先将任务添加到任务队列,同时也会判断当前工作的线程数是否为 0,如果为 0,则会创建线程来执行线程池的任务。
3). 非核心线程是如何被回收的?
‌如果某个非核心线程在一段时间内没有执行任何任务,‌那么线程池会判断这个线程是空闲的,‌然后通过空闲线程存活时间和空闲线程存活时间单位这两个参数将其回收。‌

2.线程池状态

RUNNING:线程池的初始状态,当线程池被创建后,即处于该状态,此时线程池可以接收新的任务并且处理队列中的任务。
SHUTDOWN:当调用线程池的shutdown()方法时,线程池进入此状态,处于该状态的线程池不再接受新的任务,但会继续处理队列中已有的任务。
STOP:当调用线程池的shutdownNow()方法时,线程池进入此状态,在该状态下,线程池不接受新的任务,也不处理队列中的任务,并且会尝试中断正在执行的任务。
TIDYING:当线程池处于SHUTDOWN或STOP状态,并且所有任务都已终止,即没有正在执行的任务时,线程池进入TIDYING状态,此时会执行terminated()方法,该方法是空实现,可以由用户重写以执行相应的清理操作。
TERMINATED:线程池彻底终止的状态,当线程池处于TIDYING状态并执行完terminated()方法后,线程池进入TERMINATED状态。
问题深入:
1). 一个运行的线程如何中止?
* 使用退出标志:‌通过设置一个boolean类型的退出标志,‌当线程的run方法中的循环条件检查到该标志为true时,‌线程将退出。‌这种方法允许线程在完成其任务后自然退出,‌是一种推荐的方式。
* 使用interrupt方法:‌通过调用线程的interrupt方法,‌可以设置线程的中断状态为true,‌但这并不会立即停止线程。‌线程需要自己检查这个中断状态,‌并在适当的时候响应中断。‌这是一种协作机制,‌允许线程在接收到中断请求后,‌在安全的地方停止执行。
* 使用stop方法:‌可以使用Thread类的‌stop方法会立即停止线程,‌无论它是否正在执行关键任务,但这种方法不推荐使用,‌因为它可能会导致资源泄露和其他不可预见的后果。

3.synchronized和reentrantLock的区别。

synchronized是java内置特性,而ReentrantLock是通过java代码实现的(基于AQS实现)。
synchronized可以自动获取/释放锁;而ReentrantLock需要手动加解锁。
synchronized是非公平锁,而ReentrantLock可以实现公平锁和非公平锁。
问题深入:
1). ReentrantLock如何实现可重入的?
ReentrantLock加锁的时候,看下当前持有锁的线程和当前请求的线程是否是同一个,一样就可重入。只需要简单的将state值加1,记录当前线程的重入次数。在锁释放的时候,需要确保state=0才能执行释放资源的动作。
2). synchronized修饰普通方法和修饰静态方法锁的是什么?
普通方法锁当前对象,静态方法锁当前类。
3). synchronized原理?
同步方法:同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置(没有设置就是类文件出错),则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。
同步代码块:同步代码块使用 monitorenter 和 monitorexit 两个指令实现。 可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为 0 的时候。锁将被释放,其他线程便可以获得锁。
无论是同步方法还是同步代码块,其实现其实都要依赖对象的监视器(Monitor)。
4). reentrantLock原理
在AQS内部维护了一个FIFO队列和一个volatile的int类型的state变量,在state=1时表示当前对象锁已经被占有了,state的值的修改通过CAS来完成。
FIFO队列用来实现多线程的排队工作,当线程加锁失败时,该线程会被封装成一个node节点来置于队列尾部。当持有锁的线程释放锁时,AQS会将等待队列中的第一个线程唤醒,并让其重新尝试获取锁。
AQS使用一个volatile的int类型的成员变量来表示同步状态,在state=1的时候表示当前对象锁已经被占有了。

4.如何实现单例模式

单例模式:它保证一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景下都非常有用,例如配置管理、日志记录等。

  • 饿汉式:最简单的实现方式,它在类加载时就创建了一个实例。这种方式的优点是线程安全,因为实例在类加载时就已经被创建,不可能存在多个实例。但是,这种方式的缺点是如果这个实例在程序运行过程中没有被使用到,那么它会一直占用内存空间。
public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
  • 懒汉式:在第一次调用 getInstance() 方法时才创建实例。这种方式的优点是节省了内存空间,因为只有在需要时才会创建实例。但是,这种方式的缺点是线程不安全,可能会创建多个实例。
//双重检查锁
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
//静态内部类
//静态内部类的方式是在内部类中创建实例,这样可以实现懒加载,同时保证了线程安全。这种方式的优点是避免了同步方法的性能问题。
public class Singleton {
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
//枚举类
public class MySingleton {}
}
public enum MySingletonEnum {
    SINGLETON;
    private MySingleton mySingleton = null;
    private MySingletonEnum() {
        mySingleton = new MySingleton();
    }
    public MySingleton getMySingleton() {
        return mySingleton;
    }
}

扩展:JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

  • 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:
    * new一个关键字或者一个实例化对象时;
    * 读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外);
    * 调用一个类的静态方法时;
    * 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化;
    * 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化;
    * 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
5.如何使用redis?

缓存、分布式锁
问题深入:

1. 如何保证缓存、数据库的数据一致?(参考link
方案1:先更新数据库数据,再删除缓存数据

存在的问题:
1)删除缓存失败存在脏数据
2)难以收拢所有更新数据库入口
使用同步删除方案,你必须在所有更新数据库的地方都进行缓存的删除操作,如果你有一个地方漏掉了,对应的缓存就相当于没有删除了,就会导致脏数据问题。
还有就是如果我们通过命令行直接来更新数据库的数据,或者通过公司提供的数据库管理平台来更新数据库数据,这个时候你就没法删除了,因为你的同步删除其实只是写在你的代码里面,这个时候也就导致脏数据问题了。
3)并发场景下存在脏数据

方案2:延迟双删:先删除缓存数据,更新数据库数据,等待一小段时间,再次删除缓存数据。

存在的问题:
1)延迟时间难以确认
到底是延迟一秒或者是几秒,这个其实很难确认,且在这期间查询时会无法命中缓存而去查数据库,如果有同一个key大量请求过来会发生缓存击穿,大量请求访问到数据库。
2)无法绝对保障数据的一致性(主从数据库情况下,主库还没同步到从库,其他线程读取到的还是脏数据,放入缓存中,缓存中也会有脏数据)

方案3:先更新数据库,数据库更新完发送一个异步消息,监听者接收到消息之后再异步的把缓存中的数据删除,或者借助binlog,订阅到数据库变更后异步清除缓存(不断重试删除)
更完美的方案:先删除缓存,再更新数据库,最近监听binlog删除缓存。
2. redis分布式锁如何使用?解锁如何判断是加锁的线程?

加锁:setnx
解锁:删除key
加锁时value的值保存当前线程ID,解锁时判断当前线程是否为value中的线程,是的话再解锁。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值