重排序是什么?如何避免?
程序执行的顺序按照代码的先后顺序执行。但是处理器为了提高程序运行效率,可能会对输入代码进行优化,进行重新排序(重排序),它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
排序对单线程运行是不会有任何问题,但是多线程就不一定了。
重排序有哪些分类?如何避免?
- 编译器重排序:对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
- CPU指令重排序:在指令级别,让没有依赖关系的多条指令并行。
- CPU内存重排序:CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。
避免:volatile
volatile关键字的作用
保证可见性和禁止指令重排,当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新值。
举个例子:
X=0, 线程A x=1,但此时X=1还在自己的Store Buffer里面,没有及时写入主内存中。所以,线程2看到的X还是0。
volatile 变量和 atomic 变量有什么不同?
-
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
-
而 AtomicInteger 类提供的 atomic方法可以让这种操作具有原子性如getAndIncrement()方法会原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。
说说自己是怎么使用 synchronized 关键字?
synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。
-
修饰实例方法:当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
-
修饰静态方法:那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class。
-
修饰代码块: 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
Java 中新的Lock接口相对于同步代码块(synchronized block)有什么优势?
Lock是接口有很多实现,我只用过ReentrantLock
ReentrantLock就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
主要区别:
- ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
- ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
- ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
- 二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步方法块,锁是括号里面的对象
如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
Lock接口的最大优势是它为读和写提供两个单独的锁(ReentrantReadWriteLock),ReentrantReadWriteLock的特点是:“读读共享”,“读写互斥”,“写写互斥”。
高性能缓存简易示例
public class ReadWriteMap {
private final Map<Object, Object> map;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public ReadWriteMap(Map<Object, Object> map) {
this.map = map;
}
public Object put(Object key, Object value) {
try {
writeLock.lock();
return map.put(key, value);
} finally {
writeLock.unlock();
}
}
public Object get(Object key) {
try {
readLock.lock();
return map.get(key);
} finally {
writeLock.unlock();
}
}
}
如何在Java中实现一个阻塞队列
生产者−消费者模型
- 内存队列本身要加锁,才能实现线程安全。
- 阻塞。当内存队列满了,生产者放不进去时,会被阻塞;当内存队列是空的时候,消费者无事可做,会被阻塞。
- 双向通知。消费者被阻塞之后,生产者放入新数据,要notify()消费者;反之,生产者被阻塞之后,消费者消费了数据,要notify()生产者。
1.如何阻塞?
- 办法1:线程自己阻塞自己,也就是生产者、消费者线程各自调用wait()和notify()。
- 办法2:用一个阻塞队列,当取不到或者放不进去数据的时候,入队/出队函数本身就是阻塞的。
2.如何双向通知?
- 办法1:wait()与notify()机制。
- 办法2:Condition机制。
生产者在通知消费者的同时,也通知了其他的生产者;消费者在通知生产者的同时,也通知了其他 消费者。原因在于wait()和notify()所作用的对象和synchronized所作用的对象是同一个,只能有一个对 象,无法区分队列空和列队满两个条件。这正是Condition要解决的问题。
直接使用BlockingQueue也可以实现一个阻塞队列
写一段死锁代码。说说你在Java中如何解决死锁。
public class Deadlock {
public static String str1 = "str1";
public static String str2 = "str2";
public static void main(String[] args){
Thread a = new Thread(() -> {
try{
while(true){
synchronized(Deadlock.str1){
System.out.println(Thread.currentThread().getName()+"锁住 str1");
Thread.sleep(1000);
synchronized(Deadlock.str2){
System.out.println(Thread.currentThread().getName()+"锁住 str2");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
});
Thread b = new Thread(() -> {
try{
while(true){
synchronized(Deadlock.str2){
System.out.println(Thread.currentThread().getName()+"锁住 str2");
Thread.sleep(1000);
synchronized(Deadlock.str1){
System.out.println(Thread.currentThread().getName()+"锁住 str1");
}
}
}
}catch(Exception e){
e.printStackTrace();
}
});
a.start();
b.start();
}
}
上面的代码就是一个完整的死锁程序,程序中有两个线程,线程1锁住了str1,获得锁之后休眠1秒钟,这个时候线程2锁住了str2,也进行休眠操作。
线程1休眠完了之后去锁str2,但是str2已经被线程2给锁住了,这边只能等待,同样的道理,线程2休眠完之后也要去锁str1,同样也会等待,这样死锁就产生了。
避免线程死锁
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制
创建线程的几种方式?
- 继承 Thread 类
- 实现 Runnable 接口
- 实现 Callable 接口
- 使用匿名内部类方式
- 线程池
什么是 FutureTask?
utureTask 表示一个异步运算的任务,FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。只有当运算完成的时候结果才能取回,如果运算尚未完成 get 方法将会阻塞。一个 FutureTask 对象可以对调用了 Callable 和 Runnable 的对象进行包装,由于 FutureTask 也是Runnable 接口的实现类,所以 FutureTask 也可以放入线程池中。
为什么要用线程池?
线程池是为突然大量爆发的线程设计的,通过有限的几个固定线程为大量的操作服务,减少了创建和销毁线程所需的时间,从而提高效率。
- 降低资源消耗:重用存在的线程,减少对象创建销毁的开销。
- 提高响应速度。可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
- 附加功能:提供定时执行、定期执行、单线程、并发数控制等功能。
四种构建线程池的方式?
通过Executors提供四种线程池,分别为:
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
- newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
线程池执行流程,即对应execute()方法:
- 提交一个任务,线程池里存活的核心线程数小于线程数corePoolSize时,线程池会创建一个核心线程去处理提交的任务。
- 如果线程池核心线程数已满,即线程数已经等于corePoolSize,一个新提交的任务,会被放进任务队列workQueue排队等待执行。
- 当线程池里面存活的线程数已经等于corePoolSize了,并且任务队列workQueue也满,判断线程数是否达到maximumPoolSize,即最大线程数是否已满,如果没到达,创建一个非核心线程执行提交的任务。
- 如果当前的线程数达到了maximumPoolSize,还有新的任务过来的话,直接采用拒绝策略处理。
四种拒绝策略:
- AbortPolicy(抛出一个异常,默认的)
- DiscardPolicy(直接丢弃任务)
- DiscardOldestPolicy(丢弃队列里最老的任务,将当前这个任务继续提交给线程池)
- CallerRunsPolicy(交给线程池调用所在的线程进行处理)
阿里规范:手动创建线程池,效果会更好哦。
通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
实现Runnable接口和Callable接口的区别
函数式接口
Runnable void run() 既没有参数又没有返回值的方法
Callable V call() 没有参数有返回值的方法
- Callable 接口类似于 Runnable,从名字就可以看出来了,但是 Runnable 不会返回结果,并且无法抛出返回结果的异常,而 Callable 功能更强大一些,被线程执行后,可以返回值,这个返回值可以被 Future 拿到,也就是说,Future 可以拿到异步执行任务的返回值。
- Future 接口表示异步任务,是一个可能还没有完成的异步任务的结果。所以说 Callable用于产生结果,Future 用于获取结果。
执行execute()方法和submit()方法的区别是什么呢?
- 相同点就是都可以开启线程执行池中的任务。
- 接收参数:execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
- 返回值:submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
- 异常处理:submit()方便Exception处理
守护线程和用户线程有什么区别呢?
守护线程通常用在作为垃圾收集器或缓存管理器的应用程序中,执行辅助任务。
- 用户 (User) 线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
- 守护 (Daemon) 线程:运行在后台,为其他前台线程服务。也可以说守护线程是 JVM 中非守护线程的 “佣人”。一旦所有用户线程都结束运行,守护线程会随 JVM 一起结束工作
sleep() 和 wait() 有什么区别?
两者都可以暂停线程的执行
- 类的不同:sleep() 是 Thread线程类的静态方法,wait() 是 Object类的方法。
- 是否释放锁:sleep() 不释放锁;wait() 释放锁。
- 用途不同:Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
- 用法不同:wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。
线程的 sleep()方法和 yield()方法有什么区别?
-
(1) sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
-
(2) 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;
-
(3)sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常;
-
(4)sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性,通常不建议使用yield()方法来控制并发线程的执行。
调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
- new一个Thread,线程进入了新建状态。调用start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。
- 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
线程池都有哪些状态?
- RUNNING:这是最正常的状态,接受新的任务,处理等待队列中的任务。
- SHUTDOWN:不接受新的任务提交,但是会继续处理等待队列中的任务。
- STOP:不接受新的任务提交,不再处理等待队列中的任务,中断正在执行任务的线程。
- TIDYING:所有的任务都销毁了,workCount 为 0,线程池的状态在转换为 TIDYING 状态时,会执行钩子方法 terminated()。
- TERMINATED:terminated()方法结束后,线程池的状态就会变成这个。
作者:小杰要吃蛋 链接:juejin.cn/post/684490… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
ThreadLocal 是什么?有哪些使用场景?
ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象
threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
使用场景1:
网关解析了token,把用户信息放入了header
在前端微服务拦截器里面,会取出请求header里面的用户信息,会放入一个单例的UserContextHolder(用户上下文),内部就是一个ThreadLocal,用户信息就放入了这个ThreadLocal里面,需要用户信息的时候就可以直接取。
使用场景2:
数据库的连接工具类,从数据源中获取一个连接,并将实现和线程的绑定
业务逻辑控制在一个事务就需要两次操作都是同一个Connection
怎么保证多线程的运行安全?
- 方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。