彻底搞清楚Java并发 (一) 基础
多线程编程是为了让程序运行得更快,但是不是说,线程创建地越多越好,线程切换的时候上下文切换,以及受限于硬件和软件资源的限制问题
上下文切换
单核CPU同样支持多线程编程,CPU通过给每个线程分配CPU时间片来实现这个机制,时间片是CPU分配给各个线程的时间,这个时间片非常短,所以就不得不通过切换线程来执行(时间片一般是几十毫秒)
当前任务执行一个时间片后,会切换到下一个任务,但是,在切换前会保存上一个任务的状态,这样的话下次这条线程获取到时间片之后就可以恢复这个任务的状态
协程
协程说通俗一点就是由线程调度的线程,操作系统创建一个进程,进程再创建若干个线程并行,线程的切换由操作系统负责调度,Java语言等线程其实与操作系统线程是1:1的关系,每个线程都有自己的Stack,Java在64位操作系统默默人stack大小为1024kb,所以一个进程也是不能够开启上万个线程的
基于J2EE项目都是基于每个请求占用一个线程去完成完整的业务逻辑,(包括事务)。所以系统的吞吐能力取决于每个线程的操作耗时。如果遇到很耗时的I/O行为,则整个系统的吞吐立刻下降,比如JDBC是同步阻塞的,这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。
Java的JDK里有封装很好的ThreadPool,可以用来管理大量的线程生命周期,但是本质上还是不能很好的解决线程数量的问题,以及线程空转占用CPU资源的问题。
先阶段行业里的比较流行的解决方案之一就是单线程加上异步回调。其代表派是node.js
以及Java里的新秀Vert.x
。他们的核心思想是一样的,遇到需要进行I/O操作的地方,就直接让出CPU资源,然后注册一个回调函数,其他逻辑则继续往下走,I/O结束后带着结果向事件队列里插入执行结果,然后由事件调度器调度回调函数,传入结果。
协程的本质上其实还是和上面的方法一样,只不过他的核心点在于由他进行调度,遇到阻塞操作,立刻yield掉,并且记录当前栈上的数据,阻塞完后立刻再找一个线程恢复栈并把阻塞的结果放到这个线程上去跑,这样看上去好像跟写同步代码没有任何差别,这整个流程可以称为coroutine
,而跑在由coroutine负责调度的线程称为Fiber
。比如Golang里的 go
关键字其实就是负责开启一个Fiber
,让func
逻辑跑在上面。而这一切都是发生的用户态上,没有发生在内核态上,也就是说没有切换上下文的开销。
Java线程调度
JVM必须维护一个有优先权,基于优先级的调度模式,优先级的值很重要,因为Java虚拟机和下层的操作系统之间的约定是操作系统必须选择有最高优先权的Java线程运行,所以我们说Java实现了一个基于优先权的调度程序该调度程序使用一种有优先权的方式实现,这意味着当一个有更高优先权的线程到来时,无论低优先级的线程是否在运行,都会中断(抢占)它(JVM会这么做)。这个约定对于操作系统来说并不总是这样,这意味着操作系统有时可能会选择运行一个更低优先级的线程。
yield()方法
理论上,yield意味着放手,放弃,投降。一个调用yield()方法的线程告诉虚拟机它乐意让其他线程占用自己的位置。这表明该线程没有在做一些紧急的事情。注意,这仅是一个暗示,并不能保证不会产生任何影响。
/** * A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore * this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU. * Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect. */ public static native void yield();
- Yield是一个静态的本地(native)方法
- Yield告诉当前正在执行的线程把运行机会交给线程池中拥有相同优先级的线程。
- Yield不能保证使得当前正在运行的线程迅速转换到可运行的状态
- 它仅能使一个线程从运行状态转到可运行状态,而不是等待或阻塞状态
join()方法
如果一个线程A执行了thread.join()方法目的是,当前线程A等待thread线程终止之后才从thread.join()返回,
/** * Waits for this thread to die. * * <p> An invocation of this method behaves in exactly the same * way as the invocation * * <blockquote> * {@linkplain #join(long) join}{@code (0)} * </blockquote> * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ public final void join() throws InterruptedException { join(0); } /** * Waits at most {@code millis} milliseconds for this thread to * die. A timeout of {@code 0} means to wait forever. * * <p> This implementation uses a loop of {@code this.wait} calls * conditioned on {@code this.isAlive}. As a thread terminates the * {@code this.notifyAll} method is invoked. It is recommended that * applications not use {@code wait}, {@code notify}, or * {@code notifyAll} on {@code Thread} instances. * * @param millis * the time to wait in milliseconds * * @throws IllegalArgumentException * if the value of {@code millis} is negative * * @throws InterruptedException * if any thread has interrupted the current thread. The * <i>interrupted status</i> of the current thread is * cleared when this exception is thrown. */ public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
如何减少上下文切换
- 无锁并发编程,多线程竞争锁的时候,会引起上下文切换,如将数据的ID按照Hash算法取模分段,不同线程处理不同段的数据
- CAS算法,Java的Atomic包下使用的同步类都是使用CAS和Volatile实现的无锁
- 线程数量的控制
- 协程:单线程内实现多任务的调度,在单线程中维持多个任务间的切换
如何避免死锁
- 避免一个线程同时获得多个锁
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
- 尝试使用定时锁,tryLock(time)代替内部锁的机制
- 对于数据库来说,加锁和解锁必须在同一条连接中,否则将会出现问题