Java中公平锁、递归锁、自旋锁、独占锁和共享锁的代码演示

1、公平锁和非公平锁

公平锁是指 多个线程按照申请锁的顺序来获取锁,根据先来后到的规则进行排队等候

非公平锁是指 多个线程获取锁的顺序并不是按照申请锁的先后顺序,有可能后申请锁的线程比先申请锁的线程优先获得锁 ,在高并发环境下,有可能造成优先级反转或者饥饿现象。

饥饿现象:长时间未获取到锁

举例:ReentrantLock 可以指定构造函数参数来创建公平锁或者非公平锁,默认是非公平锁。

//非公平锁
ReentrantLock lock = new ReentrantLock();

//公平锁
 ReentrantLock lock = new ReentrantLock(true);
 /**
     * Creates an instance of {@code ReentrantLock}.
     * This is equivalent to using {@code ReentrantLock(false)}.
     */
    public ReentrantLock() {
        sync = new NonfairSync();
    }

    /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

两者区别:

  • 公平锁 在并发环境中,每个线程在获取锁得锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
  • 非公平锁 不同的是,会一开始就尝试占有锁,如果尝试失败,再采用类似公平锁那种方法进行等待。synchronized 也是一种非公平锁。

非公平锁的优点是可以具有更大的吞吐量

2、可重入锁(递归锁)

可重入锁(递归锁)指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码

在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁;

也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块

例如在两个加锁的方法中,methodA方法在获得锁以后调用methodB会直接获得methodB的锁。

image-20210208162336434
ReentrantLock/synchronized  就是典型的可重入锁

可重入锁的最大作用是避免死锁。

验证可重入锁:

public class DemoClass {
    public synchronized void methodA() {
        System.out.println(Thread.currentThread().getName() + "线程 执行了methodA方法");
        methodB();
    }

    public synchronized void methodB() {
        System.out.println(Thread.currentThread().getName() + "线程执行了methodB方法");
    }
}
public class LockDemo2 {
    public static void main(String[] args) {
        DemoClass demo = new DemoClass();
        new Thread(()->{
            demo.methodA();
        },"thread-A").start();
        
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("**************");
        new Thread(()->{
            demo.methodA();
        },"thread-B").start();
    }
}

运行结果:

thread-A线程 执行了methodA方法
thread-A线程执行了methodB方法
**************
thread-B线程 执行了methodA方法
thread-B线程执行了methodB方法

从结果可以得出,一个同步方法可以进入自己代码块中的另外一个同步方法,像这样的代码也就是可重入锁(递归锁)。

3、自旋锁

自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去获取锁,这样做的好处是减少线程上下文切换的消耗,缺点是长时间未获取到锁导致的循环比较消耗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;
    }

Unsafe.getAndAddInt 方法就采用了自旋锁,通过 do...while() 循环来不断获取内存中的真实值var5。

3.1 手写一个自旋锁
/**
 * @ClassName: SpinLock
 * @Auther: 戏中人
 * @Description: 手写自旋锁
 */
public class SpinLock {

    //原子引用线程
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    //上锁
    public void lock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "线程尝试获取锁");
        while (!atomicReference.compareAndSet(null, thread)) {

        }
        System.out.println(Thread.currentThread().getName() + "线程成功获得锁");
    }

    //解锁
    public void unLock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "线程解锁了");
    }
}
/**
 * @ClassName: SpinLockDemo
 * @Auther: 戏中人
 * @Description: 手写自旋锁演示示例
 */
public class SpinLockDemo {
    public static void main(String[] args) {
        SpinLock spinLock = new SpinLock();

        new Thread(()->{
            spinLock.lock();
            System.out.println("thread-A线程处理业务开始...");
            //暂停5秒线程,模拟线程A处理业务
            try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println("thread-A线程处理业务结束,花费5秒");
            spinLock.unLock();
        },"thread-A").start();

        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(()->{
            spinLock.lock();
            System.out.println("thread-B线程处理业务结束");
            spinLock.unLock();
        },"thread-B").start();

    }
}

运行结果:

image-20210209193442736

从结果分析可以得出,线程A在获得锁之后,线程B是无法再获得锁的, 因此线程B只能不断通过while循环尝试获得锁,直到线程A解锁后才跳出循环。在这个过程中,线程B并没有阻塞,而是不断循环尝试获得锁,像这样的设计就是自旋锁的体现。

4、独占锁、共享锁、互斥锁

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

共享锁:指该锁可被多个线程所持有。

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

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

下面将通过一个仿缓存功能对ReentrantReadWriteLock 的功能进行演示:

/**
 * @ClassName: MyCache
 * @Auther: 戏中人
 * @Description: 手写一个仿缓存类
 */
public class MyCache {

    private volatile Map<String, Object> map = new HashMap<>();
    
    //模拟向缓存写入数据(未加锁)
    public void put(String key,Object value) {
        System.out.println(Thread.currentThread().getName() + "线程正在写入...");
        //暂停线程1秒,模拟写入
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
        map.put(key, value);
        System.out.println(Thread.currentThread().getName() + "线程写入完成!!!");
    }

    //模拟读取缓存中的数据(未加锁)
    public void get(String key) {
        System.out.println(Thread.currentThread().getName() + "线程正在读取数据...");
        //模拟读取数据
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
        Object value = map.get(key);
        System.out.println(Thread.currentThread().getName() + "线程已读取到数据-" + value);
    }

    //模拟清除缓存
    public void clear() {
        map.clear();
    }
}
/**
 * @ClassName: ReadWriteLockDemo
 * @Auther: 戏中人
 * @Description: 读写锁示例
 */
public class ReadWriteLockDemo {
    public static void main(String[] args) {
        MyCache cache = new MyCache();
        for (int i = 0; i < 5; i++) {
             String temp = i + "";
            new Thread(()->{
                cache.put(Thread.currentThread().getName() + temp,Thread.currentThread().getName());
            },"thread-"+i).start();
        }

        for (int i = 0; i < 5; i++) {
            String temp = i + "";
            new Thread(()->{
                cache.get(Thread.currentThread().getName() + temp);
            },"thread-" + i).start();
        }
    }
}

第一次运行未加锁的缓存,运行结果如下:

thread-0线程正在写入...
thread-1线程正在写入...
thread-2线程正在写入...
thread-3线程正在写入...
thread-4线程正在写入...
thread-0线程正在读取数据...
thread-1线程正在读取数据...
thread-2线程正在读取数据...
thread-3线程正在读取数据...
thread-4线程正在读取数据...
thread-1线程写入完成!!!
thread-0线程写入完成!!!
thread-4线程写入完成!!!
thread-3线程写入完成!!!
thread-2线程写入完成!!!
thread-1线程已读取到数据-thread-1
thread-4线程已读取到数据-thread-4
thread-3线程已读取到数据-null
thread-0线程已读取到数据-thread-0
thread-2线程已读取到数据-thread-2

从结果中可以看出,往缓存类中写入数据的线程被其他线程加塞了,出现了多个线程同时写,并且在未写入完成之前被其他线程打断进行读取等情况。出现这种情况显然是不符合我们的要求 ,即要求向缓存写入数据时只能同时有一个线程进行写入,并且在写入过程中不能被其他线程所中断。

因此现在对 MyCache 类的 putget 方法进行分别添加ReentrantReadWriteLock中的writeLock()写锁readLock()读锁,改进后的代码如下:

public class MyCache {

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

    //模拟向缓存写入数据
    public void put(String key,Object value) {
        //加写锁
        rwLock.writeLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程正在写入...");
            //暂停线程1秒,模拟写入
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            map.put(key, value);
            System.out.println(Thread.currentThread().getName() + "线程写入完成!!!");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    //模拟读取缓存中的数据
    public void get(String key) {
        //加读锁
        rwLock.readLock().lock();
        try {
            System.out.println(Thread.currentThread().getName() + "线程正在读取数据...");
            //模拟读取数据
            try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }
            Object value = map.get(key);
            System.out.println(Thread.currentThread().getName() + "线程已读取到数据-" + value);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rwLock.readLock().unlock();
        }
    }

    //模拟清除缓存
    public void clear() {
        map.clear();
    }
}

再次运行查看结果:

thread-0线程正在写入...
thread-0线程写入完成!!!
thread-1线程正在写入...
thread-1线程写入完成!!!
thread-2线程正在写入...
thread-2线程写入完成!!!
thread-3线程正在写入...
thread-3线程写入完成!!!
thread-4线程正在写入...
thread-4线程写入完成!!!
thread-0线程正在读取数据...
thread-1线程正在读取数据...
thread-2线程正在读取数据...
thread-4线程正在读取数据...
thread-3线程正在读取数据...
thread-3线程已读取到数据-thread-3
thread-0线程已读取到数据-thread-0
thread-2线程已读取到数据-thread-2
thread-1线程已读取到数据-thread-1
thread-4线程已读取到数据-thread-4

这次的结果就是我们想要实现的,当一个线程在写入数据时,不能被其他线程所打断或者加塞,也就是只能同时有一个线程进行写入;当没有线程进行写入数据时,允许多个线程并发读取数据。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 互斥 互斥用于保护共享资源,确保同一时刻只有一个线程能够访问该资源。实例代码如下: ```C++ #include <mutex> #include <thread> #include <iostream> std::mutex mtx; void print_func(int num) { mtx.lock(); std::cout << "Thread " << num << " is printing." << std::endl; mtx.unlock(); } int main() { std::thread t1(print_func, 1); std::thread t2(print_func, 2); t1.join(); t2.join(); return 0; } ``` 2. 读写 读写用于在读多写少的场景提高效率,允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。实例代码如下: ```C++ #include <iostream> #include <thread> #include <shared_mutex> std::shared_mutex rw_mutex; int shared_data = 0; void read_func(int num) { rw_mutex.lock_shared(); std::cout << "Thread " << num << " read shared_data: " << shared_data << std::endl; rw_mutex.unlock_shared(); } void write_func(int num) { rw_mutex.lock(); shared_data = num; std::cout << "Thread " << num << " wrote shared_data: " << shared_data << std::endl; rw_mutex.unlock(); } int main() { std::thread t1(write_func, 1); std::thread t2(read_func, 2); std::thread t3(read_func, 3); t1.join(); t2.join(); t3.join(); return 0; } ``` 3. 自旋锁 自旋锁适用于短时间内需要等待的场景,可以减少线程切换带来的开销。实例代码如下: ```C++ #include <iostream> #include <thread> #include <atomic> std::atomic_flag spin_lock = ATOMIC_FLAG_INIT; int shared_data = 0; void add_func(int num) { while (spin_lock.test_and_set(std::memory_order_acquire)); shared_data += num; spin_lock.clear(std::memory_order_release); } int main() { std::thread t1(add_func, 1); std::thread t2(add_func, 2); t1.join(); t2.join(); std::cout << "shared_data: " << shared_data << std::endl; return 0; } ``` 4. 递归 递归允许同一线程多次获取,避免死。实例代码如下: ```C++ #include <iostream> #include <thread> #include <mutex> std::recursive_mutex mtx; int shared_data = 0; void add_func(int num) { mtx.lock(); shared_data += num; if (num > 1) { add_func(num - 1); } mtx.unlock(); } int main() { std::thread t1(add_func, 3); t1.join(); std::cout << "shared_data: " << shared_data << std::endl; return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值