java并发编程的目的显然是为了解决单线程的对资源利用不充分到这的程序运行速度慢的问题,但是并发编程的优势自然也给并发编程带来了一系列的挑战,最明显的是程序的复杂性大幅度的增加了,写出完美的并发并不是一件容易的事儿。
- 上下文的切换
1,什么是上下文切换?
cpu在执行多线程任务的时候,给我们的感官上来说是同步在执行的,其实单个cpu在同一个时间点上只能执行一项任务,通过极其快速的切换执行的线程,给我们一种线程在同时执行的错觉,这里就涉及到了时间片的概念,cpu分配给每个线程的时间片非常短。 一个线程从被保存到再被加载继续执行的过程就是一次上下文的切换。
2,多线程是否一定快?
根据多线程的目的,我们知道多线程是比单线程更具有效率的,但是在特定的情景下,单线程反而更快,这是因为单线程没有上下文切换的消耗,但是通常来说,多线程是比单线程更有效率,运行同样的任务,速度更快。
3,如何测试单线程的切换次数?
liunx提供了一个命令: vmstat - n 来做这件事。详情见: http://blog.csdn.net/wangpengzhi19891223/article/details/76615252
4,如何来减少上下文的切换?
- 无锁的并发编程
使用锁的并发编程,在多个线程竞争锁的时候,会引发上下文的切换,可以使用一些无锁的并发编程的技巧:例如在处理一批的数据的时候我们可以根据数据的Id 对数据进行hash分组,不同的线程处理不同的组。 - CAS算法
这里主要是说我们可以使用一些原子类来避免加锁,Atomic包下面的类。 - 避免创建不必要的线程
这个主要是说,在任务量没有那么大的时候,我们应该避免创建太多的线程来做这个任务。比如从1加到10 的结果,一个线程就够了,创建2个3个来分段计算的结果一定不会快很多至少,还带来了上下文的切换。 - 协程
这个是指的协作线程,java中没有这个概念,这个是一种经验,是指我们可以创建一个线程,在这个线程中组合其他线程来做事儿,这样因为是在同一个线程里,所以不会引发过多切换。
5,减少上下文切换的实战案例。
例如我们的tomcat性能低下,我们要通过“减少上下文切换”的这种方式来实现提供tomcat的性能。这里要使用的工具就是java jdk中自带的jstack.
- 自建一台虚拟机,放一个tomcat进去,然后直接启动。
- 用jps命令查看是否启动成功,以及获取tomcat占用的进程号:可以看出tomcat的进程号为:22337
用which java 查看jdk的安装路径:
进入到jdk目录的bin下。使用命令:jstack 22337 > /home/text.txt
统计分析:进入home目录下,使用liunx命令: grep java.lang.Thread.State text.txt | awk ‘{print 2 3
4 5}’ | sort | uniq -c
其中 2 WAITING(onobjectmonitor) 是指的是waitting线程,当waitting的线程过多的时候,就像上面所讲的那样创建了过多的不必要的线程。 下一步 我们将看看这些waitting的线程都是干什么的。- 打开text.txt会发现多数的waitting线程都是tomcat的工作线程,我们可以减少tomcat配置的最大线程数来减少tomcat的上下文切换来提高tomcat的性能。
- 死锁
- 锁在并发编程中是一个很常见的概念,和使用非常多的,因为锁的存在才使得并发编程变得很简单,但是如果使用不恰当也会引入死锁的问题:
public class DeadLockDome {
private static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDome().deadLock();
}
public 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("A获取锁成功");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
System.out.println("B获取锁成功");
}
}
}
});
t1.start();
t2.start();
}
}
这是一个常见的死锁程序,我们在liunx 下新建该java文件,
javac DeadLockDome.java
java DeadLockDome
运行程序,这里会发生明显的死锁,那我们如何定位死锁呢?我们可以通过打印栈信息。
jstack -F 进程号 > /home/dead.txt
一定要加 -F 否则可能打印不出来。
查看dead.txt文件 我们会发现如下信息:
发现是37行 21行引起的死锁。
常见的避免死锁的几个方法:
- 避免同一个线程获取多个锁
- 避免同一个线程占用多个资,尽量保证没有个锁只占用一个资源
- 尝试使用定时锁, lock.tryLock(time) 代替内部锁。
- 对于数据库的锁,加锁和解锁必须在同一个链接内。
- 资源限制
资源限制一般指的是计算机的cpu 、内存 、网络的限制,解决这个问题也很简单,就是直接加资源或者搭建集群。