多线程的具体应用场景有哪些?实际中需要注意些什么?
- 后台任务,例如:发短信、发邮件、发MQ消息、记录日志。
- 并行计算,例如:调度任务处理、处理海量数据(统计用户历史订单消费总额,计算加盟商月度服务费等)。
- WEB开发,例如:Servlet容器本身就是多线程环境,只是由于大部分情况我们都是基于使用无状态单例Bean处理业务,从而避免了处理线程安全等问题。
多线程可以提高处理能力,增加性能,充分利用服务器资源,但要注意:
- 线程资源最好通过线程池提供,避免在应用中自行显式创建线程,否则可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
- 高并发时,同步调用应该去考量锁的性能损耗:能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
- 并发修改同一记录时,为了避免更新丢失,如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。另外注意:乐观锁的重试次数不得小于3次。
- 多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
- 子线程抛出异常堆栈,不能在主线程try-catch到,所以子线程执行代码时注意catch异常进行处理。
- 避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed 导致的性能下降。
- volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
java concurrent 包你们都用过哪些类?是如何使用的?
常用到的主要有:
- 线程安全集合类:ConcurrentHashMap、CopyOnWriteArrayList(少写多读场景,配置中心配置存储)、ArrayBlockingQueue(阻塞队列,生产消费模型场景)
- 原子(CAS无锁化)类:AtomicInteger(多线程环境下获取唯一id)、LongAdder(JDK1.8替代AtomicLong进行计数统计)
- 线程同步类:CountDownLatch(同步计数屏障)、CyclicBarrier(同步计数屏障可重置)、Semaphore(信号量控制)
- 锁:ReentrantLock(可重入锁)、ReentrantReadWriteLock(可重入读写锁)
- 线程池:ThreadPoolExecutor(线程池技术)、Future(线程返回值)
AtomicLong 和 LongAdder 的区别了解吗?
二者都是基于CAS实现的无锁化原子类。二者的区别是LongAdder 相对于AtomicLong 增加了分段CAS机制来降低CAS失败几率,所以在低并发场景下二者性能相当,而高并发场景下 LongAdder 更为高效。其中AtomicLong 更新值时只是简单的通过死循环的进行底层CAS操作,而 LongAdder 是在此基础之上新增了分段CAS累加然后通过对cell数组和base进行求和(调用sum()方法)取得最终结果的机制,极大的降低了各线程CAS时的冲突,与AtomicLong相比,LongAdder更多地用于收集最终统计数据,而不是细粒度的同步控制,无法实时获取原子操作结果。而且,LongAdder只提供了add(long)和decrement()方法,想要使用cas方法还是要选择AtomicLong。
无锁编程(CAS)了解吗?
java.util.concurrent.atomic包下的类通过CAS的机制实现无锁化线程安全。
CAS机制是:我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项乐观锁技术,基于CPU原子指令实现,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
AQS了解吗?原理是什么?
详情参见 Java并发编程--AQS
你们是如何使用线程池的?线程池原理了解吗?
通过线程池处理一些异步操作(发邮件、发短信、发MQ消息)、定时任务处理大批量数据等场景。
ThreadPoolExecutor 线程池在执行excute方法时,主要内容如下:

- 如果当前运行的线程少于corePoolSize,则创建新线程(Worker)来执行任务(需要获得全局锁)
- 如果运行的线程等于或多于corePoolSize ,则将任务加入BlockingQueue
- 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务(需要获得全局锁)
- 如果创建新线程将使当前运行的线程超出maxiumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法根据饱和策略进行处理。
详情参见:Java中的线程池——ThreadPoolExecutor的原理、
线程池原理详解
阻塞队列和非阻塞队列了解吗?你们是如何使用的?
- 阻塞队列:适用于很多生产者-消费者模型场景:记录关键业务日志(通过ArrayBlockingQueue+ThreadPoolExecutor+Mongodb实现,特殊情况数据量过大时降级到本地logback记录再后续处理)、处理超大数据文件(逐行加载(put进阻塞等待)数据到队列中由线程池消费(take出阻塞等待)处理,ArrayBlockingQueue+ThreadPoolExecutor)
- ArrayBlockingQueue(线程安全的阻塞队列,基于数组实现,读写是同一个锁,吞吐量受限)
- LinkedBlockingQueue(线程安全的阻塞队列,基于链表实现,读写是两个锁,吞吐量大幅提升,但入队操作始终创建一个Node,高并发时GC可能会影响性能稳定)
- 非阻塞队列:使用非阻塞队列,虽然能即时返回结果(消费结果),但必须自行编码解决返回为空的情况处理(以及消费重试、高频轮询造成的资源紧张等问题)。实际使用场景较少,Tomcat的NioEndPoint中的每个poller里面维护了一个ConcurrentLinkedQueue<Runnable>用来作为缓冲存放任务。
- ConcurrentLinkedQueue(线程安全的非阻塞队列,基于链表实现,读写通过CAS进行同步)。
详情参考并发队列-无界非阻塞队列 ConcurrentLinkedQueue 原理探究、
使用阻塞式队列处理大数据
- ConcurrentLinkedQueue(线程安全的非阻塞队列,基于链表实现,读写通过CAS进行同步)。
网友评论