Java高并发之魂:synchronized深度解析

本文整理自慕课网的讲师悟空老师,教学地址:http://www.imooc.com/learn/1086

一、synchronized简介

1 synchronized作用

1.1 官方翻译

同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

1.2 一句话概括synchronized作用

能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果。

1.3 如何实现

如果一段代码被synchronized关键词修饰,那么这段代码就会以原子的方式执行,多个线程在执行该段代码时不会相互干扰,因为多个线程不会同时执行该段代码,从而不会发生并发问题。如何保证其它线程不执行该段代码的呢?当一个线程执行改段代码,那么该线程将会获得一把锁,它将独占该段代码,其它等待执行该段代码的线程都将等待、堵塞,直到代码执行完毕或者在一定的条件下释放该锁,其它获取到该锁的线程才能执行该段代码。

2 synchronized地位

  • Synchronized是Java的关键字,被Java语言原生支持。
  • 是最基本的互斥同步手段。
  • 是并发编程中的元老级角色,是并发编程的必学内容。

3 不用并发手段的后果

3.1 代码实战:两个线程同时a++ ,最后结果会比预计的少

public class DisappearRequest1 implements Runnable {
    static DisappearRequest1 instance = new DisappearRequest1();
    static int i = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        //t1 t2线程执行完毕后再打印
        t1.join();
        t2.join();
        //结果小于等于20000
        System.out.println(i);
    }

    @Override
    public void run() {
        for (int j = 0; j < 100000; j++) {
            i++;
        }
    }
}

为什么会造成这种后果呢?
count++ ;它看上去只是一个操作,实际上包含了三个动作:

  1. 读取 count。
  2. 将 count 加1。
  3. 将 count的值写入到内存中。假设count为1,线程A进入该方法进行加1操作后count变成2,还未写入内存。线程B进入该方法对读取的count依旧为1,进行加1后count也是2,因此无论是A还是B写入内存的数值都是2,预期中本来应该为3的数值变成了2。

二、synchronized两类用法

2.1 用法介绍

  1. 对象锁:包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定锁对象)
  2. 类锁:指synchronized修饰静态的方法或指定锁为Class对象。

2.2 对象锁

2.2.1 使用this对象锁修饰代码块

this对象锁只有一个,如果有多个需要同步的代码块,他们执行的要求不是一个执行其它都不允许执行,那么就不能使用this对象锁了,需要自定义多个对象来获取锁

public class Synchronized0bjectCodeBlock1 implements Runnable {
    static Synchronized0bjectCodeBlock1 instance = new Synchronized0bjectCodeBlock1();

    @Override
    public void run() {
        /**
         * 获取this,当前实例对象锁,this只有一份
         */
        synchronized (this) {
            System.out.println("我是对象锁的代码块形式。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束。");
        }
    }

    public static void main(String[] args) {
        Thread tl = new Thread(instance);
        Thread t2 = new Thread(instance);
        tl.start();
        t2.start();
        while (tl.isAlive() || t2.isAlive()) {
        }
        System.out.println("thread:"+Thread.currentThread().getName()+"is finished");
    }
}

2.2.2 自定义对象锁修饰代码块

public class Synchronized0bjectCodeBlock2 implements Runnable {
    static Synchronized0bjectCodeBlock2 instance = new Synchronized0bjectCodeBlock2();
    Object lock1 = new Object();
    Object lock2 = new Object();
    @Override
    public void run() {
        /**
         * 获取this,当前实例对象锁,this只有一份
         */
        synchronized (lock1) {
            System.out.println("我是对象锁的代码块形式。我叫 lock1 " + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " lock1部分运行结束。");
        }
        synchronized (lock2) {
            System.out.println("我是对象锁的代码块形式。我叫 lock2 " + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " lock2部分运行结束。");
        }
    }

    public static void main(String[] args) {
        Thread tl = new Thread(instance);
        Thread t2 = new Thread(instance);
        tl.start();
        t2.start();
        while (tl.isAlive() || t2.isAlive()) {
        }
        System.out.println("thread:"+Thread.currentThread().getName()+" is finished");
    }
}

2.2.3 普通方法锁

普通方法锁使用的是this对象锁

public class Synchronized0bjectMethodBlock implements Runnable {
    static Synchronized0bjectMethodBlock instance = new Synchronized0bjectMethodBlock();

    @Override
    public void run() {
        method();
    }

    public synchronized void method() {
        System.out.println("我的对象锁的方法修饰符形式,我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

    public static void main(String[] args) {
        Thread tl = new Thread(instance);
        Thread t2 = new Thread(instance);
        tl.start();
        t2.start();
        while (tl.isAlive() || t2.isAlive()) {
        }
        System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
    }
}

2.3 类锁

2.3.1 概念

  1. 概念(重要):Java类可能有很多个对象,但只有1个Class对象。
  2. 本质:类锁就是Class对象锁,它是概念上的,并非真实存在,用于帮助我们理解实例方法与静态方法区别。
  3. 用法和效果:类锁只能同一时刻被同一对象使用。
  4. 形式:synchronized加在static方法上;synchronized ( *.class )代码块;

2.3.2 类锁的形式一:静态方法锁

public class SynchronizedClassStatic implements Runnable {
    static SynchronizedClassStatic instance1 = new SynchronizedClassStatic();
    static SynchronizedClassStatic instance2 = new SynchronizedClassStatic();

    @Override
    public void run() {
        method();
    }
	//去掉synchronized 关键字,两线程将并行运行该方法。
    public static synchronized void method() {
        System.out.println("我是类锁的第一种形式形式:static形式。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

    public static void main(String[] args) {
        Thread tl = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        tl.start();
        t2.start();
        while (tl.isAlive() || t2.isAlive()) {
        }
        System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
    }
}

2.3.2 类锁的形式二:*.class代码块

public class SynchronizedClassClass implements Runnable {
    static SynchronizedClassClass instance1 = new SynchronizedClassClass();
    static SynchronizedClassClass instance2 = new SynchronizedClassClass();

    @Override
    public void run() {
        method();
    }

    public void method() {
        synchronized (SynchronizedClassClass.class) {
            System.out.println("我是类锁的第二种形式形式:synchronized(*.class)。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

    public static void main(String[] args) {
        Thread tl = new Thread(instance1);
        Thread t2 = new Thread(instance2);
        tl.start();
        t2.start();
        while (tl.isAlive() || t2.isAlive()) {
        }
        System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
    }
}

三 多线程访问同步方法的7种情况

  1. 两个线程同时访问一个对象的同步方法。

    由于同步方法锁使用的是this对象锁,同一个对象的this锁只有一把,两个线程同一时间只能有一个线程持有该锁,所以该方法将会串行运行。

  2. 两个线程访问的是两个对象的同步方法。

    由于两个对象的this锁互不影响,synchronized将不会起作用,所以该方法将会并行运行。

  3. 两个线程访问的是synchronized的静态方法。

    synchronized修饰的静态方法获取的是当前类模板对象的锁,该锁只有一把,无论访问多少个该类对象的方法,都将串行执行。

  4. 同时访问同步方法与同步方法

    非同步方法不受影响。

  5. 访问同一个对象的不同的普通同步方法。

    由于this对象锁只有一个,不同线程访问多个普通同步方法将串行运行。

  6. 同时访问静态synchronized和非静态synchronized方法

    静态synchronized方法的锁为class对象的锁,非静态synchronized方法锁为this的锁,它们不是同一个锁,所以它们将并行运行。

  7. 方法抛异常后,会释放锁。(与Lock类不同,Lock类必须手动释放锁)

    下面代码演示了方法一抛出异常后JVM会帮助线程当前线程释放锁,方法二线程会立即获取锁正常执行代码。

    public class SynchronizedException implements Runnable {
        static SynchronizedException instance = new SynchronizedException();
    
        @Override
        public void run() {
            if ("Thread-0".equals(Thread.currentThread().getName())) {
                method1();
            } else {
                method2();
            }
        }
    
        public synchronized void method1() {
            System.out.println("我抛出异常的同步方法。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            throw new RuntimeException();
        }
        public synchronized void method2() {
            System.out.println("我正常运行的同步方法。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    
        public static void main(String[] args) {
            Thread tl = new Thread(instance);
            Thread t2 = new Thread(instance);
            tl.start();
            t2.start();
            while (tl.isAlive() || t2.isAlive()) {
            }
            System.out.println("thread:" + Thread.currentThread().getName() + " is finished");
        }
    }
    
    运行结果
    我抛出异常的同步方法。我叫Thread-0
    我正常运行的同步方法。我叫Thread-1
    Exception in thread "Thread-0" java.lang.RuntimeException
    	at com.example.demo.SynchronizedException.method1(SynchronizedException.java:22)
    	at com.example.demo.SynchronizedException.run(SynchronizedException.java:9)
    	at java.lang.Thread.run(Thread.java:748)
    Thread-1运行结束
    thread:main is finished
    

总结

  1. —把锁只能同时被一个线程获取,没有拿到锁的线程必须等待;
  2. 每个实例都对应有自己的一把锁,不同实例之间互不影响;例外:锁对象是Class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁;
  3. 无论是方法正常执行完毕或者方法抛出异常,都会释放锁;

四、synchronized性质

4.1 可重入性质

4.1.1 概念

同一线程的外层函数获得锁之后,内层函数可以直接再次获取该锁。

4.1.2 举例

北京买车需要上牌照,但是需要摇号,如果我摇到一个号,但是家里有三辆车,我能不能给家里三辆车都上牌照?但是,不行,每次摇到一个号对应一次上牌照的权力,这就是不可重入。如果是可重入的,情况就是这样,我摇到一个号就可以一直获取牌照,直到我自己不愿意获取到更多了,主动结束。
这里的“”代表线程,“摇到号”代表获取到了锁,“获取牌照的过程”就是使用锁,摇到一个号只能获取一次牌照就是不可重入的,摇到一个号可以无限次数获取牌照就是可重入的。
所以一个线程拿一旦拿到一把锁,它可以对锁进行多次使用,那么就是可重入的,所以可重入锁也叫递归锁;如果一个线程拿到一把锁后,如果想要再次使用就必须先释放锁,然后和其它线程进行竞争,这就是不可重入。

4.1.3 好处

  1. 避免死锁:比如说有两个方法A、B都被synchronized修饰了,线程1执行到了方法A,获取了这把锁,要想执行方法B也需要获取该锁。假设synchronized不具备可重入性质,线程1执行方法A虽然获取了该锁,但是想要访问方法B不能直接使用该锁,此时既想获取锁又不想释放锁,这就造成永远等待即死锁。
  2. 提升封装性:避免了多次解锁加锁过程,简化了开发难度。
  3. 粒度:synchronized的默认加锁范围是线程。

4.1.4 可重入粒度测试

4.1.4.1 证明同一个方法是可重入的

以下方法能够正常执行

public class SynchronizedRecursion {
    int a = 0;

    public static void main(String[] args) {
        SynchronizedRecursion synchronizedRecursion1 = new SynchronizedRecursion();
        synchronizedRecursion1.method1();
    }

    private synchronized void method1() {
        System.out.println("这是 method1,a = " + a);
        if (a == 0) {
            a++;
            method1();
        }
    }
}

4.1.4.2 证明可重入不要求是同一个方法

以下代码能够正常执行

public class SynchronizedOtherMethod {
    private synchronized  void method1() {
        System.out.println("我是method1");
        method2();
    }

    private synchronized  void method2() {
        System.out.println("我是method2");
    }

    public static void main(String[] args) {
        SynchronizedOtherMethod synchronizedOtherMethod = new SynchronizedOtherMethod();
        synchronizedOtherMethod.method1();
    }
}

4.1.4.3 证明可重入不要求是同一个类中的

以下方法能够正常执行

public class SynchronizedSuperClass {
    protected synchronized void method() {
        System.out.println("我是父类方法");
    }
}
class SynchronizedChildClass extends SynchronizedSuperClass{
    @Override
    protected synchronized void method() {
        System.out.println("我是子类方法");
        super.method();
    }

    public static void main(String[] args) {
        SynchronizedChildClass synchronizedChildClass = new SynchronizedChildClass();
        synchronizedChildClass.method();
    }
}

由此可见,在java中synchronized关键字的粒度范围是线程范围,也就是说在一个线程如果已经获取到某把锁后,如果接着需要这把锁去访问其它方法或者其它类的,那么可重入性质就会被激发,就可以不用显示的去释放锁、获取锁

4.2 不可中断性

一旦这个锁已经被别人获得了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程釋放这个锁。如果别人永远不释放锁,那么我只能永远地等下去。
相比之下,Lock类可以拥有中断的能力,第一点,如果我觉得我等的时间太长了,有权中断现在已经获取到锁的线程的执行;第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。

五 深入原理

5.1 加锁释放锁原理

5.1.1 现象

每个类的实例对应一把锁,每个被synchronized修饰的方法或者代码块的调用都需要一把锁才能执行,方法或者代码块一旦执行就会独占该锁知道代码执行结束或者抛出异常,才将锁释放,然后那些被阻塞的线程才能进行竞争获取该锁进入可执行状态。所有的java对象都持有一个互斥锁,该锁由JVM自动获取和释放,我们只需要指定这个对象就可以。

5.1.2 获取和释放锁的时机:内置锁

每个java对象都可以用作一个实现同步的锁,这个锁被称作内置锁,或者监视器锁(Monitor Lock)。线程在进入同步代码块或方法前会自动获取该锁,并且退出时(正常对出、抛出异常退出)会自动的释放锁。获取该锁的唯一途径就是进入synchronized保护的代码块或者同步方法中。

5.1.3 等价代码

public class SynchronizedEquals {
    private Lock lock = new ReentrantLock();

    private synchronized void method1() {
        System.out.println("我是synchronized方法");
    }

    private void method2() {
        lock.lock();
        System.out.println("我是lock方法");
        lock.unlock();
    }

    public static void main(String[] args) {
        SynchronizedEquals synchronizedEquals = new SynchronizedEquals();
        synchronizedEquals.method1();
        synchronizedEquals.method2();
    }
}

5.2 反编译、看monitor指令

5.2.1 概况

java每个对象都有一个对象头,每个头都可以存储很多东西,其中有个部分就是用来存储synchronized关键字锁的。
当线程访问一个同步代码块或者方法时必须得到这把锁,当退出或者抛出异常时会释放这把锁,进入锁释放锁是基于monitor对象来实现同步的,monitor对象主要有两个指令monitorenter(插入到同步代码开始位置)、monitorexit(插入到同步代码结束的时候),JVM会保证每个enter后有exit与之对应,但是可能会有多个exit和同一个enter对应,因为退出的时机不仅仅是方法退出也可能是方法抛出异常。每个对象都有一个monitor与之关联,一旦持有之后,就会处于锁定状态,当线程执行到monitorenter这个指令时,就会尝试获取该对象monitor所有权(尝试获取该对象锁)。

5.2.2 如何反编译

  1. 反编译源代码
public class Decompilation {
    private Object object = new Object();

    public void insert(Thread thread) {
        synchronized (object) {
            
        }
    }
}
  1. 利用javac命令生成class文件。
  2. 利用java提供的工具反编译class文件:javap -verbose .Decompilation.class
Classfile /H:/my_projects/F2FPay_Demo_Java/demo/src/main/java/com/example/demo/Decompilation.class
  Last modified 2019-2-22; size 492 bytes
  MD5 checksum a0ee0e14677988f525bcb82797fb9277
  Compiled from "Decompilation.java"
public class com.example.demo.Decompilation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // java/lang/Object
   #3 = Fieldref           #4.#22         // com/example/demo/Decompilation.object:Ljava/lang/Object;
   #4 = Class              #23            // com/example/demo/Decompilation
   #5 = Utf8               object
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               insert
  #12 = Utf8               (Ljava/lang/Thread;)V
  #13 = Utf8               StackMapTable
  #14 = Class              #23            // com/example/demo/Decompilation
  #15 = Class              #24            // java/lang/Thread
  #16 = Class              #21            // java/lang/Object
  #17 = Class              #25            // java/lang/Throwable
  #18 = Utf8               SourceFile
  #19 = Utf8               Decompilation.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Utf8               java/lang/Object
  #22 = NameAndType        #5:#6          // object:Ljava/lang/Object;
  #23 = Utf8               com/example/demo/Decompilation
  #24 = Utf8               java/lang/Thread
  #25 = Utf8               java/lang/Throwable
{
  public com.example.demo.Decompilation();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field object:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 3: 0
        line 4: 4

  public void insert(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter   //monitorenter对象
         7: aload_2
         8: monitorexit    //第一个monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit    //第二个monitorexit
        15: aload_3
        16: athrow
        17: return
      Exception table:
         from    to  target type
             7     9    12   any
            12    15    12   any
      LineNumberTable:
        line 7: 0
        line 9: 7
        line 10: 17
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class com/example/demo/Decompilation, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4
}
SourceFile: "Decompilation.java"

5.2.3 monitorenter和monitorexit指令

monitorenter和monitorexit指令会在执行的时候让锁计数减一或者加一,每个对象都与你一monitor对象关联,一个monitor的lock锁只能被一个线程在同一时间获得,一个线程在尝试获得与这个对象关联的monitor所有权时只会发生以下三种情况之一:

  1. 如果这个monitor计数器为0,意味着目前还没有被获得,然后这个线程会立刻获得然后将计数器加1,其它线程就会知道该monitor已经被持有了,这就是成功获得锁。
  2. 如果这个线程已经拿到了monitor的所有权,又重入了,技术器就会变成2、3…
  3. 如果monitor已经被其它线程持有了,现在去获取就会得到无法获取信号,那么就会进入阻塞状态,直到monitor计数器变为0再去尝试获取锁。
    monitorexit就是对锁进行释放,释放的过程就是将monitorjian减一,如果减完之后计数器变为0就意味着当前线程不再拥有对锁的所有权了。如果减完之后不为0就意味着是重入进来的,那么就继续持有该锁

5.3 可重入原理

主要利用加锁次数计数器,具体是这样:

  1. 每个对象自动含有一把锁,JVM负责跟踪对象被加锁的次数。
  2. 线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上再次获得锁时,计数会递増,只有首先获得这个锁的线程,才能在该对象上继续获取这把锁。
  3. 每当任务离开时,计数递减,当计数为0的时候,锁被完全释放。
    这就是可重入原理,利用一个可加可减的计数器清楚的知道该锁是被当前线程多次持有还是已经减到0被完全释放了。

5.4 可见性原理

下图描述了两个线程是如何使用共享变量的:为了加快程序运行,线程会将共享变量复制一份到本地内存,这样一来就可能因为不同线程之间共享变量的读写带来风险。因此线程之间就需要进行通知来告诉彼此变量的变化。(JMM是Java内存模型缩写)
在这里插入图片描述
下图描述了线程之间是如何保证共享变量的事实一致性的:线程A使用变量前会通知线程B,线程A使用完变量后会将变量值写入到主存中,然后通知线程B,线程B去主存中获取的变量值就是最新的。
在这里插入图片描述
synchronized是如何做到可见性的实现的:某个被synchronized修饰的方法或者代码块在执行完毕后,该方法或者代码块中对共享变量的所作的任何修改都要在释放锁之前从线程内存写入到主内存中,因此就保证了线程内存的变量与主内存中变量的一致性。同样,在进入同步代码块或者方法中所得到的共享变量值也是直接从主内存中获取的。

六、缺陷

6.1 效率低

  1. 锁的释放情况少:执行代码完毕、抛出异常。
  2. 试图获得锁时不能设定超时:只能一直等待,不到南墙不回头。相比之下Lock可以设置超时时间,过了设置时间就可以不必等待。
  3. 不能中断一个正在试图获得锁的线程。相比之下,Lock有中断能力。

6.2 不够灵活(读写锁更灵活):

加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),锁住找个对象,找个对象就是这把锁,除非释放这个对象才是解开这把锁。相对而言,读写锁读操作不加锁,写操作加锁。

6.3 无法知道是否成功获取到锁

相比直线Lock锁可以去尝试获取锁,返回获取结果,成功去做某些操作,不成功去做另外一些操作。

七、面试常见问题

1、使用注意点?

  1. 所对象不能为空:锁是保存在对象头里,为空则无法使用锁。
  2. 作用域不宜过大:多线程是为了提高运行效率,作用域过大虽然降低了并发可能引起的问题,但是代码串行工作会使运行速度大大降低。
  3. 避免死锁,以下代码展示了死锁。
public class SynchronizedDeadLock {

    public static void main(String[] args) {
        MyThread t1 = new MyThread(true);
        MyThread t2 = new MyThread(false);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}

class MyThread extends Thread {
    private boolean flag;

    static Object object1 = new Object();
    static Object object2 = new Object();

    public MyThread(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            if (flag) {
                synchronized (object1) {
                    System.out.println("object1锁住了" + Thread.currentThread().getName());
                    Thread.sleep(100);
//                    object1.wait(100);
                    System.out.println(Thread.currentThread().getName() + "等待获取object2的锁");
                    synchronized (object2) {
                        System.out.println("object2锁住了" + Thread.currentThread().getName());
                    }
                    System.out.println(Thread.currentThread().getName() + "释放了object1和object2");
                }
            }
            if (!flag) {
                synchronized (object2) {
                    System.out.println("object2锁住了" + Thread.currentThread().getName());
                    Thread.sleep(100);
                    System.out.println(Thread.currentThread().getName() + "等待获取object1的锁");
                    synchronized (object1) {
                        System.out.println("object1锁住了" + Thread.currentThread().getName());
                    }
                    System.out.println(Thread.currentThread().getName() + "释放了object1和object2");
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

输出结果:

object1锁住了线程1
object2锁住了线程2
线程1等待获取object2的锁
线程2等待获取object1的锁

2、如何选择synchronized和Lock?
思路:有现成工具就用,没有就优先用synchronized,用这个关键字可以减少代码编写,至少比Lock少出错,需要Lock特性就用Lock。

  1. 如果可以使用java.util.concurrent包下的类,尽量不使用synchronized和Lock。
  2. synchronized在项目中适用就优先使用。
  3. 需要使用Lock或者Condition独有特性时,才使用。

3、多线程访问同步方法的7种情况?
上面已有介绍
4、Synchronized使得同时只有4线程可以执行,性能较差,有什
么办法可以提升性能?

  1. 尽可能缩小同步代码包裹范围。
  2. 可以使用其他锁机制,Lock锁或者读写锁。
只有把命运掌握在自己手中,从今天起开始努力,即使暂时看不到希望,也要相信自己。因为比你牛几倍的人,依然在努力。
原文地址:https://www.cnblogs.com/freesky168/p/14358199.html