synchronized和ReentrantLock的区别
synchronized
和ReentrantLock
都可以用来实现 Java 中的线程同步。它们的作用类似,但是在用法和特性上还是有一些区别的。
synchronized
是 Java 内置的关键字,可以修饰代码块和方法,自动获取锁、释放锁,可以避免因为锁的释放问题导致的死锁;而ReentrantLock
是Java类,只能对某段代码进行修饰,需要手动进行锁的获取和释放。ReentrantLock
的灵活性更高,比如支持可重入锁、支持公平锁和非公平锁、支持多个条件变量等,而synchronized
则相对简化,更加方便快捷。- 多个线程争抢
synchronized
的锁时,其中一个线程拿到锁后,其他线程进入锁池等待,直到持有锁的线程释放锁,其他等待线程才能继续竞争锁。而ReentrantLock
可以灵活地控制锁的公平性和非公平性,以及等待的顺序。synchronized
在底层是依赖于 JVM 实现的,而ReentrantLock
是使用java.util.concurrent
包提供的一种基于接口的可重入锁,这种可重入锁的性能比较优秀,适用于高并发场景。综上所述,
ReentrantLock
更加灵活,支持更多的特性和操作,适用于复杂的场景;而synchronized
更加简化,使用方便,适用于一些简单的场景。
Java中锁的名词
每个名字并不都代表一个锁,有些锁的名字指的是锁的特性,锁的设计,锁的状态
乐观锁(不加锁):认为并发的操作,不加锁的方式实现是没有问题的,每次操作前判断(CAS,自旋)是否成立,不加锁实现。
悲观锁:认为对于同一个数据的并发操作,一定是会发生修改的,即使没有修改,也会认为修改。认为并发操作肯定会有问题,必须加锁,对于同一个数据的并发操作,悲观锁采用加锁的形式
总结:悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中使用的话就是利用各种的锁
乐观锁在Java中使用的话是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新
可重入锁:当一个线程获取到外层方法的同步锁对象后,可以获取到内部其他方法的同步锁,如果不使用,就容易出现死锁
读写锁(ReentrantReadWriteLock):支持读,写加锁,总的来说就是如果是读操作,就不加锁,如果一旦有写操作,那么读写互斥
特点:读读不互斥,读写互斥,写写互斥
加读锁是防止在另外的线程在此时写入数据,防止读取脏数据
一个读写锁同时只能有一个写者或多个读者(与CPU数相关),不能同时有读者和写者
在读写锁保持期间也是抢占失效的
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否者它必须自旋在那,直到没有读者或者写者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
分段锁(不是锁):是一种思想,将数据分段,并在每个分段上单独加锁,以此来将锁的粒度拆分,提高效率
自旋锁(不是锁):是一种自旋思想,是以自旋的方式重试获取,当线程抢锁失败后,重试几次,要是抢到锁了就继续,抢不到就阻塞线程,目的就是还是为了尽量不要阻塞线程。
由此可见,自旋锁是比较消耗CPU的,因为要不断的循环重试,不会释放CPU资源。加锁时间普遍较短的场景非常适合自旋锁,可以极大提高锁的效率
共享锁/独占锁:
共享锁:读写锁中的读锁,该锁可被多个线程持有,并发访问共享资源
独占锁:互斥锁,synchronized ReentrantLock都属于独占锁,一次之能被一个线程持有
公平锁/非公平锁:
公平锁:根据线程先来后到公平的获取锁,例ReentrantLock就可以实现公平锁,按照锁请求的顺序分配,拥有稳定获得锁的机会
非公平锁:不按照锁请求顺序分配,没有先来后到,谁抢到谁获得执行权,synchronized 是非公平锁
ReentrantLock默认是非公平锁,但是底层可以通过AQS来实现线程调度,所以可以使其变成公平锁
synchronized锁
对象结构
在Hotspot虚拟机中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充;
对象头中有一块区域称为 Mark Word,用于存储对象自身的运行时数据,如哈 希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID 等等。
在synchronized锁的底层实现中,提供锁的状态,又来区别对待
这个锁的状态在同步锁的对象头中,有一个区域叫Mark Word中存储
偏向锁/轻量级锁/重量级锁(都是锁的状态):这四种状态都不是Java语言中的锁,而是JVM为了提高锁(synchronized)的获取与释放效率而做的优化
无锁
偏向锁:
一直是一个线程访问,线程ID被记录,快速获取锁
轻量级锁:
当锁状态为偏向锁时,又继续有其他线程访问,此时升级为轻量锁,没有获取到锁的线程,不会阻塞,继续不断尝试获取锁
重量级锁:
当锁的状态为轻量级锁时,线程自旋达到一定的次数,进入达到阻塞状态,锁状态升级为重量级锁,等待操作系统调度
synchronized锁的实现:
synchronized锁是依赖底层编译后的指令来控制的,需要我们提供一个同步对象,来记录锁状态
线程在进入synchronized代码块时候会自动获取内部锁,这个时候其他线程访问时会被阻塞,直到执行完或抛出异常或者调用了wait方法,都会释放锁资源,主内存把变量读取到自己工作内存,在退出时会把工作内存的值写入到主内存,保证原子性
synchronized基于进入和退出监视器对象来实现方法同步和代码块同步
设置ACC_SYNCHRONIZED标记是否为同步方法,调用指令先检查是否设置,如果设置了,需要monitorenter表明线程进入该方法,使用monitorexit退出该方法
在虚拟机执行monitorenter指令时,首先要尝试获取对象的锁,如果当前线程拥有了这个对象的锁,把锁的计数器+1,当执行monitorexit指令时将所得计数器-1,当计数器为0时,锁就被释放了
Java中synchronized通过在对象头设置标记,达到了获取锁和释放锁的目的
ReentrantLock锁
ReentrantLock 是 java.util.concurrent.locks 包 下 的 类,ReentrantLock 基于 AQS,在并发编程中它可以实现公平锁和非公平锁来
ReentrantLock 类内部总共存在 Sync、NonfairSync、FairSync 三个类,
NonfairSync 与 FairSync 类 继 承 自 Sync 类 , Sync 类 继 承 自 AbstractQueuedSynchronizer 抽象类。对共享资源进行同步,同时和 synchronized 一样,
ReentrantLock 支持可重入,
除此之外,ReentrantLock 在调度上更灵活,支持更多丰富的功能。
NonfairSync 类继承了 Sync 类,表示采用非公平策略获取锁,其实现 了 Sync 类中抽象的 lock 方法.
- 非公平
NonfairSync final void lock() { if (compareAndSetState(0, 1))//线程来到后,直接尝试获取锁,是非公平 setExclusiveOwnerThread(Thread.currentThread()); else//获取不到 acquire(1); } FairSync 类也继承了 Sync 类,表示采用公平策略获取锁,其实现了 Sync 类中 的抽象 lock 方法.
- 公平实现
FairSync final void lock() { acquire(1); } ```