synchronized 关键字
-
多线程共享资源访问:当多个线程需要访问和修改共享资源时,使用
synchronized
可以确保线程安全。 -
避免竞态条件:当多个线程可能同时修改同一个变量或资源时,使用
synchronized
可以避免竞态条件。 -
控制并发访问:当需要限制多个线程对某些操作的并发访问时,使用
synchronized
可以实现互斥。
Lock
1. 语法和使用方式
-
synchronized
:-
关键字:
synchronized
是Java语言的关键字,用于同步方法或代码块。 -
使用方式:
-
同步方法:
public synchronized void method() { // 同步代码 }
-
同步代码块:
synchronized (this) { // 锁定当前对象 // 同步代码 } synchronized (Counter.class) { // 锁定类对象 // 同步代码 } synchronized (lockObject) { // 锁定任意对象 // 同步代码 }
-
-
优点:语法简单,易于使用。
-
缺点:功能相对有限,无法实现更复杂的同步操作。
-
-
Lock
:-
类:
Lock
是一个接口,常用的实现类是ReentrantLock
。 -
使用方式:
private final Lock lock = new ReentrantLock(); public void method() { lock.lock(); // 获取锁 try { // 同步代码 } finally { lock.unlock(); // 释放锁 } }
-
优点:功能更强大,支持更复杂的同步操作。
-
缺点:使用更复杂,需要手动管理锁的获取和释放。
-
2. 锁的特性
-
synchronized
:-
自动释放锁:当同步代码块执行完毕或发生异常时,锁会自动释放。
-
不可中断:线程在等待锁时无法被中断。
-
非公平锁:默认情况下,
synchronized
是非公平锁,不保证线程获取锁的顺序。 -
不支持条件变量:不支持
Condition
对象,无法实现更复杂的线程协作。
-
-
Lock
:-
手动释放锁:需要手动调用
unlock()
方法释放锁。如果忘记释放锁,可能会导致死锁。 -
可中断:线程在等待锁时可以被中断,例如通过
tryLock()
方法。 -
支持公平锁:可以通过构造函数指定锁是否为公平锁,例如:
java复制
ReentrantLock lock = new ReentrantLock(true); // 公平锁
-
支持条件变量:可以通过
newCondition()
方法创建Condition
对象,实现更复杂的线程协作。
-
3. 性能
-
synchronized
:-
性能优化:从Java 6开始,
synchronized
经过了大量优化,包括锁消除、锁粗化、自适应自旋锁等,性能已经非常接近ReentrantLock
。 -
适用场景:对于简单的同步需求,
synchronized
通常足够高效。
-
-
Lock
:-
灵活性:
ReentrantLock
提供了更多的功能,如可中断锁、公平锁等,但这些功能可能会带来一定的性能开销。 -
适用场景:对于复杂的同步需求,如需要可中断锁或公平锁,
ReentrantLock
是更好的选择。
-
4. 适用场景
-
synchronized
:-
简单同步需求:当只需要简单的同步方法或代码块时,
synchronized
是首选。 -
减少代码复杂性:
synchronized
的语法简单,易于理解和维护。
-
-
Lock
:-
复杂同步需求:当需要更复杂的同步操作,如可中断锁、公平锁、条件变量等,
ReentrantLock
是更好的选择。 -
高性能需求:虽然
synchronized
已经经过优化,但在某些极端情况下,ReentrantLock
可能会有更好的性能表现。
-
总结
-
synchronized
:适合简单同步需求,语法简单,易于使用。 -
Lock
:适合复杂同步需求,功能强大,但使用更复杂,需要手动管理锁的获取和释放。
实现线程方式
1. 继承Thread
类
通过继承Thread
类并重写run()
方法来创建线程。这种方式简单直接,但缺点是Java不支持多继承,因此如果线程类已经继承了其他类,则无法再继承Thread
类。
2. 实现Runnable
接口
通过实现Runnable
接口并实现run()
方法,然后将Runnable
实例传递给Thread
类的构造函数来创建线程。这种方式更灵活,因为Java支持多实现接口,所以可以同时实现多个接口。
3. 使用Callable
和Future
Callable
接口类似于Runnable
,但它可以返回一个结果,并且可以抛出异常。通过Future
可以获取线程执行的结果。这种方式适用于需要线程返回结果的场景。
4. 使用线程池
线程池可以复用线程,减少线程创建和销毁的开销。Java提供了ExecutorService
来管理线程池。这种方式适用于需要频繁创建和销毁线程的场景。
Java反射机制
Java反射(Reflection)是Java语言提供的一种强大的动态检查和操作类、对象、接口、字段和方法的能力。它允许程序在运行时查询类的信息、创建对象、调用方法、访问字段等,而无需在编译时知道这些类的具体实现。
锁消除,锁粗化,自旋锁
锁消除、锁粗化和自适应自旋锁是Java并发编程中用于优化锁性能的三种技术。以下是它们的详细解释:
1. 锁消除(Lock Elimination)
锁消除是JVM在即时编译时的一种优化技术,通过逃逸分析来判断某个锁对象是否只在一个线程内使用。如果确定该锁不会发生线程竞争,JVM会自动将这些锁消除。
原理
-
当一个锁对象的作用域被限制在单个线程内,且不会被其他线程访问时,该锁是多余的,可以被消除。
-
例如,局部变量的同步操作(如
StringBuffer
的append
方法)在单线程中使用时,锁操作可以被消除。
应用场景
-
局部变量的同步操作。
-
方法内部创建的对象仅在该方法内使用。
开启方式
锁消除需要在JVM的Server模式下开启逃逸分析:
bash复制
-server -XX:+DoEscapeAnalysis -XX:+EliminateLocks
2. 锁粗化(Lock Coarsening)
锁粗化是JVM在运行时对锁操作的一种优化技术,通过将多个连续的小范围加锁操作合并为一次较大的加锁操作,减少锁的频繁获取和释放。
原理
-
当JVM检测到代码中存在对同一锁对象的连续加锁和解锁操作时,会将这些操作合并为一个更大的锁范围。
-
例如,循环中对同一个对象的频繁加锁和解锁可以被合并为一个锁覆盖整个循环。
应用场景
-
循环中对同一个对象的频繁加锁和解锁。
-
多个连续的同步方法调用。
优点
-
减少锁的开销,降低线程竞争。
-
提高程序的执行效率。
注意事项
-
锁粗化可能会增加锁的持有时间,从而降低程序的并发性。
-
需要根据具体场景权衡锁粗化和并发性能。
3. 自适应自旋锁(Adaptive Spinning)
自适应自旋锁是一种优化技术,用于减少线程在尝试获取锁时的阻塞时间。当线程尝试获取一个已经被其他线程持有的锁时,它不会立即进入阻塞状态,而是会进行一定次数的自旋(即空循环)。
原理
-
自旋锁的核心思想是:如果锁的持有时间很短,那么线程在自旋过程中可能会等到锁被释放,从而避免进入阻塞状态。
-
自适应自旋锁会根据锁的持有时间和历史情况动态调整自旋的次数。
应用场景
-
适用于锁持有时间较短的场景。
-
在高并发环境下,可以减少线程的上下文切换。
优点
-
减少线程阻塞和上下文切换的开销。
-
提高系统的整体性能。
缺点
-
如果锁的持有时间较长,自旋可能会浪费CPU资源。
总结
-
锁消除:通过逃逸分析消除不必要的锁操作,适用于局部变量的同步操作。
-
锁粗化:将多个连续的锁操作合并为一个大锁,减少锁的开销,但需注意锁持有时间过长对并发性的影响。
-
自适应自旋锁:通过自旋减少线程阻塞,适用于锁持有时间较短的场景。
这些锁优化技术可以显著提高Java并发程序的性能,但需要根据具体的应用场景合理选择和使用。
分布式事务
分布式事务是指在分布式系统中,涉及多个独立节点(如不同的数据库、服务等)的事务处理过程。它与本地事务不同,本地事务的操作范围仅限于同一数据库中,而分布式事务需要跨越多个不同的数据库或服务节点。
核心特性
分布式事务需要满足事务的ACID特性,即原子性、一致性、隔离性和持久性:
-
原子性:事务中的所有操作要么全部成功,要么全部失败。
-
一致性:事务执行前后,数据的完整性约束保持一致。
-
隔离性:多个事务并发执行时,相互隔离,一个事务的中间状态对其他事务不可见。
-
持久性:事务一旦提交,其结果就是永久性的。
实现方式
常见的分布式事务实现方式包括:
-
两阶段提交(2PC):分为准备阶段和提交阶段。准备阶段,事务协调者向所有参与者发送准备提交的请求,参与者执行本地事务并返回响应;提交阶段,协调者根据参与者的响应决定提交或回滚。
-
TCC(Try-Confirm-Cancel)模式:将事务操作分为Try(预留资源)、Confirm(确认提交)和Cancel(取消回滚)三个阶段。
-
Saga模式:将一个长事务拆分为一系列有序的本地事务,每个本地事务都有一个对应的补偿操作。
-
消息队列事务:通过消息队列实现最终一致性,将操作拆分为异步消息,确保消息的可靠投递和消费。
应用场景
分布式事务在多种场景中都有应用,例如:
-
微服务架构:业务流程需要跨多个服务调用时,使用分布式事务保证数据一致性。
-
金融交易系统:如转账操作,涉及不同银行系统,需要分布式事务确保资金流动的安全和准确。
-
库存管理系统:处理订单时,需要同步更新库存、订单状态等信息,可能涉及多个服务或数据库。
公平锁、非公平锁
-
公平锁:
-
适用场景:当需要确保线程按照请求顺序获取锁,避免饥饿问题时,使用公平锁。
-
缺点:性能开销较大,因为需要维护等待队列。
-
-
非公平锁:
-
适用场景:当对性能要求较高,且锁的持有时间较短时,使用非公平锁。
-
缺点:可能导致饥饿问题,但实际场景中较少出现。
-
乐观锁、悲观锁
1. 悲观锁(Pessimistic Lock)
悲观锁假设在多线程环境中,冲突是频繁发生的,因此在访问资源时总是先加锁,以防止其他线程的干扰。
特点
-
加锁机制:在访问资源时,总是先加锁,确保当前线程独占资源。
-
适用场景:适用于写操作频繁的场景,因为写操作容易引发冲突。
-
性能开销:由于频繁加锁和解锁,性能开销较大,尤其是在高并发场景下。
-
实现方式:可以通过
synchronized
、ReentrantLock
等锁机制实现。
2. 乐观锁(Optimistic Lock)
乐观锁假设在多线程环境中,冲突是不常见的,因此在访问资源时不加锁,而是通过版本号或时间戳等机制来检测冲突。
特点
-
加锁机制:在访问资源时不加锁,而是通过版本号或时间戳等机制来检测冲突。如果检测到冲突,则重试操作。
-
适用场景:适用于读操作频繁而写操作较少的场景,因为写操作引发冲突的概率较低。
-
性能开销:由于不加锁,性能开销较小,尤其是在低冲突场景下。
-
实现方式:可以通过版本号(
version
字段)或时间戳(timestamp
字段)来实现。
偏向锁、非偏向锁
1. 偏向锁(Biased Locking)
偏向锁是一种优化机制,目的是减少锁的开销,特别是在锁被单个线程频繁获取和释放的场景下。它的核心思想是**“偏向”**某个线程,即当一个线程第一次获取锁时,锁会偏向这个线程,后续该线程再次获取锁时,无需再进行复杂的同步操作。
工作原理
-
锁标记(Mark Word):在Java对象头中,有一个字段称为锁标记(Mark Word),用于存储对象的锁状态和相关信息。
-
偏向锁状态:当锁处于偏向锁状态时,锁标记中会存储偏向线程的ID。
-
锁获取流程:
-
第一次获取锁:线程尝试获取锁时,如果锁是无锁状态(未被任何线程持有),则将锁标记设置为偏向锁状态,并记录当前线程的ID。
-
后续获取锁:如果当前线程再次尝试获取锁,且锁标记中的线程ID与当前线程ID一致,则直接获取锁,无需进行同步操作。
-
其他线程尝试获取锁:如果其他线程尝试获取锁,且锁标记中的线程ID与当前线程ID不一致,则会触发偏向锁的撤销(Revoke),将锁升级为轻量级锁或重量级锁。
-
优点
-
减少锁开销:对于单线程频繁获取锁的场景,偏向锁可以显著减少锁的开销,提高性能。
-
简化锁机制:偏向锁通过减少锁的获取和释放操作,简化了锁的管理机制。
缺点
-
锁撤销开销:如果多个线程频繁竞争同一个锁,偏向锁可能需要频繁地撤销和升级,这会增加额外的开销。
-
适用场景有限:偏向锁主要适用于单线程或少数线程竞争锁的场景,对于高并发场景效果不佳。
2. 非偏向锁(Non-Biased Locking)
非偏向锁是指不使用偏向锁机制的锁状态。在这种状态下,锁的获取和释放需要通过同步机制来完成,例如轻量级锁(Thin Locking)或重量级锁(Heavy Locking)。
工作原理
-
轻量级锁(Thin Locking):
-
当多个线程竞争锁时,锁会从偏向锁升级为轻量级锁。
-
轻量级锁通过CAS(Compare-And-Swap)操作来尝试获取锁,如果CAS成功,则线程获得锁;如果CAS失败,则进入自旋等待。
-
-
重量级锁(Heavy Locking):
-
如果轻量级锁的自旋等待失败(例如自旋次数过多),锁会进一步升级为重量级锁。
-
重量级锁会将线程挂起,放入等待队列中,直到锁被释放。
-
优点
-
适用于高并发场景:非偏向锁更适合多线程竞争锁的场景,通过轻量级锁和重量级锁的机制,可以有效地管理线程竞争。
-
避免锁撤销开销:非偏向锁不需要进行锁的撤销操作,减少了锁状态切换的开销。
缺点
-
锁开销较大:在单线程场景下,非偏向锁的开销比偏向锁大,因为每次获取锁都需要进行同步操作。
-
复杂性较高:非偏向锁的实现机制相对复杂,涉及轻量级锁和重量级锁的切换。
3. 偏向锁和非偏向锁的切换
-
偏向锁升级:当其他线程尝试获取偏向锁时,锁会从偏向锁状态升级为轻量级锁或重量级锁。
-
非偏向锁降级:在某些情况下,非偏向锁也可以降级为偏向锁,但这需要满足特定的条件,例如锁的竞争减少。
4. 配置和使用
-
默认启用偏向锁:在Java 6及以后的版本中,偏向锁默认是启用的。可以通过JVM参数
-XX:-UseBiasedLocking
来禁用偏向锁。 -
动态调整:JVM会根据运行时的锁竞争情况动态调整锁的状态,例如将偏向锁升级为轻量级锁或重量级锁。
总结
-
偏向锁:适用于单线程或少数线程竞争锁的场景,可以显著减少锁的开销,提高性能。
-
非偏向锁:适用于多线程竞争锁的场景,通过轻量级锁和重量级锁的机制有效管理线程竞争。
volatile 关键字
一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数 据的不一致。要解决这个问题,就需要把变量声明为 volatile,这就指示 JVM,这个变量是不稳定的,每次使用它都到主存中进行 读取。 说白了, volatile 关键字的主要作用就是保证变量的可见性然后还有一个作用是防止指令重排序。
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关 键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进 行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行 效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞 volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。 volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访 问资源的同步性。
AQS
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。
AQS的核心功能
-
资源状态管理:AQS通过一个整数
state
表示同步状态,子类可以定义该状态的意义和操作方式。 -
线程排队管理:AQS使用一个FIFO等待队列管理多个线程的竞争和等待。
-
提供模板方法:AQS提供了一系列模板方法,子类可以通过实现这些方法来定义具体的同步机制。
AQS的设计理念
AQS的设计理念是将同步状态和线程队列的管理逻辑抽象出来,以便不同类型的同步器可以重用这些逻辑。这样,开发者可以集中精力于具体的同步器逻辑实现,而无需关心底层的线程排队和状态管理细节。
AQS的实现原理
-
核心变量:AQS维护了一个
state
变量,表示同步状态,通过volatile
修饰保证其可见性。同时,AQS提供了compareAndSetState
方法,基于CAS机制保证对state
操作的原子性。 -
等待队列:AQS使用CLH队列(双向链表)来管理等待的线程。队列中的每个节点代表一个线程,节点包含线程引用、状态信息(如是否被中断)、前驱节点和后继节点。
-
资源共享方式:AQS定义了两种资源共享方式:
-
独占式(Exclusive):一次只能有一个线程持有资源,如
ReentrantLock
。 -
共享式(Share):允许多个线程同时访问资源,如
Semaphore
、CountDownLatch
。
-
AQS的使用
-
自定义同步器:通过继承AQS并实现其抽象方法(如
tryAcquire
、tryRelease
、tryAcquireShared
、tryReleaseShared
等),可以构建自定义的同步器。 -
常见同步器:许多常用的同步器都是基于AQS实现的,如
ReentrantLock
、Semaphore
、CountDownLatch
、CyclicBarrier
、ReadWriteLock
等。
AQS的优势
-
高效性:AQS通过队列和状态管理机制,避免了线程的频繁挂起和唤醒,提高了线程的执行效率。
-
可扩展性:AQS提供了一套通用的同步框架,方便开发者根据具体需求实现自定义的同步器。