JAVA多线程-锁机制

2023-11-15

一、synchronized

在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为重量级锁。但是,随着 Java SE 1.6 对synchronized 进行了各种优化之后,有些情况下它就并不那么重。

synchronized 有三种方式来加锁,分别是

  1. 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  2. 静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁
分类 具体分类 被锁对象 伪代码
方法 实例方法 调用该方法的实例对象 public synchronized void method(){ }
方法 静态方法 类对象Class对象 public static synchronized void method(){ }
代码块 this 调用该方法的实例对象 synchronized(this){ }
代码块 类对象 类对象 synchronized(Demo.class){ }
代码块 任意的实例对象 创建的任意对象 Object lock= new Object(); synchronized(lock){ }
1.1、实现原理

线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor 可以认为是一个同步对象,所有的Java 对象是天生携带monitor。而monitor是添加Synchronized关键字之后独有的。synchronized同步块使用了monitorenter和monitorexit指令实现同步,这两个指令,本质上都是对一个对象的监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。

线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁,而执行monitorexit,就是释放monitor的所有权。

对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)、数组类型还有一个int类型的数组长度。

我们今天看的就是其中的Mark Word

  1. Mark Word记录了对象和锁有关的信息,当这个对象被synchronized关键字当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关。
  2. Mark Word在32位JVM中的长度是32bit,在64位JVM中长度是64bit。
  3. Mark Word在不同的锁状态下存储的内容不同,在64位JVM中是这么存的:

其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态还是偏向锁状态。JVM对于同步锁的处理是从偏向锁开始的,随着竞争越来越激烈,处理方式从偏向锁升级到轻量级锁,最终升级到重量级锁。

锁升级中共涉及到四把锁

  • 无锁:不加锁

  • 偏向锁:不锁锁,只有一个线程争夺时,偏心某一个线程,这个线程来了不加锁。

  • 轻量级锁:少量线程来了之后,先尝试自旋,不挂起线程。

    注:挂起线程和恢复线程的操作都需要转入内核态中完成这些操作,给系统的并发性带来很大的压力。在许多应用上共享数据的锁定状态,只会持续很短的一段时间,为了这段时间去挂起和恢复现场并不值得,我们就可以让后边请求的线程稍等一下,不要放弃处理器的执行时间,看看持有锁的线程是否很快就会释放,锁为了让线程等待,我们只需要让线程执行一个盲循环也就是我们说的自旋,这项技术就是所谓的自旋锁。

  • 重量级锁:排队挂起线程

抢锁的过程如下

1,当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。

2,当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。

3,当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。

4,当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。

5,偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间,里面保存指向对象锁Mark Word的指针,也叫所记录(lock record),同时在对象锁Mark Word中保存指向这片空间的指针。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。

6,轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7,自旋默认10次。

7,自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞排队。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等)进入阻塞状态,等待将来被唤醒。就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源。

二、死锁

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

Java 死锁产生的四个必要条件:

  • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  • 请求和保持,即当资源请求者在请求其他资源的同时保持对原有资源的占有。
  • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。
import java.util.Date;
 
public class LockTest {
   public static String obj1 = "obj1";
   public static String obj2 = "obj2";
   public static void main(String[] args) {
      LockA la = new LockA();
      new Thread(la).start();
      LockB lb = new LockB();
      new Thread(lb).start();
   }
}
class LockA implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockA 开始执行");
         while(true){
            synchronized (LockTest.obj1) {
               System.out.println(new Date().toString() + " LockA 锁住 obj1");
               Thread.sleep(3000); // 此处等待是给B能锁住机会
               synchronized (LockTest.obj2) {
                  System.out.println(new Date().toString() + " LockA 锁住 obj2");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}
class LockB implements Runnable{
   public void run() {
      try {
         System.out.println(new Date().toString() + " LockB 开始执行");
         while(true){
            synchronized (LockTest.obj2) {
               System.out.println(new Date().toString() + " LockB 锁住 obj2");
               Thread.sleep(3000); // 此处等待是给A能锁住机会
               synchronized (LockTest.obj1) {
                  System.out.println(new Date().toString() + " LockB 锁住 obj1");
                  Thread.sleep(60 * 1000); // 为测试,占用了就不放
               }
            }
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }
}

三、线程重入

public class Test1 {
    private static final Object M1 = new Object();
    private static final Object M2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (M1){
                synchronized (M2){
                    synchronized (M1){
                        synchronized (M2){
                            System.out.println("hello lock");
                        }
                    }
                }
            }
        }).start();
    }
}

四、wait & notify

public class ThreadTest2 {
    private static final Object MONITOR = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            ThreadUtils.sleep(5);
            thread1();
        });
        Thread t2 = new Thread(() -> {
            ThreadUtils.sleep(10);
            thread2();
        });
        t1.start();
        t2.start();
    }

    public static void thread1(){
        synchronized (MONITOR){
            try {
                System.out.println("线程1开始等待");
                MONITOR.wait(2000);
                System.out.println("线程1被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void thread2(){
        synchronized (MONITOR){
            ThreadUtils.sleep(1500);
            MONITOR.notify();
            System.out.println("线程2唤醒线程1");
        }
    }
}

线程实例的方法:

  • join:是线程的方法,程序会阻塞在这里等着这个线程执行完毕,才接着向下执行。

Object的成员方法

  • wait:释放CPU资源,同时释放锁。
  • notify:唤醒等待中的线程。
  • notifyAll:唤醒所有等待的线程

五、线程的退出

使用退出标志,使线程正常退出,也就是当run()方法结束后线程终止。

class ThreadA extends Thread {

    // volatile关键字解决线程的可见性问题
    volatile boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            try {
                // 可能发生异常的操作
                System.out.println(getName() + "线程一直在运行。。。");
            } catch (Exception e) {
                System.out.println(e.getMessage());
                this.stopThread();
            }
        }
    }

    public void stopThread() {
        System.out.println("线程停止运行。。。");
        this.flag = false;
    }
}

使用interrupt()方法中断线程,会报异常错误,我们只要将异常抛出即可

public class ThreadTest2 {
    private static final Object MONITOR = new Object();

    public static void main(String[] args) {
    Thread t1 = new Thread(()->{
        ThreadUtils.sleep(1000000);
    });
    t1.start();
    t1.interrupt();
    System.out.println("程序中断");
    }
}

如果线程处于类似while(true)运行的状态,interrupt()方法无法中断线程。

六、LockSupport

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。

public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t);

这儿parkunpark其实实现了waitnotify的功能,不过还是有一些差别的。

  1. park不需要获取某个对象的锁
  2. 因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理

我们在park线程的时候可以传递一些信息,给调用者看,这个object什么都能传递。

比如在阻塞时:

LockSupport.park("我被阻塞了");

主线程可以在t1的阻塞期间获取它传入的信息:

t1.start();
Thread.sleep(1000L);
System.out.println(LockSupport.getBlocker(t1));
t2.start();

七、Lock

// 获取锁  
void lock()   

// 仅在调用时锁为空闲状态才获取该锁,可以响应中断  
boolean tryLock()   

// 如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁  
boolean tryLock(long time, TimeUnit unit)   

// 释放锁  
void unlock()  

获取锁的两种写法

Lock lock = ...;
lock.lock();
try{
    //处理任务
}catch(Exception ex){

}finally{
    lock.unlock();   //释放锁
}
Lock lock = ...;
if(lock.tryLock()) {
     try{
         //处理任务
     }catch(Exception ex){

     }finally{
         lock.unlock();   //释放锁
     } 
}else {
    //如果不能获取锁,则直接做其他事情
}
7.1、可重入锁ReentrantLock
public class Ticket implements Runnable{
    private static final ReentrantLock lock = new ReentrantLock();
    private static Integer out = 100;

    String name;

    public Ticket(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        while (Ticket.out > 0){
            ThreadUtils.sleep(100);
            lock.lock();
            try {
                System.out.println(name + "出票一张,还剩" + Ticket.out-- + "张!");
            } finally {
                lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        Thread one = new Thread(new Ticket("一号窗口"));
        Thread two = new Thread(new Ticket("二号窗口"));
        one.start();
        two.start();
        ThreadUtils.sleep(10000);
    }
}

synchronized和ReentrantLock的区别:

  • Lock是一个接口,synchronized是Java中的关键字,synchronized是内置的语言实现;
  • synchronized发生异常时,会自动释放线程占用的锁,故不会发生死锁现象。Lock发生异常,若没有主动释放,极有可能造成死锁,故需要在finally中调用unLock方法释放锁;
  • Lock可以让等待锁的线程响应中断,使用synchronized只会让等待的线程一直等待下去,不能响应中断
  • Lock可以提高多个线程进行读操作的效率
7.2、读写锁ReadWriteLock

对于一个应用而言,一般情况读操作是远远要多于写操作的,同时如果仅仅是读操作没有写操作的情况下数据又是线程安全的,读写锁给我们提供了一种锁,读的时候可以很多线程同时读,但是不能有线程写,写的时候是独占的,其他线程既不能写也不能读。在某些场景下能极大的提升效率。

本质上就是这个工具类提供了两种锁,读锁和写锁,读的时候可以多线程的读,写的时候只能一个线程去写,保证线程安全

public class ReadAndWriteTest {
    public static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public static int COUNT = 1;

    public static void main(String[] args) {
        Runnable read = () -> {
            lock.readLock().lock();
            try {
                ThreadUtils.sleep(200);
                System.out.println("i am reading:" + COUNT);
            } finally {
                lock.readLock().unlock();
            }
        };
        Runnable write = () -> {
            lock.writeLock().lock();
            try {
                ThreadUtils.sleep(200);
                System.out.println("i an writing:" + ++COUNT);
            } finally {
                lock.writeLock().unlock();
            }
        };

        for (int i = 0; i < 100; i++) {
            Random random = new Random();
            int flag = random.nextInt(100);
            if (flag > 20) {
                new Thread(read, "read").start();
            } else {
                new Thread(write, "write").start();
            }
        }
    }
}

八、CAS && AQS

8.1、CAS(Compare and Set)

它的思路其实很简单,就是给一个元素赋值的时候,先看看内存里的那个值到底变没变,如果没变我就修改,变了我就不改了,其实这是一种无锁操作,不需要挂起线程,无锁的思路就是先尝试,如果失败了,进行补偿,也就是你可以继续尝试。这样在少量竞争的情况下能很大程度提升性能。

缺点:

  1. ABA问题。当第一个线程执行CAS操作,尚未修改为新值之前,内存中的值已经被其他线程连续修改了两次,使得变量值经历 A -> B -> A的过程。绝大部分场景我们对ABA不敏感。解决方案:添加版本号作为标识,每次修改变量值时,对应增加版本号; 做CAS操作前需要校验版本号。JDK1.5之后,新增AtomicStampedReference类来处理这种情况。
  2. 循环时间长开销大。如果有很多个线程并发,CAS自旋可能会长时间不成功,会增大CPU的执行开销。
  3. 只能对一个变量进行原子操作。JDK1.5之后,新增AtomicReference类来处理这种情况,可以将多个变量放到一个对象中。
8.2、AQS

AQS中维护了一个volatile int state(共享资源)和一个CLH队列。当state=1时代表当前对象锁已经被占用,其他线程来加锁时则会失败,失败的线程被放入一个FIFO的等待队列中,然后会被**UNSAFE.park()**操作挂起,等待已经获得锁的线程释放锁才能被唤醒。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JAVA多线程-锁机制 的相关文章

随机推荐

  • groupdel: cannot remove the primary group of user 'lxh1'的解决办法

    故障现象 用groupdel删除test组时 报以上错误 原因为test组中有lxh1用户 lxh1的主组 解决办法 更改lxh1的主组后即可删除 1 2 3 4 5 6 7 8
  • yml配置map map<string,list>

    java Configuration PropertySource value classpath application yml encoding utf 8 ConfigurationProperties prefix mapvule
  • McCabe度量法

    概论 McCabe度量法是由 托马斯 麦克凯 提出的一种基于程序控制流的复杂性度量方法 又称环路度量 循环复杂度 Cyclomatic complexity 也称为条件复杂度或圈复杂度 是一种软件度量 它认为程序的复杂性很大程度上取决于程序
  • Eclipse上传项目到Git

    Git有和Svn类似的功能 我们想使用Eclipse上传项目到自己的GitHub上面该如何做呢 今天我成功上传了一个项目 在这里分享给大家 首先需要在eclipse上面安装一个插件 操作步骤 一 在自己的Eclipse上面安装EGit插件
  • Qt QTableWidget使用记录

    去除选中虚线框 ui gt tableWidget gt setFocusPolicy Qt NoFocus Qt QTableWidget详解https blog csdn net wzz953200463 article details
  • 深度学习入门教学——神经网络NN

    1 简介 神经网络是一种模拟人脑的神经网络以期能够实现类人工智能的机器学习技术 下图为人脑中的一个生物神经元 而无数个生物神经元就组成了生物神经网络 使人具备了处理复杂信息的能力 人工神经网络也试图模仿生物神经网络的原理 让计算机具备处理复
  • 03

    03 通过你的CPU主频 我们来谈谈 性能 究竟是什么 性能 这个词 不管是在日常生活还是写程序的时候 都经常被提到 比方说 买新电脑的时候 我们会说 原来的电脑性能跟不上了 写程序的时候 我们会说 这个程序性能需要优化一下 那么 你有没有
  • stm32芯片休眠模式_STM32F103 怎样进入睡眠模式及唤醒

    Function Name PWR EnterSLEEPMode Description Enters SLEEP mode Input SysCtrl Set Select the Sleep mode entry mechanism T
  • k8s kubernetes核心组件

    文章目录 引言 一 Kubernetes的核心组件 1 Master组件 1 1 kube apiserver 1 2 Kube controller manager 1 3 kube scheduler 1 4 配置存储中心 etcd 1
  • python装饰器--原来如此简单

    python装饰器 原来如此简单 今天整理装饰器 内嵌的装饰器 让装饰器带参数等多种形式 非常复杂 让人头疼不已 但是突然间发现了装饰器的奥秘 原来如此简单 第一步 从最简单的例子开始 coding gbk 示例1 使用语法糖 来装饰函数
  • Python3安装包下载(附3.8.7、3.7.9、3.6.8版本)

    三部曲 1 到 源码 网站源码 源码下载 源码之家 站长下载 搜索 Python 并下载 搜索结果在较底部 2 到官网 https www python org downloads 对应版本的页面 如 https www python or
  • Linux实现使用定时任务执行php程序(以及定时任务url带参数)

    php程序已经写好了 位置 data html XXX redis to mysql php php安装位置为 app bin php 查找php安装位置使用 whereis php which php php v which 这条命令主要
  • 1.C#/.NET开发环境安装(Windows)

    文章目录 一 VS2022 1 下载 VS 2022 Community 2 安装 3 第一个VS项目Hello World 4 补充 二 VS2019 1 下载VS 2019 Community 2 安装 三 游戏开发引擎Unity 四
  • centos 安装mysql 5.7

    centos安装mysql 1 检查系统中是否已安装 MySQL 如果已安装 请参考此文章卸载 rpm qa grep mysql 在新版本的CentOS7中 默认的数据库已更新为了Mariadb 而非 MySQL 所以执行 yum ins
  • 转:车规芯片的AEC-Q100测试标准

    距离上一次发文章已经过去了10个月了 这10个月里 不想错过跟小孩待在一起的每一个时刻 额呸 就是因为懒 一直没有更新文章 特此最近开始逼迫自己不断的学习 重新开始进行公众号的更新 最近这小半年一直在弄跟芯片相关的一些工作 并且由于缺芯的原
  • 【华为OD机试】水仙花数Ⅰ【2023 B卷

    华为OD机试 真题 点这里 华为OD机试 真题考点分类 点这里 题目描述 所谓水仙花数 是指一个n位的正整数 其各位数字的n次方和等于该数本身 例如153是水仙花数 153是一个3位数 并且153 1 3 5 3 3 3 输入描述 第一行输
  • ScrumAlliance对Agile Coach的能力定义了五个部分

    1 Assess Discovery Diretion 评估 发现 指导 评估团队 发现问题 提出指导意见 2 Balance Coaching Consulting 平衡教练和咨询的工作 提供咨询方案 也提供教练的工作 3 Catalyz
  • 2022全国职业技能大赛-网络安全赛题解析总结①(超详细)

    2022全国职业技能大赛 网络安全赛题解析总结 自己得思路 模块A 基础设施设置与安全加固 20分 模块B 网络安全事件响应 数字取证调查和应用安全 40分 模块C CTF夺旗 攻击 20分 模块D CTF夺旗 防御 20分 有什么不懂得可
  • (libevent) 基础demo

    文章目录 介绍 Code 并发服务器 signal fifo END P S 简单客户端代码 介绍 官网 libevent linux中下载 apt get install libevent dev 官网的简介 The libevent A
  • JAVA多线程-锁机制

    一 synchronized 在多线程并发编程中 synchronized 一直是元老级角色 很多人都会称呼它为重量级锁 但是 随着 Java SE 1 6 对synchronized 进行了各种优化之后 有些情况下它就并不那么重 sync