多线程篇(基本认识 - 公平锁 & 非公平锁、独占锁 & 共享锁、可重入锁、自旋锁)(持续更新迭代)

目录

锁一:公平锁与非公平锁

前言

一、Lock 锁接口

二、公平锁

1. 简介

三、非公平锁

1. 简介

四、JUC

1. ReentranLock

公平锁

非公平锁

锁二:独占锁 & 共享锁

前言

一、简介

二、代码示例

1. 未加锁状态

2. 加锁状态

锁三:可重入锁

前言

一、简介

二、代码示例

锁四:自旋锁

一、前言

二、问题思考

三、思路梳理

四、时间阈值

五、自优缺点

六、代码示例

1. 非公平自旋锁

2. 公平自旋锁


锁一:公平锁与非公平锁

前言

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁的顺序是按

照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。而非公平锁则在

运行时闯入,也就是先来不一定先得。

ReentrantLock 提供了公平和非公平锁的实现。

公平锁:ReentrantLock pairLock = new ReentrantLock(true)

非公平锁:ReentrantLock pairLock = new ReentrantLock(false)

如果构造函数不传递参数,则默认是非公平锁。

例如,假设线程 A 已经持有了锁,这时候线程B请求该锁其将会被挂起。

当线程A释放锁后,假如当前有线程 C 也需要获取该锁,如果采用非公平锁方式,则根据线程调

度策略,线程B和线程C两者之一可能获取锁,这时候不需要任何其他干涉,而如果使用公平锁

则需要把C挂起,让B获取当前锁。

在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销。

一、Lock 锁接口

了解过synchronized锁的同学们,知道synchronized锁是一种JVM提供内置锁,但synchronized

有一些缺点:比如不支持响应中断,不支持超时,不支持以非阻塞的方式获取锁等。而今天的主

角Lock锁,需要我们手动获取锁和释放锁,里面有很多方式来获取锁,比如以阻塞方式获取锁,

在指定时间内获取锁,非阻塞模式下抢占锁等,其方法源码如下

(位于package java.util.concurrent.locks包下):

//Lock接口下的方法
public interface Lock {
    //阻塞式抢占锁,如果抢到锁,则向下执行程序;抢占失败线程阻塞,直到释放锁才会进行抢占锁
    void lock();
    //可中断模式抢占锁,线程调用此方法能够中断线程
    void lockInterruptibly() throws InterruptedException;
    //非阻塞式尝试获取锁,调用此方法线程不会阻塞,抢到锁返回true,失败返回false
    boolean tryLock();
    //在指定时间内尝试获取锁,在指定时间内抢到锁成功返回true,失败返回false
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    //释放锁,线程执行完程序后,调用此方法来释放锁资源
    void unlock();
    //条件队列
    Condition newCondition();
}

二、公平锁

1. 简介

公平锁,顾名思义,所有线程获取锁都是公平的。在多线程并发情况下,线程争抢锁时,首先会

检查等待队列中是否有其他线程在等待。

如果等待队列为空,没有线程在等待,那么当前线程会拿到锁资源;如果等待队列中有其他线程

在等待,那么当前线程会排到等待队列的尾部,好比我们排队买东西一样。

三、非公平锁

1. 简介

非公平锁就是所有抢占锁的线程都是不公平的,

在我们日常生活中就相当于是插队现象,不过也与插队稍微不同。

在多线程并发时,每个线程在抢占锁的过程中,都会先尝试获取锁,如果获取成功,则直接指向

具体的业务逻辑;如果获取锁失败,则会像公平锁一样在等待队列队尾等待。

在非公平锁模式下,由于刚来的线程可以在队首位置进行一次插队,所以当插队成功时,后面的

线程可能会出现长时间等待,无法获取锁资源产生饥饿现象。但是非公平锁性能比公平锁性能更

好。

四、JUC

1. ReentranLock

公平锁

ReentranLock类实现了Lock接口,重写了里面的方法,对于ReentranLock类中的公平锁,

我们可以看到如下源码:

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

在上述构造方法中,新建锁对象是否为公平锁,在于传入的参数是true还是false,在三目运算符

中,如果传入参数为true,则会创建一个FairSync()对象赋值给sync,线程获取的锁是公平锁;

如果为false则创建一个NonfairSync()对象,线程获取的锁是非公平锁。

点入FairSync()方法,得到如下源码:

    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        /*这里将acquire方法放于此
            public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
        */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            //拿到当前锁对象的状态
            int c = getState();
            //如果没有线程获取到锁
            if (c == 0) {
                //首先会判断是否有前驱节点,如果没有就会调用CAS机制更新锁对象状态
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    //设置当前线程拥有锁资源
                    setExclusiveOwnerThread(current);
                    //如果获取锁资源成功则返回true
                    return true;
                }
            }
            //或者如果拿到锁的就是当前线程
            else if (current == getExclusiveOwnerThread()) {
                //将锁的状态+1,考虑到锁重入
                int nextc = c + acquires;
                if (nextc < 0)
                    //超过了最大锁的数量
                    throw new Error("Maximum lock count exceeded");
                //如果锁数量没有溢出,设置当前锁状态
                setState(nextc);
                //如果成功获取锁资源则返回true
                return true;
            }
            //如果前两条都不符合,则返回false
            return false;
        }
    }

上述代码涉及到了hasQueuedPredecessors()方法,返回true则代表有有前驱节点,点入查看得

到以下源码:

    public final boolean hasQueuedPredecessors() {
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        //h!=t,首节点不等于尾节点表示队列中有节点
        return h != t &&
            (
            //s为头结点的下一个节点,返回false表示队列中还有第二个节点,或运算符后面表示,第二个线程不是当前线程
            (s = h.next) == null || s.thread != Thread.currentThread()
            );
    }

因此做出总结,使用 ReentranLock 的公平锁,当线程调用 ReentranLock 类中的 lock() 方法

时,会首先调用FairSync类中的lock()方法;

然后FairSync类中的lock()方法会调用AQS类(AbstractQueuedSynchronizer)中的acquire(1)方

法获取资源,前面的文章里也提到过,acquire()方法会调用tryAcquire()方法尝试获取锁,

tryAcquire()方法里面没有具体的实现,tryAcquire()方法具体逻辑是由其子类实现的,因此调用

的还是FairSync类中的方法。在AQS中的acquire()方法中如果尝试获取资源失败,会调用

addWaiter()方法将当前线程封装为Node节点放到等待队列的尾部,并且调用AQS中的

acquireQueued方法使线程在等待队列中排队。

非公平锁

对于ReentranLock类中的非公平锁,实现方式有两种,

一种是默认的构造方式,另一种是和上面的一样,源码如下:

    //第一种方式,默认实现
    public ReentrantLock() {
        sync = new NonfairSync();
    }
    //第二种方式,传入false来创建非公平锁对象
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

对于创建的NonfairSync()类对象,其源码如下:

    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

在上述代码中,当线程获取锁时,并没有直接将当前线程放入等待队列中,而是先尝试获取锁资

源,如果获取锁成功,设置state标志位1成功,则直接将当前线程拿到锁,执行线程业务;如果

获取锁资源失败,则调用AQS中的acquire方法获取资源,而acquire()方法会回调

上面NonfairSyn类中的tryAcquire()方法,然后又会回调Sync类中的nonfairTryAcquire()方法

(NonfairSync类继承了Sync类) ,点击nonfairTryAcquire(acquires)查看源码:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

由上诉代码,没有将线程放入到等待队列中,只是对锁的状态进行了判断,若标识为0,代表没

有线程拿到锁,当前线程会使用CAS机制改变锁状态,并调用

setExclusiveOwnerThread(current)方法让当前线程拿到锁。

因此综上所述,在使用ReentranLock中的非公平锁时,首先会调用lock()方法,,而

ReentranLock类中lock()方法会调用NonfairSync类中的lock()方法,接着NonfairSync类中的

lock()方法会调用AQS中的acquire()方法来获取锁资源,AQS中的acquire()方法又会回调

NonfairSync类中tryAcquire()方法尝试获取资源,NonfairSync类中tryAcquire()方法会调用Sync

类中的nonfairTryAcquire方法尝试非公平锁获取资源;获取失败的话,AQS中的acquire()方法会

调用addWaiter()方法将当前线程封装成Node节点放入到等待队列的队尾,而后AQS中的

acquire()方法会调用AQS中的acquireQueued()方法让线程在等待队列中排队。

这就是非公平锁的整个流程。

锁二:独占锁 & 共享锁

前言

根据锁只能被单个线程持有还是能被多个线程共同持有,锁可以分为独占锁和共享锁。

独占锁保证任何时候都只有一个线程能得到锁,ReentrantLock就是以独占方式实现的。

共享锁则可以同时由多个线程持有,例如:ReadWriteLock读写锁,它允许一个资源可以被多线

程同时进行读操作。独占锁是一种悲观锁,由于每次访问资源都先加上互斥锁,这限制了并发

性,因为读操作并不会影响数据的一致性,而独占锁只允许在同一时间由一个线程读取数据,其

他线程必须等待当前线程释放锁才能进行读取。

共享锁则是一种乐观锁,它放宽了加锁的条件,允许多个线程同时进行读操作。

一、简介

  • 独占锁:指该锁一次只能被一个线程所持有。对ReentrantLock和Synchronized而言都是独占锁
  • 共享锁:指该锁可被多个线程所持有。

多个线程同时读一个资源类没有任何问题,

所以为了满足并发量,读取共享资源应该可以同时进行。

但是,如果有一个线程想去写共享资源时,就不应该再有其它线程可以对该资源进行读或写。

对 ReentrantReadWriteLock 其读锁是共享锁,其写锁是独占锁。

读锁的共享锁可保证并发读是非常高效的,读写,写读,写写的过程是互斥的。

二、代码示例

1. 未加锁状态

实现一个读写缓存的操作,假设开始没有加锁的时候,会出现什么情况:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();

    public void put(String key, Object value) {
        System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "\t 写入完成");
    }

    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "\t 正在读取:");
        try {
            // 模拟网络拥堵,延迟0.3秒
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);
    }
}

public class ReadWriteWithoutLockDemo {

    public static void main(String[] args) {
        MyCache myCache = new MyCache();
        // 线程操作资源类,5个线程写
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }
        
        // 线程操作资源类, 5个线程读
        for (int i = 0; i < 5; i++) {
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

输出结果:

0	 正在写入:0
1	 正在写入:1
3	 正在写入:3
2	 正在写入:2
4	 正在写入:4
0	 正在读取:
1	 正在读取:
2	 正在读取:
4	 正在读取:
3	 正在读取:
1	 写入完成
4	 写入完成
0	 写入完成
2	 写入完成
3	 写入完成
3	 读取完成:3
0	 读取完成:0
2	 读取完成:2
1	 读取完成:null
4	 读取完成:null

2. 加锁状态

上面的代码是没有加锁的,这样就会造成线程在进行写入操作的时候,被其它线程频繁打断,从

而不具备原子性,这个时候,我们就需要用到读写锁来解决了

package com.lun.concurrency;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class MyCache2 {

    private volatile Map<String, Object> map = new HashMap<>();

    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void put(String key, Object value) {

        // 创建一个写锁
        rwLock.writeLock().lock();

        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在写入:" + key);

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            map.put(key, value);

            System.out.println(Thread.currentThread().getName() + "\t 写入完成");

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 写锁 释放
            rwLock.writeLock().unlock();
        }
    }

    public void get(String key) {

        // 读锁
        rwLock.readLock().lock();
        try {

            System.out.println(Thread.currentThread().getName() + "\t 正在读取:");

            try {
                // 模拟网络拥堵,延迟0.3秒
                TimeUnit.MILLISECONDS.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            Object value = map.get(key);

            System.out.println(Thread.currentThread().getName() + "\t 读取完成:" + value);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 读锁释放
            rwLock.readLock().unlock();
        }
    }

    public void clean() {
        map.clear();
    }


}

public class ReadWriteWithLockDemo {
    public static void main(String[] args) {

        MyCache2 myCache = new MyCache2();

        // 线程操作资源类,5个线程写
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.put(tempInt + "", tempInt +  "");
            }, String.valueOf(i)).start();
        }

        // 线程操作资源类, 5个线程读
        for (int i = 1; i <= 5; i++) {
            // lambda表达式内部必须是final
            final int tempInt = i;
            new Thread(() -> {
                myCache.get(tempInt + "");
            }, String.valueOf(i)).start();
        }
    }
}

执行结果:

1	 正在写入:1
1	 写入完成
2	 正在写入:2
2	 写入完成
3	 正在写入:3
3	 写入完成
5	 正在写入:5
5	 写入完成
4	 正在写入:4
4	 写入完成
2	 正在读取:
3	 正在读取:
1	 正在读取:
5	 正在读取:
4	 正在读取:
3	 读取完成:3
2	 读取完成:2
1	 读取完成:1
5	 读取完成:5
4	 读取完成:4

锁三:可重入锁

前言

当一个线程要获取一个被其他线程持有的独占锁时,该线程会被阻塞,那么当一个线程再次获取

它自己已经获取的锁时是否会被阻塞呢?如果不被阻塞,那么我们说该锁是可重入的,也就是只

要该线程获取了该锁,那么可以无限次数(严格来说是有限次数)地进入被该锁锁住的代码。

实际上,synchronized 内部锁是可重入锁。可重入锁的原理是在锁内部维护一个线程标示,用来

标示该锁目前被哪个线程占用,然后关联一个计数器。一开始计数器值为 0,说明该锁没有被任何

线程占用。当一个线程获取了该锁时,计数器的值会变成1,这时其他线程再来获取该锁时会发

现锁的所有者不是自己而被阻塞挂起。

但是当获取了该锁的线程再次获取锁时发现锁拥有者是自己,就会把计数器值加+1,当释放锁后

计数器值-1。当计数器值为 0 时,锁里面的线程标示被重置为 null,这时候被阻塞的线程会被唤

醒来竞争获取该锁。

一、简介

可重入锁是指一个线程可以多次获得同一个锁,而不会被自己所持有的锁所阻塞。Java中的可重

入锁由ReentrantLock类实现。

ReentrantLock提供了与synchronized关键字类似的互斥保护,但具有更好的控制能力。

ReentrantLock提供了公平锁和非公平锁两种实现方式,可以通过构造函数进行选择。公平锁会

尽量使线程在等待队列中按照请求的顺序获取锁,而非公平锁则不保证这一点。

二、代码示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class MyThread implements Runnable {
    private Lock lock;
 
    public MyThread(Lock lock) {
        this.lock = lock;
    }
 
    public void run() {
        lock.lock();    // 获取锁
        try {
            System.out.println(Thread.currentThread().getName() + " 获取了锁");
            Thread.sleep(1000);    // 模拟业务处理
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();    // 释放锁
            System.out.println(Thread.currentThread().getName() + " 释放了锁");
        }
    }
 
    public static void main(String[] args) {
        Lock lock = new ReentrantLock();
        MyThread thread1 = new MyThread(lock);
        MyThread thread2 = new MyThread(lock);
        Thread t1 = new Thread(thread1, "Thread 1");
        Thread t2 = new Thread(thread2, "Thread 2");
        t1.start();
        t2.start();
    }
}

在上面的例子中,MyThread类实现了Runnable接口,在run()方法中调用了lock.lock()获取锁,

并在业务处理结束后调用lock.unlock()释放锁。在main()方法中创建了两个线程分别执行

MyThread实例,它们会交替地获取锁并进行业务处理。由于是同一个锁可以在多个线程中共

享,所以它是一个可重入锁。

锁四:自旋锁

一、前言

由于Java中的线程是与操作系统中的线程一一对应的,所以当一个线程在获取锁(比如独占锁)失

败后,会被切换到内核状态而被挂起。

当该线程获取到锁时又需要将其切换到内核状态而唤醒该线程。

而从用户状态切换到内核状态的开销是比较大的,在一定程度上会影响并发性能。

自旋锁则是,当前线程在获取锁时,如果发现锁已经被其他线程占有,它不马上阻塞自己,在不

放弃 CPU 使用权的情况下,多次尝试获取(默认次数是10,可以使用-XX:PreBlockSpinsh 参数

设置该值),很有可能在后面几次尝试中其他线程已经释放了锁。

如果尝试指定的次数后仍没有获取到锁则当前线程才会被阻塞挂起。

由此看来自旋锁是使用 CPU 时间换取线程阻塞与调度的开销,但是很有可能这些 CPU 时间白

白浪费。

二、问题思考

在理解自旋锁之前,必须要先知道自旋锁要解决的难题是什么:阻塞或唤醒一个Java线程需要操

作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。如果同步代码块中的内容过于

简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。

三、思路梳理

如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态

和用户态之间的切换进入阻塞、挂起状态,只需要等一等(也叫做自旋),在等待持有锁的线程

释放锁后即可立即获取锁,这样就避免了用户线程在用户态和内核态之间的频繁切换而导致的时

间消耗。

这时可能有读者要问了,线程在自旋时会占用CPU,在线程长时间自旋获取不到锁时,将会导致

CPU的浪费,甚至有时线程永远无法获取锁而导致CPU资源被永久占用。那该如何解决这个问

题呢?常见的做法是设定一个自旋等待的最大时间(即时间阈值,如何设置将在下段中说明),

在线程执行的时间超过自旋等待的最大时间后,线程会立即释放锁。

四、时间阈值

自旋锁用于使当前线程占着CPU资源不释放,等到下次自旋获取锁资源后立即执行相关操作,但

如何选择自旋锁的执行时间呢?

如果自旋的执行时间太长,则会有大量的线程处于自旋状态且占用CPU资源,

造成系统资源的浪费。

因此,对自旋的周期选择将直接影响系统的性能。

Java中JDK的不同版本所采用的自选周期不同,JDK1.5为固定的时间,JDK1.6引入了适应性自

旋锁,适应性自旋锁的自旋时间不再是固定值,而是由上一次在同一锁上的自旋时间及锁的拥有

者的状态来决定的,可基本认为一个线程上下文切换的时间就是一个锁自旋的最佳时间。

五、自优缺点

优点

自旋锁可以减少CPU上下文的切换,对于占用锁时间非常短或锁竞争不激烈的代码块来说性能大

幅提升,因为自旋的CPU耗时明显少于线程阻塞、挂起、再次唤醒时两次CPU上下文切换的耗

时。

缺点

在持有锁的线程占用锁时间过长或锁的竞争过于激烈时,线程在自旋过程中会长时间获取不到锁

资源,将引起CPU资源的浪费。所以在系统中有复杂锁依赖的情况下不适合采用自旋锁。

六、代码示例

1. 非公平自旋锁

import java.util.concurrent.atomic.AtomicReference;

public class SpinLockDemo {
    /**
     * 锁的持有者
     */
    private AtomicReference<Thread> owner = new AtomicReference<>();

    /**
     * 记录锁重入次数
     */
    private volatile int count = 0;

    public void lock() {
        Thread current = Thread.currentThread();
        // 当前线程已经持有锁, 则记录重入次数即可
        if( current == owner.get() ) {
            count++;
            return;
        }

        while ( !owner.compareAndSet(null, current) );
    }

    public void unlock() {
        Thread current = Thread.currentThread();
        if( current == owner.get() ) {
            if( count>0 ) {
                // 锁重入, 直接自减即可
                count--;
            } else {
                owner.set(null);
            }
        }
    }

    public static void main(String[] args) {
        SpinLockDemo spinLock = new SpinLockDemo();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
                }
            }
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
    }
}

运行结果如下所示:

Thread-1开始尝试获取自旋锁
Thread-0开始尝试获取自旋锁
Thread-1获取到了自旋锁
Thread-1释放了了自旋锁
Thread-0获取到了自旋锁
Thread-0释放了了自旋锁

缺点:

  1. 加锁线程需要不停自旋来检测锁的状态,明显浪费CPU,这也是自旋锁的通用弊端
  2. 当多个线程想要获取锁时,谁最先将设置owner谁便能取到锁,这可能会造成某些线程一直都未获取到锁造成线程饥饿,因此说该实现方式是一个非公平的自旋锁。

2. 公平自旋锁

如何实现公平的自旋锁呢?举个例子,假设银行只有一个窗口,一群人都想去去抢占这个窗口,

谁先抢到窗口谁就可以办理业务,如何保证公平性呢?那肯定是挂号叫号咯,所以我们可以给当

前线程分配一个排队号码,然后该线程开始自旋。直到被它叫到号才退出自旋,即它的排队号码

等于当前服务号码。

import java.util.concurrent.atomic.AtomicInteger;

public class SpinLock_Fair {
    /**
     * 当前持有锁的号码
     */
    private AtomicInteger serviceNum = new AtomicInteger(0);

    /**
     * 记录锁重入次数
     */
    private volatile int count = 0;

    /**
     * 排队号码
     */
    private AtomicInteger ticketNum = new AtomicInteger(0);

    /**
     * 各线程存放自己所申请的排队号码
     */
    private static ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();

    public void lock() {
        Integer num = threadLocalNum.get();
        if( num!=null && num==serviceNum.get() ) {
            // 当前线程已经持有锁, 则记录重入次数即可
            count++;
            return;
        }

        // 申请一个排队号码
        num = requestTicketNum();
        threadLocalNum.set( num );
        // 自旋等待, 直到该排队号码与serviceNum相等
        while ( num != this.serviceNum.get() );
    }

    public void unlock() {
        Integer num = threadLocalNum.get();
        if( num!=null && num==serviceNum.get() ) {
            if( count>0 ) {
                // 锁重入, 直接自减即可
                count--;
            } else {
                threadLocalNum.remove();
                // 自增serviceNum, 以便下一个排队号码的线程能够退出自旋
                serviceNum.set( num+1 );
            }
        }
    }

    /**
     * 申请一个排队号码
     */
    private int requestTicketNum() {
        return ticketNum.getAndIncrement();
    }

    public static void main(String[] args) {
        SpinLock_Fair spinLock = new SpinLock_Fair();
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
                spinLock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    spinLock.unlock();
                    System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
                }
            }
        };

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);
        Thread t4 = new Thread(runnable);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

执行结果如下:

Thread-0开始尝试获取自旋锁
Thread-1开始尝试获取自旋锁
Thread-2开始尝试获取自旋锁
Thread-3开始尝试获取自旋锁
Thread-0获取到了自旋锁
Thread-0释放了了自旋锁
Thread-1获取到了自旋锁
Thread-1释放了了自旋锁
Thread-2获取到了自旋锁
Thread-2释放了了自旋锁
Thread-3获取到了自旋锁
Thread-3释放了了自旋锁

 

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wclass-zhengge

你的鼓励是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值