上周的面试中,被问及了几个关于Java并发编程的问题,自己回答的都不是很系统和全面,可以说是“头皮发麻”,哈哈。因此果断购入《Java并发编程的艺术》一书,学习后的体会是要想快速上手Java并发编程,最需要掌握的是线程、线程池概念的理解和Executor框架的使用。 Tip: 实践请见github-multiThread,不会介绍Java内存模型等更底层的内容。看看下图的“糙汉”身上错综复杂的线[程],愿通过学习,能化繁为简,[高效]的编出[高效]的多线程代码。
在实践中,为了更好的利用资源提高系统整体的吞吐量,会选择并发编程。但由于上下文切换和死锁等问题,并发编程不一定能提高性能,因此如何合理的进行并发编程时本文的重点,接下来介绍关于锁最基本的一些知识(选学)。
monitorenter, monitorexit
的代码。pause
指令减少自旋带来的开销;只能保证一个共享变量的原子操作,通过AtomicRefence
保证引用对象间的原子性,接下来看一个最简单的CAS操作示例。
protected void safeCount() { for (;;) { int i = atomicI.get(); if (atomicI.compareAndSet(i, ++i)) break; } }这部分和之后的锁是基础部分的核心内容,需要好好理解。一般来说,线程都是操作系统最小的调度单元,一个进程中可以包含多个线程,每个线程都拥有自己的计数器、堆栈和局部变量。系统会采用分时的形式调度运行的线程,OS会分出一个个的时间片到线程,此外还可以给线程设置优先级,来保证优先级高的线程获得更多的CPU时间。通过下面的示例代码可以发现,java程序的运行不仅就是main线程,还有清楚Reference的线程、调用对象finalize方法的线程、分发处理发送给JVM信息的线程、Attach Listener线程等。
// 获取管理线程的MXbean
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(true, true);
// 打印线程信息
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName());
}
NEW
初始状态,线程被构建但未start;RUNNABLE
运行状态,Java线程将OS中的就绪和运行两种状态都称作“运行中”;BLOCKED
阻塞状态,表示线程阻塞于锁;WAITING
等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出特定动作(通知或中断);TIME_WAITING
超时等待状态,该状态不同于WAITING
,其会在指定的时候后返回;TERMINATED
终止状态,可以使用interrupt()
合理的终止线程,表示当前线程已经执行完毕,之后通过一张Java线程状态图来做个形象的了解。
Daemon守护线程概念非常简单,java的虚拟机只有在不存在Daemon线程时才会退出。
如果线程A执行了Thread.join(),表示当线程A等待的线程终止之后才从thread.join()返回,其还提供了join(long millis)和join(int millis, int nanos)方法,当给点时间内前驱线程未结束则强制返回。
ThreadLocal
线程变量是以ThreadLocal
对象为键,任意对象为值的存储结构。此外,这部分常见的应用实例包括等待超时模式,数据库线程池,基于线程池的简单Web服务器等。
锁是用来控制多个线程访问共享资源的方式,在Lock接口出现前都是通过synchronized
来处理线程间同步问题。锁的主要方法包括lock
, tryLock
, unlock
, newCondition
获取等待通知组件等方法。其相关的实现包括队列同步器AbstractQueuedSynchronizer
、重入锁ReentrantLock
、读写锁ReentrantReadWriteLock
、LockSupport和Condition接口,这部分的重点讲是可重入锁ReenterLock。
ReentrantLock
表示该锁可以支持一个线程对资源的重复加锁,并支持获取琐时的公平性的选择。默认是非公平锁,其特点是性能要远高于公平锁(严格按照请求时间顺序获取所,FIFO)。
ReentrantLock lock = new ReentrantLock(true); lock.lock(); try { // TODO } finally { lock.unlock(); }ReentrantReadWriteLock
同时维护一个读锁和一个写锁,允许多个读线程同时访问共享数据,只会在写线程访问时阻塞,和数据库的锁机制很类似,该方式使得并发性等到很大提升。其除了公平性选择、可重入等特性外,还支持锁降级,遵循获取写锁、获取读锁再释放写锁的次序,写锁能降级为读锁。park
阻塞,unpark
唤醒的静态方法。wait()
、notify()
等,这些方法与synchronized
关键字配合可以实现等待/通知模式,Condition
接口也提供了类似的监视器方法,但功能更加强大。Segment
对HashEntry
进行包装,达到了记录级别的锁粒度,和数据库相关知识类似。HashTable由于只支持[表]级锁,因此性能比较低下。ConcurrentLinkedQueue
则是队列的线程安全版本,没有什么特别要说的。ArrayBlockingQueue
,LinkedBlockingQueue
,DelayQueue
等,不是重点。work-stealing
算法,可以使得线程可以从其他队列里窃取任务来执行,优点是充分利用线程进行并行计算,减少了线程间的竞争;缺点是在某些情况下存在竞争,比如双端队列里只有一个任务时,该算法会消耗更多的系统资源。这部分的内容非常重要,之后介绍的一些常见模式可以很好的应用在日常的开发场景中,一定要掌握牢靠。
AtomicBoolean
和AtomicInteger
,AtomicIntegerArray
,AutomicReference
等,接下来选择一个比较复杂的作为示例。
User user = new User("xionger", 30); atomicUserRef.set(user); User updateUser = new User("xiongerda", 32); atomicUserRef.compareAndSet(user, updateUser); System.out.println(atomicUserRef.get().getName()); System.out.println(atomicUserRef.get().getOld());CountDownLatch
有些相似,不过其特点是可以使用reset
方法重置,并通过isBroken()
判断线程是否中断。
1.如果当前运行的线程少于corePoolSize直接创建新线程来执行任务,需要获取全局锁。
2.如果运行的线程等于或多余corePoolSize则将任务加入BlockingQueue。
3.如果由于队列已满,无法将任务加到BlockingQueue,则创建新的线程来处任务,需要获取全局锁。
4.如果创建新线程将操作maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
ThreadPoolExecutor采用上述步骤,保证了执行execute()
时,尽可能的避免了获取全局锁,大部分的可能都会执行步骤2,而无需获取全局锁。
在引入Executor框架前,Java线程既是工作单元,也是执行机制。而在Executor框架中,工作单元和执行机制被分离开来,前者包括Runnable
和Callable
,而执行机制由Executor框架提供。该框架是一个两级的调度模型,在上层,通过调度器Executor将多个任务映射到固定数量的线程;在底层,操作系统内核将这些线程再映射到处理器上。而我们的应用程序只需通过E该框架控制上层的调度即可。
Tip:
在合理配置线程池时,需要根据具体场景给出对应的解决方案,总体来说,推荐使用有界队列,便于控制。
CPU密集型:配置尽可能少的线程,如cpu数量+1
,可以通过Runtime.getRuntime().availableProcessors()
获取CPU个数
IO密集型:配置尽可能多的线程,如2*cpu数量
,常见场景,等待数据库或服务接口的返回。
优先级:可以通过PriorityBlockingQueue
来处理
监控:可以通过taskCount
,completedTaskCount
,getActiveSize
等函数来监控线程池的运行。
Runnable
和Callable
b.任务的执行,包括任务执行机制的核心接口Executor
和其子类ExecutorService
,相关的实现类包括ThreadPoolExecutor
和ScheduledThreadPoolExecutor
。
c.异步计算的结果,包括Future
和其实现FutureTask
。
corePool
, maximumPool
, BlockingQueue
, RejectedExecutionHandler
4部分组成,可以由工具类Executors
创建。具体老说,工具类可以创建FixedThreadPool
固定线程数(最推荐)、SingleThreadExecutor
、CachedThreadPool
三种类型的ThreadPoolExecutor
。Timer
对象更加全面,其通过DelayQueue
来执行周期性或定时的任务。AbstractQueuedSynchronizer
(AQS),之前介绍的ReentrantLock
、CountDownLatch
等其实都是基于AQS来实现的。AQS是一个同步框架,提供通用机制来原子性的管理同步状态、阻塞&唤醒线程、维护被阻塞的线程队列。每个基于AQS的实现都会包含两类操作,acquire用于阻塞调用线程,对应futureTask.get()
,知道AQS状态允许这个线程才能继续执行;另一个为release,对应futureTask.cancel()&run()
,该操作改变AQS状态,改变后的状态允许一个或多个阻塞线程解除阻塞。
public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService executor = Executors.newSingleThreadExecutor(); Future<BigDecimal> result = executor.submit(new Callable<BigDecimal>() { @Override public BigDecimal call() throws Exception { return getSalaryByService(); } }); System.out.println(result.get()); }top
命令查看进程的情况,之后可以使用交互命令1
查看CPU性能,H
查看每个线程的性能信息。
性能测试:比如使用Jmeter来做压测,可以通过netstat -nat | grep 3306 -c
来查看数据的压力情况。参考资料