• Java的锁机制--synchronsized关键字


    引言

    高并发环境下,多线程可能需要同时访问一个资源,并交替执行非原子性的操作,很容易出现最终结果与期望值相违背的情况,或者直接引发程序错误。

    举个简单示例,存在一个初始静态变量count=0,两个线程分别对count进行100000次加1操作,期望的结果是200000,实际是这样的吗?写个程序跑下看看:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class CountWithoutSyn {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {

    CountDownLatch countDownLatch = new CountDownLatch(2);

    // 启动线程A
    new Thread(new Runnable() {
    @Override
    public void run() {
    for(int i=0; i<100000; i++){
    count++;
    }
    countDownLatch.countDown();
    }
    }).start();

    // 启动线程B
    new Thread(new Runnable() {
    @Override
    public void run() {
    for(int i=0; i<100000; i++){
    count++;
    }
    countDownLatch.countDown();
    }
    }).start();

    // main线程等待线程A和B计算完毕
    countDownLatch.await();

    // main线程打印结果
    System.out.println("count: " + CountWithoutSyn.count);
    }

    }

    多次运行上述程序,会发现最终结果可能出现不是200000的情况,如:

    1
    2
    3
    count: 150218

    Process finished with exit code 0

    之所以出现这种情况的原因是,count++不是一个原子性的操作,所谓原子性,说的就是操作不可分割。

    count++分为3个步骤:

    • 从内存读取count的值;
    • 对count值执行+1操作;
    • 将count的值写回内存;

    比如当前count累加到了101,此时,线程A和B同时拿到了count的值为101,线程A对count加1后将102写回内存,同时线程B也对count加1后将102写回内存,而实际结果应该为103,所以丢失了1次更新。

    故高并发环境下,多线程同时对共享变量执行非原子的操作,很容易出现丢失更新的问题。

    解决办法很简单,将整个非原子的操作加锁,从而变成原子性的操作就可以了。

    Java加锁的方式主要有2种,synchronnized关键字和Lock接口。

    下面分别阐述这两种方式,本文先讲解synchronnized。

    synchronized

    在Java中,每一个对象都有一个锁标记(monitor),也称之为监视器,多线程同时访问某个对象时,线程只有获取了该对象的锁才能访问。

    该锁属于典型的互斥锁,即一旦一个线程获取到锁之后,其他线程只能等待。

    synchronize关键字可以标记方法或者代码块,当某个线程调用该对象的synchronize方法或者访问synchronize代码块时,该线程便获得了该对象的锁,其他线程暂时无法访问这个方法,只有等待这个方法执行完毕或者代码块执行完毕,该线程才会释放该对象的锁,其他线程才能执行这个方法或者代码块。

    对引言中的程序通过synchronized来进行改造:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class CountWithSyn {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {

    CountDownLatch countDownLatch = new CountDownLatch(2);

    Object lock = new Object();

    // 启动线程A
    new Thread(new Runnable() {
    @Override
    public void run() {
    synchronized (lock){
    for(int i=0; i<100000; i++){
    count++;
    }
    }
    countDownLatch.countDown();
    }
    }).start();

    // 启动线程B
    new Thread(new Runnable() {
    @Override
    public void run() {
    synchronized (lock){
    for(int i=0; i<100000; i++){
    count++;
    }
    }
    countDownLatch.countDown();
    }
    }).start();

    大专栏  Java的锁机制--synchronsized关键字ne"> // main线程等待线程A和B计算完毕
    countDownLatch.await();

    // main线程打印结果
    System.out.println("count: " + CountWithSyn.count);
    }

    }

    多次运行该程序,其结果均是:

    1
    2
    3
    count: 200000

    Process finished with exit code 0

    synchronized代码块使用起来比synchronized方法要灵活得多。因为也许一个方法中只有一部分代码只需要同步,如果此时对整个方法用synchronized进行同步,会影响程序执行效率。而使用synchronized代码块就可以避免这个问题,synchronized代码块可以实现只对需要同步的地方进行同步。

    因为上述程序的非原子操作仅是count++,所以synchronized仅修饰count++即可实现线程安全。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public class CountWithSyn {

    private volatile static int count = 0;

    public static void main(String[] args) throws InterruptedException {

    CountDownLatch countDownLatch = new CountDownLatch(2);

    Object lock = new Object();

    // 启动线程A
    new Thread(new Runnable() {
    @Override
    public void run() {
    for(int i=0; i<100000; i++){
    synchronized (lock){
    count++;
    }

    }
    countDownLatch.countDown();
    }
    }).start();

    // 启动线程B
    new Thread(new Runnable() {
    @Override
    public void run() {
    for(int i=0; i<100000; i++){
    synchronized (lock){
    count++;
    }
    }
    countDownLatch.countDown();
    }
    }).start();

    // main线程等待线程A和B计算完毕
    countDownLatch.await();

    // main线程打印结果
    System.out.println("count: " + CountWithSyn.count);
    }
    }

    需要注意的是:

    1. 当一个线程正在访问一个对象的synchronized方法,那么其他线程不能访问该对象的其他synchronized方法。这个原因很简单,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized方法。

    2. 当一个线程正在访问一个对象的synchronized方法,那么其他线程能访问该对象的非synchronized方法。这个原因很简单,访问非synchronized方法不需要获得该对象的锁,假如一个方法没用synchronized关键字修饰,说明它不会使用到临界资源,那么其他线程是可以访问这个方法的,

    3. 如果一个线程A需要访问对象object1的synchronized方法fun1,另外一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),也不会产生线程安全问题,因为他们访问的是不同的对象,所以不存在互斥问题。

    那么,synchronized关键字底层是如何实现的呢?反编译它的字节码看一下,如下述代码的字节码为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class SynCode {

    private Object lock = new Object();

    public void method1(){
    synchronized (lock){

    }
    }

    public synchronized void method2(){

    }

    public void method3(){

    }
    }

    从反编译获得的字节码可以看出,synchronized代码块实际上多了monitorenter和monitorexit两条指令。monitorenter指令执行时会让对象的锁计数加1,而monitorexit指令执行时会让对象的锁计数减1,其实这个与操作系统里面的PV操作很像,操作系统里面的PV操作就是用来控制多个线程对临界资源的访问。

    对于synchronized方法,执行中的线程识别该方法的method_info结构是否有ACC_SYNCHRONIZED标记设置,然后它自动获取对象的锁,调用方法,最后释放锁。如果有异常发生,线程自动释放锁。

    对于synchronized方法或者synchronized代码块,当出现异常时,JVM会自动释放当前线程占用的锁,因此不会由于异常导致出现死锁现象。

    参考文献:

    欢迎您扫一扫上面的二维码,关注我的微信公众号!

  • 相关阅读:
    数据库设计优化(一)--基础
    数据库设计--范式原则
    迭代器 与 foreach 的区别
    DBeaver中如何调整SQL编辑器的字体大小
    腾讯课堂下载回放视频
    超级美味的大盘鸡做法
    关闭或开启Win10系统的自动更新
    geoserver发布地图瓦片影像数据
    使用GeoServer发布Shapfile数据
    GeoServer下载与安装(Windows版)
  • 原文地址:https://www.cnblogs.com/lijianming180/p/12099650.html
Copyright © 2020-2023  润新知