主页 > 创业  > 

javaSE学习笔记21-线程(thread)-锁(synchronized与Lock)

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 > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方法体之外)

标签:

javaSE学习笔记21-线程(thread)-锁(synchronized与Lock)由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“javaSE学习笔记21-线程(thread)-锁(synchronized与Lock)