Java多线程,对锁机制的进一步分析

点击上方 "程序员小乐"关注, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

A lot of people would rather stay single because they're tired giving their everything and end up with nothing.

人们宁愿选择单身是因为,他们厌倦了付出太多而得到太少。

每日掏心

时间在变,人也在变。生命那就是一团欲望,欲望不满足便痛苦。期望愈大,失望也便是愈大。

来自:hsm_computer | 责编:乐乐

链接:cnblogs.com/JavaArchitect/p/12251778.html

程序员小乐(ID:study_tech)第 936 次推文  图源:百度

往日回顾:蚂蚁金服上市估值1.4万亿,阿里整栋楼沸腾,员工能分多少钱?

     

   正文   

1 可重入锁

可重入锁,也叫递归锁。它有两层含义,第一,当一个线程在外层函数得到可重入锁后,能直接递归地调用该函数,第二,同一线程在外层函数获得可重入锁后,内层函数可以直接获取该锁对应其它代码的控制权。之前我们提到的 synchronized 和 ReentrantLock 都是可重入锁。

通过 ReEnterSyncDemo.java,我们来演示下 synchronized 关键字的可重入性。 

  

class SyncReEnter implements Runnable{
   public synchronized void get(){
     System.out.print(Thread.currentThread().getId() + "\t");
      //在get方法里调用set
      set();
    }
    public synchronized void set()
    {System.out.print(Thread.currentThread().getId()+"\t"); }
    public void run() //run方法里调用了get方法
    { get();}
}
public class ReEnterSyncDemo {
    public static void main(String[] args) {
        SyncReEnter demo=new SyncReEnter();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

第1行里,我们是让 syncReEnter 类通过实现 Runnable 的方式来实现多线程,在其中第2和第7行所定义的 get 和 set 方法均带有 synchronized 关键字。在第9行定义的 run 方法里,我们调用了 get 方法。在 main 函数的第15和16行里,我们启动了2次线程,这段代码的输出如下。

8   8   9   9

第15行第一次启动线程时,在 run 方法里,会调用包含 synchronized 关键字的 get 方法,这时这个线程会得到 get 方法的锁。当执行到 get 里的 set 方法时,由于 set 方法也包含 synchronized 关键字,而且 set 是包含在 get 里的,所以这里无需再次申请 set 的锁,能继续执行。所以通过输出,大家能看到 get 和 set 的打印语句是连续输出的。同理我们能理解第16行第二次启动线程的输出。关注公众号程序员小乐回复关键字“offer”获取算法面试题和答案。

通过 ReEnterLock.java,我们来演示下 ReentrantLock 的可重入性。      

import java.util.concurrent.locks.ReentrantLock;
class LockReEnter implements Runnable {
    ReentrantLock lock = new ReentrantLock();
    public void get() {
      lock.lock();
      System.out.print(Thread.currentThread().getId()+"\t");
      // 在get方法里调用set
      set();
      lock.unlock();
   }
   public void set() {
    lock.lock();
    System.out.print(Thread.currentThread().getId() + "\t");
    lock.unlock();
   }
   public void run()
   { get(); }
}
public class ReEnterLock {
    public static void main(String[] args) {
        LockReEnter demo = new LockReEnter();
        new Thread(demo).start();
        new Thread(demo).start();
    }
}

第2行创建的 LockReEnter 类里,我们同样包含了 get 和 set 方法,并在 get 方法里调用了 set 方法。只不过在 get 和 set 方法里,我们不是用synchronized,而是用第3行定义的 ReentrantLock 类型的 lock 对象来管理多线程的并发,在第16行的 run 方法里,我们同样地调用了 get 方法。

在 main 函数里,我们同样地在第22和23行里启动了两次线程,这段代码的运行结果如下。

    8   8   9   9

当在第22行里第一次启动 LockReEnter 类型的线程后,在调用 get 方法时,能得到第5行的锁对象,get 方法会调用 set 方法,虽然 set 方法里的第12行会再次申请锁。但由于 LockReEnter 线程在 get 方法里已经得到了锁,所以在 set 方法里也能得到锁,所以第一次运行时,get 和 set 方法会一起执行。同样地,在第23行第二次其中线程时,也会同时打印 get 和 set 方法里的输出。

在项目的一些场景里,一个线程有可能需要多次进入被锁关联的方法,比如某数据库的操作的线程需要多次调用被锁管理的“获取数据库连接”的方法。这时,如果使用可重入锁就能避免死锁的问题。相反,如果我们不是用可重入锁,那么在第二次调用“获取数据库连接”方法时,就有可能被锁住,从而导致死锁问题。

2 公平锁和非公平锁

在创建 Semaphore 对象时,我们可以通过第2个参数,来指定该 Semaphore 对象是否以公平锁的方式来调度资源。

公平锁会维护一个等待队列,多个在阻塞状态等待的线程会被插入到这个等待队列。在调度时是按它们所发请求的时间顺序获取锁,而对于非公平锁,当一个线程请求非公平锁时,如果此时该锁变成可用状态,那么这个线程会跳过等待队列中所有的等待线程而获得锁。

我们在创建可重入锁时,也可以通过调用带布尔类型参数的构造函数来指定该锁是否是公平锁。ReentrantLock(boolean fair)。

在项目里,如果请求锁的平均时间间隔较长,建议使用公平锁,反之建议使用非公平锁。比如有个服务窗口,如果采用非公平锁的方式,当窗口空闲时,不是让下一号来,而是只要来人就服务,这样能缩短窗口的空闲等待时间,从而提升单位时间内的服务数量(也就是吞吐量)。相反,如果这是个比较冷门的服务窗口,在很多时间里来请求服务的频次并不高,比如一小时才来一个人,那么就可以选用公平锁了。或者,如果要缩短用户的平均等待时间,那么可以选用公平锁,这样就能避免“早到的请求晚处理“的情况。

3 读写锁

之前我们通过 synchronized 和 ReentrantLock 来管理临界资源时,只要是一个线程得到锁,其它线程不能操作这个临界资源,这种锁可以叫做“互斥锁”。

和这种管理方式相比,ReentrantReadWriteLock 对象会使用两把锁来管理临界资源,一个是“读锁“,另一个是“写锁“。

如果一个线程获得了某资源上的“读锁“,那么其它对该资源执行“读操作“的线程还是可以继续获得该锁,也就是说,“读操作“可以并发执行,但执行“写操作“的线程会被阻塞。如果一个线程获得了某资源的“写锁“,那么其它任何企图获得该资源“读锁“和“写锁“的线程都将被阻塞。

和互斥锁相比,读写锁在保证并发时数据准确性的同时,允许多个线程同时“读“某资源,从而能提升效率。通过下面的 ReadWriteLockDemo.java,我们来观察下通过读写锁管理读写并发线程的方式。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReadWriteTool {
    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();
    private int num = 0;
    public void read() {//读的方法
        int cnt = 0;
        while (cnt++ < 3) {
            try {
                readLock.lock();                System.out.println(Thread.currentThread().getId()
                        + " start to read");
                Thread.sleep(1000);    
    System.out.println(Thread.currentThread().getId() + " reading," + num);
            } catch (Exception e)
            { e.printStackTrace();}
            finally { readLock.unlock();    }
        }
    }
    public void write() {//写的方法
        int cnt = 0;
        while (cnt++ < 3) {
            try {
                writeLock.lock();      
        System.out.println(Thread.currentThread().getId()
                        + " start to write");
                Thread.sleep(1000);
                num = (int) (Math.random() * 10);
            System.out.println(Thread.currentThread().getId() + " write," + num);
            } catch (Exception e)
            { e.printStackTrace();}
            finally { writeLock.unlock();}
        }
    }
}

第3行定义的 ReadWriteTool 类里,我们在第4行创建了一个读写锁,并在第5和第6行,分别通过这个读写锁的 readLock 和 writeLock 方法,分别创建了读锁和写锁。关注公众号程序员小乐回复关键字“Java”获取大厂面试题和答案。

第8行的 read 方法里,我们是先通过第12行的代码加“读锁“,随后在第15行进行读操作。在第21行的 write 方法里,我们是先通过第25行的代码加“写锁”,随后在第30行进行写操作。

class ReadThread extends Thread {
    private ReadWriteTool readTool;
    public ReadThread(ReadWriteTool readTool)
{ this.readTool = readTool; }
        public void run()
{ readTool.read();}
}
class WriteThread extends Thread {
    private ReadWriteTool writeTool;
    public WriteThread(ReadWriteTool writeTool)
{ this.writeTool = writeTool; }
    public void run()
{ writeTool.write();    }
}

第37行和第44行里,我们分别定义了读和写这两个线程,在 ReadThread 线程的 run 方法里,我们调用了 ReadWriteTool 类的 read 方法,而在 WriteThread 线程的 run 方法里,则调用了 write 方法。

public class ReadWriteLockDemo {
    public static void main(String[] args) {
        ReadWriteTool tool = new ReadWriteTool();
        for (int i = 0; i < 3; i++) {
            new ReadThread(tool).start();
            new WriteThread(tool).start();
        }
    }
}

在 main 函数第53行,我们创建了一个 ReadWriteTool 类型的 tool 对象。在第55和56行初始化读写线程时,我们传入了该 tool 对象,也就是说,通过54行 for 循环创建并启动的多个读写线程是通过同一个读写锁来控制读写并发操作的。

出于多线程并发调度的原因,我们每次运行都可能得到不同的结果,但从这些不同的结果里,我们都態明显地看出读写锁协调管理读写线程的方式,比如来看下如下的部分输出结果。

  

8 start to read
10 start to read
12 start to read
8 reading,0
10 reading,0
12 reading,0
9 start to write
9 write,2
11 start to write
11 write,6

这里我们是通过 ReadWriteTool 类里的读写锁管理其中的 num 值,从第1到第6行的输出中我们能看到,虽然8号线程已经得到读锁开始读 num 资源时,10号和12号读线程依然可以得到读锁,从而能并发地读取 num 资源。但在读操作期间,是不允许有写操作的线程进入,也就是说,当 num 资源上有读锁期间,其它线程是无法得到该资源上的“写锁”的。

第7到第10行的输出中我们能看到,当9号线程得到 num 资源上的“写锁”时,其它线程是无法得到该资源上的“读锁“和“写锁“的,而11号线程一定得当9号线程释放了“写锁”后,才能得到 num 资源的“写锁”。

如果在项目里对某些资源(比如文件)有读写操作,这时大家不妨可以使用读写锁,如果读操作的数量要远超过写操作时,那么更可以用读写锁来让读操作可以并发执行,从而提升性能。

欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。欢迎加入程序员小乐技术交流群,在后台回复“加群”或者“学习”即可。

猜你还想看

阿里、腾讯、百度、华为、京东最新面试题汇集

SpringBoot 整合Shiro实现动态权限加载更新+Session共享+单点登录

Spring Boot 最最最常用的注解梳理

是什么让我放弃了Restful API?

关注订阅号「程序员小乐」,收看更多精彩内容

嘿,你在看吗

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值