synchronized 的 8 种使用案例分析

你知道 synchronized 是怎么用的吗?😮

现在有一个 HuaWei 类,它有三个方法 sendMailsendSMShello

class HuaWei {
    
    public synchronized void sendMail() {
        System.out.println(Thread.currentThread().getName() + ": send Mail");
    }

    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + ": send SMS");
    }

    public void hello() {
        System.out.println(Thread.currentThread().getName() + ": say hello");
    }

}

我们会在以下 8 种案例下对方法进行访问并分析原因:

  1. 标准访问有 A,B 两个线程,请问先打印邮件还是短信?
  2. sendEmail 方法中加入暂停 3 秒钟,请问先打印邮件还是短信?
  3. 添加一个普通的 hello 方法,请问先打印邮件还是 hello?
  4. 有 2 部手机,请问先打印邮件还是短信?
  5. 有两个静态同步方法,有 1 部手机,请问先打印邮件还是短信?
  6. 有两个静态同步方法,有 2 部手机,请问先打印邮件还是短信?
  7. 有 1 个静态同步方法,有 1 个普通同步方法,有 1 部手机,请问先打印邮件还是短信?
  8. 有 1 个静态同步方法,有 1 个普通同步方法,有 2 部手机,请问先打印邮件还是短信?

标准访问有 A,B 两个线程,请问先打印邮件还是短信?

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        HuaWei huaWei = new HuaWei();
        new Thread(() -> {
            huaWei.sendMail();
        }, "线程A").start();
        Thread.sleep(200); // 保证线程 A 先执行
        new Thread(() -> {
            huaWei.sendSMS();
        }, "线程B").start();
    }
}

结果如下:

线程A: send Mail
线程B: send SMS

结果分析:

加在普通方法上的 synchronized 使用当前对象作为监视器,是对象锁,同一时刻仅允许单个线程访问同一个对象实例。这里线程 A 先启动抢占锁,线程 B 后启动等待。虽然访问方法不同,但是是同一对象,必须等待锁释放后 B 才能访问该对象实例,故 A 先一步完成并打印结果。

sendEmail 方法中加入暂停 3 秒钟,请问先打印邮件还是短信?

class HuaWei {
    
    public synchronized void sendMail() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": send Mail");
    }

    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + ": send SMS");
    }

    public void hello() {
        System.out.println(Thread.currentThread().getName() + ": say hello");
    }

}

结果如下:

线程A: send Mail
线程B: send SMS

结果分析:

虽然 A 睡了 1 秒,但 Thread.sleep() 方法不会使线程释放锁,因此 B 依旧无法拿到锁被迫等待,A 醒来后执行完毕并释放锁,B 拿到锁后才能执行。

添加一个普通的 hello 方法,请问先打印邮件还是 hello?

HuaWei 类不变,MyTest 如下:

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        HuaWei huaWei = new HuaWei();
        new Thread(() -> {
            huaWei.sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            huaWei.hello(); // 调用 hello()
        }, "线程B").start();
    }
}

结果如下:

线程B: say hello
线程A: send Mail

结果分析:

加在普通方法上的 synchronized 对象锁,仅会锁住对象实例上的所有同步方法,与普通方法不产生互斥。这里 A 先启动抢占锁后睡眠,B 后启动访问普通方法 hello() 不会产生互斥,无需等待锁释放即可执行完毕,因此 B 先一步打印结果。

有 2 部手机,请问先打印邮件还是短信?

HuaWei 类不变,MyTest 如下:

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            new HuaWei().sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            new HuaWei().sendSMS();
        }, "线程B").start();
    }

结果如下:

线程B: say hello
线程A: send Mail

结果分析:

对象锁只锁同一对象实例,不同对象之间的锁不相同。B 无需等待 A 的锁释放就能拿到自己对象的锁并执行方法,两者拿到的是不同的锁。又因为 A 要睡 1 秒,所以 B 先打印结果。

有两个静态同步方法,有 1 部手机,请问先打印邮件还是短信?

package fun.hualiang.test;

class HuaWei {
    
    public static synchronized void sendMail() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": send Mail");
    }

    public static synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + ": send SMS");
    }

    public void hello() {
        System.out.println(Thread.currentThread().getName() + ": say hello");
    }

}

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        HuaWei huaWei = new HuaWei();
        new Thread(() -> {
            huaWei.sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            huaWei.sendSMS();
        }, "线程B").start();
    }
}

结果如下:

线程A: send Mail
线程B: send SMS

结果分析:

加在静态方法上的 synchronized 使用当前类作为监视器,是类锁,同一时刻仅允许单个线程访问同一个类。这里的原因跟情况二类似,区别在于情况二拿到的是对象锁,这里拿到的是类锁。

有两个静态同步方法,有 2 部手机,请问先打印邮件还是短信?

HuaWei 类不变,MyTest 如下:

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            new HuaWei().sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            new HuaWei().sendSMS();
        }, "线程B").start();
    }
}

结果如下:

线程A: send Mail
线程B: send SMS

结果分析:

这种情况与情况四的区别在于,情况四中 A 和 B 分别拿到的是不同对象上的对象锁,不会产生互斥。而这里因为静态方法是属于类的,所以 A 和 B 拿到的是同一个类的类锁,既然是同一把锁就会产生互斥。A 先启动拿到锁,B 就只能等 A 执行完释放锁后在执行,故 A 先一步打印结果。

有 1 个静态同步方法,有 1 个普通同步方法,有 1 部手机,请问先打印邮件还是短信?

package fun.hualiang.test;

class HuaWei {
    
    public static synchronized void sendMail() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ": send Mail");
    }

    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + ": send SMS");
    }

    public void hello() {
        System.out.println(Thread.currentThread().getName() + ": say hello");
    }

}

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        HuaWei huaWei = new HuaWei();
        new Thread(() -> {
            huaWei.sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            huaWei.sendSMS();
        }, "线程B").start();
    }
}

结果如下:

线程B: send SMS
线程A: send Mail

结果分析:

B 拿的是对象锁,A 拿的是类锁,不同的锁不会发生互斥,因此结果与情况四类似,都是拿到不同的锁。

有 1 个静态同步方法,有 1 个普通同步方法,有 2 部手机,请问先打印邮件还是短信?

HuaWei 类不变,MyTest 如下:

public class MyTest {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            new HuaWei().sendMail();
        }, "线程A").start();
        Thread.sleep(200);
        new Thread(() -> {
            new HuaWei().sendSMS();
        }, "线程B").start();
    }
}

结果如下:

线程B: send SMS
线程A: send Mail

结果分析:

与情况七一致,即使是不同对象,也不影响 B 拿的是对象锁,A 拿的是类锁,因此结果一致。


总结

综上所述,多线程是否同步的关键在于:它们是否拿到的是同一把锁