浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

时间:2023-03-09 06:01:21
浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

公平锁和非公平锁

这里主要体现在ReentrantLock这个类里面了 公平锁、非公平锁的创建方式:

//创建一个非公平锁,默认是非公平锁

Lock lock = new ReentrantLock();

Lock lock = new ReentrantLock(false);

//创建一个公平锁,构造传参true

Lock lock = new ReentrantLock(true);

相关源码:

public ReentrantLock() {

sync = new NonfairSync();

}

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

————————————————

公平锁是指多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。公平锁的优点是等待锁的线程不会饿死。缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大.

对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。

浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()。

再进入hasQueuedPredecessors(),可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。

综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁,Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁

先写个demo大致的看一下:

public class AtomicDemo {

    public static void main(String[] args) {

        person person = new person();
        for (int i = 0; i < 10; i++) {
            new Thread(){
                @Override
                public void run() {
                    person.aaa();
                }
            }.start();
        }
    }
}
class person{

    public synchronized void aaa(){
        System.out.println(Thread.currentThread().getName()+"aaa");
        this.bbb();
    }
    public synchronized void bbb(){
        System.out.println(Thread.currentThread().getName()+"bbb");
    }
}

结果:

Thread-1aaa

Thread-1bbb

Thread-2aaa

Thread-2bbb

Thread-0aaa

Thread-0bbb

Thread-3aaa

Thread-3bbb

Thread-4aaa

Thread-4bbb

Thread-5aaa

Thread-5bbb

Thread-7aaa

Thread-7bbb

Thread-6aaa

Thread-6bbb

Thread-9aaa

Thread-9bbb

Thread-8aaa

Thread-8bbb

说明:

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,aaa()方法中调用bbb()方法。因为内置锁是可重入的,所以同一个线程在调用bbb()时可以直接获得当前对象的锁,进入bbb()进行操作。

如果是一个不可重入锁,那么当前线程在调用bbb()之前需要将执行aaa()时获取当前对象的锁释放掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。

浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

自旋锁

先说说概念

阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。如果物理机器有多个处理器,能够让两个或以上的线程同时并行执行,我们就可以让后面那个请求锁的线程不放弃CPU的执行时间,看看持有锁的线程是否很快就会释放锁。

而为了让当前线程“稍等一下”,我们需让当前线程进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。

自旋锁本身是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

自旋锁的实现原理同样也是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

看看源码中的CAS方法:

浅谈Java中的公平锁和非公平锁,可重入锁,自旋锁

简单的说一下参数:

Var1---->this(也就是当前的对象)

Var2---->当前对象的主内存地址

Var4---->要加的值

Var5---->自旋完成后返回的值

再说细一点:

var5 = this.getIntVolatile(var1, var2);

这个方法的就是在内存中找到var1对象的内存地址为var2的值赋值给var5

再去判断:

this.compareAndSwapInt(var1, var2, var5, var5 + var4)

当前var5的值与获取到主内存中的值是不是一样的,一样的就进行var4+var5的操作,返回true,否则就不做操作

随便写了一下,先到这里,后面再对其他的锁进行总结,喜欢的支持一下