并发编程有3个源头性问题:缓存致使的可见性问题,编译优化致使的有序性问题,以及线程切换致使的原子性问题。解决可见性问题和有序性问题的方法是按需禁用缓存和编译优化,Java的内存模型就是一种按需禁用缓存和编译优化的规则,它规定了 JVM 如何提供相关的方法,这些已经在Java内存模型与Hppens-Before规则进行了描述。java
咱们把一个或者多个操做在 CPU 执行过程当中不被中断的特性称为原子性。因为操做系统的时间片轮起色制,以及高级语言可能包含多个指令,致使一句高级语言在执行过程当中可能出现线程切换。在并发编程中就会由于线程切换致使原子性问题。编程
锁模型是解决原子性问题的通用方案。线程在进入临界区以前必须持有锁,退出临界区时释放锁,此时其余线程就能再次获取锁。segmentfault
锁与资源之间是 1:N 的关系,即一把锁能够保护多个资源。同时要注意不能用本身的锁保护别人的资源;要让代码实现互斥,必须使用同一把锁。缓存
synchronized
关键字是 Java 语言对锁模型的实现,它能够修饰方法或者代码块,被修饰的方法和代码块会隐式地添加lock()
和unlock()
方法。并发
现代操做系统都是基于线程的分时调度系统,CPU会为线程分配时间片,线程分配都时间片就获取到CPU的使用权。好比说线程 A 读取文件,它能够将本身标记为「休眠状态」,让出 CPU 的使用权。文件读取完成以后,操做系统再将其唤醒,线程 A 就有机会从新得到 CPU 的使用权。app
线程切换为何致使并发问题呢?Java 是一门高级语言,高级语言的一条语句每每包含多个 CPU 指定,好比说 count += 1
这条语句,至少包含 3 条 CPU 指令:优化
操做系统以指令为单位执行,期间伴随着线程切换。这就致使 count += 1
执行到一半,就有可能碰到线程切换,致使并发问题的产生,以下图所示:this
咱们把一个或者多个操做在 CPU 执行过程当中不被中断的特性称为原子性,即咱们指望 count += 1
在执行过程当中是原子同样的,不可分割的总体,线程切换不会在执行这条语句相关的CPU指令时发生,但容许线程切换在count += 1
执行以前或者以后发生。spa
锁模型是一种解决原子性问题的通用技术方案。在锁模型中,临界区是一段要互斥执行的代码,在进入临界区以前咱们要执行 lock()
操做持有锁,只有获取到锁的线程才能执行临界区的代码;执行完临界区代码执行 unlock()
操做释放锁,此时其余线程就能够尝试获取锁。操作系统
在现实生活中,咱们用锁来保护咱们的东西,但不能用本身的锁来锁别人的东西。在锁模型中,锁与临界区中被保护的资源也有着关联关系,图中用箭头来表示它们之间的关联。
咱们不能用一把锁来保护范围以外的资源,代码要实现互斥则要使用同一把锁。
锁是一种通用的技术方案,Java 语言提供的 synchronized
关键字,就是锁的一种实现。synchronized
关键字能够用来修饰方法,也能够用来修饰代码块,它的使用示例基本上都是下面这个样子:
class X { // 修饰非静态方法 synchronized void foo() { // 临界区 } // 修饰静态方法 synchronized static void bar() { // 临界区 } // 修饰代码块 Object obj = new Object(); void baz() { synchronized(obj) { // 临界区 } } }
前面说过,锁模型中有锁以及它保护的资源,synchronized 修饰代码块的时候锁显然是 obj 对象,那么 synchronized 修饰非静态方法和静态方法的时候,它建立的锁是什么呢?
Java 中有一条隐式规则:
当修饰静态方法的时候,锁定的是当前类的 Class 对象; 当修饰非静态方法的时候,锁定的是当前实例对象 this。
至关于
class X { // 修饰静态方法 synchronized(X.class) static void bar() { // 临界区 } } class X { // 修饰非静态方法 synchronized(this) void foo() { // 临界区 } }
锁能够保护一个或者多个资源。咱们能够用一个范围较大的锁,好比说 X.class
保护多个相关的资源;也能够用不一样的锁对被保护资源进行精细化管理,这就叫细粒度锁。
class SafeCalc { long value = 0L; long get() { return value; } synchronized void addOne() { value += 1; } }
这是一段想解决 count += 1
问题的代码,咱们对 addOne()
使用 synchronized 加上互斥锁,能够保证其原子性。根据 Happens-before 管程中锁的规则:对一个锁的解锁 Happens-Before 于后续对这个锁的加锁,也能够保证其可见性。即便是 1000 个线程同时执行 addOne()
也能够保证 value 增长 1000。
但咱们没法保证 get()
的可见性,管程中锁的规则,是只保证后续对这个锁的加锁的可见性,而 get() 方法并无加锁操做,因此可见性无法保证。因此咱们给 get()
也加上锁:
class SafeCalc { long value = 0L; synchronized long get() { return value; } synchronized void addOne() { value += 1; } }
此时 get()
和 addOne()
都持有 this 这把锁,此时 get()
和 addOne()
是互斥的,而且保证了可见性,缩模型以下图所示:
若是将 value 改成 static 的,addOne()
变为静态方法:
class SafeCalc { static long value = 0L; synchronized long get() { return value; } synchronized static void addOne() { value += 1; } }
此时 get()
和 addOne()
分别持有不一样的锁,get()
和 addOne()
不互斥,也就不能保证可见性,就会致使并发问题。
如今要写一个银行转帐的方法,用户 A 给用户 B 转帐,将其转换成代码:
class Account { private int balance; // 转帐 synchronized void transfer( Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
用户 A 给用户 B 转帐 100,要保证 A 的余额减小 100,B 的余额增长 100。因为转帐操做能够是并发的,因此要保证转帐操做没有并发问题。好比说 A 的余额只有 100,两个线程分别执行 A 给 B 转帐 100,A 给 C 转帐 100,这两个线程有可能同时从内存中读取到 A 的余额是 100,这就产生了并发问题。
解决这个问题的第一反应,就是给 transfer(Account target, int amt)
加上 synchronized。这样作真的对么?transfer()
此时有两个须要被保护的资源 target.balance
和 this.balance
即别人钱和本身的钱,但咱们使用的锁是 this
锁,以下图所示:
本身的锁 this
能保护本身的 this.balance
可是没法保护别人的 target.balance
,就像个人锁不能即保护我家的东西,又保护你家的东西同样。
因此咱们须要一把锁的范围更大一点,让它可以覆盖到全部的被保护资源,好比说传入同一个对象做为锁:
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; } } } }
或者使用类锁 Accout.class
,因为 Accoutn.class
是在 Java 虚拟机加载 Account 类时建立的,因此 Account.class
是全部 Account 对象共享且惟一的一把锁。
class Account { private int balance; // 转帐 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
Accout.class
就能够同时保护两个不一样对象的临界区资源: