在多线程的学习中,想必大家都听说过synchronized,使用synchronized可以解决线程安全问题,提到了synchronized,那又不得不提到ReentrantLock,那么它们的具体使用和区别到底是什么呢?这就是今天我们所要学习的内容!
1.传统的Synchronized锁
1-1 查看Runnable接口源码
//我们发现,在Runable接口前,有一个@FunctionalInterface注解,表明该接口是函数式接口
@FunctionalInterface
public interface Runnable {
//Runnable接口的公共抽象run方法
public abstract void run();
}
1-2 什么是函数式接口?
- 只包含一个抽象方法的接口,称为函数式接口
- 可以通过Lamdba表达式来创建该接口的对象 (如果Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)
- 可以在任意函数式接口上使用==@FunctionalInterface注解==,这样做可以检查它是否是一个函数式接口,同时javadoc也会包含一条声明,说明该接口是一个函数式接口
好了,对函数式接口有了一个大致了解后,让我们来看一下Runnable函数式接口的具体用法:
1-3 Runnable的函数式接口的使用
-
方式一:
直接创建Runnable接口
package com.kuang.demo1;
/**
* @ClassName SaleTicketTest
* @Description 模拟火车站窗口卖票问题
* @Author 狂奔の蜗牛rz
* @Date 2021/7/19
*/
public class SaleTicketTest {
//主方法测试
public static void main(String[] args) {
//并发:多个线程共同操作同一个资源对象
//获取Ticket对象
Ticket ticket = new Ticket();
/**
* 方式一:创建Runnable接口
* 由于Runnable是一个函数式接口,所以可以直接创建Runnable接口,
* 此时它是一个匿名内部类,我们只需重写run方法即可,跟直接实现Runnable接口相同,
* 但是一般不建议实现Runnable接口,这样代码耦合度太高
*/
new Thread(new Runnable() {
@Override
public void run() {
ticket.sale();
}
}).start();
}
}
//共享资源对象 OOP思想(属性+方法)
class Ticket {
//属性:设置初始车票数为10张
private int number = 10;
//方法:卖票的方式
public void sale() {
//判断剩余票数是否大于0
if(number > 0) {
//打印买出第几张票以及剩余票数信息
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票,剩余:"+number+"张票");
}
}
}
-
方式二:
使用Lambda表达式
实现代码如下:
package com.kuang.demo1; /** * @ClassName SaleTicketTest * @Description 模拟火车站窗口卖票问题 * @Author 狂奔の蜗牛rz * @Date 2021/7/19 */ public class SaleTicketTest { //主方法测试 public static void main(String[] args) { //并发:多个线程共同操作同一个资源对象 //获取Ticket对象 Ticket ticket = new Ticket(); /** * 方式二:使用Lambda表达式 * 如果你觉得使用函数式接口还是过于麻烦, * 那么我们可以使用Lambda表达式(JDK1.8的新特性) * 来简化我们的代码,其基本格式为:()->{} */ new Thread(()->{ //直接在{}内调用ticket资源对象的sale方法即可 ticket.sale(); //后面的"窗口A"是线程的名字 },"窗口A").start(); } } //共享资源对象 OOP思想(属性+方法) class Ticket { //属性:设置初始车票数为10张 private int number = 10; //方法:卖票的方式 public void sale() { //判断剩余票数是否大于0 if(number > 0) { //打印买出第几张票以及剩余票数信息 System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票,剩余:"+number+"张票"); } } }
这里我们使用Lamdba表达式来模拟多个线程同时抢票:
1-4 使用Lamdba表达式模拟多线程抢票
- 实现代码:
package com.kuang.demo1;
/**
* @ClassName SaleTicketTest
* @Description 模拟火车站窗口卖票问题
* @Author 狂奔の蜗牛rz
* @Date 2021/7/19
*/
public class SaleTicketTest {
//主方法测试
public static void main(String[] args) {
//并发:多个线程共同操作同一个资源对象
//获取Ticket对象
Ticket ticket = new Ticket();
//设置标志位,初始值为true
boolean flag = true;
//模拟多个窗口同时抢票
//使用Lambda表达式
//窗口A
new Thread(()->{
//使用for循环来模拟卖票
for (int i = 0; i < 10; i++) {
try {
//卖票
ticket.sale();
//让线程进行休眠(防止票被一个人全抢走了)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//后面的"窗口A"是线程的名字
},"窗口A").start();
//窗口B
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
ticket.sale();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"窗口B").start();
//窗口C
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
ticket.sale();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"窗口C").start();
}
}
//共享资源对象 OOP思想(属性+方法)
class Ticket {
//属性:设置初始车票数为10张
private int number = 10;
//方法:卖票的方式
public void sale() {
//判断剩余票数是否大于0
if(number > 0) {
//打印买出第几张票以及剩余票数信息
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票,剩余:"+number+"张票");
}
}
}
- 测试结果:
结果:出现了两个人同时抢到一张票的问题!
为了解决两个人同时抢到一张票的问题,我们可以使用synchronized同步器实现同步控制!
1-5 使用synchronized同步器解决抢票问题
- 代码实现:
package com.kuang.demo1;
/**
* @ClassName SaleTicketTest
* @Description 模拟火车站窗口卖票问题
* @Author 狂奔の蜗牛rz
* @Date 2021/7/19
*/
public class SaleTicketTest {
//主方法测试
public static void main(String[] args) {
//并发:多个线程共同操作同一个资源对象
//获取Ticket对象
Ticket ticket = new Ticket();
//设置标志位,初始值为true
boolean flag = true;
//模拟多个窗口同时抢票
new Thread(()->{
//直接在{}内调用ticket资源对象的sale方法即可
//使用for循环来模拟卖票
for (int i = 0; i < 10; i++) {
try {
//卖票
ticket.sale();
//让线程进行休眠(防止票被一个人全抢走了)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//后面的"窗口A"是线程的名字
},"窗口A").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
ticket.sale();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}},"窗口B").start();
new Thread(()->{
for (int i = 0; i < 10; i++) {
try {
ticket.sale();
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"窗口C").start();
}
}
//共享资源对象 OOP思想(属性+方法)
class Ticket {
//属性:设置初始车票数为10张
private int number = 10;
//方法:卖票的方式
/**
* 数据紊乱,出现两个人同时抢到一张票的问题
* 使用synchronized同步器修饰sale方法,
* 本质上是:队列和锁
*/
public synchronized void sale() {
//判断剩余票数是否大于0
if(number > 0) {
//打印买出第几张票以及剩余票数信息
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票,剩余:"+number+"张票");
}
}
}
- 测试结果
结果:与预期结果相同,没有出现两个人抢到同一张票的问题!
2.使用ReentrantLock锁
2-1 Lock锁简单介绍
- 在JDK1.8 的API文档中,我们可以在java.util.concurrent.locks包下,找到Lock锁接口
-
然后我们可以在文档中看到Lock接口的一些具体实现类,包括
ReentrantLock (可重入锁)、ReadLock(读锁) 和 WriteLock(写锁)
-
那么,锁是什么呢?
JDK8文档中关于Lock锁的定义:
锁是用于通过多个线程控制对共享资源的访问的工具。
通常,锁提供对共享资源的独占访问;一次只能有一个线程可获取锁,并且对共享资源的所有访问都要求首先获取锁。
但是,一些锁可能允许并发访问共享资源,如ReadWriteLock(读写锁)
-
Lock锁的主要方法有:
lock()上锁和unlock()解锁
并且文档中还说明:
当在不同范围内发生锁定和解锁时,必须注意确保在锁定时执行的所有代码由try-finally或try-catch保护,以确保在必要时释放锁定。
2-2 公平锁和非公平锁
查看ReentrantLock可重用锁源码:
//ReentrantLock(可重入锁),Lock锁接口的实现类
public class ReentrantLock implements Lock, java.io.Serializable {
//无参构造方法
public ReentrantLock() {
//默认是创建一个NonfairSync(非公平锁)
sync = new NonfairSync();
}
//有参构造方法
public ReentrantLock(boolean fair) {
//判断标志位fair的值,如果为true就创建公平锁,否则创建非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
}
公平锁和非公平锁区别:
-
公平锁:如同名字一般,它十分公平,遵循先来后到的原则,不允许插队,即先来先执行,后来就后执行
-
非公平锁:如同名字一般,它十分不公平,因为它允许进行插队 (如果某个线程执行了很久,其他线程不能在那一直等它,所以默认是非公平锁)
-
公平锁以请求锁的顺序来获取锁,非公平锁无法保证请求的顺序执行
2-3 ReentrantLock锁实现多线程抢票
- 实现代码:
package com.kuang.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName SaleTicketTest
* @Description 模拟火车站窗口卖票问题,
* 使用Lock锁来实现
* @Author 狂奔の蜗牛rz
* @Date 2021/7/19
*/
public class SaleTicketTest2 {
//主方法测试
public static void main(String[] args) {
//并发:多个线程共同操作同一个资源对象
//获取Ticket对象
Ticket ticket = new Ticket();
//模拟多个窗口同时抢票
//窗口A
//直接在{}内调用ticket资源对象的sale方法即可
//使用for循环来模拟卖票
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
try {
//让线程进行休眠(防止票被一个人全抢走了)
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//后面的"窗口A"是线程的名字
},"窗口A").start();
//窗口B
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"窗口B").start();
//窗口C
new Thread(()->{
for (int i = 0; i < 10; i++) {
ticket.sale();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"窗口C").start();
}
}
/**
* Lock的使用步骤:
* 1.创建Lock锁对象
* 2.给资源对象加锁
* 3.执行完后释放锁
*/
//共享资源对象 OOP思想(属性+方法)
class Ticket {
//属性:设置初始车票数为10张
private int number = 10;
//1.创建Lock锁对象
Lock lock = new ReentrantLock();
//卖票的方法
public void sale() {
//2.首先需要加锁
lock.lock();
try {
//业务代码放在try{}中
//判断剩余票数是否大于0
if(number > 0) {
//打印买出第几张票以及剩余票数信息
System.out.println(Thread.currentThread().getName()+"卖出了第"+(number--)+"张票,剩余:"+number+"张票");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//3.业务执行完后,最后还需要解锁
lock.unlock();
}
}
}
- 测试结果:
结果:与预期结果相同,没有出现两个人抢到同一张票的问题!
3.Synchronized 和 ReentrantLock的区别
-
Synchronized 是内置的Java关键字;而 ReentrantLock 是一个Java类
-
Synchronized 无法获取锁的状态;而 ReentrantLock 可以判断是否获取到了锁
-
Synchronized 会自动释放锁;而 ReentrantLock 必须要手动释放,否则将会造成死锁!
使用synchronized锁
//共享资源对象 class Test { //测试方法 public synchronized void save() { //业务代码 } }
使用ReentrantLock锁
//共享资源对象 class Test { //获取Lock锁对象 Lock lock = new ReentrantLock(); //测试方法 public void save() { //加锁 lock.lock(); try { //业务代码 } } catch (Exception e) { e.printStackTrace(); } finally { //解锁 lock.unlock(); } } }
-
使用 Synchronized 时 ,假设有A和B两个线程,当线程A获得锁后进入了阻塞状态,线程B还是会一直傻傻的等待线程A去释放锁;而 ReentrantLock 就不会一直等待下去!
-
Synchronized 和 ReentrantLock 都是可重入锁,但 Synchronized 不可以被中断,而 ReentrantLock 可以被中断
可重入锁:
可重入锁是指同一个线程可以多次获取同一把锁,ReentrantLock 和 Synchronized 都是可重入锁
可中断锁:
可中断锁是指线程尝试获取锁的过程中,是否可以响应中断,ReentrantLock是可中断锁,Synchronized 是不可中断锁
-
Synchronized 只是非公平锁,ReentrantLock 默认为非公平锁 ,但可以设置为公平锁
//ReentrantLock(可重入锁),Lock锁接口的实现类 public class ReentrantLock implements Lock, java.io.Serializable { //无参构造方法 public ReentrantLock() { //默认是创建一个NonfairSync(非公平锁) sync = new NonfairSync(); } //有参构造方法 public ReentrantLock(boolean fair) { //判断标志位fair的值,如果为true就创建公平锁,否则创建非公平锁 sync = fair ? new FairSync() : new NonfairSync(); } }
-
Synchronized 适合锁少量的代码同步问题,ReentrantLock 适合锁大量的同步代码!
怎么样,通过今天的学习,你是否对Synchronized锁和ReentrantLock锁的使用更加得心应手,对它们之间的区别也能够略知一二,欢迎大家学习和讨论!
参考视频链接:https://www.bilibili.com/video/BV1B7411L7tE (B站UP主遇见狂神说的JUC并发编程基础)