synchronized深入实现原理分析与总结

296 阅读4分钟

内部锁是通过synchronized关键字实现的。synchronized关键字可以用来修饰方法以及代码块(花括号 "{}"包裹的代码)。synchronized关键字修饰的方法就被称为同步方法 (SynchronizedMethod) 。 synchronized修饰的静态方法就被称为同步静态方法, synchronized修饰的实例方法就被称为同步实例方法。 同步方法的整个方法体就是一个临界区。

内部锁的使用并不会导致锁泄漏。这是因为 Java 编译器 (javac) 在将同步块代码编译为字节码的时候,对临界区中可能抛出的而程序代码中又未捕获的异常进行了特殊(代为)处理,这使得临界区的代码即使抛出异常也不会妨碍内部锁的释放。

使用synchronized可以使用在代码块和方法中,如图:

使用场景
如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。

底层原理

synchronized的具体底层实现。先写一个简单的demo:

public class SynchronizedLockTest {

    public static void main(String[] args) {
        synchronized (SynchronizedLockTest.class){

        }
        methodTest();
    }
    private static void methodTest() {
    }
}

上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。编译之后,切换到SynchronizedLockTest.class的同级目录之后,然后用javap -v SynchronizedLockTest.class查看字节码文件:

image.png

不清楚java对象头,monitor概念的可以再看下我这篇文章的对象头部分: juejin.cn/post/684490… 可以看到,在执行同步代码块之后,先执行monitorenter该指令(该指令是获取java对象头),在退出的时候会执行:monitorexit(释放对象头)指令。使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。

然后我们继续看,执行静态方法时,这个方法锁的对象任然是类对象,此时没有再执行一次monitorenter指令,说明不需要再获取一次。 这里说明synchronized是可重入锁。即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。

任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态。

任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

从JMM(java内存模型)中我们也可以看一下,对synchronized是如何支持的。

当线程A去读取主内存进行写入时,先加锁,此时线程B无法看到主内存,然后接着把主内存中数据更新到工作内存后,更新后,再刷新回主内存,此时再释放锁。

而线程B是在线程A释放锁之后,才会去获得对象头的锁,然后去读主内存的数据,将其冲刷到工作内存,来读取,从而保证线程A的刷新对线程B来说是可见的。

synchronized是一个重量级的锁,对性能的影响是最大的,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视(monitor),从而进入到同步代码块或者同步方法之中,即表现为互斥性(排它性)。这种方式肯定效率低下,每次只能通过一个线程.改进的话,可以使用CAS算法: juejin.cn/post/684490… 进行改进。

更新待续