java synchronized关键字原理

java synchronized关键字原理

synchronized关键字是jdk内置的同步锁,它是解决并发问题的一种最常用的方法。synchronized可以修饰普通方法、静态方法、代码块,首先看下synchronized的用法,接着介绍synchronized的原理。

synchronized的用法

synchronized关键字是为了解决多线程并发的问题。

先看下这一段代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class CountTest {

    private static int count = 0;

    /**
     * 有多线程并发问题
     */
    public static void increment() {
        count++;
    }

    public static void main(String[] args) {
        //10个线程去递增count变量
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10000; i++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    increment();
                }
            });
        }
        executorService.shutdown();
        try {
            executorService.awaitTermination(Long.MAX_VALUE, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }
}

这里通过10个线程去递增count的变量,通过执行10000个任务,希望将count变量的值递增到10000。这段代码的输出基本都是小于10000,大约在9000多。

这是因为increment()方法存在线程并发的问题,如果当前count的值为78,此时线程A和线程B都进入到了increment方法,并且读到的count值为78,然后在78的基础上将count递增到79,最终这两个线程都将count的值递增到79。显然,有一个线程任务是无效的,它并没有将count的值递增,我们希望有一个线程将count的值递增到79,另一个线程将count递增到80。

好了,找到问题,怎么解决呢?

可以在increment方法上加上synchronized的关键字,这表示该方法是线程安全的,如果该线程没有释放锁(synchronized其实获取的对象锁),其他线程是无法进入该方法去执行的。看下面的代码片断:

public synchronized static void increment() {
    count++;
}

这样可以解决问题,但是问题也是很明显的,锁的粒度太粗了,如果该方法内部逻辑很多,并且并不是全部都有线程并发问题,那就可以将synchronized关键字移到方法内部,这就是synchronized修饰代码块的用法。看下面的代码片断:

public static void increment() {
    //synchronized修饰代码块,实现更细粒度的锁
    synchronized (CountTest.class) {
      count++;
    }
}

现在为止,上面例子中都是通过synchronized关键字修饰静态方法,synchronized也可以修饰普通的方法。修饰普通方法和修饰静态方法的不同之处在于synchronized获取的锁不同:修饰普通方法获取的锁是对象锁,而修饰静态方法获取的是类锁。

什么是对象锁?什么是类锁?

每个对象都有唯一的一把锁,称为对象锁。线程进入synchronized方法区(或者代码块)时如果发现对应的对象锁计数为0,说明这个对象锁是空闲的,没有其他线程占有,这个线程就可以获得这个对象锁并进入synchronized块。否则,线程将阻塞在这个对象锁上。

线程进入静态方法区需要去获取类锁,不论这个类有多少个实例对象,类锁只有一个。

对象锁和类锁都是可重入的,同一个线程可以多次获取之前获取的锁,每当线程进入之前已经获得锁的synchronized块时,对象锁或者类锁的计数都将加1。每当线程离开synchronized块时,锁的计数减1,当计数减到0时,该对象锁变为可获得的。

synchronized原理

上面其实已经提到了synchronized的原理,这里我们站在字节码的角度看下,JVM是怎么实现对象锁的。

利用javap命令,反编译出字节码,观察上例CountTest类的字节码:

localhost:lock lms$ javap -c CountTest.class 
Compiled from "CountTest.java"
public class com.lms.lock.CountTest {
  public com.lms.lock.CountTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void increment();
    Code:
       0: ldc_w         #2                  // class com/lms/lock/CountTest
       3: dup
       4: astore_0
       5: monitorenter
       6: getstatic     #3                  // Field count:I
       9: iconst_1
      10: iadd
      11: putstatic     #3                  // Field count:I
      14: aload_0
      15: monitorexit
      16: goto          24
      19: astore_1
      20: aload_0
      21: monitorexit
      22: aload_1
      23: athrow
      24: return
    //……
}

观察到第5行和第21行的指令,参考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),如果监视器被占用,它就会被锁住。线程在执行到monitorenter指令时,会执行如下动作:

  • 如果线程发现该监视器锁的计数为0,该线程进入monitorenter并且设置监视器锁的计数为1,这个线程就拥有了该监视器锁。
  • 如果线程发现自己已经拥有该监视器锁,该线程进入monitorenter并且将监视器锁的计数加1。
  • 如果线程发现监视器锁已经被其他线程占有,当前线程会阻塞,直到监视器锁的计数为0,当前线程继续尝试获取锁。

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的线程必须是该监视器的拥有者。线程执行monitorexit后将该监视器锁的计数减1,如果执行monitorexit指令后监视器锁计数变为0,线程将会释放该监视器锁,其他在该监视器锁上阻塞的线程将会醒来并尝试继续获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值