Java锁,真的有这么复杂吗?

为什么使用synchronized
volatile,在多线程下可以保证变量的可见性,但是不能保证原子性,下面一段代码说明:

 

运行上面代码,会发现输出flag的值不是理想中10000,虽然volatile写入时候会通知其他线程的工作内存值无效,从主内存重写读取。i++是三步操作,读取-赋值-写入不能保证原子性。 原子性:不能被中断要么成功要么失败。

比如此时主内存的flag值10,线程1和线程2读取到自己工作内存都是10,然后线程1在进行赋值的时候,线程2执行了,这时线程2发现自己内存的值和主内存的值一样,并没有修改,然后赋值写入11,此时线程1运行,因为之前读过了,会往下继续运行写入也是11。那么两个线程相当于只增加了一次。要想达到理想值,只需要修改 public synchronized void increase() { flag++; } 就行了。

什么是synchronized
Java提供的一种原子性性内置锁,Java每个对象都可以把它当做是监视器锁,线程代码执行在进入synchronized代码块时候会自动获取内部锁,这个时候其他线程访问时候会被阻塞到队列,直到进入synchronized中的代码执行完毕或者抛出异常或者调用了wait方法,都会释放锁资源。在进入synchronized会从主内存把变量读取到自己工作内存,在退出的时候会把工作内存的值写入到主内存,保证了原子性。

synchronized机制


编译后执行 javap -v Test.class 就会发现两条指令。

 

synchronized是使用一种monitor机制,在进入锁时候先执行monitorenter指令。退出的时候执行monitorexit指令。synchronized是可重入锁,每个对象中都含有一个计数器当前线程再次获取锁,计数器+1,退出时候计算器-1,直到计数器为0才释放锁资源,唤醒其他线程来争抢资源。任意一个对象都拥有自己的监视器,只有在线程获取到监视器锁时才会进入代码中,否则就进入阻塞状态。

 

synchronized使用场景
对于普通方法,锁是当前类实例对象。
对于静态方法,锁是当前类对象。
对于同步代码块,锁是synchronized括号里的对象。
synchronized锁升级
synchronized在1.6以前是重量级锁,当前只有一个线程执行,其他线程阻塞。为了减少获得锁和释放锁带来的性能问题,而引入了偏向锁、轻量级锁以及锁的存储过程和升级过程。在1.6后锁分为了无锁、偏向锁、轻量锁、重量锁,锁的状态在多线程竞争的情况下会逐渐升级,只能升级而不能降级,这样是为了提高锁获取和释放的效率。

synchronized的锁是存贮在Java对象头里的,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果对象是非数组类型,则用2字宽存储对象头。1个字宽等于4个字节。

 

Java对象头中的Mark Word里默认存储了对象是HashCode、分代年龄、和锁标记。

 

在运行的时候,Mark Word里存储的数据会随着锁标志位的变化而变化,可能会变化为存储以下四种形式。

 

偏向锁
偏向锁的意思未来只有一个线程使用锁,不会有其他线程来争取。

获取锁:

首先检查Mark word中锁的标志是否为01。
如果是01,判断对象头的Mark word记录是否为当前线程ID,如果是执行5,否则执行3.
线程ID并未只指向自己,发送CAS竞争,如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,执行5;如果未成功执行4。
当到达全局安全点(在这个时间点上没有正在执行的字节码)时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
执行同步代码。
撤销锁:偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。需要等待全局安全点,它首先暂停原持有偏向锁的线程,然后检查线程是否还在活着,如果线程处于未活动状态,则释放锁标记,如果处于活动状态则升级为轻量级锁。

CAS
CAS全称是Compare And Swap 即比较并交换,使用乐观锁机制,包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么才会将该位置值更新为新值 。否则,处理器不做任何操作。

轻量级锁
线程在执行同步代码块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。

加锁:

CAS修改Mark Word,如果成功指向栈中锁记录的指针执行3,如果失败执行2.
发生自旋,自旋到一定次数,如果修改成功执行3,否则锁膨胀为重量级锁。
执行同步代码块。
解锁:

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

锁的优缺点


彻底搞懂锁升级


lock
它是在1.5之后提供的一个独占锁接口,它的实现类是ReentrantLock,相比较synchronized这种隐式锁(不用手动加锁和释放锁)的便捷性,但是提供了更加锁的可操作性、可中断的获取锁以及超时获取锁等多种synchronized不具备的特性。

使用方法


在finally中释放锁,目的保证获取锁最终被释放。不要在获取锁写在try里,因为如果在获取锁时发生了异常,异常抛出的同时,也会导致锁无故释放。

 

AQS
AQS是队列同步器(AbstractQueuedSynchronizer),是用来构建锁或者其他同步器的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取的线程排队工作问题。AQS在内部维护了一个单一的状态信息state,可以通过getState、setState、compareAndSetState(CAS操作)修改此值,对于ReentrantLock来说,state可以用来表示当前线程获取锁的可重入次数。ReentrantLock中当一个线程获取了锁,在AQS的内部会进行compareAndSetState将state变为1,如果再次获取就设置为2,释放锁也会去修改state值,只有当值变为0时,其他线程才能获得锁。

锁的介绍
AQS底层维护state和队列来实现独占和共享两种锁。

独占锁:每次只能有一个线程能持有锁,如lock、synchronized。

共享锁:允许多个线程同时获取锁,并发访问共享资源,如ReadWriteLock。

lock分为公平锁和非公平锁,实现了AQS接口,通过FIFO设置锁的优先级。

公平锁:根据线程获取锁的时间来判断,等待时间越久的线程优先被执行。Lock中初始化的时候ReentrantLock(true),默认为false,效率较低因为需要判断线程的等待时间。

非公平锁:抢占锁资源,不能保证获取锁的线程优先级,效率较高,因为获取锁是竞争的。

两者不同
synchronized是Java的关键字,lock是提供的类。
synchronized提供不需要手动加锁和释放的隐式锁,释放锁的条件是代码执行完或者抛出异常自动释放。lock必须手动加锁和释放锁,另外还提供了可中断锁、超时获取锁、判断锁状态。
synchronized是可重入、不可中断、非公平,lock是可重入、可中断、公平(两者皆可)
synchronized适合代码量少的同步,lock适合代码量同步多的。**
Condition接口
还记得在 Java并发二 中有一道生产者消费者,使用的是synchronized+wait(notify),lock中也提供了这种等待通知类型的方法await和signal,当前线程调用这些方法时,需要提前获取到Condition对象关联的锁,Condition是依赖于Lock对象,调用lock对象中的newCondition。

老样子还是先定义一个容器:

 

生产者:启5个线程往容器里添加数据。

 

消费者:启10线程消费数据

 

最后
注释基本明确,就不多说了。wait和notify是配合synchronized使用,await和signal是配合lock使用,区别在于唤醒时notify不能指定线程唤醒,signal可以唤醒具体的线程,更小的粒度控制锁。
--------------------- 

转载于:https://www.cnblogs.com/hyhy904/p/10954453.html

### 乐观与悲观的实现原理 #### 乐观 乐观的实现方式主要有两种:**CAS(Compare and Swap)机制**和**版本号机制**。乐观Java中是通过使用无编程来实现的,最常采用的是CAS算法。Java原子类中的递增操作就通过CAS自旋实现的。CAS是一种无算法,它允许多个线程在不使用的情况下更新共享变量,从而避免了的开销。CAS操作包括三个操作数:内存位置(V)、预期原值(A)和新值(B)。只有当内存位置的值等于预期原值时,才会将内存位置的值更新为新值,否则不会执行任何操作。这种机制确保了操作的原子性[^2]。 在Java中,`java.util.concurrent.atomic`包提供了多种原子类,如`AtomicInteger`、`AtomicLong`等,它们内部使用了CAS算法来保证线程安全。例如,`AtomicInteger`的`incrementAndGet`方法就是通过CAS操作来实现的。 ```java public final int incrementAndGet() { return unsafe.getAndAddInt(this, valueOffset, 1) + 1; } ``` 上述代码中,`unsafe.getAndAddInt`方法实际上是调用了底层的CAS指令来实现原子操作。 #### 悲观 悲观则假设数据在并发访问时一定会发生冲突,因此在访问数据时总是先加Java中的`synchronized`关键字和`ReentrantLock`类都是悲观的典型实现。当一个线程获取到后,其他线程必须等待该线程释放后才能继续执行。这种方式虽然简单易用,但在高并发场景下可能会导致性能瓶颈,因为大量的线程会因为等待而阻塞。 ### 使用场景 #### 乐观 乐观适用于**读多写少**的场景,因为在这种情况下,数据冲突的概率较低,乐观可以减少不必要的开销,提高系统的并发性能。此外,乐观也常用于高并发场景下的计数器或状态更新,例如在金融系统中处理账户余额的更新操作。在这种情况下,使用乐观可以避免长时间持有,从而减少资源的占用。 #### 悲观 悲观适用于**写多读少**的场景,因为在这种情况下,数据冲突的概率较高,悲观可以有效地防止数据不一致的问题。例如,在一个需要频繁修改数据的场景中,使用悲观可以确保每次操作都能正确地获取到,从而保证数据的一致性。然而,悲观在高并发场景下可能会导致性能问题,因为它会导致大量的线程阻塞。 ### 优缺点及解决方式 #### 乐观的缺点 乐观的主要缺点在于其**ABA问题**和**自旋开销**。ABA问题指的是,如果一个变量在被读取后,被其他线程修改并恢复原值,那么当前线程可能无法察觉这一变化,从而导致错误的结果。为了解决这个问题,可以使用带有版本号的CAS操作,即每次修改变量时,版本号也会随之增加。这样,即使变量的值没有发生变化,版本号的变化也能被检测到。 另外,乐观在失败重试时通常会采用自旋的方式,这会导致CPU资源的浪费。为了解决这个问题,可以在自旋次数达到一定阈值后,放弃重试并抛出异常,或者采用更复杂的调度策略来减少自旋开销。 #### 悲观的缺点 悲观的主要缺点在于其**性能问题**。在高并发场景下,悲观会导致大量的线程阻塞,从而降低系统的吞吐量。为了解决这个问题,可以考虑使用**分段**或**读写**。分段通过将数据分成多个段,每个段独立加,从而减少了的竞争。例如,`ConcurrentHashMap`就是通过分段来实现高效的并发访问。读写则允许多个读线程同时访问共享资源,但只允许一个写线程访问,从而提高了读操作的并发性能。 ### 示例代码 以下是一个使用`AtomicInteger`实现乐观的示例: ```java import java.util.concurrent.atomic.AtomicInteger; public class OptimisticLockExample { private AtomicInteger value = new AtomicInteger(0); public void increment() { int expectedValue; do { expectedValue = value.get(); } while (!value.compareAndSet(expectedValue, expectedValue + 1)); } public static void main(String[] args) { OptimisticLockExample example = new OptimisticLockExample(); for (int i = 0; i < 1000; i++) { new Thread(example::increment).start(); } System.out.println("Final value: " + example.value.get()); } } ``` 在这个示例中,`increment`方法使用了`AtomicInteger`的`compareAndSet`方法来实现乐观。多个线程并发地调用`increment`方法,最终输出的值应该是1000。 ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值