Java 并发编程(二)Synchronized原理剖析及使用

Java 并发编程之Synchronized原理剖析及使用

在开始介绍Synchronize之前,先了解一下在并发中极其重要的三个概念:原子性,可见行和有序性

  • 原子性: 是指一个操作不可以被中断.比如赋值操作a=1和返回操作return a,这样的操作在JVM中只需要一步就可以完成,因此具有原子性,而想自增操作a++这样的操作就不具备原子性,a++在JVM中要一般经历三个步骤:
    1. 从内存中取出a.
    2. 计算a+1.
    3. 将计算结果写回内存中去.
  • 可见性: 一个线程对于共享变量的修改,能够及时地被其他线程看到.
  • 有序性: 程序执行的顺序按照代码的先后逻辑顺序执行.

只有同时保证了这三个特性才能认为操作是线程安全的.
对于Java来说, 被关键字Synchronized修饰的同步代码块或者同步方法能保证每个时刻只有一个线程执行该同步代码,自然便保证了有序性.在Java内存模型中,synchronized规定,在工作线程获得所要加锁的对象的锁后:

  1. 获得对象的互斥锁
  2. 清空工作内存
  3. 在主内存中拷贝最新变量的副本到工作内存
  4. 执行同步代码块
  5. 将更改后的共享变量的值刷新到主内存中
  6. 释放互斥锁

其中步骤3与步骤5保证了操作的可见性,而Java内存模型保证从获取互斥锁到释放互斥锁的整个过程不会被其他线程中断,因此也保证了操作的原子性.
接下来开始剖析Synchronized关键字的原理

Synchronized原理剖析


首先通过反编译下面的代码来看看JVM中Synchronized是如何实现对代码块进行同步的.

package com.paddx.test.concurrent; 
public class SynchronizedDemo { 
    public void method() { 
        synchronized (this) { 
             System.out.println("Method 1 start"); 
        } 
    } 
 } 

反编译结果:

所以monitorentermonitorexit这两个字节码指令是个什么鬼呢?来看看JVM规范中的描述:

monitorenter

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在每个时刻最多只能被一个线程拥有,线程执行monitorenter字节码指令时尝试获取该对象的监视器锁(monitor)的所有权,获取步骤如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者
  2. 如果本线程已经拥有该monitor的所有权,只是重新进入,则monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则本线程进入阻塞状态(Synchronize在改进后是线程先进入自旋状态等待,等待持有该对象锁的线程释放锁就可立即获得锁,如果超过自旋等待的最大时间仍未能获得该对象锁,则本线程停止自旋进入阻塞状态),直到monitor的进入数为0,再重新尝试获取monitor的所有权.

monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

大概意思:
执行字节码指令monitorexit的线程必须是拥有该监视器(monitor)的所有者.
执行该命令后,monitor的进入数-1,当monitor的进入数变为0时,该线程就失去了该监视器的所有权,即释放该对象的监视器锁,其它被该monitor阻塞的线程可以开始尝试获取该对象的监视器锁.

JVM基于进入和退出Monitor对象来实现代码块同步和方法同步,两者实现形式不同,但是实现原理无本质区别.
经过上面的分析可知同步代码块使用monitorentermonitorexit字节码指令实现,而方法同步是在其常量池中多了ACC_SYNCHRONIZED标志符号.

JVM实现同步方法原理:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor.
在方法执行期间,其他任何线程都无法再获得同一个monitor对象.

Synchronized的使用


总体上,Synchronized一般有三种用法:

  • 修饰代码块
  • 修饰普通方法
  • 修饰静态方法
  1. 同步一个代码块
public void func() {
    synchronized (this) {
        // ...
    }
}

this指的是当前类的对象的引用,使用synchronized修饰this的意思就是尝试获取当前类的对象的监视器锁(monitor),当本线程拥有该对象监视器锁时,其他尝试获取该对象的监视器锁的线程自然会被阻塞.如果其他线程调用的是同一个类的不同对象上的同步代码块,就不会被阻塞.

public class SynchronizedExample {

    public void func1() {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e1.func1());
}

代码运行结果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

调用同一个类的不同对象的同步代码块,不会出现同步.

public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func1());
    executorService.execute(() -> e2.func1());
}

代码运行结果:(如果没有出现该不同步的现象,主要是现在cpu运行速度太快了,将数值设置大一些即可)

0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
  1. 同步一个普通方法
public synchronized void func () {
    // ...
}

与同步代码块一样,只作用于同一个对象,不再赘述.

  1. 同步一个静态方法
public synchronized static void fun() {
    // ...
}

作用于整个类.在同一时刻该类的同步方法只能被一个线程调用.
4. 同步一个类

public class SynchronizedExample {

    public void func2() {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                System.out.print(i + " ");
            }
        }
    }
}
public static void main(String[] args) {
    SynchronizedExample e1 = new SynchronizedExample();
    SynchronizedExample e2 = new SynchronizedExample();
    ExecutorService executorService = Executors.newCachedThreadPool();
    executorService.execute(() -> e1.func2());
    executorService.execute(() -> e2.func2());
}

代码运行结果:

0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9

作用于整个类,两个线程调用同一个类的不同对象上的这种同步语句,也会进行同步。

学习Synchronized关键字遇到的一个小困惑

废话不多说,直接上代码

class SynchronizedExample{
    private int value;
    public synchronized void setValue(int value){
       this.value=value;
    }
    public int getValue(){
        return value;
    }
}

使用多个线程共享value值,假如某个线程调用了setValue()方法,另一个线程看到的并不一定是更新后的value值.这里就涉及了内存可见性问题.getValue()方法并不是同步方法,调用getValue()不需要持有对象锁,
为了确保所有线程能够看到共享变量的最新值,可以在所有执行读操作和写操作的线程上加上同一把对象锁.这样读操作,就可以看到最新的写操作之前所有的共享变量的状态变化了.

当线程 A 执行某个同步代码块时,线程 B 随后进入由同一个锁保护的同步代码块,这种情况下可以保证,当锁被释放前,A 看到的所有变量值(锁释放前,A 看到的变量包括 y 和 x)在 B 获得同一个锁后同样可以由 B 看到。换句话说,当线程 B 执行由锁保护的同步代码块时,可以看到线程 A 之前在同一个锁保护的同步代码块中的所有操作结果。如果在线程 A unlock M 之后,线程 B 才进入 lock M,那么线程 B 都可以看到线程 A unlock M 之前的操作,可以得到 i=1,j=1。如果在线程 B unlock M 之后,线程 A 才进入 lock M,那么线程 B 就不一定能看到线程 A 中的操作,因此 j 的值就不一定是 1。

class SynchronizedExample{
    private int value;
    public synchronized void setValue(int value){
       this.value=value;
    }
    public synchronized int getValue(){
        return value;
    }
}

setValue()方法和getValue()方法都采用Synchronized关键字修饰,变成同步方法,即不同线程在调用同一个对象中的这两个方法时需要获得同一把锁,这样每次通过getValue()方法获得的value值都是最新的值.

Java 并发编程(一)Volatile原理剖析及使用
Java 并发编程(二)Synchronized原理剖析及使用
Java 并发编程(三)Synchronized底层优化(偏向锁与轻量级锁)
Java 并发编程(四)JVM中锁的优化
Java 并发编程(五)原子操作类

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