【Java】线程安全与数据同步

学而不思则罔,思而不学则殆


什么是共享资源?共享资源指的是多个线程同时对一份资源进行访问(读写操作),被多个线程访问的资源就称为共享资源。如何保证多个线程访问到的数据是一致的,则被称为数据同步或者资源同步。


初识synchronized关键字

什么是synchronized

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

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

synchronized关键字的用法

synchronized修饰方法

    public synchronized void show5() {
        show("普通方法锁");
    }

    public static synchronized void show6() {
        show("静态方法锁");
    }

synchronized修饰代码块

    private static final Object mStaticLock = new Object();
    private final Object mLock = new Object();
    public void show2() {
        synchronized (mStaticLock) {
            show("静态变量锁 代码块");
        }
    }

    public void show3() {
        synchronized (mLock) {
            show("普通对象锁 代码块");
        }
    }

深入synchronized关键字

线程堆栈分析

一个简单的例子:

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

    public void accessResource() {
        System.out.println("accessResource:" + Thread.currentThread() + " synchronized");
        synchronized (MUTEX) {
            try {
                System.out.println("accessResource:" + Thread.currentThread() + " sleep");
                TimeUnit.MINUTES.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        final SynchronizedMainTest synchronizedMainTest = new SynchronizedMainTest();
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronizedMainTest.accessResource();
                }
            }).start();

        }
    }
}

展示如下:

accessResource:Thread[Thread-0,5,main] synchronized
accessResource:Thread[Thread-0,5,main] sleep
accessResource:Thread[Thread-1,5,main] synchronized
accessResource:Thread[Thread-2,5,main] synchronized
accessResource:Thread[Thread-3,5,main] synchronized
accessResource:Thread[Thread-4,5,main] synchronized

除了线程【Thread-0】进入了休眠,其他线程都没有执行休眠方法,说明没有拿到锁。

通过Jconsole查看线程

情况如下:
在这里插入图片描述

在这里插入图片描述
除了线程【Thread-0】- 处于 【TIMED_WAITING】状态,其他四个线程都处于【BLOCKED】,还告诉我们对象当前的拥有者是Thread-0.

通过Jstack查看线程

情况如下:
Thread-0
在这里插入图片描述

其他四个线程:
在这里插入图片描述

线程【Thread-0】持有了【0x000000074069fa10】对象,进入了休眠,没有释放锁,状态为【TIMED_WAITING】
另外四个线程都在等待锁【0x000000074069fa10】的释放,进入了堵塞状态【BLOCKED】。

JVM指令分析

命令javap:

 javap -c SynchronizedMainTest.class

之前打印log的代码去掉,如下:

Compiled from "SynchronizedMainTest.java"
public class com.thread.SynchronizedMainTest {
  public com.thread.SynchronizedMainTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void accessResource();
    Code:
       0: getstatic     #2                  // Field MUTEX:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: 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
      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

  public static void main(java.lang.String[]);
    Code:
       0: new           #9                  // class com/thread/SynchronizedMainTest
       3: dup
       4: invokespecial #10                 // Method "<init>":()V
       7: astore_1
       8: iconst_0
       9: istore_2
      10: iload_2
      11: iconst_5
      12: if_icmpge     39
      15: new           #11                 // class java/lang/Thread
      18: dup
      19: new           #12                 // class com/thread/SynchronizedMainTest$1
      22: dup
      23: aload_1
      24: invokespecial #13                 // Method com/thread/SynchronizedMainTest$1."<init>":(Lcom/thread/SynchronizedMainTest;)V
      27: invokespecial #14                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
      30: invokevirtual #15                 // Method java/lang/Thread.start:()V
      33: iinc          2, 1
      36: goto          10
      39: return

  static {};
    Code:
       0: new           #16                 // class java/lang/Object
       3: dup
       4: invokespecial #1                  // Method java/lang/Object."<init>":()V
       7: putstatic     #2                  // Field MUTEX:Ljava/lang/Object;
      10: return
}

重点片段:

  public void accessResource();
    Code:
       0: getstatic     #2                  // Field MUTEX:Ljava/lang/Object;    1.获取MUTEX
       3: dup
       4: astore_1
       5: monitorenter                      //2.执行monitorenter JVM指令
       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                  //3.跳转到23行
      18: astore_2
      19: aload_2
      20: invokevirtual #8                  // Method java/lang/InterruptedException.printStackTrace:()V
      23: aload_1                           //4
      24: monitorexit                       //5.执行monitor exit JVM指令
      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

选取其中重要的片段:

  1. 获取MUTEX
  2. 执行monitorenter JVM指令
  3. 休眠结束后goto到23行
  4. 执行monitor exit JVM指令

Monitorenter

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

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

Monitorexit

释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是你曾经获得了所有权。
释放monitor所有权的过程比较简单,就是讲monitor的计数器减一,如果计数器为0,那就意味着该线程不在拥有对该monitor的所有权,通俗的将就是解锁
与此同时被该monitor block的线程将会再次尝试获得该monitor的所有权。

使用synchronized 需要注意的问题

  1. 与monitor关联的对象不能为空
  2. synchronized 作用域太大
  3. 不同的monitor企图锁相同的方法
  4. 多个锁的交叉导致死锁
    private final static Object MUTEX = null;

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

报错异常:

Exception in thread "Thread-0" java.lang.NullPointerException
	at com.thread.SynchronizedMainTest.accessResource(SynchronizedMainTest.java:9)
	at com.thread.SynchronizedMainTest$1.run(SynchronizedMainTest.java:24)
	at java.lang.Thread.run(Thread.java:748)

This Monitor和Class Monrtor的详细介绍

this monitor - 对象锁

在这里插入图片描述

class monitor - 类锁

在这里插入图片描述

程序死锁的原因以及如何诊断

程序死锁

  1. 交叉锁可导致程序出现死锁

线程A持有R1锁,等待获取R2锁,线程B持有R2锁等待获取R1锁,这种情况下爱最容易发生程序死锁

  1. 内存不足
  2. 一问一答式的数据交换
  3. 数据库锁
  4. 文件锁
  5. 死循环引起的死锁

程序死锁举例

public class SynchronizedMainTest {
    private final static Object READ = new Object();
    private final static Object WRITE = new Object();

    public void read() {
        synchronized (READ) {
            System.out.println(Thread.currentThread().getName() + " get READ lock");
            synchronized (WRITE) {
                System.out.println(Thread.currentThread().getName() + " get WRITE lock");
            }
            System.out.println(Thread.currentThread().getName() + " release WRITE lock");
        }
        System.out.println(Thread.currentThread().getName() + " release READ lock");
    }


    public void write() {
        synchronized (WRITE) {
            System.out.println(Thread.currentThread().getName() + " get WRITE lock");
            synchronized (READ) {
                System.out.println(Thread.currentThread().getName() + " get READ lock");
            }
            System.out.println(Thread.currentThread().getName() + " release READ lock");
        }
        System.out.println(Thread.currentThread().getName() + " release WRITE lock");
    }

    public static void main(String[] args) {
        final SynchronizedMainTest synchronizedMainTest = new SynchronizedMainTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronizedMainTest.read();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    synchronizedMainTest.write();
                }
            }
        }).start();

    }
}

这段代码执行后很快就会发生死锁:

Thread-0 get READ lock
Thread-1 get WRITE lock

死锁诊断

通过命令:

 jstack.exe -l 11708

查看死锁检测:
在这里插入图片描述
【Thread-1】等待的锁被线程【Thread-0】held
【Thread-0】等待的锁被线程【Thread-1】held

一般交叉死锁引起的思考所很容易进入BLOCKED状态,cpu占用不高们很容易借助工具来发现。

总结

了解synchroized关键字的内在原理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值