# Synchronized

# 1. 简介

# 1.1 作用

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

# 1.2 地位

  • synchronized 是 Java 的关键字,被 Java 语言原生支持;
  • 是最基本的互斥同步手段;

# 2. 两种用法

# 2.1 对象锁

# [同步代码块]

/**
 * 对象锁 —— 同步代码块
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:01 PM
 */
public class SynchronizedObjectLockOne implements Runnable{

    //对象锁,加 static 确保每一个实例对象都是用的同一个对象作为锁
    static SynchronizedObjectLockOne instance = new SynchronizedObjectLockOne();
    static int a = 0;

    @Override
    public void run() {
        //同步代码块
        synchronized (instance){
            System.out.println(Thread.currentThread().getName()+"start.....");
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName()+"finished....");
        }
    }


    public static void main(String[] args) {

        Thread thread1 = new Thread(new SynchronizedObjectLockOne());
        Thread thread2 = new Thread(new SynchronizedObjectLockOne());
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
        }
        System.out.println("a==" + a);   //a==20000
        System.out.println("finished......");
      
    }
}

# [同步方法]

/**
 * 对象锁 —— 同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:01 PM
 */
public class SynchronizedObjectLockTwo implements Runnable{

    //实例对象,必须为同一个实例对象,多个线程中的同步方法才是加的同一个对象锁
    static SynchronizedObjectLockTwo instance = new SynchronizedObjectLockTwo();

    static int a = 0;

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

    //同步方法
    public synchronized void plusA(){
        System.out.println(Thread.currentThread().getName()+"start.....");
        for (int i = 0; i < 10000; i++) {
            a++;
        }
        System.out.println(Thread.currentThread().getName()+"finished....");
    }


    public static void main(String[] args) {

        //这里必须是同一个 instance,才会是同一把锁
        Thread thread1 = new Thread(instance);
        Thread thread2 = new Thread(instance);
        thread1.start();
        thread2.start();
        while (thread1.isAlive() || thread2.isAlive()){
        }
        System.out.println("a==" + a);			//20000
        System.out.println("finished......");
      
    }
}

# 2.2 类锁

# [静态方法锁]

/**
 * 类锁:静态方法锁
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:14 PM
 */
public class SynchronizedClassLockOne implements Runnable{

    static int a = 0;

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

    //静态方法锁
    public static synchronized void plusA(){
        System.out.println(Thread.currentThread().getName()+"...started....");
        for (int i = 0; i < 10000; i++) {
            a++;
        }
        System.out.println(Thread.currentThread().getName()+"...finished....");
    }

    public static void main(String[] args) {
        //不同的实例
        Thread t1 = new Thread(new SynchronizedClassLockOne());
        Thread t2 = new Thread(new SynchronizedClassLockOne());
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){
        }
        System.out.println("a == " + a);			//a==20000
        System.out.println("finished......");
    }
}

# [Class 类作锁]

/**
 * 类锁:Class类作锁
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:14 PM
 */
public class SynchronizedClassLockTwo implements Runnable{

    static int a = 0;

    @Override
    public void run() {
        
        //Class类作锁
        synchronized (SynchronizedObjectLockTwo.class){
            System.out.println(Thread.currentThread().getName()+"...started....");
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName()+"...finished....");
        }
    }
    

    public static void main(String[] args) {
        //不同的实例
        Thread t1 = new Thread(new SynchronizedClassLockTwo());
        Thread t2 = new Thread(new SynchronizedClassLockTwo());
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){
        }
        System.out.println("a == " + a);			//a==20000
        System.out.println("finished......");
    }
}

# 3. 八种情况

核心思想:

  1. 一把锁同时只能被一个线程锁获取,其他没用获得该锁的线程只能等待;
  2. 关键是看是不是同一把锁;
  3. 无论方法是否正常执行,JVM 都会释放锁。

# ① 两个线程同时访问一个对象的同步方法

锁生效,先 Thread0  后 Thread1。

/**
 * ① 两个线程同时访问一个对象的同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case1 implements Runnable {

    static Case1 instance = new Case1();
    static int a = 0;

    @Override
    public void run() {
        synchronized (instance){
            System.out.println(Thread.currentThread().getName()+"...started....");
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName()+"...finished....");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("a==" + a);
        System.out.println("finished.......");
    }
}

结果:

Thread-0...started....
Thread-0...finished....
Thread-1...started....
Thread-1...finished....
a==20000
finished.......

# ② 两个线程同时访问两个对象的同步方法

看用的是不是同一个锁,如果是 this 的话,那么锁不生效,两个线程同时进行。

/**
 * ② 两个线程同时访问两个对象的同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case2 implements Runnable {

    static Case2 instance = new Case2();
    static int a = 0;

    @Override
    public void run() {
        //如果是 instance,那么还是同一把锁,a==20000
        //synchronized (instance){

        //如果是 this,那么不同对象就是使用不同的锁 a!=20000
        synchronized (this){
            System.out.println(Thread.currentThread().getName()+"...started....");
            for (int i = 0; i < 10000; i++) {
                a++;
            }
            System.out.println(Thread.currentThread().getName()+"...finished....");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new Case2());
        Thread t2 = new Thread(new Case2());
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("a==" + a);
        System.out.println("finished.......");
    }
}

结果:

Thread-1...started....
Thread-0...started....
Thread-1...finished....
Thread-0...finished....
a==11529
finished.......

# ③ 两个线程同时访问 synchronized 静态方法

锁生效,先 Thread0 后 Thread1。

package sychro;

/**
 * ③ 两个线程同时访问 synchronized 静态方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case3 implements Runnable {

    static int a = 0;

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

    public static synchronized void plusA(){
        System.out.println(Thread.currentThread().getName()+"...started....");
        for (int i = 0; i < 10000; i++) {
            a++;
        }
        System.out.println(Thread.currentThread().getName()+"...finished....");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(new Case3());
        Thread t2 = new Thread(new Case3());
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("a==" + a);
        System.out.println("finished.......");
    }
}

结果:

Thread-0...started....
Thread-0...finished....
Thread-1...started....
Thread-1...finished....
a==20000
finished.......

# ④ 两个线程同时访问同步与非同步方法

锁无效,非同步方法不受到影响,两个线程并行。

/**
 * ④ 两个线程同时访问同步与非同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case4 implements Runnable {

    static Case4 instance = new Case4();

    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")){
            methodA();
        }else{
            methodB();
        }
    }

    //同步方法
    public synchronized void methodA(){
        System.out.println(Thread.currentThread().getName()+"...同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...同步方法 finished....");
    }

    //非同步方法
    public void methodB(){
        System.out.println(Thread.currentThread().getName()+"...非同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...非同步方法 finished....");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("finished.......");
    }
}

结果:

Thread-1...非同步方法 started....
Thread-0...同步方法 started....
Thread-0...同步方法 finished....
Thread-1...非同步方法 finished....
finished.......

# ⑤ 两个线程同时访问同一个对象的不同普通同步方法

锁生效,普通同步方法默认使用 this 作为锁,先 Thread0 后 Thread1。

/**
 * ⑤ 两个线程同时访问同一个对象的不同普通同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case5 implements Runnable {

    static Case5 instance = new Case5();


    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")){
            methodA();
        }else{
            methodB();
        }
    }

    //第一个同步方法
    public synchronized void methodA(){
        System.out.println(Thread.currentThread().getName()+"...第一个同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...第一个同步方法 finished....");
    }

    //第二个同步方法
    public synchronized void methodB(){
        System.out.println(Thread.currentThread().getName()+"...第二个同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...第二个同步方法 finished....");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("finished.......");
    }
}

结果:

Thread-0...第一个同步方法 started....
Thread-0...第一个同步方法 finished....
Thread-1...第二个同步方法 started....
Thread-1...第二个同步方法 finished....
finished.......

# ⑥ 两个线程同时访问静态 synchronized 方法和非静态 synchronized 方法

锁无效,一个使用的是 instance 对象锁,一个使用的是 Class 类锁。

/**
 * ⑥ 两个线程同时访问静态 synchronized 方法和非静态 synchronized 方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case6 implements Runnable {

    static Case6 instance = new Case6();


    @Override
    public void run() {
        if (Thread.currentThread().getName().equals("Thread-0")){
            methodA();
        }else{
            methodB();
        }
    }

    //静态同步方法
    public static synchronized void methodA(){
        System.out.println(Thread.currentThread().getName()+"...静态同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...静态同步方法 finished....");
    }

    //非静态同步方法
    public synchronized void methodB(){
        System.out.println(Thread.currentThread().getName()+"...非静态同步方法 started....");
        try {
            Thread.sleep(3000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"...非静态同步方法 finished....");
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("finished.......");
    }
}

结果:

Thread-0...静态同步方法 started....
Thread-1...非静态同步方法 started....
Thread-0...静态同步方法 finished....
Thread-1...非静态同步方法 finished....
finished.......

# ⑦ 方法抛异常后,会释放锁吗?

synchronized 会。Lock 不会,需要手动在 finally 里面释放。

/**
 * ⑦ 方法抛异常后,会释放锁吗?
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case7 implements Runnable {

    static Case7 instance = new Case7();


    @Override
    public void run() {

        synchronized (instance){
            System.out.println(Thread.currentThread().getName()+"进入同步代码块了");
            try{
                Thread.sleep(3000);
            }catch (Exception e){
                e.printStackTrace();
            }
            int i=1/0; //故意抛出异常
            System.out.println(Thread.currentThread().getName()+"释放锁");
        }
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("finished.......");
    }
}

结果:

Thread-0进入同步代码块了
Thread-1进入同步代码块了
Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
	at sychro.Case7.run(Case7.java:24)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "Thread-1" java.lang.ArithmeticException: / by zero
	at sychro.Case7.run(Case7.java:24)
	at java.lang.Thread.run(Thread.java:748)
finished.......

# ⑧ 两个线程同时访问一个对象中含有非同步方法的同步方法

非同步方法不会被上锁。

/**
 * ⑧ 两个线程同时访问一个对象中含有非同步方法的同步方法
 *
 * @author Hedon Wang
 * @create 2021-02-22 9:37 PM
 */
public class Case8 implements Runnable {

    static Case8 instance = new Case8();

    static int b = 0;


    @Override
    public void run() {
        //一个线程去访问 methodA(),methodA() 中还有 methodB()
        if (Thread.currentThread().getName().equals("Thread-0")){
            methodA();
        }else{
            //另一个线程去访问 methodB()
            methodB();
        }
    }

    //同步方法
    public synchronized void methodA(){
        System.out.println(Thread.currentThread().getName()+"...同步方法 started....");
        //同步方法中含有非同步方法
        methodB();
        System.out.println(Thread.currentThread().getName()+"...同步方法 finished....");
    }

    //非同步方法
    public void methodB(){
        System.out.println(Thread.currentThread().getName()+"...非同步方法 started....");
        for (int i = 0; i < 10000; i++) {
            b++;
        }
        System.out.println(Thread.currentThread().getName()+"...非同步方法 finished....");
    }



    public static void main(String[] args) {
        Thread t1 = new Thread(instance);
        Thread t2 = new Thread(instance);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()){};
        System.out.println("b==" + b);
        System.out.println("finished.......");
    }
}

结果:

Thread-0...同步方法 started....
Thread-1...非同步方法 started....
Thread-0...非同步方法 started....
Thread-0...非同步方法 finished....
Thread-1...非同步方法 finished....
Thread-0...同步方法 finished....
b==10282
finished.......

# 4. 性质

# 4.1 可重入

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

好处:避免死锁、提升封装性。

粒度:线程而非调用。(pthread 是调用粒度的)

# [递归调用本方法]

package sychro;

/**
 * @author Hedon Wang
 * @create 2021-02-22 11:01 PM
 */
public class Reentrancy1 implements Runnable {

    int a = 0;

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

    public synchronized void plusA(){
        System.out.println(Thread.currentThread().getName() + "  " +  a);
        if (a == 0){
            a++;
            //递归调用本身,能调用说明 —— 可重入
            plusA();
        }
    }

    public static void main(String[] args) {
        new Thread(new Reentrancy1()).start();
    }
}

# [递归调用本类不同同步方法]

/**
 * @author Hedon Wang
 * @create 2021-02-22 11:01 PM
 */
public class Reentrancy2 implements Runnable {

    int a = 0;
    int b = 0;

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

    public synchronized void plusA(){
        if (a == 0){
            a++;
            System.out.println(Thread.currentThread().getName() + " a == " +  a);
            //递归调用本对象的不同方法,能调用说明 —— 可重入
            plusA2();
        }
    }

    public synchronized void plusA2(){
        if (b == 0){
            b++;
            System.out.println(Thread.currentThread().getName() + " b == " + b);
        }
    }

    public static void main(String[] args) {
        new Thread(new Reentrancy2()).start();
    }
}

# [调用不同类的同步方法]

package sychro;

/**
 * @author Hedon Wang
 * @create 2021-02-22 11:01 PM
 */
public class Reentrancy3 {

    public synchronized void method(){
        System.out.println("我是父类...");
    }

    public static void main(String[] args) {
        Reentrancy3Child reentrancy3Child = new Reentrancy3Child();
        reentrancy3Child.methodChild();
    }
}

class Reentrancy3Child extends Reentrancy3{
    public synchronized void methodChild(){
        System.out.println("我是子类...");
        super.method();
    }
}

结果:

我是子类...
我是父类...

# 4.2 不可中断

一旦这个锁已经被一个线程获得了,其他线程只能等待这个锁被释放。

相比之前,相比之下,Lock 类可以拥有中断的能力:

  • 第一点,如果我觉得我等的时间太长了, 有权中断现在已经获取到锁的线程的执行;

  • 第二点,如果我觉得我等待的时间太长了不想再等了,也可以退出。

# 5. 原理

# 5.1 加锁/释放锁

image-20210223002208529

# monitorenter

  • 每个对象都与一个 monitor 相关联。
  • 当且仅当拥有所有者时(被拥有),monitor 才会被锁定。执行到 monitorenter 指令的线程,会尝试去获得对应的 monitor,如下:
    • 每个对象维护着一个记录着被锁次数的计数器,对象未被锁定时,该计数器为 0。线程进入 monitor(执行 monitorenter 指令)时,会把计数器设置为1。
    • 当同一个线程再次获得该对象的锁的时候,计数器再次自增。(所以只有一次 monitorenter 而可能有多个 monitorexit)
  • 当其他线程想获得该 monitor 的时候,就会阻塞,直到计数器为 0 才能成功。

# monitorexit

  • monitor 的拥有者线程才能执行 monitorexit 指令。
  • 线程执行 monitorexit 指令,就会让 monitor 的计数器减一。 如果计数器为 0,表明该线程不再拥有 monitor。其他线程就允许尝试去获得该 monitor 了。

# 5.2 可重入

  • JVM负责跟踪对象被加锁的次数。
  • 线程第一次给对象加锁的时候,计数变为 1。每当这个相同的线程在此对象上再次获得锁时,计数会递增。
  • 每当任务离开时,计数递减,当计数为 0 的时候,锁被完全释放。

# 5.3 可见性

synchronized 关键字保证线程在释放锁的时候,JVM 就帮助我们将该线程修改的东西写入到内存当中,并且保证了其他线程在锁被释放之前不可以访问该变量,从而保证了每一个线程访问该变量的时候都是正确的。

image-20210223003110546

# 6. 缺陷

  1. 效率低
    • 锁的释放情况少
    • 执行完毕
    • 异常抛出
    • 在 synchronized 代码被执行期间,线程可以调用对象的 wait() 方法,释放对象锁标志,进入等待状态,并且可以调用 notify() 或者 notifyAll() 方法通知正在等待的其他线程。notify()通知等待队列中的第一个线程,notifyAll() 通知的是等待队列中的所有线程。
    • 试图获得锁不能设定超时
    • 不能中断一个正在试图获得锁的线程
  2. 不够灵活
    • 加锁和释放锁时机单一
  3. 无法知道是否成功获得锁

# 7. 面试

  1. 锁对象不能为空

锁信息是保存在对象头中的,对象没有的话,更没有对象头了。

  1. 作用域不宜过大

    代码覆盖太多的话,效率太低。

  2. 避免死锁

  3. 如何选择 Lock 和 synchronized?

    # 相同
    二者都可以解决线程的安全问题;
    
    --------------------------------------------------------------------------------------------------------
    # 不同
    ## ① 用法
    在需要同步的对象中加入 synchronized 控制,synchronized 既可以加在方法上,也可以加在特定代码中,括号中表示需要锁的对象。而 Lock 需要显示的指定起始位置和终点位置。synchronized 是托管给 JVM 执行的,而 Lock 的锁定是通过代码实现的,它有比 synchronized 更精确的线程定义。
    ## ② 性能
    在 JDK 5 中增加的 ReentrantLock。它不仅拥有和 synchronized 相同的并发性和内存语义,还增加了锁投票,定时锁,等候和中断锁等。它们的性能在不同情况下会不同:在资源竞争不是很激励的情况下,synchronized 的性能要优于 ReentrantLock,带在资源紧张很激烈的情况下,synchronized 的性能会下降的很快(锁升级),而 ReentrantLock 的性能基本保持不变。
    ## ③ 锁机制
    锁机制不一样。
    synchronized 获得锁和释放锁的机制都在代码块结构中,当获得锁时,必须以相反的机制去释放,并且自动解锁,不会因为异常导致没有被释放而导致死锁。
    而 Lock 需要开发人员手动去释放,并且写在 finally 代码块中,否则会可能引起死锁问题的发生。
    此外,Lock 还提供的更强大的功能,可以通过 tryLock 的方式采用非阻塞的方式取获得锁。
    而且,Lock 还提供公平锁,即锁的获得按照线程先来后到的顺序依次获得,不会产生饥饿现象。默认是非公平锁,可通过传入构造方法的参数实现公平锁。 synchronized 提供的是非公平锁。
    
    --------------------------------------------------------------------------------------------------------
    # 用哪个
    Lock -> 同步代码块 -> 同步方法
    
  4. 多个线程等待同一个 synchronized 锁的时候,JVM 如何选择下一个获取锁的线程?

    随机。

  5. synchronized 提升性能的思路

    • 优化覆盖范围;
    • 使用其他类型的 Lock;
  6. 什么是锁的升级?降级?什么是 JVM 里的偏斜锁?轻量级锁?重量级锁?

    • 所谓锁的升级就是锁随着并发量的上升,锁从轻量级锁升级为重量级锁而导致锁的性能急剧下降的过程,降级同理。
    • 偏斜锁:就是 JVM 会倾向于把锁给当前拥有锁的线程,在无竞争情况下,直接把整个同步消除了,连乐观锁都不用,从而提高性能。;
    • 轻量级锁:轻量级是相对于传统锁机制而言的,本意是没有多线程竞争的情况下,减少传统锁机制使用 OS 实现互斥所产生的性能损耗。实现原理很简单,就是类似乐观锁的方式。
    • 重量级锁:即严格采用同步方法来加锁,效率最低。
上次更新: 9/19/2021, 8:15:19 PM