面试篇四:多线程并发编程
- 创建线程池的方式,以及各自的特点
1.使用ThreadPoolExecutor类创建线程池。
ThreadPoolExecutor(int corePoolSize, // 线程池的核心线程数 int maximumPoolSize, // 线程池的最大线程数 long keepAliveTime, // 线程池空闲时线程的存活时长 TimeUnit unit, // 线程存活时长单位 BlockingQueue<Runnable> workQueue // 存放任务的队列,使用的是阻塞队列 )
2.使用Executors类提供的方法创建线程池。
(1) Executors.newCachedThreadPool(); 创建的是可缓存线程池。
工作线程的创建数据不超过Interger.MAX_VALUE即可;在线程池空闲达到一定时间,会释放工作线程,释放资源;等提交了新任务后会重新创建工作线程。
注意事项:CachedThreadPool因为不限制线程数,故我们使用时,要注意自己控制好线程数,防止大量线程同时运行,导致系统瘫痪。
ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); cachedThreadPool.execute(new Runnable() { public void run() { // TODO } }
(2) Executors.newFixedThreadPool(3); 创建一个指定工作线程数量的线程池。
拥有固定的最大线程数,当达到最大值的工作线程数,则新提交的线程会进入池队列中。但空闲时不会释放工作线程。
(3) Executors.newSingleThreadExecutor(); 创建单线程化的线程池,即只有一个工作线程。保证线程顺序执行。
(4) Executors.newScheduledThreadPool(5); 创建一个支持定时及周期性执行的线程池。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); // 延迟3s执行,使用schedule()方法 scheduledThreadPool.schedule(new Runnable() { public void run() { } }, 3, TimeUnit.SECONDS); // 延时3s,每间隔5秒执行一次,使用scheduleAtFixedRate()方法 scheduledThreadPool.scheduleAtFixedRate(new Runnable() { public void run() { } }, 3, 5, TimeUnit.SECONDS);
- 如何保证缓存一致性
一般是在数据发生变更时,主动更新缓存中的数据或者移除对应缓存。
当缓存中的某个key在被更新时,同时被大量请求要获取该key,这时怎么解决数据一致性问题?
锁:在缓存更新或过期时,先获取锁,等到更新或从数据库获取完成并存入缓存后,再释放锁,这个过程,其他线程就一直处理等待中。
- 缓存雪崩
缓存没查到的数据,则会去后端数据库查询;这时如果多个请求并发的要从数据库获取数据,对数据库造成很大的压力,就可能出现缓存雪崩。
如某个时间节点内,缓存集中失效,如果大量请求获取数据,就可能出现缓存雪崩。
该数据是可以从数据库查询到的,这是和下面说的缓存穿透不一样的地方。
怎么解决?
(1)借助限流、降级、熔断等手段可降低影响。
(2)设置多级缓存,分别设置不同的过期策略。
- 缓存穿透
缓存查不到的数据,出现大量请求并发查询,这时候就会一直访问后端数据库,实际上,后端数据库本身也不存在该数据,实际上是不必要的查询,这种就是缓存穿透。怎么解决?
(1)缓存一个空对象,设置较短的过期时间。这种解决方法适合命中率不高,但更新频繁的数据。
(2)把可能出现数据为null的所有key,存在在一个集合中,在每次请求前先拦截。这种解决方法适合命中率不高、更新不频繁的数据。
- 限流
限流就是只允许程序能处理的请求数进来,其他的丢弃掉。
- 降级
降级就是把非核心功能暂时关闭掉。
- 熔断
熔断是应对外部系统的故障。比如A服务调用B服务的某个接口,响应很慢时,导致A服务的线程都卡在这里了,A服务的其它功能变慢。此时就需要熔断机制,让A服务调用B服务的接口就直接返回错误,从而避免整个A服务被拖慢。
- 为什么要使用多线程
主要是在CPU是多核的情况下,如果用单线程,就只用到了一个,使用多线程就可以尽量不让CPU空闲下来,提高系统的资源利用率。
- 多线程有哪几种实现方式
1.继承Thread类,重写run方法。无返回值,不能返回处理结果。
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target。无返回值,不能返回处理结果。
3.通过Callable和FutureTask创建线程。有返回值,通过Callable接口,就要实现call方法。
4.通过线程池创建线程。
- 线程安全三要素:原子性、可见性、有序性。
原子性:提供了互斥访问,同一时刻只能有一个线程对它进行操作。
(1)Atomic包:AtomicInteger、AtomicBoolean、AtomicLong、AtomicIntegerArray、AtomicReferenceArray、AtomicReference。
采用的是乐观锁策略去原子更新数据,来保证更新基本类型变量的线程安全问题。
(2)CAS算法:多个线程访问共享变量时,通过比较交换来鉴别线程是否出现冲突,对出现冲突的会重试当前操作直到没有冲突为止。
CAS比较交换的过程包括三个值:V-内存地址存放的实际值;O-预期的值(旧值);N-更新的新值。
V = O,则可以直接赋新值V = N;
V != O,表明该值已经被其他线程更新过,O不是最新的值了,所以不能将新值N赋给V,直接返回V,更新失败,对于这部分失败的,在多线程中,就会重写尝试继续更新,当然一定时间不成功,我们也可以选择挂起唤醒操作。
(3)互斥同步锁(Synchronized、Lock):Synchronized也可以实现线程安全的问题,但是采用的是悲观锁方式。
可见性:多个线程访问同一个变量,一个线程对同一变量的修改可以立即就被其他线程看到。
volatile:保证可见性,不保证原子性。一个volatile变量进行写操作时,JVM会把本地内存的变量强制刷新到主内存中,其他现在读时,只能从主内存读取。
也就是说,volatile的写操作对其他线程是可见的。另外,volatile还可以禁止指令重排序。
volatile可以解决只有一个线程写,其他线程读的情况,但不能解决多个线程同时写的情况。同时写的情况可以考虑使用Synchronized。
有序性:程序执行顺序按照代码的先后顺序执行。对于变量,读这个变量前先进行对这个变量的写操作。关于锁,需释放锁后才能对同一个锁进行操作。
- 如何实现线程安全
(1)只有一个写,其他线程都是读操作,可以用volatile。
(2)并发不严重,使用Synchronized,如果要通过代码释放锁和设置超时时间,使用ReentrantLock。这两个锁都不能同一时刻只有一个线程能进入同步代码中,其他线程都会处于等待中。
(3)使用并发包,如ConcurrentHashMap、LinkBlockingQueue、AtomicInteger。
(4)合理使用ThreadLocal,ThreadLocal是一个变量的本地副本,线程对变量的操作不会影响到其他线程,解决了变量并发访问的冲突问题。在不同的线程中访问同一个 ThreadLocal 对象的 set 和 get 进行存取数据是不会相互干扰的。常用来解决数据库连接、Session管理。
- 怎么创建线程池、关闭线程池
使用ThreadPoolExecutor类实现。execute()方法是提交一个任务到线程池中,没有返回值;submit()也可以执行一个线程,但是有返回值。
核心参数:corePoolSize-核心线程数最大值;maximumPoolSize-最大线程数大小;keepAliveTime-非核心线程空闲的存活时间大小;unit-存活时间单位;workQueue-存放任务的阻塞队列;threadFactory-创建新线程的工厂;handler-线程池的拒绝策略。
关闭线程池,可以使用shutdown()、shutdownNow()。
shutdown:线程池不接受新的任务,不影响终止前的任务。
shutdownNow:对正在执行的任务中断执行,对未开始的任务取消执行,并返回还没开始的任务列表。
- 配置线程要注意哪些
(1)线程池的大小根据CPU个数来,如果线程不是一直在运行,通常设置为CPU核数/(1-阻塞系数),如阻塞系数为0.5,则CPU个数*2;如果运行过于密集,设置为和CPU个数相当。
(2)对线程池进行隔离,也就是按照业务划分,一个任务用一个线程池,这样即使某一个任务出现问题把线程池耗尽,也不会影响到其他任务运行。
(3)对请求数进行限制,防止请求过载。
- 进程和线程
进程:进程是线程的容器,进程是正在运行的程序的实例。它是活动的且有状态变化的。
在三态模型中,进程状态分为三个基本状态,即运行态,就绪态,阻塞态。
在五态模型中,进程分为新建态、终止态,运行态,就绪态,阻塞态。
通常,一个进程在创建后将处于就绪状态。
(1)运行(running)态:进程占有处理器正在运行。
(2)就绪(ready)态:进程具备运行条件,等待系统分配处理器以便运行。
(3)等待(wait)态:又称为阻塞(blocked)态或睡眠(sleep)态,指进程不具备运行条件,正在等待某个事件的完成。
线程:线程是程序执行的最小单元,线程是进程中的一个实体,具有就绪,阻塞和运行三种基本状态。
一个进程可以有很多线程,每条线程并行执行不同的任务。
- 多线程编程中需要考虑的问题:什么是竞争资源、什么时候考虑同步、怎么同步。
通常需要考虑的设计有:
(1)把竞争访问的资源标识为private,禁止直接访问。
(2)通过volatile关键字作用于共享变量,保证变量在多个线程之间的可见性,以及防止指令重排序。
(3)同步那些修改变量的代码,使用synchronized 关键字同步方法或代码,或者使用ReenTrantLock可重入锁。
(4)使用单例bean时,使用双重检查锁,保证创建的是一个单例bean。

浙公网安备 33010602011771号