Java中的锁(一)

一、前言

在Java中,锁是用于多线程同步的重要概念。它可以保护共享资源,确保多个线程在访问共享资源时的数据一致性。

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

线程同步是指在多线程编程中,为了保证多个线程按照某种特定的方式正确、有序地执行,需要进行线程间的协作与同步。在多线程编程中,当多个线程共享同一份资源时,由于线程的执行顺序是不确定的,因此会存在一些并发问题(线程不安全问题),如死锁、数据不一致、竞态条件等问题。为了避免这些问题,需要对线程进行同步。线程同步实际上就是通过线程之间的协作,使得线程能够按照一定的顺序来访问共享资源,从而避免并发问题的发生。常用的线程同步方式有互斥锁、读写锁、信号量、条件变量等。

二、Java中锁的概念

在Java中,锁主要用来解决多线程并发访问共享资源的问题;可能存在的并发问题(线程不安全问题):

1、死锁(Deadlock)

当多个线程同时持有某些资源的锁,并且彼此都在等待对方释放资源的锁时,就会发生死锁。这时,线程无法继续执行,造成系统的停滞。

2、数据不一致性(Data Inconsistency)

当多个线程同时对共享数据进行读写操作,并且它们之间缺少同步机制时,可能导致数据不一致。例如,一个线程正在修改某个对象的属性值,而另一个线程正在读取该属性值,由于缺乏同步,读取到的值可能是不正确或不一致的。

3、竞态条件(Race Condition)

当多个线程对共享数据进行读写操作,并且执行的顺序会影响最终结果时,就可能发生竞态条件。例如,多个线程同时对一个变量进行自增操作,由于不可预知的执行顺序,最终结果可能与期望不符。

4、非原子性操作(Non-atomic Operation)

某些操作在执行过程中不是原子性的,即不能一次性完成,而需要多个步骤。如果多个线程同时执行这样的操作,就可能导致不一致的结果。例如,在多线程环境下对int类型变量进行自增操作,由于该操作涉及两个步骤(读取和写入),可能产生不正确的结果。

5、资源争用(Resource Contention)

多个线程同时竞争同一份资源,导致资源的使用效率下降,总体性能降低。

6、饥饿(Starvation)

某些线程可能因为无法获取资源而一直等待,导致无法正常执行,进而影响整个程序的性能。

三、线程同步的方式

1、互斥锁(Mutex):

互斥锁(又名互斥量)强调的是资源之间的访问互斥:每个线程在对共享资源操作前都会尝试先加锁,加锁成功才能操作,操作结束之后释放锁。

某个线程对互斥量加锁后,任何其他试图再对互斥量加锁的线程都将被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态。第一个变成运行状态的线程可以对互斥量加锁,其余线程将会看到互斥量依然被锁住,只能回去再次等待它重新变为可用。

2、读写锁(Read-Write Lock):

读写锁和互斥量类似,是另一种实现线程同步的方式,但是它将操作分为读、写两种方式,可以多个线程同时占用读模式,这样使得读写锁具有更高的并行性。相较于互斥锁而言读写锁有一定的性能提升,应对的是单写多读模型:

  • 写独占:写锁占用时,其他线程加读锁或者写锁时都会阻塞(并非失败)
  • 读共享:读锁占用时,其他线程加写锁时会阻塞,加读锁会成功
3、条件变量(Condition Variable):

条件变量本质上也是一个多线程间共享的全局变量,它的功能是阻塞线程,被阻塞的线程直到接收到"条件成立"的信号后才能继续执行。

需要注意的是:

  • 条件变量并不是锁(但它几乎总是和互斥量一起使用的),而是线程间的一种通讯机制
  • 条件变量本身也不包含条件,它被称为条件变量是因为它经常和条件语句(if/while)一起使用
4、信号量(Semaphore):

使用信号量来控制多个线程对有限数量资源的访问。信号量表示资源的数量,每个线程在使用完资源后必须释放信号量,以便其他线程可以使用资源。

信号量分为有名信号量和无名信号量,无名信号量用于线程同步,有名信号量一般用于进程之间管理。

信号量本质上是一个非负的整数计数器,用于控制公共资源的访问,也被称为PV原子操作:

  • P操作:即信号量sem减一,若sem小于等于0则P操作被阻塞,直到sem变量大于0为止
  • V操作:即信号量sem加一

四、锁的基本原理

锁的基本原理是防止竞争条件,保证线程安全性和可见性,避免死锁等问题。下面是关于锁的基本原理的介绍:

1、防止竞争条件

当多个线程同时访问共享资源时,可能会发生竞争条件。竞争条件是指当多个线程同时执行同一段代码时,由于执行顺序的不同而导致结果的不确定性。

锁的作用就是在多个线程访问共享资源时保证同一时刻只有一个线程访问,从而避免竞争条件的发生。当一个线程获取到锁时,其他线程必须等待锁的释放才能继续访问共享资源。

2、保证线程安全性和可见性

线程安全性和可见性是Java并发编程中非常重要的概念。线程安全性是指当多个线程同时访问共享资源时,不会出现数据损坏或程序崩溃等问题。可见性是指当一个线程修改了共享资源时,其他线程能够立即看到这个修改。

锁机制可以保证线程安全性和可见性。当一个线程获取到锁时,其他线程无法修改共享资源,从而避免了数据损坏和程序崩溃等问题。而锁机制也可以保证共享资源的可见性,因为当一个线程释放锁时,其他线程能够立即看到共享资源的最新状态。

3、避免死锁

死锁是指两个或多个线程相互等待对方释放锁,从而导致程序无法继续执行的情况。死锁是Java并发编程中一个非常严重的问题,必须避免发生。

为了避免死锁,必须采取一些策略,例如避免嵌套锁、避免长时间占用锁、按照相同的顺序获取锁等。另外,还可以使用专门的工具来检测和避免死锁,例如死锁检测器和避免死锁算法等。

总之,锁机制是Java并发编程中非常重要的一部分,了解锁的基本原理,包括如何防止竞争条件、如何保证线程安全性和可见性,以及如何避免死锁等问题,对于编写高效、可靠的并发程序非常有帮助。

相关推荐
likuolei25 分钟前
XQuery 完整语法速查表(2025 最新版,XQuery 3.1)
xml·java·数据库
雨中飘荡的记忆30 分钟前
LangChain4j 实战指南
java·langchain
okseekw32 分钟前
Java 中的方法:从定义到重载的完整指南
java
雨中飘荡的记忆33 分钟前
深入理解设计模式之适配器模式
java·设计模式
用户849137175471634 分钟前
生产级故障排查实战:从制造 OOM 到 IDEA Profiler 深度破案
java·jvm
雨中飘荡的记忆37 分钟前
深入理解设计模式之装饰者模式
java·设计模式
雨中飘荡的记忆41 分钟前
秒杀系统设计与实现
java·redis·lua
CryptoPP1 小时前
使用 KLineChart 这个轻量级的前端图表库
服务器·开发语言·前端·windows·后端·golang
18你磊哥1 小时前
chromedriver.exe的使用和python基本处理
开发语言·python
小坏讲微服务1 小时前
Spring Cloud Alibaba 整合 Scala 教程完整使用
java·开发语言·分布式·spring cloud·sentinel·scala·后端开发