并发编程——线程安全与数据同步

线程安全与数据同步

共享资源:
多个线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就是共享资源

数据同步/资源同步:如何保证多个线程访问到的数据是一致的

1 数据同步

1.1数据不一致问题

例子:累加数据

package com.spring.zcl.study.springbootstudy.thread;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @Author: zcl
 * @Date: 2021-12-17 16:03
 */
public class Thread02 implements Runnable{
    private int index = 0;
    static AtomicInteger integer = new AtomicInteger(0);
    private static final int MAX = 30;
    @Override
    public void run() {
        while (index < MAX) {
            System.out.println(Thread.currentThread() + "的号码是:" + (index++));
            integer.getAndIncrement();
            System.out.println(Thread.currentThread() + "" + integer.get());
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        // Thread02 thread02 = new Thread02();
        Thread02 thread02 = new Thread02();
        Thread t1 = new Thread(thread02, "一号");
        Thread t2 = new Thread(thread02, "二号");
        Thread t3 = new Thread(thread02, "三号");
        Thread t4 = new Thread(thread02, "四号");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        try {
            TimeUnit.HOURS.sleep(1);
            TimeUnit.SECONDS.sleep(5);
            Thread.yield();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("main---------" + integer.get());
    }
}
  1. 某段代码未执行

线程的执行是由CPU时间片轮询调度的,假设此时线程1和2都执行到了index=17的位置,其中线程1将index累加为18,此时CPU调度器将执行的权利交给线程2,线程2直接累加到19,则18则会被跳过;

  1. 某段代码被执行多次

线程1执行index+1,此时还未赋值,线程2同时也执行到此位置,由于线程1此时还未赋值,所以线程1和2会执行相同的数据累加;

  1. 超过限定条件

线程1和2都执行到最大值-1,比较之后都满足条件,当线程1,执行累加到Max值之后出现了短暂停顿,线程2执行,则会出现MAX+1的情况,超出限定大小条件;

1.2 初始synchronized关键字

1.2.1 synchronized 简介

上述问题出现的原因是因为多个线程对index变量同事操作引起的,这里的index变量也就是共享资源或者叫做共享变量,JDK1.5之前解决需要使用synchronized关键字

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

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

1.2.2 synchronized用法

可以对于代码块或方法进行修饰,不能够用于对class以及变量进行修饰

  1. 同步方法

    public synchronized void sync()
    {

    }
    public synchronized static void sync()
    {

    }

  2. 同步代码块

    private final Object MUTEX = new Object();
    public void sync() {
    synchronized(MUTEX) {

    }
    }

1.3 深入synchronized关键字

1.3.1 线程堆栈分析

synchronized关键字提供了一种互斥机制,也就是在同一时刻,只能有一个线程访问同步资源,很多书籍和资料将synchronized(MUTEX)称为锁,准确的来说,应该是某个线程获取了与MUTEX关联的monitor锁。

JConsole工具以及jstack命令均可查看当前堆栈信息

1.3.2 JVM指令分析

使用JDK命令javap对Mutex class进行反汇编,输出大量的JVN命令,在这些命令中你会发现monitor enter和monitor exit是成对出现的(有时候会出现一个monitor enter多个monitor exit, 但是每一个monitor exit之前必须有对应的monitor enter,这是肯定的)

  1. Monitorenter

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

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

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

1.3.3 使用synchronized需要注意的问题
  1. 与monitor关联的对象不能为空
  2. synchronized作用域太大
  3. 不同的monitor企图锁相同的方法
  4. 多个锁的交叉导致死锁

1.4 This Monitor和Class Monitor的详细介绍

1.4.1 this monitor
public void method02() {
    synchronized (this) {
        System.out.println(Thread.currentThread().getName() + "method02");
        try {
            TimeUnit.MINUTES.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
public synchronized void method02() {
    System.out.println(Thread.currentThread().getName() + "method02");
    try {
        TimeUnit.MINUTES.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

效果一样,使用synchronized关键字同步类的不同实例方法,争抢的是同一个monitor的lock,如上述代码,一个是同步方法,一个是同步块,这两种方式在最终的实现效果上都是一样的,均是争抢同一个monitor的lock。

JDK文档中描述:
when a thread invokes a synchronized method, it automatically acquies the intrinsic lock for that method’s object and release it when the method returns.The lock release occurs even if the return was caused by an uncaught exception.

当线程调用一个同步方法时,它会自动获取该方法对象的内在锁,并在该方法返回时释放锁。即使返回是由未捕获的异常引起的,也会释放锁。

1.4.2 class monitor
public static synchronized void method01() {
    System.out.println(Thread.currentThread().getName() + "method01");
    try {
        TimeUnit.MINUTES.sleep(10);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

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

public static void method03() {
    synchronized (Thread08ClassMonitor.class) {
        System.out.println(Thread.currentThread().getName() + "method02");
        try {
            TimeUnit.MINUTES.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

根据jstack对比堆栈信息,可以得出synchronized同步某个类的不同静态方法争抢的是同一个monitor的lock,查看堆栈信息,你会发现 a java.lang.Class … 信息,由此可以知道与该monitor关联的引用是当前实体类。

jdk官方文档解释:
since a static is associated with a class, not a object .In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class’s static fields is controlled by a lock that’s distinct from the lock for any instance of the class.

因为static是与类关联的,而不是与对象关联的。在这种情况下,线程获得与类关联的class对象的内在锁。因此,对类的静态字段的访问是由一个锁控制的,这个锁不同于任何类实例的锁。

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

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

线程A持有R1的锁等待获取R2的锁,线程B持有R2的锁等待获取R1的锁(典型的哲学家吃面)

  1. 内存不足

当并发请求系统可用内存时,如果此时系统内存不足,则可能会出现死锁的情况。

  1. 一问一答式的数据交换

服务端开启某个端口,等待客户端访问,客户端发送请求立即等待接收,由于某种原因服务端错过了客户端的请求,仍然在等待一问一答式的数据交换,此时服务端和客户端都在等待着双方发送数据。

  1. 数据库锁

无论是数据库级别的锁,还是行级别的锁,比如某个线程执行for update语句退出了事务,其他线程访问该数据库时都将陷入死锁。

  1. 文件锁

某线程获得文件锁意外突出,其他读取该文件的线程也将会进入死锁系统释放文件句柄资源。

  1. 死循环引起的死锁

程序由于代码原因或者对某些异常处理不得当,进入死循环,虽然查看线程堆栈信息不会发现任何死锁的迹象,但是程序不工作,CPU居高不下,这种死锁一般称之为系统假死。

1.5.2 死锁诊断
  1. 交叉锁引起的死锁

一般交叉锁引起的死锁线程都会进入BLOCKED状态,CPU资源占用不高,很容易借助工具发现,jconsole和jstack。

  1. 死循环引起的死锁(假死)

严格意义上来说死循环会导致程序假死,算不上真正的死锁,但是某个线程对CPU消耗过多,导致其他线程等待CPU,内存等资源也会陷入死锁等待。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值