java公平锁和非公平锁(Reentrantlock)
- 公平锁―是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。
- 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后中请的线程比先中请的线程优先获取锁。在高并发的情况下,有可能会造成优先级反转或者饥饿现象
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁。
此类的构造函数接受可选的公平性参数。当设置为true时,在争用下,锁有利于向等待时间最长的线程授予访问权限。否则,此锁不保证任何特定的访问顺序。与使用默认设置的程序相比,使用由许多线程访问的公平锁的程序可能显示出较低的总体吞吐量(即,较慢;通常要慢得多),但是在获得锁和保证没有饥饿的时间上差异较小。
但是请注意,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的多个线程中的一个线程可以连续多次获得公平锁,而其他活动线程则没有进行并且当前没有持有该锁。还要注意,不计时的 tryLock()方法不支持公平性设置。如果锁可用,即使其他线程正在等待,它也会成功。
两者区别
关于两者区别:
公平锁
Threads acquire a fair lock in the order in which they requested it.
公平锁就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
非公平锁
a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lockhappens to be available when it is requested.
非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。
Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。
非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁
值得注意的是:jvm对线程的调度策略是抢占式的,优先级越高的线程,可能越会获取cpu的处理权
java可重入锁和递归锁(Reentrantlock/synchronized)
可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁(也即线程对于内存锁代码,不需要再次争抢锁)。
也即是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块。
ReentrantLock/synchronized就是一个典型的可重入锁。
可重入锁最大的作用是避免死锁。
synchronized可重入锁
class Phone {
public synchronized void sendSMS() throws Exception{
System.out.println(Thread.currentThread().getName() + "\t invoked sendSMS()");
// 在同步方法中,调用另外一个同步方法
sendEmail();
}
public synchronized void sendEmail() throws Exception{
System.out.println(Thread.currentThread().getId() + "\t invoked sendEmail()");
}
}
public static void main(String[] args) {
Phone phone = new Phone();
new Thread(()->{
phone.sendSMS();
},"t1").start();
new Thread(()->{
phone.sendSMS();
},"t2").start();
}
输出结果
t1 invoked sendSMS()
11 invoked sendEmail()
t2 invoked sendSMS()
12 invoked sendEmail()
ReentrantLock可重入锁
public class ReentrantLockTest {
public static void sendSMS(){
System.out.println(Thread.currentThread().getName()+" sendSMS");
sendEmail();
}
public static void sendEmail(){
System.out.println(Thread.currentThread().getName()+" sendEmail");
}
public static void main(String[] args) {
Lock lock = new ReentrantLock();
new Thread(()->{
lock.lock();
try {
sendSMS();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(()->{
lock.lock();
try {
sendSMS();
}finally {
lock.unlock();
}
},"t2").start();
}
}
输出结果
t1 invoked sendSMS()
11 invoked sendEmail()
t2 invoked sendSMS()
12 invoked sendEmail()
java自旋锁(spinLock)
自旋锁(Spin Lock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU (以循环方式消耗cpu的代价,不让线程堵塞(挂起))
unsafe中的getAndAddInt就是一个良好示例
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
提到了互斥同步对性能最大的影响阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上, 共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
《深入理解JVM.2nd》Page 398
为什么要使用自旋锁,就是多个线程对同一变量进行访问,为了线程安全加锁,但是由于线程使用共享变量很短一段时间,挂起线程进入堵塞状态,然后回复,消耗大。因此采用循环访问锁的方式,获取锁。
public class SpinLockTest {
AtomicReference<String> atomicReference = new AtomicReference<>("unlock");
public void lock(){
System.out.println(Thread.currentThread().getName()+ " come in");
// 如果值不为unlock时, 进行当前线程进行循环获取锁。值为unlock时,获取该锁并退出
while (!atomicReference.compareAndSet("unlock","lock")){
}
System.out.println(Thread.currentThread().getName()+ " lock");
}
public void unlock(){
atomicReference.compareAndSet("lock","unlock");
System.out.println(Thread.currentThread().getName()+ " unlock");
}
public static void main(String[] args) {
// 假设我们希望当原子引用共享对象为unlock值时,才可以访问这个共享对象
SpinLockTest spinLock = new SpinLockTest();
new Thread(()->{
spinLock.lock();
try {
TimeUnit.SECONDS.sleep(8); }catch (InterruptedException e){
}finally {
spinLock.unlock();
}
},"t1").start();
try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){}
new Thread(()->{
spinLock.lock();
try {
}finally {
spinLock.unlock();
}
},"t2").start();
}
}
java读写锁(ReentrantReadWriteLock)
独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有。
多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。但是,如果有一个线程想去写共享资源来,就不应该再有其它线程可以对该资源进行读或写。
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。