【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐)

本文主要是介绍【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 0. 稍微回顾点基础
    • 0.1 如何起多线程
      • 0.1.1 继承Thread
      • 0.1.2 实现Runnable接口
      • 0.1.3 Callable接口+Future接口
    • 0.2 如何加锁
      • 0.2.1 synchronized
    • 0.2.2 Lock的实现类
  • 1. 案例:卖电影票
    • 1.1 案例实现:Runnable接口
      • 1.1.1 synchronized 同步代码块
      • 1.1.2 synchronized 同步方法
      • 1.1.3 Lock手动上锁
    • 1.2 案例实现:Thread
  • 2. 案例:两个线程交替打印0-100
  • 3. 面试官:你来写个死锁吧(哲学家就餐问题)
    • 3.* 有关死锁
      • 预防死锁
      • 解除死锁

练习下Thread,Runnable,synchronized, Lock,稍微记录下

0. 稍微回顾点基础

0.1 如何起多线程

一般就是三种方式:

  1. Thread的类,
  2. Runnable接口
  3. Callable+FutureTask接口

0.1.1 继承Thread

主要就是重写run方法,实例化后调用Thread.start()开启线程

public class MyThread extends Thread{@Overridepublic void run() {super.run();for (int i = 0; i < 1000; i++) {System.out.println(getName()+": Hello MyThread!");}}
}
// ----------------------------------------
public class Main {public static void main(String[] args) {MyThread t1 = new MyThread();t1.setName("T1");MyThread t2 = new MyThread();t2.setName("T2");t1.start();t2.start();}
}

0.1.2 实现Runnable接口

实现run方法,并实例化作为参数传入Thread实例
最后也是通过调用Thread.start()开启线程

public class MyRun implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {Thread t = Thread.currentThread(); // 获取当前线程对象System.out.println(t.getName()+"Hello Runnable");// System.out.println(Thread.currentThread().getName()+"Hello Runnable");}}
}
// ---------------------------
public class Main {public static void main(String[] args) {// 创建MyRun对象,表示多线程要执行的任务MyRun myRun = new MyRun();// 创建线程对象Thread t1 = new Thread(myRun);Thread t2 = new Thread(myRun);// 启动线程t1.start();t2.start();}
}

0.1.3 Callable接口+Future接口

最特别的就是可以获取多线程的运行结果,也就是run有返回值

public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {// 求1-100和并返回int sum = 0;for (int i = 0; i < 100; i++) {sum += i;}return sum;}
}
// ---------------------------
public class Main {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable mc = new MyCallable();FutureTask<Integer> ft = new FutureTask<>(mc);Thread t1 = new Thread(ft);t1.start();int sum = ft.get();System.out.println(sum);}
}

0.2 如何加锁

主要就是synchronized修饰符或者Lock接口的实现类

0.2.1 synchronized

大概有需要注意几点:

  • synchronized是基于悲观锁的,当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁。
  • 锁对象:锁对象一定要唯一。
    • 可以设为本类的class对象,因为一个类的class对象是唯一的,哪怕有很多实例;
    • 也可以在类内增加一个static final的Object实例,也不一定是Object实例,只要是static保证所有类实例都相同,final保证不可更改,也就是一起确保了锁对象的唯一性。
    • 锁膨胀:无锁->偏向锁->轻量级锁->重量级锁

有两种用法:

  1. synchronized 同步代码块
synchronized(锁对象){...
}
  1. synchronized 同步方法
修饰符 synchronized 返回值类型 方法名(方法参数) {...}

注意一下,同步方法会锁住方法里的所有代码,并且锁对象不能自己指定:

  • 非静态方法:this,即当前方法的调用者。此时也叫方法锁
  • 静态方法:当前类的字节码文件。此时也叫类锁

0.2.2 Lock的实现类

比较常用的就是ReentranLock
需要自己手动上锁,一般会用try{要上锁的代码}finally{解锁}来保证所以定会被释放

Lock lock=new ReentrantLock();
lock.lock();
try{...
}finally{lock.unlock();
}

下面来看点案例练练手

1. 案例:卖电影票

1000张电影票,在两个窗口领取,每次领取一张,假设被刺领取的时间为3000ms
用多线程模拟卖票过程,并打印剩余电影票的数量

1.1 案例实现:Runnable接口

1.1.1 synchronized 同步代码块

Runnable实现类

// 实现Runnable接口+synchronized同步代码块
public class TicketRunnable1 implements Runnable {static int tickets = 1000;
//    定义一个锁对象,也可以用当前类的class对象private static Object lock = new Object();@Overridepublic void run() {while (tickets > 0) {synchronized (lock) {if (tickets > 0) {try {Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}}}
}

1.1.2 synchronized 同步方法

就是把上面同步代码块抽出一个函数,并且不用自己制定锁对象

// 实现Runnable接口+synchronized同步方法
public class TicketRunnable2 implements Runnable{static int tickets = 1000;// 静态@Overridepublic void run() {while(tickets > 0){sell();}}private synchronized void sell() {if (tickets > 0){try{Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}
}

开两个线程测试

public class TestRunnable {// 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms// 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程// 用多线程模拟卖票过程,并打印剩余电影票的数量public static void main(String[] args) {
//        TicketRunnable1 = new TicketRunnable1();
//        Thread window1 = new Thread(ticketRunnable1, "Window1");
//        Thread window2 = new Thread(ticketRunnable1, "Window2");TicketRunnable2 ticketRunnable2 = new TicketRunnable2();Thread window1 = new Thread(ticketRunnable2, "Window1");Thread window2 = new Thread(ticketRunnable2, "Window2");window1.start();window2.start();}
}

运行结果
在这里插入图片描述
没啥问题。
如果你想实例化两个runnable实现类也没问题,因为锁对象是唯一,比如下面这样

public class TestRunnable {// 1000张电影票,在两个窗口领取,假设被领取的时间为3000ms// 时间可以调小点,比如50ms,给cpu上点强度,可以更好体现出线程交替互斥的过程// 用多线程模拟卖票过程,并打印剩余电影票的数量public static void main(String[] args) {TicketRunnable1 ticketRunnable1 = new TicketRunnable1();TicketRunnable1 ticketRunnable11 = new TicketRunnable1();Thread window1 = new Thread(ticketRunnable1, "Window1");Thread window2 = new Thread(ticketRunnable11, "Window2");window1.start();window2.start();}
}

1.1.3 Lock手动上锁

// 实现Runnable接口+Lock
public class TicketRunnable3 implements Runnable{static int tickets = 1000;@Overridepublic void run() {ReentrantLock lock = new ReentrantLock();while(tickets > 0){lock.lock();try {Thread.sleep(50);if (tickets > 0){System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}}
}

测试结果
在这里插入图片描述

1.2 案例实现:Thread

先写个Thread的继承类,这里就用同步方法写了,其他锁的办法也差不多,就不多讲咯

public class MyThread1 extends Thread{static int tickets = 1000;@Overridepublic void run() {super.run();while (tickets > 0){sell();}}private static synchronized void sell() {if (tickets > 0){try{Thread.sleep(50);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " 卖出1张票,剩余电影票:" + --tickets);}}
}

需要注意的是,这里synchronized的方法sell()必须是静态的,
因为静态的同步方法的锁对象是当前类的字节码文件对象,
而非静态的同步方法的锁对象是this,也就是调用类的实例,这里我们需要实例化两个对象window1和window2,他们的锁对象是不同的,因此会出现不同步的问题,如下图
在这里插入图片描述
票卖的只剩-1了,明显线程间没有同步

2. 案例:两个线程交替打印0-100

这是之前牛客上看到个面试手撕题,用wait()notifyAll()让线程等待与唤醒就行
先给段代码,大家可以思考一下对不对

public class Printer implements Runnable {private static int number = 0;int printId;public Printer(int printId) {this.printId = printId;}@Overridepublic void run() {while (number <= 100) {printNum();}}private synchronized void printNum() {if (number % 2 != printId) {// 不是当前线程就等待try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 被唤醒后if (number <= 100) {System.out.println(Thread.currentThread().getName() + ": " + number++);this.notifyAll();}}
}

测试一下

public class Test {public static void main(String[] args) {Printer printer1 = new Printer(0);Printer printer2 = new Printer(1);Thread thread1 = new Thread(printer1);Thread thread2 = new Thread(printer2);thread1.start();;thread2.start();}
}

在这里插入图片描述
很明显,寄了。主要问题就出在代码里用的synchronized同步方法,因为我们是给两个线程一个printId(0或1)以便通过number的奇偶来判断该哪个线程打印,显然,我们需要实例化两个Runnable实现类的对象,前面也说了synchronized非静态同步方法的锁对象是this,也就是实例本身,因此这里其实是没有同步了。
那,是不是直接把这个要同步的printNum方法给static了就行了呢?
很遗憾,也不行,因为printNum方法需要用printId来判断是不是当前线程的回合,而printId是需要通过构造函数初始化的,不可以是static,所以这样直接用同步方法行不通。
换成同步代码块,加一个唯一的锁对象就行了

public class Printer implements Runnable {private static int number = 0;int printId;private static final Object lock = new Object();public Printer(int printId) {this.printId = printId;}@Overridepublic void run() {while (number <= 100) {synchronized (lock){if (number % 2 != printId) {// 不是当前线程就等待try {lock.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}// 被唤醒后if (number <= 100) {System.out.println(Thread.currentThread().getName() + ": " + number++);lock.notifyAll();}}}}
}

当然这个锁对象用当前类的字节码文件也可以

synchronized (Printer.class){...Printer.class.wait();...Printer.class.notifyAll();
}

结果没毛病
在这里插入图片描述
当然,lock手动加锁就没这些麻烦,记得unlock就好

3. 面试官:你来写个死锁吧(哲学家就餐问题)

这也是今天在牛客上看到的(第一次听到这种请求)
死锁,简单来说就是两个线程互相等对方释放锁资源,然后你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你你等我我等你…一直等下去

public class Test {static Object lock1 = new Object();static Object lock2 = new Object();public static void main(String[] args) {Thread thread1 = new Thread(() -> {synchronized (lock1){System.out.println(Thread.currentThread().getName() + " get lock1!");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " wait lock2...");synchronized (lock2){System.out.println(Thread.currentThread().getName() + " get lock2!");}}});Thread thread2 = new Thread(() -> {synchronized (lock2){System.out.println(Thread.currentThread().getName() + " get lock2!");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(Thread.currentThread().getName() + " wait lock1...");synchronized (lock1){System.out.println(Thread.currentThread().getName() + " get lock1!");}}});thread1.start();thread2.start();}
}

启动一下
在这里插入图片描述
就一直在wait力。哲学家就餐问题来说,这里两个线程就是两个哲学家,两个锁就是两支筷子,只有凑够两支筷子才能吃上饭。

3.* 有关死锁

预防死锁

上面这个问题出现的很大一部分原因就是锁的嵌套,实际写代码的时候也要尽量避免这种情况来预防死锁。此外也有银行家算法之类的来评估预防死锁,但今天就先不展开了。

解除死锁

一般就是资源剥夺法,撤销进程法,进程回退法

  • 撤销进程:强制结束一个或多个进程并回收它们的资源,以打破死锁(比如直接kill掉);
  • 进程回退:将一个或多个进程回退到某一安全状态,这些状态之前未涉及死锁。通过回退并重新计算资源分配,系统尝试解决死锁问题;
  • 资源剥夺:挂起/激活机制。挂起一些进程,剥夺它们的资源以解除死锁,待条件满足时,再激活进程。
    这里【进程回退】时其实也涉及到了【资源剥夺】

这篇关于【Java笔记】多线程2: 加锁小练习(卖票+交替打印+哲学家就餐)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/920215

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input

Spring Gateway动态路由实现方案

《SpringGateway动态路由实现方案》本文主要介绍了SpringGateway动态路由实现方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随... 目录前沿何为路由RouteDefinitionRouteLocator工作流程动态路由实现尾巴前沿S