JAVA- 锁机制介绍 线程锁

锁的介绍

锁的定义: 锁是一种用于控制并发访问共享资源的机制,以确保共享资源数据的一致性和竞争访问的性能问题

锁的大分类 进程锁和线程锁

并发常分两种 多线程和多进程
进程: 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,比如在Windows系统中,一个运行的xx.exe就是一个进程,任务管理器里面一个任务就是一个进程,比如打开运行的谷歌浏览器就是一个进程
线程: 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据

多进程并发访问共享资源:多个应用程序访问同一资源,比如迅雷播放电影黑客帝国,在使用别的播放器播放该电影文件就出问题了
多线程并发访问共享资源:比如迅雷播放电影和下载电影两个任务。边下边看 如果这个时候取消下载任务,那么播放任务就受到影响了

上面提到的多进程并发还是在单机多线程的情况下,实际工作中都是集群部署,通过负载均衡多台服务器工作,服务器1找电影黑客帝国,服务器2改电影文件名,这种并发问题更常见,所以在java中锁分为 进程锁和线程锁进程锁的介绍点击链接,本文剩余内容介绍是线程锁
锁的核心作用
1.互斥访问:确保同一时刻只有拿到锁的线程能访问共享资源。
2.可见性保证:确保锁释放后,修改后的共享变量对其他线程可见(遵循 happens-before 规则)
3.有序性控制:防止指令重排序导致的并发问题。

线程锁的框架代码

关键字synchronized

在java中,定义了关键字synchronized,是Java的内置特性,在JVM层面实现了对临界资源的同步互斥访问
synchronized加锁的三种使用方式

  • 修饰实例方法
  • 修饰静态代码块
  • 修饰同步代码块

synchronized释放锁的几种方式

  • 占有锁的线程执行完了该代码块,然后释放对锁的占有;
  • 占有锁线程执行发生异常,此时JVM会让线程自动释放锁;
  • 占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等

但是synchronized不够细腻粒度有些大,使用局限性很大。比如
Case 1 :线程有没有成功获取到锁,使用synchronized无法显示的判断得到是否获取到锁
Case 2 :占有锁的线程由于要等待IO或者其他原因(比如调用sleep方法)被阻塞了,但是又没有释放锁,那么其他线程就只能一直等待,别无他法,浪费资源
Case 3 :多个线程读写某个文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,这是因为读操作不会改变文件在内核空间的值,而写会改变文件在内核空间的内容,当多个线程对共享资源(文件)有改变的操作行为,那么多个线程就会出现冲突,如果多个线程都只是对文件进行读操作不会有线程冲突现象,采用synchronized关键字实现同步的话,就会导致一个问题,即当多个线程都只是进行读操作时,也只有一个线程在可以进行读操作,其他线程只能等待锁的释放而无法进行读操作,资源分配不合理

Lock类

针对synchronized以上使用的局限性,java关于锁给出了解决方案 提供了一些类,调用者根据需求显示的调用方法处理锁的问题,以下是Lock框架
在这里插入图片描述

这个图就是常见的显示锁相关类或接口的框架,看着有点乱,梳理说明一下

首先就是Lock接口以及相关,提供了6个方法

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;  // 可以响应中断
    boolean tryLock();
    // 可以响应中断
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;  
    void unlock();
    // 通过Lockc newCondition 产生新的条件变量实例
    Condition newCondition();
}

其实现的子类有ReentrantLock ,同时实现的还有ReentrantReadWriteLock类的内部类ReadLock ,内部类WriteLock,而ReentrantReadWriteLock是又实现了 ReadWriteLock接口

再有就是AOS AQS AQSL,是对锁管理的核心类,抽象类AQS提供了基础的锁管理形式 队列管理,又提供了基础的模板方法 以及相当部分可供子类控制的钩子方法,是整个Lock显示锁的核心,而AQSL和AQS的区别在于 state值的修饰符在AQS是int 而AQLS中是long 意味着AQSL有更大的
锁的区分定义出来的概念,可以认为是一种思想
java中的锁
1.乐观锁与悲观锁
乐观锁:乐观锁认为在操作数据时,其他线程不会修改数据,因此不会加锁。它通常使用CAS(Compare and Swap)机制或版本号机制来实现。
悲观锁:悲观锁认为在操作数据时,其他线程可能会修改数据,因此在操作数据时会加锁,防止其他线程访问。Java中的synchronized关键字和ReentrantLock类都是悲观锁的实现。

2.独享锁与共享锁
独享锁:独享锁也称为排他锁,一次只能被一个线程持有。如果线程T对数据A加上排它锁后,其他线程不能再对A加任何类型的锁。
共享锁:共享锁允许多个线程同时持有。如果线程T对数据A加上共享锁后,其他线程可以对A再加共享锁,但不能加排它锁(写锁)

3.公平锁与非公平锁
公平锁:公平锁要求多个线程按照申请锁的顺序来获取锁,即先来后到。这样可以保证等待锁的线程不会饿死。
非公平锁:非公平锁允许线程直接尝试获取锁,获取不到才会到等待队列的队尾等待。这种方式可能导致后申请锁的线程先获取到锁。

4.可重入锁与不可重入锁
可重入锁:可重入锁允许同一个线程在外层方法获取锁后,在内层方法再次获取同一把锁,而不会因为之前已经获取过而阻塞。Java中的synchronized和ReentrantLock都是可重入锁。
不可重入锁:不可重入锁不允许同一个线程在释放锁之前再次获取同一把锁。

5.互斥锁与读写锁
互斥锁:互斥锁是一种独享锁,一次只能被一个线程持有。Java中的synchronized关键字和ReentrantLock类都是互斥锁的实现。
读写锁:读写锁允许多个读线程同时访问,但写线程需要独占资源。Java中ReentrantReadWriteLock实现了读写锁。

6.偏向锁、轻量级锁与重量级锁
偏向锁:偏向锁是一种针对单线程访问的优化,当锁对象从未被其他线程获得过时,该线程可以直接获得锁,无需进行CAS操作。
轻量级锁:轻量级锁用于替代偏向锁,当有其他线程尝试获取偏向锁时,偏向锁会被撤销,并升级为轻量级锁。轻量级锁通过CAS操作来实现。
重量级锁:重量级锁是传统的锁方式,当发生锁争抢时,线程会进入阻塞状态。重量级锁会导致线程阻塞和唤醒的开销较大。

线程锁的设计分类

  1. 内置锁(synchronized)
  2. 显式锁(ReentrantLock)
  3. 读写锁(ReentrantReadWriteLock)
  4. 分布式锁(如 Redis/ZooKeeper 实现)

公平锁/非公平锁

  • 公平锁是指多个线程按照申请锁的顺序来获取锁。
  • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
    对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁非公平锁的优点在于吞吐量比公平锁大。
    对于synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(就是可以重新进入获得锁)对于Java ReentrantLock而言, 其名字是Reentrant Lock即是重新进入锁。对于synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁

synchronized void setA() throws Exception{
    Thread.sleep(1000);
    setB();
}
synchronized void setB() throws Exception{
    Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有;共享锁是指该锁可被多个线程所持有
对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写、写读 、写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于synchronized而言,当然是独享锁。

互斥锁/读写锁

独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock;读写锁在Java中的具体实现就是Read/WriteLock。

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

  • 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里面的同步原语 synchronized 关键字的实现也是悲观锁
  • 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,其实都是提供的乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子变量类就是使用了乐观锁的一种实现方式 CAS 实现的
  • 在Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS(Compare and Swap 比较并交换)实现的。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取HashMap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对synchronized。在Java 5通过引入锁升级的机制来实现高效synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。

  • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
  • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能
  • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU

线程锁的注意事项

锁升级 锁降级

锁的使用粒度

什么是线程死锁

  • 死锁是指两个或两个以上的进程(线程)在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程(线程)称为死锁进程(线程)
  • 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻
    塞,因此程序不可能正常终止。
  • 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态
    在这里插入图片描述

形成死锁的四个必要条件是什么

  • 互斥条件:在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,就只能等待,直至占有资源的进程用毕释放。如上线程一已经占有锁A 线程二也想要锁A,只能等
  • 占有且等待条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。线程一有了A还想要B,但是B被线程二占有,线程一又不放弃锁A
  • 不可抢占条件:别人已经占有了某项资源,你不能因为自己也需要该资源,就去把别人的资源抢过来。
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(比如一个进程集合,A在等B,B在等C,C在等A)

如何避免线程死锁

  • 避免一个线程同时获得多个锁
  • 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源
  • 尝试使用定时锁,使用lock.tryLock(timeout)来替代使用内部锁机制

死锁与活锁的区别,死锁与饥饿的区别?

  • 死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
  • 活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复,尝试,失败,尝试,失败
  • 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,这就是所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能
  • 饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行的状态。
    Java 中导致饥饿的原因:
    1、高优先级线程吞噬所有的低优先级线程的 CPU 时间。
    2、线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该
    同步块进行访问。
    3、线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法),因为其
    他线程总是被持续地获得唤醒

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值