线程安全与数据同步

本文,我们主要将探讨如何在安全的前提下高效的共享数据

  • 数据同步
  • synchronized关键字
  • This monitor 和 Class monitor的详细介绍
  • 程序死锁的原因和诊断
数据同步

首先我们来看一个数据不一致的例子

public class TicketWindowRunnable implements Runnable {
    private  int index = 1;
    private final  static  int  MAX = 500;
    @Override
    public void run() {
        while (index <= MAX)
            System.out.println(Thread.currentThread() + " 的号码是: " + (index++));
    }

    public static void main(String[] args) {
        final TicketWindowRunnable task = new TicketWindowRunnable();
        Thread wind1 = new Thread(task, " 1 号 窗口");
        Thread wind2 = new Thread(task, " 2 号 窗口");
        Thread wind3 = new Thread(task, " 3 号 窗口");
        Thread wind4 = new Thread(task, " 4 号 窗口");

        wind1.start();
        wind2.start();
        wind3.start();
        wind4.start();
    }
}

运行多次我们发现

  • 某个号码被忽略过没有出现
  • 某个号码多次显示
  • 号码超过了最大值500

数据不一致的分析
问题的发生主要是下列这个片段代码的引入

    public void run() {
        while (index <= MAX)
            System.out.println(Thread.currentThread() + " 的号码是: " + (index++));
    }

某个号码被忽略过没有出现
线程的执行有CPU轮询调度,假设线程1 和 2 都执行到 index = 65 位置,其中线程 2 将 index 修改为 66 之后为输出,此时CPU调度将执行权交给了线程 1,线程 1 直接拿到其累加的67,此时 66 就被忽略了
在这里插入图片描述
某个号码多次显示
线程 1 执行 index + 1,然后CPU执行到线程 2 手上,由于线程 1 没有给index 赋值,导致计算结果重复出现
在这里插入图片描述

号码超过了最大值
当 index = 499 的时候,线程 1 和 线程 2 都看到条件满足,线程 2 暂停,线程 1 将index加 到 500 ,此时线程 2 恢复后又将 500 添加到501,此时就出现超过最大值的情况
在这里插入图片描述

synchronized关键字

通过上述分析,不难发现数据的不一致主要原因是多个线程对index变量(共享变量)同时操作,要解决这个问题只需要引入synchronized关键字即可,synchronized 提供了一种排他锁的机制,可以保证在同一时间只能有一个线程执行某些操作

什么是synchronized
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都可以通过同步的方式进行,具体表现如下

  • synchronized 关键字提供一种锁的机制,能确保共享变量的互斥访问,从而防止数据不一致的问题出现
  • synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能保证在任何线程执行到monitor enter 成功之前都必须从主内存中获取,而不是缓存中,字monitor exit 运行成功之后,共享变量被更新后的值必须刷入主内存
  • synchronized 的指令严格遵守Java happens-before 规则,一个monitor exit 指令之前必定要有一个monitor enter

synchronized 关键字的使用

synchronized可以用来对代码块或者方法修饰,而不能对class以及变量进行修饰
synchronized关键字提供了一种互斥机制,也就是在同一时刻,只能有一个线程访问同步资源,具体来说某线程获取了与synchronized关联的monitor锁synchronized关键字

通过上述分析,不难发现数据的不一致主要原因是多个线程对index变量(共享变量)同时操作,要解决这个问题只需要引入synchronized关键字即可,synchronized 提供了一种排他锁的机制,可以保证在同一时间只能有一个线程执行某些操作

什么是synchronized
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都可以通过同步的方式进行,具体表现如下

  • synchronized 关键字提供一种锁的机制,能确保共享变量的互斥访问,从而防止数据不一致的问题出现
  • synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能保证在任何线程执行到monitor enter 成功之前都必须从主内存中获取,而不是缓存中,字monitor exit 运行成功之后,共享变量被更新后的值必须刷入主内存
  • synchronized 的指令严格遵守Java happens-before 规则,一个monitor exit 指令之前必定要有一个monitor enter

synchronized 关键字的使用

synchronized可以用来对代码块或者方法修饰,而不能对class以及变量进行修饰
synchronized关键字提供了一种互斥机制,也就是在同一时刻,只能有一个线程访问同步资源,具体来说某线程获取了与synchronized关联的monitor锁

JVM指令分析

通过javap对Mutex class 进行反编译,可以发现monitor enter 和 monitor exit 是成对出现的

public class Mutex {
    private final static Object MUTEX = new Object();

    public void accessResource(){
        synchronized (MUTEX){
            try {
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final Mutex mutex = new Mutex();
        for(int i=0; i<5; i++){
            new Thread(mutex::accessResource, "main-" + i).start();
        }
    }
}

运行 javac -c

public class chapter4.Mutex {
  public chapter4.Mutex();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void accessResource();
    Code:
       0: getstatic     #2                // 获取MUTEX
       3: dup
       4: astore_1
       5: monitorenter                   // 执行 monitorenter 指令
       6: getstatic     #3                  // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
       9: ldc2_w        #4                  // long 10l
      12: invokevirtual #6                  // Method java/util/concurrent/TimeUnit.sleep:(J)V
      15: goto          23
      18: astore_2
      19: aload_2
      20: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1                    
      24: monitorexit                     // 执行 monitorexit 指令
      25: goto          33
      28: astore_3
      29: aload_1
      30: monitorexit
      31: aload_3
      32: athrow
      33: return
    Exception table:
       from    to  target type
           6    15    18   Class java/lang/InterruptedException
           6    25    28   any
          28    31    28   any
}

Monitorenter

每一个对象都与一个 monitor 相关联,一个 monitor 的 lock 的锁只能被一个线程再同一时间获得,在一个线程尝试获得与对象关联的 monitor 的所有权会发生如下几个事情

  • 如果 monitor 的计数器为0,则意味该 monitor 的 lock 还没有被获得,某个线程获得之后将立即对该计数器+1,从这该线程就是这个 monitor 的所有者
  • 如果一个已经拥有该 monitor 的所有权线程重入,则会导致 monitor 计数器再次累加
  • 如果 monitor 已经被其他线程所拥有,则其他线程尝试获取该monitor 的所有权时,会被陷入阻塞状态直到 monitor计数器变为0,才能再次尝试获取对 monitor 的所有权

Monitorexit

释放对 monitor 所有权,想释放对某个对象关联的 monitor 的所有权的前提是,你曾经获得过所有权,释放 monitor 所有权的过程比较简单,就是将 monitor 的计数器 -1,如果计数器的结果为0,就意味着该线程不再拥 monitor 的所有权,此时被该 monitor block 的线程将再次尝试获取对 monitor 的所有权

使用 synchronized需要注意的问题

  1. 与 monitor 关联的对象不能为 null (每一个对象 都有 一个 monitor 相关联)

  2. synchronized 作用域太大,由于synchronized 具有排他性,如果作用域太大,代表其效率越低

  3. 不同的 monitor 企图锁相同的方法
    public class Task implements Runnable {
    private final Object MUTEX = new Object();
    @Override
    public void run() {
    synchronized (MUTEX){
    // …
    }
    }

        public static void main(String[] args) {
            for(int i=0; i<5; i++)
                new Thread(Task::new).start();
        }
    }
    

    上面的代码构造了5个线程,同时也构造了5个Runnable实例,Runnable作为线程逻辑执行单元传递给Thread,但是互斥不了对应的作用域,线程之间进行 monitor lock 的争抢智能与monitor 关联的同一个引用上,上面的代码争抢的 monitor 关联引用都是彼此独立的,因此不可能起到互斥的作用

  4. 多个锁的交叉导致死锁(稍后讲解)

    this monitor
    使用 synchronized 关键字同步类的不同实例方法,争抢的是同一个 monitor 的lock,与之关联的引用是对象的引用

    class monitor
    使用 synchronized 关键字同步类的不同静态方法,争抢的是同一个 monitor 的lock,与之关联的引用是对象class 的实例

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值