【Java多线程与并发】——Synchronized关键字详解

目录

一、syncronized关键字介绍

二、synchronized使用场景

三 、synchronized的一些特性

1)synchronized具有锁重入的功能

2)同步不具有继承性

3)退出或者异常发生时自动释放锁

4)synchronized(string)使用的注意

5)synchronized使用基本原则

四、synchronized实现原理及应用

1)同步方法实现原理

2)同步代码块实现原理


一、syncronized关键字介绍

synchronized关键字是用来解决Java语言中非线程安全的相关问题,一句话说就是synchronized是为了解决多个线程访问资源的同步性。

二、synchronized使用场景

synchronized既可以用来修饰方法,也可以用来修饰代码块,如下图(此图来源于:让你彻底理解Synchronized)所示:

如上图所示 :当synchronized修饰实例方法时,锁的是类的实例对象,而修饰静态方法时,锁的是整个类对象,即对类的所有实例对象都起同步的效果。

需要知道的是:

  • 多个线程调用同一个对象中的不同名称的synchronized同步方法或synchronized(this)同步代码块时,调用的效果就是同步的
  • 对于同一个对象,当一个线程对象的同步方法时,其他线程可以调用对象的非同步方法,而不会发生同步阻塞

三 、synchronized的一些特性

1)synchronized具有锁重入的功能

比如说当一个线程获得了某个对象锁,此时这个对象锁还没有释放,当其想再此获得锁时还是可以获取的,如下代码示例:

class Service {
    synchronized public void serviceA(){
        System.out.println("serviceA");
        serviceB();
    }
    synchronized public void serviceB(){
        System.out.println("serviceB");
        serviceC();
    }
    synchronized public void serviceC(){
        System.out.println("serviceC");
    }
}
public class MyThread extends Thread{
    @Override
    public void run() {
        Service service = new Service();
        service.serviceA();
    }
}
public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
}

运行结果:

 

当存在父子类继承关系时,子类完全可以通过“可重入锁”调用父类的同步方法

2)同步不具有继承性

当子类重写了父类的同步方法,但是没有加synchronized关键字,即使对于同一个对象,多线程调用子类方法,子类方法调用父类方法时,此时子类方法的执行和父类的同步方法执行时是异步的效果,而非同步。

3)退出或者异常发生时自动释放锁

当一个线程执行一个方法出现异常时,其所持有的锁会自动释放

4)synchronized(string)使用的注意

因为String类有常量池,当使用synchronized(string)同步代码块的时候,会出现同步阻塞,大多数情况不会使用String作为锁对象

5)synchronized使用基本原则

第一条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的该“synchronized方法”或者“synchronized代码块”的访问将被阻塞。
           第二条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程仍然可以访问“该对象”的非同步代码块
           第三条:当一个线程访问“某对象”的“synchronized方法”或者“synchronized代码块”时,其他线程对“该对象”的其他的“synchronized方法”或者“synchronized代码块”的访问将被阻塞。

 

四、synchronized实现原理及应用

从Java虚拟机规范中我们可以看到synchronized的实现原理,Java虚拟机支持方法和指令序列的同步通过一个单一的同步构造,monitor监视器对象。但是同步方法和同步代码块这两者的实现细节是不一样的,尽管同步方法使用monitorenter和monitorexit指令同样达到与同步代码块一样的效果。

1)同步方法实现原理

方法级别的同步是隐式的,同步方法在运行时常量池中的method_info结构中通过一个被方法调用指令检查的ACC_SYNCRONIZED标志来区分。当调用一个被ACC_SYNCHRONIZED标志设置的方法时,正在执行的线程会进入到一个monitor,即获取到对象的monitor所有权,当方法调用正常完成或者突然中断会退出monitor,即释放对象的monitor所有权。如果在执行同步方法时发生异常,在异常被重新抛出同步的方法外部之前会自动退出monitor,即释放掉monitor所有权。

2)同步代码块实现原理

同步指令序列通常被用于编码Java语言的同步代码块。通过使用monitorenter和monitorexit指令来实现。

我们来先看一下monitorenter,直接看一下JVM规范描述:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

意思就是:每一个对象都关联一个monitor,即对象监视器

  • 如果对象引用关联的monitor进入数为0,线程进入monitor并将进入数设为1,此线程就是monitor所有权拥有者
  • 如果线程已经拥有了对象引用关联的monitor,就会重新进入monitor,将进入数加1,这就是synchronized锁的可重入特性
  • 如果另一个线程已经拥有了对象引用关联的monitor,线程阻塞直到monitor监视器的进入数为0,然后尝试重新获取monitor所有权
public class ThreadDemo extends Thread {
    private Service1 service1;
    public ThreadDemo(Service1 service1){
        this.service1 =service1;
    }
    @Override
    public void run() {
        service1.getName();
    }
    public static void main(String[] args) {
        Service1 service1 = new Service1();
        ThreadDemo thread = new ThreadDemo(service1);
        thread.start();

    }
}

class Service1{
    public void getName(){
        synchronized (this){
            System.out.println("lalala");
            //当前实例已经拥有了monitor所有权,当再次调用其他同步块代码时,会再次获得锁
            //synchronized可重入锁功能
            getAge();
        }
    }
    public void getAge(){
        synchronized (this){
            System.out.println("hahaha");
        }
    }
}

我们通过命令:javap -v Service1.class 查看Service1.class的class指令,如下图:

从上图可以看出,执行getAge()方法时只有一个monitorexit指令,并没有monitorenter指令,这就是锁的可重入性,当一个线程获得了某个对象的锁后,此时这个对象的锁还没有被释放,当其再次想获得这个对象的锁时还是可以获取的。当我们执行getAge()方法的同步代码块时,我们会重新进入monitor将进入数加1,所以,虽然一个monitorenter对应两个monitorexit,但是最后全部执行完,进入数最终是减为0的,即释放锁monitor对象的所有权。

下图(摘自《Java并发编程的艺术》一书)表现了对象,对象监视器,同步队列以及执行线程状态之间的关系:

 

从图中可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章