javaSE学习笔记21-线程(thread)-锁(synchronized 与Lock)
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程 都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能 会发生“死锁"的问题;
死锁是指多个线程在执行过程中,因为争夺资源而造成的一种互相等待的现象,导致这些线程都无法继续执行下去。
练习代码
package com.lock;
/*
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程
都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能
会发生“死锁"的问题;
*/
//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"灰姑凉");
Makeup g2 = new Makeup(0,"白雪公主");
g1.start();
g2.start();
}
}
//口红(Lipstick)
class Lipstick{
}
//镜子(Mirror)
class Mirror{
}
//化妆(Makeup)
class Makeup extends Thread{
//需要的资源只有一份,用static来抱着只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//使用化妆品的人
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
synchronized (mirror){//1s钟后想获得镜子 的锁
System.out.println(this.girlName + "获得镜子的锁");
}
}
}else {
synchronized (mirror) {//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
synchronized (lipstick){//2s钟后想获得口红 的锁
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
}
代码结构
-
DeadLock类:这是主类,包含
main
方法,用于启动两个线程。 -
Lipstick类和Mirror类:这两个类分别代表口红和镜子,是共享资源。
-
Makeup类:继承自
Thread
类,表示一个化妆的线程。每个线程代表一个女孩,她们需要使用口红和镜子来化妆。
代码逻辑
-
共享资源:
-
Lipstick
和Mirror
是两个共享资源,分别代表口红和镜子。 -
这两个资源被声明为
static
,确保它们在所有Makeup
实例之间共享。
-
-
Makeup类:
-
choice
:表示女孩的选择,决定她们先获取哪个资源。 -
girlName
:表示女孩的名字。 -
run()
方法:线程启动后执行的方法,调用makeup()
方法。 -
makeup()
方法:模拟化妆过程,尝试获取口红和镜子的锁。
-
-
死锁的产生:
-
如果
choice
为0,线程会先获取口红的锁,然后尝试获取镜子的锁。 -
如果
choice
为1,线程会先获取镜子的锁,然后尝试获取口红的锁。 -
由于两个线程的执行顺序不同,可能会导致以下情况:
-
线程1(灰姑凉)持有口红的锁,等待镜子的锁。
-
线程2(白雪公主)持有镜子的锁,等待口红的锁。
-
-
这样,两个线程互相等待对方释放资源,导致死锁。
-
代码执行流程
-
启动线程:
-
g1
和g2
两个线程分别启动,代表灰姑凉和白雪公主。 -
g1
的choice
为0,g2
的choice
为1。
-
-
线程执行:
-
g1
先获取口红的锁,然后尝试获取镜子的锁。 -
g2
先获取镜子的锁,然后尝试获取口红的锁。
-
-
死锁发生:
-
g1
持有口红的锁,等待g2
释放镜子的锁。 -
g2
持有镜子的锁,等待g1
释放口红的锁。 -
两个线程都无法继续执行,形成死锁。
-
如何避免死锁
-
锁的顺序:确保所有线程以相同的顺序获取锁。例如,所有线程都先获取口红的锁,再获取镜子的锁。
-
超时机制:在获取锁时设置超时时间,如果超时则释放已持有的锁并重试。
-
死锁检测:使用工具或算法检测死锁,并采取相应措施解除死锁。
优化后代码
package com.lock;
/*
死锁
多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能运行,而导致两个或者多个线程
都在等待对方释放资源,都停止执行的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能
会发生“死锁"的问题;
死锁避免方法
产生死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个进程使用;
2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放;
3.不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺;
4,循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
只要想办法破其中的任意一个或多个条件就可以避免死锁发生
*/
//死锁:多个线程互相抱着对方需要的资源,然后形成僵持
public class DeadLock {
public static void main(String[] args) {
Makeup g1 = new Makeup(0,"灰姑凉");
Makeup g2 = new Makeup(0,"白雪公主");
g1.start();
g2.start();
}
}
//口红(Lipstick)
class Lipstick{
}
//镜子(Mirror)
class Mirror{
}
//化妆(Makeup)
class Makeup extends Thread{
//需要的资源只有一份,用static来抱着只有一份
static Lipstick lipstick = new Lipstick();
static Mirror mirror = new Mirror();
int choice;//选择
String girlName;//使用化妆品的人
Makeup(int choice,String girlName){
this.choice = choice;
this.girlName = girlName;
}
@Override
public void run() {
//化妆
try {
makeup();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//化妆,互相持有对方的锁,就是需要拿到对方的资源
private void makeup() throws InterruptedException {
if (choice == 0) {
synchronized (lipstick) {//获得口红的锁
System.out.println(this.girlName + "获得口红的锁");
Thread.sleep(1000);
}
synchronized (mirror){//1s钟后想获得镜子 的锁
System.out.println(this.girlName + "获得镜子的锁");
}
}else {
synchronized (mirror) {//获得镜子的锁
System.out.println(this.girlName + "获得镜子的锁");
Thread.sleep(2000);
}
synchronized (lipstick){//2s钟后想获得口红 的锁
System.out.println(this.girlName + "获得口红的锁");
}
}
}
}
修改后的代码分析
关键修改点
-
锁的嵌套被移除:
-
在原始代码中,
synchronized
块是嵌套的,即一个线程在持有第一个锁的情况下尝试获取第二个锁。 -
在修改后的代码中,
synchronized
块是分开的,线程在释放第一个锁之后才会尝试获取第二个锁。
-
-
锁的获取顺序:
-
修改后的代码中,线程不会同时持有两个锁,而是先释放一个锁,再尝试获取另一个锁。
-
这样就不会出现两个线程互相等待对方释放锁的情况。
-
修改后的代码执行逻辑
线程1(灰姑凉)的执行流程:
-
获取
lipstick
的锁。 -
打印“灰姑凉获得口红的锁”。
-
释放
lipstick
的锁。 -
获取
mirror
的锁。 -
打印“灰姑凉获得镜子的锁”。
-
释放
mirror
的锁。
线程2(白雪公主)的执行流程:
-
获取
mirror
的锁。 -
打印“白雪公主获得镜子的锁”。
-
释放
mirror
的锁。 -
获取
lipstick
的锁。 -
打印“白雪公主获得口红的锁”。
-
释放
lipstick
的锁。
为什么避免了死锁?
-
锁的释放:
-
每个线程在获取一个锁后,会先释放它,再尝试获取另一个锁。
-
这样就不会出现一个线程持有
lipstick
的锁并等待mirror
的锁,而另一个线程持有mirror
的锁并等待lipstick
的锁的情况。
-
-
没有互相等待:
-
线程1和线程2不会同时持有对方需要的锁,因此不会形成互相等待的僵局。
-
Lock锁
1、JDK5.0开始,Java提供了更强大的线程同步机制--通过显式定义同步锁对象来实现同步。
同步锁使用Lock对象充当
2、java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLoc类(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是
ReentrantLock,可以显式加锁、释放锁
以下代码演示了如何使用ReentrantLock
来实现线程同步,确保多个线程安全地访问共享资源。ReentrantLock
是Java中提供的一种显式锁机制,相比于synchronized
关键字,它提供了更灵活的锁控制方式。
package com.lock;
import java.util.concurrent.locks.ReentrantLock;
/*
Lock(锁)
1、JDK5.0开始,Java提供了更强大的线程同步机制--通过显式定义同步锁对象来实现同步。
同步锁使用Lock对象充当
2、java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象
ReentrantLoc类(可重入锁)实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是
ReentrantLock,可以显式加锁、释放锁
*/
//测试lock锁
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
new Thread(testLock2).start();
new Thread(testLock2).start();
new Thread(testLock2).start();
}
}
class TestLock2 implements Runnable{
int ticketNums = 10;
//定义lock锁
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true){
try {
lock.lock();//加锁
if (ticketNums > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(ticketNums--);
}else {
break;
}
}finally {
//解锁
lock.unlock();
}
}
}
}
代码结构
-
TestLock类:
-
这是主类,包含
main
方法,用于启动多个线程。 -
创建了一个
TestLock2
对象,并启动三个线程来执行该对象的run
方法。
-
-
TestLock2类:
-
实现了
Runnable
接口,表示一个任务,可以被多个线程执行。 -
包含一个共享资源
ticketNums
(票数),多个线程会竞争访问和修改这个资源。 -
使用
ReentrantLock
来确保对ticketNums
的访问是线程安全的。
-
代码逻辑
1. 共享资源
-
ticketNums
:表示剩余的票数,初始值为10。 -
多个线程会同时访问和修改
ticketNums
,因此需要确保线程安全。
2. ReentrantLock
-
ReentrantLock
是一个可重入锁,允许线程多次获取同一把锁。 -
通过
lock()
方法加锁,通过unlock()
方法解锁。 -
使用
try-finally
块确保锁一定会被释放,避免死锁。
3. 线程执行逻辑
-
每个线程执行
TestLock2
的run
方法。 -
在
while (true)
循环中,线程不断尝试获取锁并访问共享资源ticketNums
。 -
如果
ticketNums > 0
,线程会休眠1秒(模拟耗时操作),然后打印并减少ticketNums
的值。 -
如果
ticketNums <= 0
,线程退出循环,任务结束。
代码执行流程
-
启动线程:
-
在
main
方法中,创建了一个TestLock2
对象,并启动三个线程。 -
这三个线程会并发执行
TestLock2
的run
方法。
-
-
线程竞争锁:
-
每个线程在执行
run
方法时,会先调用lock.lock()
尝试获取锁。 -
只有一个线程能成功获取锁,其他线程会被阻塞,直到锁被释放。
-
-
访问共享资源:
-
获取锁的线程会检查
ticketNums
的值。 -
如果
ticketNums > 0
,线程会休眠1秒,然后打印ticketNums
的值并将其减1。 -
如果
ticketNums <= 0
,线程会退出循环。
-
-
释放锁:
-
线程在完成对共享资源的操作后,会调用
lock.unlock()
释放锁。 -
其他被阻塞的线程会竞争获取锁,继续执行。
-
-
任务结束:
-
当
ticketNums
的值减少到0时,所有线程都会退出循环,任务结束。
-
关键点
-
ReentrantLock的作用:
-
确保多个线程对共享资源
ticketNums
的访问是互斥的,避免数据竞争。 -
相比于
synchronized
,ReentrantLock
提供了更灵活的锁控制,例如可中断锁、超时锁等。
-
-
try-finally的作用:
-
在
try
块中加锁,在finally
块中解锁,确保锁一定会被释放,避免死锁。
-
-
线程安全:
-
通过
ReentrantLock
实现了对共享资源的线程安全访问。
-
改进建议
-
锁的粒度:
-
当前代码中,锁的粒度较大(整个
while
循环都在锁内),可能会影响并发性能。可以根据实际需求调整锁的粒度。
-
-
公平锁:
-
ReentrantLock
默认是非公平锁,可以通过构造函数new ReentrantLock(true)
创建公平锁,确保线程按顺序获取锁。
-
-
锁的可中断性:
-
ReentrantLock
支持可中断的锁获取(lockInterruptibly()
),可以在线程等待锁时响应中断。
-
改进后代码:
package com.lock;
import java.util.concurrent.locks.ReentrantLock;
/*
改进后的TestLock示例:
1. 缩小锁的粒度,只对共享资源的访问和修改加锁。
2. 使用公平锁,确保线程按顺序获取锁。
3. 优化代码结构,提高可读性和可维护性。
*/
public class TestLock {
public static void main(String[] args) {
TestLock2 testLock2 = new TestLock2();
// 启动多个线程
new Thread(testLock2, "线程1").start();
new Thread(testLock2, "线程2").start();
new Thread(testLock2, "线程3").start();
}
}
class TestLock2 implements Runnable {
private int ticketNums = 10; // 共享资源,表示剩余的票数
// 定义公平锁
private final ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
// 尝试获取锁
lock.lock();
try {
if (ticketNums > 0) {
// 模拟耗时操作
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 打印当前线程名和剩余的票数
System.out.println(Thread.currentThread().getName() + " 售出票号:" + ticketNums--);
} else {
// 票已售完,退出循环
break;
}
} finally {
// 释放锁
lock.unlock();
}
// 模拟线程切换,增加并发性
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
synchronized 与Lock的对比
1、lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了作用域自动释放
2、Lock只有代码块锁,synchronized有代码块锁和方法锁;
3、使用Lock锁,JVM将花费较少的时间来调度线程(性能更好,并且具有更好的扩展性(提供更多的子类))
4、优先使用顺序:
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)