并发编程的目的是为了让程序运行的更快,但是并不是启动更多的线程就能让程序更大限度地并发执行。--例如上下文切换的问题,死锁的问题,受限于软件和硬件的资源问题。
单核处理器也可以支持多线程编码:
CPU通过给每个线程分配CPU时间片来实现这个机制。时间片非常短,所以CPU通过不停地切换线程执行,让我们感受到多个线程同时执行,一般时间片的大小为几十毫秒(ms).
CPU通过时间片分配算法来循环执行任务,当一个任务执行一定的时间片后就会切换另一个任务,在切换钱会保存上一个任务的状态,一边下一切换回去的时候可以再加载这个任务的状态。所以人物从保存到再次加载的过程称为一次上下文切换。
多线程不一定比单线程快,在操作量不大的情况下,线程的创建和上下文切换反而使多线程比单线程更加慢。
减少上下文切换的方式:
1、无锁并发编程。多线程竞争锁的时候,会引起上下文切换,尽可能避免使用锁可以减少上下文的切换:如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。
2、CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。
3、使用最少的线程数量。大量的空闲线程(waitting状态),除了增加创建开销,还有切换上下文的开销。在任务很少的情况下尽量减少不必要的线程。
4、协程。在单线程里实现多任务的调度,并在单线程里维持多任务间的切换。
死锁
一个死锁的发现过程Demo
贴入死锁Demo代码
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.currentThread().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();
}
}
一、获取pid
方法一:在jdk/bin目录下打开控制台,敲击jps-v找到自己的程序的Pid
方法二:在jdk/bin目录下有一个叫做jvisualvm的可执行文件,打开
二、用jstack查看日志(这里dump似乎跟我的电脑八字犯冲,stackoverflow上的方法都用了也不行,所以选择曲线救国,打印日志。)
在jdk/bin下打开cmd,输入 jstack -l 5444 > jstack.log
(jstack -l pid > 文件名.后缀)
文件太大就不放上来了,但是其中有两段引起我的注意。
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00e75444 (object 0x0f6b7850, a java.lang.String),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00e76164 (object 0x0f6b7870, a java.lang.String),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.jathonkatu.day20190716.DeadLockDemo$2.run(DeadLockDemo.java:37)
- waiting to lock <0x0f6b7850> (a java.lang.String)
- locked <0x0f6b7870> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at com.jathonkatu.day20190716.DeadLockDemo$1.run(DeadLockDemo.java:27)
- waiting to lock <0x0f6b7870> (a java.lang.String)
- locked <0x0f6b7850> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
分析一下:
线程1在等待锁0x0F6b7850,正在被锁0x0f6b7870锁住,且两者都是String类型(两个String在常量池中的地址)。
相反,线程2在等待锁0x0f6b7870,正在被锁0x0F6b7850锁住。
从上面不难得出,事实上就是一个死锁的行为,结合代码就不难分析,一个持有String对象A的锁,请求String对象B的锁,一个持有String对象B的锁,请求String对象A的锁。
避免死锁的常见方法:
1、避免一个线程同时获取多个锁
2、避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。
3、尝试使用定时锁,ReentrantLock类中有个方法tryLock(long timeout,TimeUnit unit)来代替内部锁机制。
4、对于数据库锁,加锁和解锁必须在一个数据库链接里,否则会出现失败的情况。(释放锁失败抛异常后仍然持有锁)
资源限制
在并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。
硬件资源的限制有:带宽的上传/下载速度,硬盘读写速度和CPU的处理速度。
软件资源的限制有:数据库的连接数和socket连接数等。
资源限制引发的问题
将代码串行执行的部分改成并行执行固然能加快代码运行,但如果受限于资源后,期望并行执行的代码其实还是串行执行。并且不仅仅不会加快代码执行,反而会更慢,因为增加了上下文切换和资源调度时间。
如何解决资源限制问题
考虑使用集群并行执行程序,如ODPS、Hadoop或者自己搭建的服务器集群。不同的机器处理不同的数据,可以通过“数据ID%机器数”,计算计算机编号,根据不同的编号用不同的机器处理。
资源限制情况下进行并发编程
根据不同的资源限制调整程序的并发度。有数据库操作时,设计数据库连接数,如果Sql执行非常快,而线程数量比数据库连接数大很多,则某些线程会被阻塞,等待数据库链接。如下载文件就依赖于贷款和硬盘读写速度。