1.上下文切换
- 即使是单核处理器也支持多线程执行代码,CPU通过给每个线程分配CPU时间片来实现
这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切
换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。
- CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个
任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这
个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
- 这就像我们同时读两本书,当我们在读一本英文的技术书时,发现某个单词不认识,于是
便打开中英文字典,但是在放下英文技术书之前,大脑必须先记住这本书读到了多少页的第
多少行,等查完单词之后,能够继续读这本书。这样的切换是会影响读书效率的,同样上下文
切换也会影响多线程的执行速度。
1.1多线程一定快吗?
- 当并发执行累加操作不超过百万次时,速度会比串行执行累加操作要
慢。那么,为什么并发执行的速度会比串行慢呢?这是因为线程有创建和上下文切换的开销。
1.2 如何减少上下文切换
- 无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一
些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
- CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
- 使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这
样会造成大量线程都处于等待状态。
- 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。
2.死锁
- 锁是个非常有用的工具,运用场景非常多,因为它使用起来非常简单,而且易于理解。但同时它也会带来一些困扰,那就是可能会引起死锁,一旦产生死锁,就会造成系统功能不可用。让我们先来看一段代码,这段代码会引起死锁,使线程t1和线程t2互相等待对方释放锁
public class DeadLockDemo {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}
- 这段代码只是演示死锁的场景,在现实中你可能不会写出这样的代码。但是,在一些更为
复杂的场景中,你可能会遇到这样的问题,比如t1拿到锁之后,因为一些异常情况没有释放锁
(死循环)。又或者是t1拿到一个数据库锁,释放锁的时候抛出了异常,没释放掉。
一旦出现死锁,业务是可感知的,因为不能继续提供服务了。
2.1如何定位死锁
- 确定进程id:jps或者系统的ps、任务管理器等工具
- jstack Pid
hsfxuebaodeMacBook-Pro:java-study hsfxuebao$ jps
51761 Jps
51721 DeadLockDemo
366
hsfxuebaodeMacBook-Pro:java-study hsfxuebao$ jstack 51721
2020-04-12 09:35:39
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.181-b13 mixed mode):
"Attach Listener" #13 daemon prio=9 os_prio=31 tid=0x00007fe6fb006800 nid=0x4303 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #12 prio=5 os_prio=31 tid=0x00007fe6fc048800 nid=0x1903 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #11 prio=5 os_prio=31 tid=0x00007fe6fc8ac800 nid=0x4503 waiting for monitor entry [0x0000700011178000]
java.lang.Thread.State: BLOCKED (on object monitor)
at concurrent.DeadLockDemo$2.run(DeadLockDemo.java:42)
- waiting to lock <0x0000000795796fd8> (a java.lang.String)
- locked <0x0000000795797008> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0" #10 prio=5 os_prio=31 tid=0x00007fe6fc8ab800 nid=0x3f03 waiting for monitor entry [0x0000700011075000]
java.lang.Thread.State: BLOCKED (on object monitor)
at concurrent.DeadLockDemo$1.run(DeadLockDemo.java:31)
- waiting to lock <0x0000000795797008> (a java.lang.String)
- locked <0x0000000795796fd8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #9 daemon prio=9 os_prio=31 tid=0x00007fe6fb881800 nid=0x3c03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x00007fe6fc8a0000 nid=0x4803 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x00007fe6fc89f000 nid=0x4903 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x00007fe6fb886800 nid=0x3803 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x00007fe6fb878000 nid=0x3603 runnable [0x0000700010a63000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000079570a880> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000079570a880> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:64)
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fe6fc80a800 nid=0x3403 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007fe6fb803800 nid=0x2d03 in Object.wait() [0x000070001085d000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000795588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x0000000795588ed0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007fe6fb007800 nid=0x2c03 in Object.wait() [0x000070001075a000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x0000000795586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x0000000795586bf8> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=31 tid=0x00007fe6fb81f000 nid=0x5103 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007fe6fc004000 nid=0x2007 runnable
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007fe6fb804800 nid=0x1e03 runnable
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007fe6fb805000 nid=0x5403 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007fe6fb805800 nid=0x5303 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x00007fe6fb0ac000 nid=0x3d03 waiting on condition
JNI global references: 15
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007fe6fb02dd58 (object 0x0000000795796fd8, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007fe6fb02b418 (object 0x0000000795797008, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at concurrent.DeadLockDemo$2.run(DeadLockDemo.java:42)
- waiting to lock <0x0000000795796fd8> (a java.lang.String)
- locked <0x0000000795797008> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
"Thread-0":
at concurrent.DeadLockDemo$1.run(DeadLockDemo.java:31)
- waiting to lock <0x0000000795797008> (a java.lang.String)
- locked <0x0000000795796fd8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
2.2如何避免死锁
- 避免一个线程同时获取多个锁。
- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
- 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制。
- 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。
3.资源限制的挑战
- 什么是资源限制
- 资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。例如,服务器的带宽只有2Mb/s,某个资源的下载速度是1Mb/s每秒,系统启动10个线程下载资源,下载速度不会变成10Mb/s,所以在进行并发编程时,要考虑这些资源的限制。硬件资源限制有带宽的上传/下载速度、硬盘读写速度和CPU的处理速度。软件资源限制有数据库的连接数和socket连接数等。
- 资源限制引发的问题
- 在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。例如,之前看到一段程序使用多线程在办公网并发地下载和处理数据时,导致CPU利用率达到100%,几个小时都不能运行完成任务,后来修改成单线程,一个小时就执行完成了。
- 如何解决资源限制的问题
- 对于硬件资源限制,可以考虑使用集群并行执行程序。既然单机的资源有限制,那么就让程序在多机上运行。比如使用ODPS、Hadoop或者自己搭建服务器集群,不同的机器处理不同的数据。可以通过“数据ID%机器数”,计算得到一个机器编号,然后由对应编号的机器处理这笔数据。对于软件资源限制,可以考虑使用资源池将资源复用。比如使用连接池将数据库和Socket连接复用,或者在调用对方webservice接口获取数据时,只建立一个连接。
- 在资源限制情况下进行并发编程
- 如何在资源限制的情况下,让程序执行得更快呢?方法就是,根据不同的资源限制调整程序的并发度,比如下载文件程序依赖于两个资源——带宽和硬盘读写速度。有数据库作时,涉及数据库连接数,如果SQL语句执行非常快,而线程的数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库连接。