编程技术分享平台

网站首页 > 技术教程 正文

锁分类详解(锁分为哪几类)

xnh888 2024-10-30 04:43:43 技术教程 23 ℃ 0 评论

是否可重入

可重入锁

可重入锁就是当前线程已经获取到了锁后,再次获取这个锁的时候,无需再次加锁,可以直接获取到这个锁资源(通过重入次数实现)

不可重入锁

当锁资源已被抢占后,即使相同的线程,在此获取锁的时候,也无法获取锁资源

我们在业务中使用的锁基本都是可重入锁,像synchronized,ReentrantLock都是可重入锁,那么锁为什么需要是可重入的呢,我们来看一个简单的例子

public class ReentryTest {


    public static void main(String[] args) {
        testA();
    }

    public static synchronized void testA() {
        System.out.println("execute method testA");
        testB();
    }

    public static synchronized void testB() {
        System.out.println("execute method testB");
    }
}

如果synchronized不是可重入的会发生什么,执行方法testA,获取了ReentryTest.class对象的锁,这个时候调用方法testB,此时,testB也会获取ReentryTest.class的锁,这个时候就会发生死锁,因此必须是可重入的,否则,获得锁的线程,再次获取当前锁的时候,就会发生死锁


是否公平

公平锁

当有线程正在排队获取锁的时候,这个时候,新加入竞争锁的线程,会直接排队获取锁,保证先到的线程优先获取到锁

非公平锁

无论当前是否有线程在排队获取锁,新加入竞争锁的线程,都会去抢占锁,当锁获取失败后,才会排队获取锁

我们来看一下的公平锁和非公平锁的实现方式

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

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 有线程正在排队获取锁,则不竞争锁,而是直接获取锁失败,加入同步队列进行排队
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}


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);
    }
}

我们都知道,非公平锁比公平锁的性能更高,这是什么原因呢,公平锁,当有线程排队时,后续的线程都会调用操作系统底层的方法阻塞线程,在获取锁的时候,又需要调用操作系统的方法去唤醒线程,这些方法的性能都是比较低的,而非公平锁,通过抢占的模式,知道获取到了锁,就可以不用阻塞和唤醒线程,可以极大的提升性能,当然,非公平锁也会造成锁饥饿的问题,如果锁一直被抢占,就会造成之前排队的线程一直无法去抢占锁,造成锁饥饿

自旋锁

我们都听说在,在JDK1.6的时候,对synchronized进行了一系列的优化,其中包含偏向锁,轻量级锁,线程在获取轻量级锁的时候,并不是直接cas一次就去排队,而是会通过循环的形式去重复一定次数去获取锁,那么为什么需要自旋呢,这难道不消耗性能么,为了尽可能的提升性能,在很多时候,我们资源一定的次数,便可以获取到锁,而不需要将线程挂起,同时,控制自旋的次数,便可以防止CPU空执行的次数过多。

乐观锁

在大部分时候,数据并不会发生冲突,同时,通过重试还可以解决这种冲突,我们便可以使用乐观锁,通过不断地重试来解决冲突

读写锁

读读写锁就是包含读锁和写锁这两把锁,读锁和读锁之前不互斥,读锁和写锁,写锁和写锁之前互斥,通过这种形式,提升了读的并发性能,我们来看一下读写锁的使用方式

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();

    private Map<Long, Student> studentMap = new HashMap<>();

    public Student getStudent(Long id) {
        readLock.lock();
        try {
            Student student = studentMap.get(id);
            return student;
        } finally {
            readLock.unlock();
        }
    }

    @Scheduled(cron = "0 5 * * * ?")
    public void initCache() {
        writeLock.lock();
        try {
            List<Student> studentList = studentDao.findAll();
            Map<Long, Student> oldMap = studentMap;
            Map<Long, Student> map = studentList.stream().collect(Collectors.toMap(Student::getId, Function.identity()));
            studentMap = map;
            oldMap.clear();
        } finally {
            writeLock.unlock();
        }
    }
}

ReentrantReadWriteLock虽然提升了读的并发性能,但是读会阻塞写,导致写出现问题,因此,这种锁也会带来写饥饿的问题,我们来看一下一种更高性能的读写锁

@Service
public class StudentService {

    @Autowired
    private StudentDao studentDao;

    private StampedLock stampedLock = new StampedLock();


    private Map<Long, Student> studentMap = new HashMap<>();

    public Student getStudent(Long id) {
        // 使用乐观读锁
        long stamp = stampedLock.tryOptimisticRead();
        try {
            Student student = studentMap.get(id);
            // 读写锁出现冲突
            if (!stampedLock.validate(stamp)) {
                // 使用读锁,会阻塞写
                stamp = stampedLock.readLock();
                student = studentMap.get(id);
            }
            return student;
        } finally {
            stampedLock.unlockRead(stamp);
        }
    }

    @Scheduled(cron = "0 5 * * * ?")
    public void initCache() {
        long stamp = stampedLock.writeLock();
        try {
            List<Student> studentList = studentDao.findAll();
            Map<Long, Student> oldMap = studentMap;
            Map<Long, Student> map = studentList.stream().collect(Collectors.toMap(Student::getId, Function.identity()));
            studentMap = map;
            oldMap.clear();
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }
}

通过代码,我们发现StampedLock在读数据时,只会使用乐观锁,当发现冲突的时候,才会使用读锁,以此来解决读写冲突的问题,也就是说,在没有发生冲突的时候,读并不会阻塞写,通过这种形式,提升了读写的并发能力。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表