2014年底,就在大家都认为并行计算必然成为未来的大趋势时,在Avoiding ping pong论坛上,Linus Torvalds提出了一个截然不同的观点:“忘掉那该死的并行吧!”(原文:Give it up. The whole”parallel computing is the futrue” is a bunch of crock)
忘掉那该死的并行
对于并行计算,Linus提出两个特例,那就是图像处理和服务端程序是可以也是需要使用并行技术的,那么为什么说这两个是特例呢?
图像处理:图像处理往往拥有极大的计算量,对于一张1024*768像素的图片来说, 包含了78万6千多个像素,将所有的像素遍历一遍也是需要花上不少的时间的,而且图像处理设计大量的矩阵计算,其计算的规模和数量是非常庞大的。面对如此多的计算,单核CPU的计算能力很可能是不够的,所以自然需要引入多核计算了
服务端程序:服务端程序需要承受很重的用户访问压力,就拿典型的淘宝来说,在双十一那天,支付宝核心数据库集群处理了41亿个事务,执行285亿次sql,生成15tb日志。如此密集的访问,恐怕任何一台单机都是难以胜任的;另一方面,服务端程序也拥有更加复杂的业务模型,处理不同的业务需求。因此,并行程序也就成了唯一的出路
你必须知道的几个概念
同步:同步方法调用一旦开始,调用者必须等到方法调用返回之后,才能继续后续的行为
异步:异步方法调用更像是一个消息传递,一旦开始,方法调用就会立即返回,也就是说调用者立马就可以继续后续的操作
并行:多个任务同时执行,并行只可能出现在拥有多个CPU的系统中
并发:多个任务交替执行
并发和并行的区别:并发和并行其实都可以表示多个任务一起执行,不过并发更偏重于多个任务之间交替执行,多个任务之间有可能还是穿行的,而并行是真正意义上的同时执行;
临界区:临界区用来表示一种公共资源或者说是共享数据,类似于jmm(java虚拟机)中的方法区(存储的是类加载信息、常量、类变量等线程之间共享的数据),他能被多个线程共享使用。但是在任何时刻都只可能有一个线程使用它,一旦临界区的资源被占用,其他的线程想要使用这个资源就必须等待,否则将会出现安全和数据错误等问题。就比如一个打印机,张三和李四之间同时只能有一个人使用,如果两个人同时使用,最终打印出来的文件就会是损坏的文件,既不是张三想要的,也不是李四想要的
阻塞:当一个线程占用了临界区资源时,其他所有需要使用这个资源的线程就会处于阻塞等待状态;阻塞用来形容多线程之间的相互影响。在阻塞状态下,经常容易产生死锁的情况
非阻塞:它表示没有一个线程可以妨碍其他线程执行,所有的线程都会尝试不断向前执行而不会产生阻塞状态。非阻塞也是用来形容多线程之间的相互影响。在非阻塞状态下,经常容易产生线程不安全的问题
线程的并发级别
阻塞:对于一个阻塞线程,在其他线程释放所需资源之前,该线程当前无法继续执行下去,通常我们使用synchronized关键字或者使用重入锁的时候得到的就是阻塞线程
无饥饿:如果线程之间设置了不同的优先级,那么线程调度的时候总是会倾向于高优先级的线程先执行,对于资源的分配也是不公平的,最终有可能会导致低优先级的线程一直得不到所需要的资源(高优先级线程插队)。对于非公平的锁来说,系统遵从优先级高的先执行的原则;但是如果锁是公平的,那么饥饿就不会产生,不管新来的线程优先级多高,也要遵从先来后到的原则,所有的线程都有机会执行
无障碍:如果两个线程之间不会因为临界区资源的问题而导致另外一方被挂起,那么这两个线程之间就可以进行无障碍的执行
无锁:无锁的并行都是无障碍的;在无锁的情况下,所有的线程都能尝试对临界区的资源进行访问,但是不同的是,无锁的并发能保证必然有一个线程能够在有限步内完成操作离开临界区(不至于全军覆没)。如下一段代码可以用来表示无锁
while(!atomicVar.compareAndSet(localVar,localVar+1)){
localVar = atomicVar.get();
}
无等待:无锁要求有一个线程能够在有限步内完成操作,而无等待就要求所有的线程都能够在有限步内完成所有的操作,这样就满足了无饥饿。一种典型的无等待结构就是RCU(read-copy-update):对数据的读可以不加控制,所有的读线程都是无等待的,而对数据的写进行相应控制:先取得原始数据的副本,接着只修改副本数据,待修改完成后,在合适的时间回写数据
多线程的原子性
原子性表示一个操作是不可被中断,即使在多线程环境下,一旦这个操作开始,就不会被其他的线程干扰
对静态变量int i赋值就是一种原子性操作
public static int i = 0;
public void set1(){
i = 1;
}
public void set2(){
i = 2;
}
//创建两个多线程AB分别执行set1和set2最终的结果也只能是1或者2,AB两线程之间是没有影响的,这就是原子性的特点:不可被中断
现在来看看一个反例:在32位操作系统上面对long类型的值进行上述相同的操作
public static long d = 0;
public void set1(){
d = 111;
}
public void set2(){
d = 222;
}
创建两个多线程AB分别进行set1和set2操作,然后我们输出d值会发现他的值将会是一格与111和222完全不同的值,这是为什么呢?
因为long类型的值是64位的,而此时运行的是32位机,所以此时对long类型的值进行的读和写操作都不是原子性的,long111将会被拆分成高32位和低32位,long222同样也会被拆分成高32位和低32位,线程之间相互干扰,最终的值有可能成为两个值的高32位和低32位重组了
多线程的可见性
可见性是指当一个线程修改了某一个临界区共享变量的值,其他线程是否能够立即知道这个修改。对于串行程序来说,可见性问题是不存在的(因为在任何一个操作中修改了某个变量,在后续的操作中读取该变量的值都一定是被修改后的值)
然而在多线程中,经常可能会导致一个线程的修改不会立即被其他线程可见,从而造成读取到的数据是未更新的
多线程的有序性
看如下代码
public int x = 0;
public boolean flag = true;
public void a(){
x = 1;
flag = false;
}
public void b(){
if(flag)
x = 2;
}
在这里,我们创建两个线程AB,首先启动A执行a方法,然后启动B执行b方法,我们期望的是x=2,然而我们会发现x最终可能为1,我们将测试代码完整写上并加上测试输出语句
package org.blog.controller;
/**
* @ClassName: threadTest
* @Description: 多线程测试类 主要用于测试多线程的几种实现方式
* @author Chengxi
* @Date: 2017-10-18上午10:41:16
*
*
*/
public static void main(String[] args) throws InterruptedException{
Thread t1= new Thread(){
public void run(){
threadTest.a();
}
};
Thread t2 = new Thread(){
public void run(){
threadTest.b();
}
};
t1.start();
t2.start();
}
public static int x = 0;
public static boolean flag = true;
public static void a(){
System.out.println("a1");
x = 1;
flag = true;
System.out.println("a2");
}
public static void b(){
System.out.println("b1");
if(flag){
System.out.println("b2");
x = 2;
}
System.out.println("b3");
}
}
期望的输出应该是an和bn分成各自的组进行输出的,而结果可能出现如下这样:
a1
b1
b2
b3
a2
这表示在执行a方法的时候,b方法中的语句也插入进来执行了。这就是多线程有序性的问题:对于一个线程的执行代码而言,我们总是习惯的认为代码的执行是从先往后依次执行的,但是在并发的情况下,执行的程序却可能会出现乱序。造成有序性问题的原因是:程序在执行的时候,可能会进行指令重排,重排后的执行顺序与原来的顺序未必一致了
指令重排对于提高CPU处理性能是十分必要的,虽然确实带来了乱序的问题,但是相比之下,这点牺牲是十分值得的
参考文献
java高并发程序设计书籍