深入理解synchronized

1、synchronized的三种应用方式

(1)修饰实例方法,作用于实例加锁,进入同步代码块前要获得当前实例的锁。

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 输出结果:
     * 2000000
     */
}注:如果将上述的main函数写成如下,便不是线程安全的,因为两个线程对应着两个不同的实例对象锁。 public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
 }
            

(2)修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态成员的并发操作。需要注意的是,如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁时当前类的class对象,而访问非静态synchronized方法占用的锁时当前实例对象锁。

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }


    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }


    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();//调用的是静态同步方法,所以该程序是线程安全的。
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();


        t1.join();t2.join();
        System.out.println(i);
    }

}

由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

(3)修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得指定对象的锁。

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }

}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}


//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }

}


注:join()方法

join() 定义在Thread.java中。

join() 的作用:让“主线程”等待“子线程”结束之后才能继续运行。这句话可能有点晦涩,我们还是通过例子去理解:

// 主线程
public class Father extends Thread {
    public void run() {
        Son s = new Son();
        s.start();
        s.join();
        ...
    }
}
// 子线程
public class Son extends Thread {
    public void run() {
        ...
    }

}

说明
上面的有两个类Father(主线程类)和Son(子线程类)。因为Son是在Father中创建并启动的,所以,Father是主线程类,Son是子线程类。
在Father主线程中,通过new Son()新建“子线程s”。接着通过s.start()启动“子线程s”,并且调用s.join()。在调用s.join()之后,Father主线程会一直等待,直到“子线程s”运行完毕;在“子线程s”运行完毕之后,Father主线程才能接着运行。 这也就是我们所说的“join()的作用,是让主线程会等待子线程结束之后才能继续运行”!

 


2、synchronized的底层实现原理

(1)同步代码块

通过反编译下面的代码来看一下synchronized同步代码块如何实现同步

public class SynchronizedDemo {
     public void method() {
        synchronized (this) {
             System.out.println("Method 1 start");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

反编译结果如下:


使用monitorenter和monitorexit指令实现,monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,jvm需要保证每一个monitorenter都有一个monitorexit与之对应。任何一个对象都有一个monitor与之相关联,当它的monitor被持有之后,它将处于锁定状态。线程执行到monitorenter指令前,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁;将线程执行到monitorenter时就会释放锁。

每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:
  1. 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
    • 当同一个线程再次获得该monitor的时候,计数器再次自增;
    • 当不同线程想要获得该monitor的时候,就会被阻塞。
  2. 当同一个线程释放monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。

(2)同步方法(隐式的)

在通过下面代码的反编译来看一下同步方法的实现原理:

public class SynchronizedDemo {
     public synchronized void method() {      
             System.out.println("Method 1 start");        
    }
}

反编译的结果如下:



不过相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。

在jvm字节码层面并没有任何特别的指令来实现synchronized修饰的方法,而是在class文件中将该方法的access_flags字段中的acc_synchronized标志位设置为1,表示该方法为synchronized方法。

在java设计中,每一个对象自打娘胎里出来就带了一把看不见的锁,即monitor锁。monitor是线程私有的数据结构,每一个线程都有一个monitor record列表,同时还有一个全局可用列表。每一个被锁住对象都会和一个monitor关联。monitor中有一个owner字段存放拥有该对象的线程的唯一标识,表示该锁被这个线程占有。owner:初始时为null,表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后,owner保存线程唯一标识,当锁被释放时,owner又变为null。

总结:

同步方法和同步代码块底层都是通过monitor来实现同步的。两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现我们知道了每个对象都与一个monitor相关联。而monitor可以被线程拥有或释放。


synchronized用的锁在java对象头里。对象头分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄(Generational GC Age)等,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另一部分用于存储指向方法区对象类型数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。

锁主要有四种状态:无锁状态、偏向状态、轻量级状态、重量级状态。它们会随着竞争的激烈而逐渐升级,锁可以升级但不能降级。

自旋锁、自适应自旋锁、锁消除。锁消除的依据是逃逸分析(当一个对象在方法中被定义后,它可能被外部方法所引用)的数据支持。锁粗化:将多个连续加锁解锁操作链接起来扩展为一个范围更大的锁。

轻量级锁:传统的锁时重量级锁,它是用系统的互斥量来实现。轻量级锁的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。

偏向锁:目的是消除数据在无竞争情况下的同步原语。如果说轻量级锁是在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那么偏向锁就是在无竞争的情况下把整个同步消除掉。



猜你喜欢

转载自blog.csdn.net/g1607058603/article/details/81007335
今日推荐