Java线程锁

本文详细介绍了锁在并发编程中的作用,区分了不同类型的锁(如公平锁、非公平锁、悲观锁、乐观锁等),着重讨论了synchronized和Lock接口的特性,并以ReentrantLock和读写锁为例探讨了多线程环境下的一写多读场景。
摘要由CSDN通过智能技术生成

1. 锁概述

1.1 并发中的锁

        在计算机科学中,锁(Lock)是一种同步原语:一种在有许多执行线程时强制限制对资源的访问的机制。锁旨在强制执行互斥并发控制策略,但是在控制的细节上会有很多差异,这种差异称为锁的特性。

        根据锁的特性,可以将锁分为很多不同的类型,例如:公平锁/非公平锁、悲观锁/乐观锁、可重入锁/不可重入锁、共享锁/排他锁等,如下图所示。

        这里需要注意,由于一个锁可能同时具备多个特性,可能被同时划入不同的类型。

1.2 从synchronized看锁的分类

        可以从不同的维度来看synchronized锁的特性:

  • 多个线程在竞争synchronized的锁时,不会按照先来后到的顺序获取锁,因此synchronized属于非公平锁
  • synchronized会锁住同步资源,因此synchronized属于悲观锁
  • 一个线程在程序中可以重复的获取synchronized的锁,例如前面一些案例中使用的嵌套的synchronized代码块,因此synchronized属于可重入锁
  • 多个线程不能同时持有synchronized的锁,因此synchronized属于排他锁

2. Lock接口及实现

2.1 Lock接口

        Lock接口是Java 5版本在并发工具包中新增的内容,主要用来实现锁功能,其实现类提供了各种类型的锁,应用场景非常丰富。

        Lock接口提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字不具备的同步特性。

        Lock的使用方式非常简单:

Lock lock = new XXXLock();

lock.lock(); // 获取锁

lock.unlock(); // 释放锁

        Lock接口提供的并且synchronized关键字所不具备的主要特性如下:

  • 尝试非阻塞地获取锁:当前线程尝试获取锁,如果这一时刻锁没有被其他线程获取到,则成功获取并持有锁
  • 能被中断地获取锁:获取到锁的线程能够响应中断,当获取到锁的线程被中断时,中断异常将会被抛出,同时锁会被释放
  • 超时获取锁:在指定的截止时间之前获取锁,如果截止时间到了仍旧无法获取锁,则返回

        Lock接口的常用方法如下:

  • lock():获取锁,调用该方法的当前线程会尝试获取锁,获得锁后,从该方法返回
  • lockInterruptibly():可中断地获取锁,该方法会响应中断,即在锁的获取中可以中断当前线程
  • tryLock():尝试非阻塞的获取锁,调用该方法后立刻返回,如果能否获取则返回true,否则返回false
  • tryLock(long time, TimeUnit unit):超时的获取锁,超时时间结束,返回false
  • unlock():释放锁
public class LockDemo1 {
    public static void main(String[] args) {
        MyRun2 run2 = new MyRun2();
        ExecutorService service = Executors.newFixedThreadPool(4);
        for(int i=1; i<=4; i++){
            service.execute(run2);
        }
        service.shutdown();
    }
}
class MyRun2 implements Runnable {
    int i = 0;
    Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            lock.lock();
            String name = Thread.currentThread().getName();
            try {
                if (i > 50) {
                    break;
                }
                System.out.println(name + " " + i);
                i++;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

2.2 ReentrantLock

        ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁,如下图所示:

        同时,该锁还支持获取锁时的公平和非公平性选择:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class FairLockDemo1 {
    public static void main(String[] args) throws InterruptedException {
        boolean fairFlag = true; // 是否使用公平锁
        // 基于fairFlag创建公平锁或非公平锁
        Lock lock = new ReentrantLock(fairFlag);
        MyRun3 run3 =new MyRun3(lock, fairFlag? "fair": "nonFair");
        ExecutorService service = Executors.newFixedThreadPool(8);
        for(int i=1;i<=16;i++){
            service.execute(run3);
            TimeUnit.MILLISECONDS.sleep(10);
        }
        service.shutdown();
    }
}
class MyRun3 implements Runnable {
    Lock lock;
    String label;
    public MyRun3(Lock lock, String label){
        this.lock = lock;
        this.label = label;
    }
    @Override
    public void run() {
        lock.lock();
        String name = Thread.currentThread().getName();
        System.out.println(label + ": "
                + "name: " + name + "获取到锁");
        try {
            TimeUnit.MILLISECONDS.sleep(100); // 休眠1秒
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        lock.unlock();
    }
}

2.3 一写多读场景

        一写多读,顾名思义,是指对数据的并发读取频率较高,而并发写入频率较低的场景,是现实中非常普遍的一种并发场景。

        例如,某电影网站的电影实时票房量:

        在这个案例中,实时票房量是共享资源,被多个线程并发的访问,其中包含读线程,也包含写线程。为了防止出现线程安全问题,需要对共享资源进行加锁。

2.4 读写锁

        但是目前讲到的锁都是排他锁:即同一时间仅能有一个线程获取锁。在一写多读场景中,读线程之前也会阻塞,同一时间仅能有一个线程读取票房数据,这大大降低了读的效率:

        在一些多读场景中,读线程和写线程应该互斥,写线程和写线程之间应该互斥,但是读线程和读线程之间可以不互斥。行业中通常使用“读写锁“的方案来实现上述需求。

        读写锁维护了一对锁:一个读锁和一个写锁。通过分离读锁和写锁,使得并发性相比一般的排他锁有很大的提升。

2.5 ReadWriteLock

        Java中使用ReadWriteLock接口作为读写锁的父接口,该接口定义了两个方法:

  • Lock readLock():返回该读写锁对中的读锁对象
  • Lock writeLock():返回该读写锁对中的写锁对象

        ReentrantReadWriteLock作为ReadWriteLock接口的实现类,除了实现了上述的2个方法外,还提供了一些便于外界监控器内部工作状态的方法:

  • getReadLockCount():返回当前读锁被获取的次数,该次数不等于获取读锁的线程数
  • getReadHoldCount():返回当前线程获取读锁的次数
  • isWriteLocked():判断写锁是否被获取
  • getWriteHoldCount():返回当前写锁被获取的次数
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        StudentInfoManagementSystem system = new StudentInfoManagementSystem();
        Runnable readTask = () -> {
            for(int i = 0; i <5 ;i++){
                system.getStudents();
            }
        };
        Runnable writeTask = () -> {
            system.addStudent("Tom");
        };
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(10);
        // 创建多个读任务读取学生信息
        for (int i = 0; i < 10; i++) {
            if (i % 4 ==0){
                executor.execute(writeTask);
            } else {
                executor.execute(readTask);
            }
        }
        // 关闭线程池
        executor.shutdown();
    }
}

class StudentInfoManagementSystem {
    private volatile List<String> studentList = new ArrayList<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public void addStudent(String studentName) {
        lock.writeLock().lock();
        try {
            // 写入学生信息
            studentList.add(studentName);
            System.out.println("Added student: " + studentName);
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void getStudents() {
        lock.readLock().lock();
        try {
            System.out.println("Read students: " + studentList);
        } finally {
            lock.readLock().unlock();
        }
    }
}

3. 总结

        1. 在计算机科学中,锁(Lock)是一种同步原语:一种在有许多执行线程时强制限制对资源的访问的机制

  • 公平锁/非公平锁
  • 悲观锁/乐观锁
  • 可重入锁/不可重入锁
  • 共享锁/排他锁

        2. Lock 接口是Java 5版本在并发工具包中新增的内容,主要用来实现锁功能,其实现类提供了各种类型的锁,应用场景非常丰富

  • ReentrantLock
  • ReadWriteLock
  • 28
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

zhangyan_1010

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

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

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

打赏作者

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

抵扣说明:

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

余额充值