【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

相关文章

springboot security快速使用示例详解

《springbootsecurity快速使用示例详解》:本文主要介绍springbootsecurity快速使用示例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝... 目录创www.chinasem.cn建spring boot项目生成脚手架配置依赖接口示例代码项目结构启用s

java之Objects.nonNull用法代码解读

《java之Objects.nonNull用法代码解读》:本文主要介绍java之Objects.nonNull用法代码,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐... 目录Java之Objects.nonwww.chinasem.cnNull用法代码Objects.nonN

springboot security之前后端分离配置方式

《springbootsecurity之前后端分离配置方式》:本文主要介绍springbootsecurity之前后端分离配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的... 目录前言自定义配置认证失败自定义处理登录相关接口匿名访问前置文章总结前言spring boot secu

一文详解SpringBoot响应压缩功能的配置与优化

《一文详解SpringBoot响应压缩功能的配置与优化》SpringBoot的响应压缩功能基于智能协商机制,需同时满足很多条件,本文主要为大家详细介绍了SpringBoot响应压缩功能的配置与优化,需... 目录一、核心工作机制1.1 自动协商触发条件1.2 压缩处理流程二、配置方案详解2.1 基础YAML

java中使用POI生成Excel并导出过程

《java中使用POI生成Excel并导出过程》:本文主要介绍java中使用POI生成Excel并导出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求说明及实现方式需求完成通用代码版本1版本2结果展示type参数为atype参数为b总结注:本文章中代码均为

springboot简单集成Security配置的教程

《springboot简单集成Security配置的教程》:本文主要介绍springboot简单集成Security配置的教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,... 目录集成Security安全框架引入依赖编写配置类WebSecurityConfig(自定义资源权限规则

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

java中反射(Reflection)机制举例详解

《java中反射(Reflection)机制举例详解》Java中的反射机制是指Java程序在运行期间可以获取到一个对象的全部信息,:本文主要介绍java中反射(Reflection)机制的相关资料... 目录一、什么是反射?二、反射的用途三、获取Class对象四、Class类型的对象使用场景1五、Class

SpringBoot中封装Cors自动配置方式

《SpringBoot中封装Cors自动配置方式》:本文主要介绍SpringBoot中封装Cors自动配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot封装Cors自动配置背景实现步骤1. 创建 GlobalCorsProperties

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3