并发编程的三个核心问题:java
这个其实不难理解,作个简单的比喻,咱们团队作一个项目的时候确定是先分配任务(分工),而后等到任务完成进行合并对接(同步),在开发过程当中,使用版本控制工具访问,一个代码只能被一我的修改,不然会报错,须要meger(互斥).程序员
学习攻略:编程
核心: 分工(拆分) - 同步(一个线程执行完成如何通知后续任务的线程开始工做) - 互斥(同一时刻,只容许一个线程访问共享变量)缓存
全景: 多线程
本质 : 知其然知其因此然,有理论作基础.技术的本质是背后的理论模型并发
我从个人角度看,一个是并发编程的API不是很了解,第二个就是出现了问题不会解决,若是说还有,那就是是在不知道并发编程是用来干啥的?有什么用?app
每一中技术的出现都有他出现的必然性,对于并发来讲无疑是提升性能,那单线程为啥就不能提升性能,缘由就在于CPU,内存和IO设备三者的速度差别太大,举个例子来讲: CPU一天,内存一年,IO一百年; 而木桶理论告诉咱们程序的性能是由短板决定,因此只要合理的平衡三者的速度差别,就能够提升性能.函数
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
上面是经典的双重检查建立单例对象,在咱们的印象中new的操做应该是: 分配内存,在内存上初始化对象,地址赋值. 实际上优化后是: 分配内存,地址赋值,初始化. 优化后的顺序就会出现问题,地址赋值后发生了线程切换,这时候其余线程读取到了对象不为null,可是实际上只有地址,这个时候访问成员变量就会出现空指针异常,这个就是编译优化可能会出现的问题.工具
也就是说,不少的并发Bug是由可见性,原子性,有序性的原理形成的,从这三个方面去考虑,能够理解诊断很大部分一部分Bug. 缓存致使可见性问题,线程切换带来的原子性,编译优化带来的有序性,本质都是提升程序性能,可是在带来性能的时候可能也会出现其余问题,因此在运用一项技术的时候必定要清楚它带来的问题是什么,以及如何实现.性能
可见性的缘由是缓存,有序性的缘由是编译优化,那解决的最直接的办法就是禁用缓存和编译优化,可是有缓存和编译优化的目的是提升程序性能,禁用了程序的性能如何保证? 合理的方案是按需禁用缓存和编译优化,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法,具体的,这些方法包括volatile,synchronized和final三个关键字,以及六项Happens-Before规则
volatile关键字用来声明变量,告诉编译器这个变量的读写不能使用CPU缓存,必须从内存中读写.
// 如下代码来源于【参考 1】 class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 这里 x 会是多少呢? } } }
上面的代码x的值是多少呢?直觉上应该是42,可是在jdk1.5以前,可能的值是0或者42,1.5以后就是42,为何?缘由是变量x可能被CPU缓存而致使可见性问题,也就是x=42可能不被v=true可见,那Java的内存模型在1.5版本以后是如何解决的呢? 就是Happens-before规则.
Happens-before指的是前一个操做的结果对后续操做是可见的,具体以下.
这个规则说的是在一个线程中,按照程序顺序,前面的操做Happens-Before于后续的任意操做. 简单理解就是: 程序前面对于某个变量的修改必定是对后续操做可见的.也就是前面的代码x=42对于v=true是可见的.
这条规则指的是对一个volatile变量的写操做,Happens-Before于后续对这个volatile变量的读操做,即volatile变量的写操做对于读操做是可见的.
这条规则指的是A Happens-Before C,且B Happens-Before C,那么A Happens-Before C,以下图:
这样就很明显了,x=42 Happens-Before v=true,写v=true Happens-Before 读v=true,那也就是说x=42 Happens Before 读v=true,这样下来,其余线程就能够看到x=42这个操做了.
这个规则是指对一个锁的解锁Happens-Before与后续对这个锁的加锁. 管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现.管程中的锁在Java中是隐式实现的,也就是进入同步块以前,会自动加锁,而在代码块执行完后自动释放锁,加锁以及解锁都是编译器帮咱们实现的.
synchronized (this) { // 此处自动加锁 // x 是共享变量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此处自动解锁
这个是线程启动的,指的是主线程A启动子线程B,子线程B可以看到主线程在启动子线程B前的操做.
Thread B = new Thread(()->{ // 主线程调用 B.start() 以前 // 全部对共享变量的修改,此处皆可见 // 此例中,var==77 }); // 此处对共享变量 var 修改 var = 77; // 主线程启动子线程 B.start();
这条规则是关于线程等待的.它是指主席爱能成A经过调用子线程B的join方法,子线程B执行完成以后,主线程能够看到子线程中的操做.这里指的是对共享变量的操做.
Thread B = new Thread(()->{ // 此处对共享变量 var 修改 var = 66; }); // 例如此处对共享变量修改, // 则这个修改结果对线程 B 可见 // 主线程启动子线程 B.start(); B.join() // 子线程全部对共享变量的修改 // 在主线程调用 B.join() 以后皆可见 // 此例中,var==66
final修饰变量是告诉编译器: 这个变量生而不变,能够可劲儿优化.在 1.5 之后 Java 内存模型对 final 类型变量的重排进行了约束。如今只要咱们提供正确构造函数没有“逸出”,就不会出问题了。下面的例子,在构造函数里将this赋值给全局变量global.obj,这就是逸出(逸出就是对象尚未构造完成,就被发布出去),线程global.obj读取到x有可能读到0.
// 如下代码来源于【参考 1】 final int x; // 错误的构造函数 public FinalFieldExample() { x = 3; y = 4; // 此处就是讲 this 逸出, global.obj = this; }
在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来讲是可见的,不管 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
前面看了Java的内存模型,解决了可见性和编译优化的重排序问题,哪还有一个原子性如何解决?答案就是使用互斥锁实现.
先探究源头,long在32位机器上操做可能出现Bug,缘由是线程的切换,那只要保证同一时刻只有一个线程执行,就能够了,这就是互斥.
互斥锁模型:
Java中如何实现这种互斥锁呢?
java中的synchronized关键字就是锁的一种实现,synchronized关键字能够用来修饰方法,也能够用来修饰代码块,以下:
class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } } }
先说一下那个加锁和释放锁,synchronized并无显示的进行这一操做,而是编译器会在synchronized修饰的方法或代码块先后自动加锁lock()和解锁unlock(),不须要编程人员手动加锁和释放锁(省的忘记,程序员很忙的).
synchronized锁的规则是什么: 当修饰静态方法的时候,锁定的是当前的类对象. 修饰非静态方法和代码块的时候,锁定的是当前的对象this.以下
class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 } } class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 } }
下面的代码能够解决多线程问题吗?
class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }
答案是并不能够,缘由是虽然对addOne进行了加锁操做(对一个锁的解锁Happens-Before于后续对这个锁的加锁),保证了后续addOne的操做的共享变量是能够看到前面addOne操做后的共享变量的值,可是get方法却没有,多个线程get方法可能获取到的值相同,addOne()以后就会乱套,因此并不能解决.那下面的代码能够解决问题吗?
class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; } }
这种是能够解决多线程问题,也就是能够解决多个线程操做同一个对象的并发问题.那若是要解决多个线程操做不一样对象的并发问题呢?
受保护资源和锁之间的关联关系是N:1的关系.也就是说一个锁能够保护多个受保护的资源,这个就是现实生活中的包场,可是我以为这个也要分状况,多个受保护的资源和锁之间必定要有关系,否则锁不起做用就麻烦了,举个例子来讲就是本身家门的锁确定保护本身东西,不能用本身家门的锁去保护别人家的东西.
下面的例子:
class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; } }
分析如图:
因此说addOne对value的修改对临界区get()没有可见性保证,会致使并发问题.将get方法也改成静态的就能够解决了.
synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有不少其余类型的锁,但做为互斥锁,原理都是相通的:锁,必定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情了。
受保护的资源和锁之间合理的关联关系应该是N:1的关系.使用一把锁保护多个资源也是分状况的,在于多个资源之间存不存在关系,这是要分状况讨论的.
举个例子来讲明,Account类有两个成员变量,分别是帐户余额balance和帐户密码password. 取款和查看余额会访问balance,建立一个final对象balLock来做为balance的锁;更改密码和查看密码会操做password,建立一个final对象pwLock来做为password的锁.不一样的资源用不一样的锁保护.代码示例以下:
class Account { // 锁:保护帐户余额 private final Object balLock = new Object(); // 帐户余额 private Integer balance; // 锁:保护帐户密码 private final Object pwLock = new Object(); // 帐户密码 private String password; // 取款 void withdraw(Integer amt) { synchronized(balLock) { if (this.balance > amt){ this.balance -= amt; } } } // 查看余额 Integer getBalance() { synchronized(balLock) { return balance; } } // 更改密码 void updatePassword(String pw){ synchronized(pwLock) { this.password = pw; } } // 查看密码 String getPassword() { synchronized(pwLock) { return password; } } }
那还有没有其余的解决方案? 可使用this来进行加锁,可是这种状况性能会不好,由于password和balance使用同一把锁,操做也就串行了,使用两把锁,password和balance的操做是能够并行的,用不一样的锁对受保护资源进行精细化关系,可以提高性能.这个叫细粒度锁
若是多个资源之间有关联关系,那就比较复杂,经典的转帐问题.看下面代码可能发生并发问题吗?
class Account { private int balance; // 转帐 synchronized void transfer( Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
开起来没问题,其实否则,只对当前对象进行了加锁,那目标对象的访问呢?也就是说当前的对象是没法保护target.balance的.
上面的案例两我的之间的转帐或许没有问题,可是涉及三我的呢?
这个时候B的余额可能为100,也可能为300,看哪一个执行在后了.那应该如何解决这种有关联的资源呢,找公共的锁就能够,也就是要锁能覆盖全部受保护资源,解决方案其实很多,以下
class Account { private Object lock; private int balance; private Account(); // 建立 Account 时传入同一个 lock 对象 public Account(Object lock) { this.lock = lock; } // 转帐 void transfer(Account target, int amt){ // 此处检查全部对象共享的锁 synchronized(lock) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
这个解决方案缺点在于须要传入共享的lock,还有一种方案
class Account { private int balance; // 转帐 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
这个是否是很简单.
上图展现了如何使用共享的锁来保护不一样对象的临界区.
解决原子性问题,是要保证中间状态对外不可见.