synchronized关键字和锁

示例代码

public class MyThreadTest2 {

    public static void main(String[] args) {
MyClass myClass = new MyClass();
MyClass myClass2 = new MyClass(); Thread t1 = new Thread1(myClass);
//Thread t2 = new Thread2(myClass);
Thread t2 = new Thread2(myClass2); t1.start(); try {
Thread.sleep(700);
} catch (InterruptedException e) {
e.printStackTrace();
} t2.start();
}
} class MyClass { public synchronized void hello () {
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
} System.out.println("hello");
} public synchronized void world() {
System.out.println("world");
}
} class Thread1 extends Thread { private MyClass myClass; public Thread1(MyClass myClass) {
this.myClass = myClass;
} @Override
public void run() {
myClass.hello();
}
} class Thread2 extends Thread { private MyClass myClass; public Thread2(MyClass myClass) {
this.myClass = myClass;
} @Override
public void run() {
myClass.world();
}
}

精通java并发-synchronized关键字和锁-LMLPHP

精通java并发-synchronized关键字和锁-LMLPHP

精通java并发-synchronized关键字和锁-LMLPHP

精通java并发-synchronized关键字和锁-LMLPHP

结论

精通java并发-synchronized关键字和锁-LMLPHP

透过字节码理解synchronized关键字

修饰代码块

public class MyTest1 {

    private Object object = new Object();

    public void method() {
synchronized (object) {
System.out.println("hello world");
throw new RuntimeException();
}
} public void method2() {
synchronized (object) {
System.out.println("welcome");
}
}
}
  • method方法为一定抛异常的方法

  • method2方法为一个普通方法

  • 通过javap -v MyTest1.class查看字节码文件

精通java并发-synchronized关键字和锁-LMLPHP

精通java并发-synchronized关键字和锁-LMLPHP

修饰普通方法和静态方法

public class MyTest2 {
public synchronized void method() {
System.out.println("hello world");
}
}

精通java并发-synchronized关键字和锁-LMLPHP

public class MyTest3 {
public static synchronized void method() {
System.out.println("hello world");
}
}

精通java并发-synchronized关键字和锁-LMLPHP

monitor

  • 总结一下:同步锁在这种实现方式当中,因为Monitor是依赖于底层的操作系统实现,这样就存在用户态与内核态之间的切换,所以会增加性能开销。
  • 自旋(Spin)

互斥锁

普通锁(PTHREAD_MUTEX_TIMED_NP)

  • 当一个线程加锁以后,其余请求锁的线程将会形成一个等待队列,并且在解锁后按照优先级获取到锁。这种策略可以确保资源分配的公平性。(缺省值)

嵌套锁(PTHREAD_MUTEX_RECURSIVE_NP)

  • 允许一个线程对同一个锁成功获取多次,并通过unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新进行竞争。

检错锁(PTHREAD_MUTEX_ERRORCHECK_NP)

  • 如果一个线程请求同一个锁,则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同,这样就保证了当不允许多次加锁时不会出现最简单情况下的死锁。

适应锁(PTHREAD_MUTEX_ADAPTIVE_NP)

  • 适应锁,动作最简单的锁类型,仅仅等待解锁后重新竞争。

synchronized关键字的优化

  • 在JDK 1.5之前,我们若想实现线程同步,只能通过synchronized关键字这一种方式来达成;底层,Java也是通过synchronized关键字来做到数据的原子性维护的;synchronized关键字是JVM实现的一种内置锁,从底层角度来说,这种锁的获取与释放都是由JVM帮助我们隐式实现的。

  • 从JDK 1.5开始,并发包引入了Lock锁,Lock同步锁是基于Java来实现的,因此锁的获取与释放都是通过Java代码来实现与控制的;然而,synchronized是基于底层操作系统的Mutex Lock来实现的,每次对锁的获取与释放动作都会带来用户态与内核态之间的切换,这种切换会极大地增加系统的负担;在并发量较高时,也就是说锁的竞争比较激烈时,synchronized锁在性能上的表现就非常差。

  • 从JDK 1.6开始,synchronized锁的实现发生了很大的变化;JVM引入了相应的优化手段来提升synchronized锁的性能,这种提升涉及到偏向锁、轻量级锁及重量级锁等,从而减少锁的竞争所带来的用户态与内核态之间的切换;这种锁的优化实际上是通过Java对象头中的一些标志位来去实现的;对于锁的访问与改变,实际上都与Java对象头息息相关。

  • 从JDK 1.6开始,对象实例在堆当中会被划分为三个组成部分:对象头、实例数据与对齐填充。

    • 对象头主要也是由3块内容来构成:

      • Mark Word

      • 指向类的指针

      • 数组长度

    • 其中Mark Word(它记录了对象、锁及垃圾回收相关的信息,在64位的JVM中,其长度也是64bit)的位信息包括了如下组成部分:

      • 无锁标记
      • 偏向锁标记
      • 轻量级锁标记
      • 重量级锁标记
      • GC标记
  • 对于锁的演化来说,它会经历如下阶段:

偏向锁

如果是另外一个线程访问这个synchronized方法,那么实际情况会如何呢?

偏向锁会被取消掉。

轻量级锁

自旋锁

重量级锁

编译器对于锁的优化措施

锁消除技术

public class MyTest4 {
// private Object object = new Object();
public void method() {
Object object = new Object();
synchronized (object) {
System.out.println("hello world");
}
}
}

锁粗化

public class MyTest5 {
private Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello world");
}
synchronized (object) {
System.out.println("welcome");
}
synchronized (object) {
System.out.println("person");
}
}
}

死锁,活锁和饿死

  • 死锁:线程1等待线程2互斥持有的资源,而线程2也在等待线程1互斥持有的资源,两个线程都无法继续执行

  • 活锁:线程持续重试一个总是失败的操作,导致无法继续执行

  • 饿死:线程一直被调度器延迟访问其赖以执行的资源,也许是调度器先于低优先级的线程而执行高优先级的线程,同时总是会有一个高优先级的线程可以执行,饿死也叫做无限延迟

05-11 11:28