synchronized从使用到原理详解(内含高频面试题)

1、Synchronized

1.1 作用

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

1.2 地位

Synchronized是Java的关键字,被Java语言原生支持

最基本的互斥同步手段

是并发编程中的元老级角色,是并发编程的必学内容

1.3 不用并发手段会有什么后果?

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

public class ShowUnsafe1 implements Runnable{
    static ShowUnsafe1 showUnsafe = new ShowUnsafe1();
    static int j;
    @Override
    public void run () {
        for (int i = 0; i < 10000; i++) {
            j++;
        }
    }

    public static void main (String[] args) throws InterruptedException {
        Thread thread1 = new Thread(showUnsafe);
        Thread thread2 = new Thread(showUnsafe);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(j);
    }
}

输出

15930 or 13690等等数值在不断变化

原因

i++,虽然是一行代码,但是实际上至少包含了以下这三个动作:

  1. 读取 i 的值
  2. 计算 i+1
  3. 把 i+1 的计算结果写回到内存中,赋给 i

因为上面三个步骤不具备原子性,所以出现线程不安全现象。

1.4 Synchronized的两个用法(本次重点明白每个锁的锁对象是谁)

对象锁

包括方法锁默认锁对象为this当前实例对象)和同步代码块锁自己指定锁对象

类锁

指Synchronized修饰静态的方法或指定锁为Class对象

1.5 对象锁

代码块形式:手动指定锁对象

方法锁形式:Synchronized修饰普通方法,锁对象默认为this

1.5.1 代码展示:对象锁—代码块形式

/**
 * 对象锁的形式1:代码块形式
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    private static SynchronizedObjectCodeBlock2 instant = new SynchronizedObjectCodeBlock2();

    @Override
    public void run () {
        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 t1 = new Thread(instant);
        Thread t2 = new Thread(instant);
        t1.start();
        t2.start();
        while( t1.isAlive() || t2.isAlive() ){
        }
        System.out.println("finished");
    }
}

输出

我是对象锁的代码块形式。我叫Thread-0
我是对象锁的代码块形式。我叫Thread-1
Thread-1运行结束
Thread-0运行结束
finished

此时线程1和2几乎是同时参加run方法

这时我们加入Synchronized

/**
 * 对象锁的形式1:代码块形式
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    private static SynchronizedObjectCodeBlock2 instant = new SynchronizedObjectCodeBlock2();

    @Override
    public void run () {
        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 t1 = new Thread(instant);
        Thread t2 = new Thread(instant);
        t1.start();
        t2.start();
        while( t1.isAlive() || t2.isAlive() ){
        }
        System.out.println("finished");
    }
}

输出

我是对象锁的代码块形式。我叫Thread-0
Thread-0运行结束
我是对象锁的代码块形式。我叫Thread-1
Thread-1运行结束
finished

看到加入synchronized之后,我们的synchronized代码块之中的代码已经必须要求只有一个线程能够访问

这是我们新建一个锁对象Object lock1 = new Object();我们将synchronized的锁对象指定为lock1。synchronized(lock1)

此时,我们使用两个synchronized代码块,来看看最后运行结果

/**
 * 对象锁的形式1:代码块形式
 */
public class SynchronizedObjectCodeBlock2 implements Runnable{
    private static SynchronizedObjectCodeBlock2 instant = new SynchronizedObjectCodeBlock2();
    Object lock1 = new Object();
    Object lock2 = new Object();

    @Override
    public void run () {
        synchronized(lock1) {
            System.out.println("我是对象锁lock1的代码块形式。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
        synchronized(lock2) {
            System.out.println("我是对象锁lock2的代码块形式。我叫" + 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 t1 = new Thread(instant);
        Thread t2 = new Thread(instant);
        t1.start();
        t2.start();
        while( t1.isAlive() || t2.isAlive() ){
        }
        System.out.println("finished");
    }
}

输出

我是对象锁lock1的代码块形式。我叫Thread-0
Thread-0运行结束
我是对象锁lock2的代码块形式。我叫Thread-0
我是对象锁lock1的代码块形式。我叫Thread-1
Thread-0运行结束
Thread-1运行结束
我是对象锁lock2的代码块形式。我叫Thread-1
Thread-1运行结束
finished

1.5.2 代码展示:对象锁—方法锁形式

//锁对象为this当前实例对象
public class SynchronizedObjectMethod3 implements Runnable{
    private static SynchronizedObjectMethod3 instant = new SynchronizedObjectMethod3();

    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() + "运行结束");
    }

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

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

输出

我是对象锁的方法锁形式。我叫Thread-0
Thread-0运行结束
我是对象锁的方法锁形式。我叫Thread-1
Thread-1运行结束
finished

1.6 类锁

概念

  1. 只有一个Class对象:Java类可能有很多对象,但是只有一个Class对象
  2. 本质:所以所谓类锁,不过是锁对象是 Class对象
  3. 用法和效果:同一时刻最多被一个线程拥有

形式1:synchronized加在static方法上

形式2:synchronized(*.class)代码块

1.6.1 代码展示:类锁—静态方法锁形式

现在我们发现一个有趣的现象

public class SynchronizedClassStatic4 implements Runnable{
    private static SynchronizedClassStatic4 instant1 = new SynchronizedClassStatic4();
    private static SynchronizedClassStatic4 instant2 = new SynchronizedClassStatic4();

    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() + "运行结束");
    }

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

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

输出

我是类锁的静态方法锁形式。我叫Thread-1
我是类锁的静态方法锁形式。我叫Thread-0
Thread-1运行结束
Thread-0运行结束
finished

看到了没,我们的synchronized好像失效了。照成这种现象的原因是两点:

  1. Synchronized修饰普通方法,锁对象默认为this
  2. 线程1和2之间的是不同对象

所以,对于线程1,synchronized锁的是instant1对象。对于线程2,synchronized锁的是instant2对象。

他们之间锁都不同,当然不会发生冲突。

这时候,我们在方法上加上static

public class SynchronizedClassStatic4 implements Runnable{
    private static SynchronizedClassStatic4 instant1 = new SynchronizedClassStatic4();
    private static SynchronizedClassStatic4 instant2 = new SynchronizedClassStatic4();

    public static synchronized void method(){
        System.out.println("我是类锁的静态方法锁形式。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

输出

我是类锁的静态方法锁形式。我叫Thread-0
Thread-0运行结束
我是类锁的静态方法锁形式。我叫Thread-1
Thread-1运行结束
finished

我们只是将方法变成了静态方法public static synchronized void method()。这个时候不会出现上面代码的问题了。为什么?

我们将方法变成了静态的,此时锁从对象锁变成了类锁。即,锁对象是SynchronizedClassStatic4.class对象。所以,此时变成了线程1和2共同竞争一把锁

1.6.2 代码展示:类锁—代码块锁形式

public class SynchronizedClassClass5 implements Runnable{
    private static SynchronizedClassClass5 instant1 = new SynchronizedClassClass5();
    private static SynchronizedClassClass5 instant2 = new SynchronizedClassClass5();

    public void method(){
        synchronized (SynchronizedClassClass5.class) {
            System.out.println("我是类锁的静态方法锁形式。我叫" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

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

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

输出

我是类锁的静态方法锁形式。我叫Thread-1
Thread-1运行结束
我是类锁的静态方法锁形式。我叫Thread-0
Thread-0运行结束
finished

我们此时,使用synchronized修饰代码块,并且传入锁对象SynchronizedClassClass5.class。此时这个所是类锁

1.7 面试题

多线程访问同步方法的7种情况(面试常考)

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

public class SynchronizedObjectMethod3 implements Runnable{
    private static SynchronizedObjectMethod3 instant = new SynchronizedObjectMethod3();

    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() + "运行结束");
    }

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

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

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

public class SynchronizedClassStatic4 implements Runnable{
    private static SynchronizedClassStatic4 instant1 = 
        new SynchronizedClassStatic4();
    private static SynchronizedClassStatic4 instant2 = 
        new SynchronizedClassStatic4();

    public void method(){
        synchronized(this){
            System.out.println("我是类锁的静态方法锁形式。我叫" 
                               + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "运行结束");
        }
    }

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

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

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

public class SynchronizedClassStatic4 implements Runnable{
    private static SynchronizedClassStatic4 instant = new SynchronizedClassStatic4();

    public static synchronized void method(){
        System.out.println("我是类锁的静态方法锁形式。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

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

public class SynchronizedYesAndNo implements Runnable{

    private static SynchronizedYesAndNo instant = new SynchronizedYesAndNo();

    public synchronized void method1(){
        System.out.println("我是加锁的方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

    public void method2(){
        System.out.println("我是没加锁的方法。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

输出

我是加锁的方法。我叫Thread-0
我是没加锁的方法。我叫Thread-1
Thread-0运行结束
Thread-1运行结束
finished

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

/**
 * 访问同一个类的不同的普通同步方法
 */
public class SynchronizedDifferentMethod7 implements Runnable{
    private static SynchronizedDifferentMethod7 instant = new SynchronizedDifferentMethod7();

    public synchronized void method1(){
        System.out.println("我是加锁的方法1。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是加锁的方法2。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

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

/**
 * 同时访问静态synchronized和非静态的synchronized方法
 */
public class SynchronizedStaticAndNormal8 implements Runnable{
    private static SynchronizedDifferentMethod7 instant = new SynchronizedDifferentMethod7();

    public synchronized static void method1(){
        System.out.println("我是加锁的静态方法1。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是加锁的方法2。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

7 方法抛出异常后,会释放锁

/**
 * 方法抛出异常后,会释放锁
 */
public class SynchronizedException9 implements Runnable{
    private static SynchronizedException9 instant = new SynchronizedException9();

    public synchronized void method1(){
        System.out.println("我是加锁的方法1。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        throw new RuntimeException();
    }

    public synchronized void method2(){
        System.out.println("我是加锁的方法2。我叫" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "运行结束");
    }

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

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

1.8 Synchronized性质

1.8.1 可重入

指的是某个线程试图获得一个已经由他持有的锁,那么它会请求成功。

好处:避免死锁、提升封装性,简化面向对象代码的开发

//可重入现象的代码
public class SynchronizedOtherMethod11 {
    public synchronized void method1(){
        System.out.println("这是方法1");
        method2();
    }

    public synchronized void method2 () {
        System.out.println("这是方法2");
    }

    public static void main (String[] args) {
        SynchronizedOtherMethod11 s = new SynchronizedOtherMethod11();
        s.method1();
    }
}
//对象s获得的是一把锁:this,我们可以使用这把锁访问方法2(因为方法2的锁和这把锁是同一把)

重入的一种实现方式是,为每个锁关联一个获取计数器和一个所有者线程。当计数器值为0时,这个锁就认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM会记下锁的持有者,并且将计数器值变为1。如果同一个线程再次获取到了这个锁,计数器值将加一,而当线程退出同步代码块时,计数器值递减。当计数器为0时,这个锁将被释放。

1.8.2 不可中断

一旦这个锁被别人拿走了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,我们就要永远等下去

1.8.3 可见性

  • 一个线程执行的结果,另外的线程不一定可见
  • 线程1操作x=5,之后线程2可能读取x=3
  • synchronized可以保证可见性:线程B执行由锁保护的同步代码块时,可以看到线程A之前在同一个同步代码块中的所有操作结果。

在代码中说明

int x = 3;
public void method1(){
    x++;
}

public void method2(){
    System.out.println(x)
}

此时,不一定满足可见性。所以method2读到的x的值有可能是3

但是我们将两个方法全部加上synchronized关键词

int x = 3;
public synchronized void method1(){
    x++;
}

public synchronized void method2(){
    System.out.println(x)
}

此时,method2读到的x的值一定是4

1.9 原理

1.9.1 加锁和释放锁的原理

  • 获取和释放锁的时机:进入和退出同步代码块(包括异常)
  • 等价代码

synchronized会为我们自动加锁和释放锁

public class SynchronizedToLock13 {
    Lock lock = new ReentrantLock();

    //自动为我们加锁解锁
    private synchronized void method1 () {
        System.out.println("我是synchronized形式");
    }

    //我们自己来加锁和解锁
    private void method2(){
        lock.lock();//上锁
        try {
            System.out.println("我是Lock形式");
        }finally {
            lock.unlock();//解锁
        }
    }

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

method2其实就是和method1等价的

  • 看字节码:monitor相关指令

我们来看看synchronized的字节码

/**
 * 看字节码
 */
public class Decompilation14 {
    private Object object = new Object();
    public void insert(Thread thread){
        synchronized (object){

        }
    }
}

老师使用反编译操作,看到了上面代码的字节码文件

Code:
	stack = 2,local = 4,args_size = 2
        0:aload_0
        1:getfield
        4:dup
		5:astore_2
		6:monitorenter//加锁
		7:aload_2
		8:monitorexit//释放锁
		9:goto
		10:astore_3
		11:aload_2
		12:monitorexit//释放锁
		13:aload_3
		14:athrow
		15:return

synchronized的原理说明(💟):当我们使用代码synchronized (object){}在底层就会执行monitor相关指令

1.9.2 可重入原理:加锁次数计数器

重入的一种实现方式是,为每个锁关联一个获取计数器和一个所有者线程。当计数器值为0时,这个锁就认为是没有被任何线程持有。当线程请求一个未被持有的锁时,JVM会记下锁的持有者,并且将计数器值变为1。如果同一个线程再次获取到了这个锁,计数器值将加一,而当线程退出同步代码块时,计数器值递减。当计数器为0时,这个锁将被释放。

1.10 synchronized的缺陷

  • 效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
  • 不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
  • 无法知道是否成功获取锁

1.11 面试题(这里以后回来完善一下)

  1. 使用注意点:锁的范围不宜过大、避免锁的嵌套

  2. 如何选择Lock和synchronized关键字?

    Lock确实比较灵活,我们可以自主选择加锁和释放锁的时机。优先使用synchronized。只有当不能使用synchronized并必须使用Lock的时候才去使用Lock

  3. 多线程访问同步方法的各种具体情况(前面讲到)

  4. synchronized使得同时只有一个线程可以执行,性能较差,有什么可以提升性能?

    如果是读多写少的情况下,我们可以使用读写锁

  5. 我想更灵活的控制锁的获取和释放(现在释放锁的时机都被规定死了),怎么办?

    我们可以使用Lock

本篇文章参考:
《Java并发编程实战》
慕课网悟空老师的sychronized课 https://www.imooc.com/learn/1086
谢谢大家看到这里,如有不对的地方请在评论区指正

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值