Synchronized 的特性
- 有序性 (读读、读写、写读、写写 互斥)
- 可见性 (可见性是指多个线程访问⼀个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronized和volatile都具有可见性,其中synchronized对⼀个类或对象加锁时,⼀个线程如果要访问该类或对象必须先获得它的锁,⽽这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。)
- 原子性 (本质上是线程互斥保证的原子性)
- 可重入性
可重入性原理
可重入锁又称递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提是锁对象必须是同一对象或者class),不会因为之前已经获取过还没实方而发生阻塞。即同一线程可执行多个持有同一个锁的方法。
/**
* Synchronized
* 同步方法或者同步代码块 Synchronized 使用同一把锁对象可实现:可重入锁
*/
public class SynchronizedRepeatLockDemo {
public static void main(String[] args) {
new SynchronizedLock().m1();
}
}
/**
* javap -c SynchronizedLock.class 反编译来解析synchronized可重入锁原理
* synchronized通过monitor计数器实现
* 当执行monitorenter命令时:
* 判断当前monitor计数器值是否为0
* 如果为0,则说明当前线程可直接获取当前锁对象
* 如果不为0,判断当前线程是否和获取锁对象线程是同一个线程
* 若是同一个线程,则monitor计数器累加1,当前线程能再次获取到锁
* 若不是同一个线程,则只能等待其它线程释放锁资源
* 当执行完synchronized锁对象的代码后,就会执行monitorexit命令,
* 此时monitor计数器就减1,直至monitor计数器为0时,说明锁被释放了。
*/
class SynchronizedLock {
static Object lockB = new Object();
public void m1() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + " 外层执行!");
m2();
}
}
public void m2() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + " 中层执行!");
m3();
}
}
public void m3() {
synchronized (lockB) {
System.out.println(Thread.currentThread().getName() + "\t" + " 内层执行!");
}
}
}
Synchronized 锁升级 - Mark Word (32 位虚拟机)
Synchronized 锁升级 - 偏向锁
偏向锁使用的前提:
- 至少JDK1.6 版本且开启了偏向锁配置。
偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。 - 被加锁的对象,没有真正、或者隐式的调用父类 Object 里边的hashcode方法。
为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
死锁
死锁产生的四个必要条件:
- 互斥:一个资源每次只能被一个进程使用 (资源独立)。
- 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放 (不释放锁)。
- 不剥夺:进程已获得的资源,在未使用之前,不能强行剥夺 (抢夺资源)。
- 循环等待:若干进程之间形成一种头尾相接的循环等待的资源关闭 (死循环)。
如何避免死锁?
- 破坏” 互斥” 条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般 “互斥” 条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏 “互斥” 条件。
- 破坏 “请求和保持” 条件:
方法 1:所有的进程在开始运行之前,必须一次性的申请其在整个运行过程各种所需要的全部资源。
优点:简单易实施且安全。
缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。
方法 2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到,已经使用完毕的资源,然后再去请求新的资源。这样的话资源的利用率会得到提高,也会减少进程的饥饿问题。 - 破坏 “不剥夺” 条件:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。
- 破坏 “循环等待” 条件:可以通过定义资源类型的线性顺序来预防,可以将每个资源编号,当一个进程占有编号为 i 的资源时,那么它下一次申请资源只能申请编号大于 i 的资源。
现在我们介绍避免死锁的几个常见方法。