什么是并发编程
简单的说,所谓的并发编程指的是同一台处理器“同时”处理多个任务。
并发的三种场景
1、分工
合理的拆解不同的任务,并能分配到线程,使多个任务更高效的执行。
2、同步
线程的执行依赖其他线程的执行结果。
3、互斥
多个线程需要抢占共享资源。
并发问题的源头
多线程的出现虽然可以提高应用程序的执行效率,但是不可避免的,也会引入一些问题,这些问题的源头如下:
1、缓存带来的可见性问题
由于CPU的读写速度远远大于内存的读写速度,故CPU利用缓存来缓和CPU和内存读写速度差异带来的问题;
对于多核处理器,每个核都有独立的缓存,这样CPU在计算完数值后,将数值存入缓存,但是写到内存的时机是不确定的,因此会发生缓存可见性问题
示例:如下程序,预期结果为20000,但实际执行结果为10000~20000之间
public class Add {
private static long count = 0;
public static long testAdd() throws InterruptedException {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <10000; i++) {
count ++;
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i <10000; i++) {
count ++;
}
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
return count;
}
}
2、线程切换带来的原子性问题
操作系统支持的最大线程数要远远大于操作系统核数,这是为了缓解CPU的IO速度差异,采用了分时复用机制。
原子性问题的原因是:多线程操作共享变量时,一个线程还未对该变量操作完成,由于分时复用策略,另外一个线程获取了执行权,这个线程获取到的值有可能是错误的。
常见的面试题:long型的变量在32位系统的高并发应用程序中,为什么会有线程安全问题?
原因:long型变量是64位的,32位操作系统对long型变量赋值的操作步骤为:先对高32位赋值,再对低32位赋值;这样,如果中间发生了线程切换,就可能获取到错误的值
3、编译优化带来的有序性问题
有序性问题是有编译器会对我们的指令进行优化重排序,这样不会影响最终的执行结果;但是有时还是会发生一些意想不到的问题;
举例:单例模式下双重检查
public class Singleton {
private static Singleton instance = null;
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
return instance;
}
}
}
return instance;
}
}
第一层判空是为了避免加锁导致的性能问题;第二层判空是为了避免创建多个实例;这看起来并没有什么问题,但是由于编译器指令重排,可能会出现问题。
正常创建实例的指令顺序为:分配内存--->内存初始化---->变量指向内存地址
编译优化后指令顺序可能为:分配内存--->变量指向内存地址--->内存初始化
如果线程执行到第二步的时候被剥夺执行权,另一个线程判空的结果为非空,从而直接返回了instance;由于此时instance未初始化,可能会导致空指针异常
并发带来的三个问题
1、安全性问题
安全性问题的本质就是数据的正确性,为了保证线程安全,应该避免同一时刻不同线程操作共享数据。
2、活跃性问题
饥饿:由于线程优先级低等原因,可能会导致线程一直不能被执行
死锁:线程竞争共享资源,并且互相持有对方的锁,造成多个线程一直等待,造成死锁。
活锁:和死锁相反,活锁是由于“过于谦让”导致的问题;线程访问共享资源,发现另一个线程也需要访问共享资源,于是退出,等待重试;
另外的线程也是如此,因此出现活锁问题。
3、性能问题
锁的过度使用,导致程序串行执行的范围过大,这样就违背了并发编程的优势;
在实际应用中,应尽量减少不必要锁的使用,尽量减少串行
并发编程001 --- 初识并发