Java锁基本使用及AQS

synchronized可以解决Java的并发问题,为什么jdk1.5之后还要推出Java并发包并提供多种锁呢?

1、synchronized与Lock的区别

虽然synchronized和Lock都能够实现同步功能,但是两者之间还是有一定区别的。

  1. synchronized隐式获取和释放锁,Lock需显示的获取和释放锁,具有更高的灵活性,但是如果不释放锁,容易造成死锁问题;
  2. synchronized如果获取不到锁,会阻塞线程,但是Lock提供非阻塞api,可以立即返回结果;
  3. synchronized是java提供的同步关键字,但是Lock接口下面的都是实现类,是Java类;
  4. synchronized不可中断,lock.lockInterruptibly()可以响应中断;
  5. synchronized是非公平锁,但是Lock可以提供公平及非公平两种;

2、Lock基本使用

Java中对锁也提供了不同的实现类,使用的最多的有ReentrantLock和ReentrantReadWriteLock,下面分别介绍重入锁和读写锁。

2.1、ReentrantLock

重入锁ReentrantLock,顾名思义,就是支持重新进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁的还支持获取锁时的公平和非公平性选择。

使用ReentrantLock进行n次lock之后,同样需要n次unlock(),否则其他的线程就无法拿到锁,从而造成死锁。

/**
     * 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();
    }

通过构造函数我们可以看到,直接传入boolean即可构造出公平锁和非公平锁。公平锁使用FIFO原则,在等待队列中等待时间最长的线程会最先抢到锁。但是公平锁的效率没有非公平锁的效率高,因为既然要保证严格的FIFO,那么就会出现频繁的线程上下文切换,在cpu进行现场上下文切换的时候,会耗费大量的cpu资源。对于非公平锁来说,允许同一个线程多次拿到锁,或者不是严格的遵循FIFO,所以线程的上下文切换相对于公平锁而言并不会那么频繁。

下面通过一个例子我们来了解下重入锁的基本使用:

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription: 可重入锁
 * @Author: yangchenhui
 */
public class ReentrantLockDemo1 {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {

        lock.lock();  // block until condition holds
        try {
            System.out.println("第一次获取锁");
            System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
            lock.lock();
            System.out.println("第二次获取锁了");
            System.out.println("当前线程获取锁的次数" + lock.getHoldCount());
        } finally {
            lock.unlock();
            lock.unlock();
        }
        System.out.println("当前线程获取锁的次数" + lock.getHoldCount());

        // 如果不释放,此时其他线程是拿不到锁的
        new Thread(() -> {
            System.out.println(Thread.currentThread() + " 期望抢到锁");
            lock.lock();
            System.out.println(Thread.currentThread() + " 线程拿到了锁");
        }).start();

    }

}

重入锁可以响应线程中断,下面通过一个代码示例来验证:

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription: 重入锁响应中断示例
 * @Author: yangchenhui
 */
public class ReentrantLockDemo2 {

    private Lock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo2 demo2 = new ReentrantLockDemo2();
        Runnable runnable = () -> {
            try {
                demo2.test(Thread.currentThread());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        // 等待0.5秒,让thread1先执行
        Thread.sleep(500);

        thread2.start();
        // 两秒后,中断thread2
        Thread.sleep(2000);
        // 如果不使用lock.lockInterruptibly(),此时中断线程是,不会响应,获取到锁的线程会继续执行
        thread2.interrupt();
    }

    public void test(Thread thread) throws InterruptedException {
        System.out.println(Thread.currentThread().getName() + ", 想获取锁");
        //注意,如果需要正确中断等待锁的线程,必须将获取锁放在外面,然后将InterruptedException抛出
//        lock.lock();
        lock.lockInterruptibly();
        try {
            System.out.println(thread.getName() + "得到了锁");
            // 抢到锁,10秒不释放
            Thread.sleep(10000);
        } finally {
            System.out.println(Thread.currentThread().getName() + "执行finally");
            lock.unlock();
            System.out.println(thread.getName() + "释放了锁");
        }
    }

}

执行结果如下:

响应中断后,Thread-1后面的代码没有执行,如果使用lock.lock()或者使用同步关键之synchronized就不能够得到这种结果。

使用tryLock()如果能够获取到锁,会返回true,如果不能获取到锁,会立即返回false。

package com.xiaohuihui.lock.reentrantlock;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Desription:
 * @Author: yangchenhui
 */
public class ReentrantLockDemo3 {

    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            if (lock.tryLock()) {
                System.out.println("当前线程为" + Thread.currentThread().getName() + "获取到锁");
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            lock.unlock();
            System.out.println("当前线程为" + Thread.currentThread().getName() + "释放锁");
        };
        Thread thread1 = new Thread(runnable);
        thread1.start();
    }

}

2.1、ReentrantReadWriteLock

synchronized和ReentrantLock都是属于排他锁,在同一时间内,只允许有一个线程获取到锁,但是ReentrantReadWriteLock是共享锁,在同一时间内,允许多个线程获取到读锁。

为了避免出现脏读的情况,ReentrantReadWriteLock提供锁降级机制:获取写锁 --> 获取读锁 --> 释放写锁。

读写锁是一对互斥锁,如果当前有线程持有读锁,那么所有线程(包含当前获取到唯一读锁的线程)不能够获取写锁。如果当前线程获取到写锁,可以再次获取写锁或者读锁。正是基于这样的机制,才保证了读写锁的锁降级机制能够有效的避免脏读。

下面使用一个示例了解读写锁的基本使用:

package com.xiaohuihui.lock.readwritelock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @Desription: 读写锁示例
 * @Author: yangchenhui
 */
public class ReentrantReadWriteLockDemo1 {

    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    public static void main(String[] args) {
        final ReentrantReadWriteLockDemo1 readWriteLock = new ReentrantReadWriteLockDemo1();
        // 多线程同时读/写
        new Thread(() -> {
            readWriteLock.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLock.read(Thread.currentThread());
        }).start();

        new Thread(() -> {
            readWriteLock.write(Thread.currentThread());
        }).start();
    }

    /**
     * 多线程读,共享锁
     * @param thread
     */
    public void read(Thread thread) {
        readWriteLock.readLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在进行“读”操作");
            }
            System.out.println(thread.getName() + "“读”操作完毕");
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    /**
     * 写
     */
    public void write(Thread thread) {
        readWriteLock.writeLock().lock();
        try {
            long start = System.currentTimeMillis();
            while (System.currentTimeMillis() - start <= 1) {
                System.out.println(thread.getName() + "正在进行“写”操作");
            }
            System.out.println(thread.getName() + "“写”操作完毕");
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }

}

在AQS中,读锁主要操作state,可以通过加减来控制当前获取到读锁的线程数,但是写锁操作的是owner,如果state=0,并且owner=null,说明当前没有线程获取到读锁,并且也没有线程获取到写锁,使用CAS机制修改owner,如果修改成功,说明加锁成功。

 

3、AQS机制

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect, int update))来进行操作,能够保证状态的改变是安全的。

如果我们要设计一个同步框架,首先我们需要一个状态位用来表示当前锁的状态,其次参与锁争抢的线程,如果没有获取到锁,需要将其放到一个等待队列当中,同时为了保证可重入性,我们需要一个字段来存储当前获得锁的线程。遵循这个思路,我们去看看AQS中的类图。

在ReentrantLock当中,有Sync内部类来继承AQS,使用父类提供的方法线程修改同步器中的锁状态。

AbstractQueuedSynchronizer中使用Node节点来存储等待线程集合,从源码中可以看到是一个双向链表的数据结构。

AbstractOwnableSynchronizer中使用exclusiveOwnerThread存储独占锁的锁拥有者。

当我们调用lock.lock()时,会调用同步器中的compareAndSetState(int expect, int update)方法,使用unsafe操作state状态,如果操作成功,设置线程拥有者。

stateOffset在AbstractQueuedSynchronizer的静态代码块中进行初始化操作,state是一个volatile变量,保持线程可见性。

总体来说,AQS同步框架使用volatile + Unsafe的cas操作保持state的同步操作,同时使用LockSupport的park(),unpark()机制及Node链表来进行等待线程队列的添加和通知。

AQS提供模版方法和勾子函数,提供了一个扩展性很强的基础框架用于同步工具的构建,这个设计模式值得我们学习。

unlock()的源代码跟lock()差不多,大家有时间的可以跟一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值