Java多线程,对锁机制的进一步分析
1 可重入锁
可重入锁,也叫递归锁。它有两层含义,第一,当一个线程在外层函数得到可重入锁后,能直接递归地调用该函数,第二,同一线程在外层函数获得可重入锁后,内层函数可以直接获取该锁对应其它代码的控制权。之前我们提到的synchronized和ReentrantLock都是可重入锁。
通过ReEnterSyncDemo.java,我们来演示下synchronized关键字的可重入性。
1 class SyncReEnter implements Runnable{
2 public synchronized void get(){
3 System.out.print(Thread.currentThread().getId() + "\t");
4 //在get方法里调用set
5 set();
6 }
7 public synchronized void set()
8 {System.out.print(Thread.currentThread().getId()+"\t"); }
9 public void run() //run方法里调用了get方法
10 { get();}
11 }
12 public class ReEnterSyncDemo {
13 public static void main(String[] args) {
14 SyncReEnter demo=new SyncReEnter();
15 new Thread(demo).start();
16 new Thread(demo).start();
17 }
18 }
在第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行第二次启动线程的输出。
通过ReEnterLock.java,我们来演示下ReentrantLock的可重入性。
1 import java.util.concurrent.locks.ReentrantLock;
2 class LockReEnter implements Runnable {
3 ReentrantLock lock = new ReentrantLock();
4 public void get() {
5 lock.lock();
6 System.out.print(Thread.currentThread().getId()+"\t");
7 // 在get方法里调用set
8 set();
9 lock.unlock();
10 }
11 public void set() {
12 lock.lock();
13 System.out.print(Thread.currentThread().getId() + "\t");
14 lock.unlock();
15 }
16 public void run()
17 { get(); }
18 }
19 public class ReEnterLock {
20 public static void main(String[] args) {
21 LockReEnter demo = new LockReEnter();
22 new Thread(demo).start();
23 new Thread(demo).start();
24 }
25 }
在第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,我们来观察下通过读写锁管理读写并发线程的方式。
1 import java.util.concurrent.locks.Lock;
2 import java.util.concurrent.locks.ReentrantReadWriteLock;
3 class ReadWriteTool {
4 private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
5 private Lock readLock = lock.readLock();
6 private Lock writeLock = lock.writeLock();
7 private int num = 0;
8 public void read() {//读的方法
9 int cnt = 0;
10 while (cnt++ < 3) {
11 try {
12 readLock.lock(); System.out.println(Thread.currentThread().getId()
13 + " start to read");
14 Thread.sleep(1000);
15 System.out.println(Thread.currentThread().getId() + " reading," + num);
16 } catch (Exception e)
17 { e.printStackTrace();}
18 finally { readLock.unlock(); }
19 }
20 }
21 public void write() {//写的方法
22 int cnt = 0;
23 while (cnt++ < 3) {
24 try {
25 writeLock.lock();
26 System.out.println(Thread.currentThread().getId()
27 + " start to write");
28 Thread.sleep(1000);
29 num = (int) (Math.random() * 10);
30 System.out.println(Thread.currentThread().getId() + " write," + num);
31 } catch (Exception e)
32 { e.printStackTrace();}
33 finally { writeLock.unlock();}
34 }
35 }
36 }
在第3行定义的ReadWriteTool 类里,我们在第4行创建了一个读写锁,并在第5和第6行,分别通过这个读写锁的readLock和writeLock方法,分别创建了读锁和写锁。
在第8行的read方法里,我们是先通过第12行的代码加“读锁“,随后在第15行进行读操作。在第21行的write方法里,我们是先通过第25行的代码加“写锁”,随后在第30行进行写操作。
37 class ReadThread extends Thread {
38 private ReadWriteTool readTool;
39 public ReadThread(ReadWriteTool readTool)
40 { this.readTool = readTool; }
41 public void run()
42 { readTool.read();}
43 }
44 class WriteThread extends Thread {
45 private ReadWriteTool writeTool;
46 public WriteThread(ReadWriteTool writeTool)
47 { this.writeTool = writeTool; }
48 public void run()
49 { writeTool.write(); }
50 }
在第37行和第44行里,我们分别定义了读和写这两个线程,在ReadThread线程的run方法里,我们调用了ReadWriteTool类的read方法,而在WriteThread线程的run方法里,则调用了write方法。
51 public class ReadWriteLockDemo {
52 public static void main(String[] args) {
53 ReadWriteTool tool = new ReadWriteTool();
54 for (int i = 0; i < 3; i++) {
55 new ReadThread(tool).start();
56 new WriteThread(tool).start();
57 }
58 }
59 }
在main函数的第53行,我们创建了一个ReadWriteTool类型的tool对象,在第55和56行初始化读写线程时,我们传入了该tool对象,也就是说,通过54行for循环创建并启动的多个读写线程是通过同一个读写锁来控制读写并发操作的。
出于多线程并发调度的原因,我们每次运行都可能得到不同的结果,但从这些不同的结果里,我们都態明显地看出读写锁协调管理读写线程的方式,比如来看下如下的部分输出结果。
1 8 start to read
2 10 start to read
3 12 start to read
4 8 reading,0
5 10 reading,0
6 12 reading,0
7 9 start to write
8 9 write,2
9 11 start to write
10 11 write,6
这里我们是通过ReadWriteTool类里的读写锁管理其中的num值,从第1到第6行的输出中我们能看到,虽然8号线程已经得到读锁开始读num资源时,10号和12号读线程依然可以得到读锁,从而能并发地读取num资源。但在读操作期间,是不允许有写操作的线程进入,也就是说,当num资源上有读锁期间,其它线程是无法得到该资源上的“写锁”的。
从第7到第10行的输出中我们能看到,当9号线程得到num资源上的“写锁”时,其它线程是无法得到该资源上的“读锁“和“写锁“的,而11号线程一定得当9号线程释放了“写锁”后,才能得到num资源的“写锁”。
如果在项目里对某些资源(比如文件)有读写操作,这时大家不妨可以使用读写锁,如果读操作的数量要远超过写操作时,那么更可以用读写锁来让读操作可以并发执行,从而提升性能。
Java多线程,对锁机制的进一步分析的更多相关文章
- JAVA多线程与锁机制
JAVA多线程与锁机制 1 关于Synchronized和lock synchronized是Java的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码 ...
- java多线程(三)——锁机制synchronized(同步语句块)
用关键字synchronized声明方法在某些情况下是有弊端的,比如A线程调用同步方法之行一个长时间的任务,那么B线程必须等待比较长的时间,在这样的情况下可以使用synchronized同步语句快来解 ...
- java多线程(二)——锁机制synchronized(同步方法)
synchronized Java语言的关键字,可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码.当两个并发线程访问同一个对象object中 ...
- java多线程系列(九)---ArrayBlockingQueue源码分析
java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...
- JAVA中关于锁机制
本文转自 http://blog.csdn.net/yangzhijun_cau/article/details/6432216 一段synchronized的代码被一个线程执行之前,他要先拿到执行这 ...
- 深入浅出Java并发包—锁机制(一)
前面我们看到了Lock和synchronized都能正常的保证数据的一致性(上文例子中执行的结果都是20000000),也看到了Lock的优势,那究竟他们是什么原理来保障的呢?今天我们就来探讨下Jav ...
- Java多线程的同步机制(synchronized)
一段synchronized的代码被一个线程执行之前,他要先拿到执行这段代码的权限,在 java里边就是拿到某个同步对象的锁(一个对象只有一把锁): 如果这个时候同步对象的锁被其他线程拿走了,他(这个 ...
- Java 多线程:锁(三)
Java 多线程:锁(三) 作者:Grey 原文地址: 博客园:Java 多线程:锁(三) CSDN:Java 多线程:锁(三) StampedLock StampedLock其实是对读写锁的一种改进 ...
- 深入浅出Java并发包—锁机制(三)
接上文<深入浅出Java并发包—锁机制(二)> 由锁衍生的下一个对象是条件变量,这个对象的存在很大程度上是为了解决Object.wait/notify/notifyAll难以使用的问题. ...
随机推荐
- VisualStudio 2019 新特性
很多小伙伴都好奇 VisualStudio 2019 有哪些功能,下面让我介绍一些好玩的特性 在安装完成之后会看到创新的欢迎界面,这个欢迎界面支持输入关键字搜项目,同时支持选择语言平台 很多小伙伴都说 ...
- 微信群打卡机器人XiaoV项目开源 | 蔡培培的独立博客
原文首发于蔡培培的独立博客.原文链接<微信群打卡机器人XiaoV项目开源>. 5月21日,在米花(后面" 亚里士多德式友谊"专题会提及)的影响下,决定搞个私人运动群,拉 ...
- python OrderedDict
15年16年接触python时候,还不知道这个函数,只知道dict的无序,造成了一些麻烦 今天view 代码,发现了 OrderedDict() 在python2.7中比较吃内存 pop(获取指定ke ...
- 谈谈模型融合之一 —— 集成学习与 AdaBoost
前言 前面的文章中介绍了决策树以及其它一些算法,但是,会发现,有时候使用使用这些算法并不能达到特别好的效果.于是乎就有了集成学习(Ensemble Learning),通过构建多个学习器一起结合来完成 ...
- Batch Normalization批量归一化
BN的深度理解:https://www.cnblogs.com/guoyaohua/p/8724433.html BN: BN的意义:在激活函数之前将输入归一化到高斯分布,控制到激活函数的敏感区域,避 ...
- Effective TestStand Operator Interfaces
目录 为什么要使用操作员界面? 是什么决定一个好的界面? 用户的类型 和 界面的必要元素 TestStand 架构 TestStand 自带的例子 自定义用户界面 TestStand 提供的三个管理控 ...
- 使用easyExcel遇到的坑
最近有个功能,用easyExcel代替poi ,这个确实方便了不少,但是使用easyExcel也踩到了很多坑,在这里记录下easyExcel存在的问题,希望阅读这篇文档的人,可以更好的避免这些. 1. ...
- ArrayList、LinkedList、Vector、CopyOnWriteArrayList的区别和源码分析
1. ArrayList ArrayList 是一个数组队列,相当于动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAccess, ...
- Linux 学习笔记 4 创建、复制、移动、文件的基本操作
写在前面 通过上一节的学习,我们基本的了解到在Linux 里面对于设备的挂载.卸载以及设备存在的目录.挂载目录.都有了一个基本的了解 本节主要了解文件.以及目录的相关操作,比如文件.目录的创建.以及删 ...
- Python基础(一):初识基本数据类型
这个系列主要是对以往学过的Python3基础的总结和回顾. Python的基本数据类型包含数字.字符串.列表.元组.字典.集合几大类. 在介绍基本数据类型之前,先说明三个Python内建方法,有助于认 ...