【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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

在cscode中通过maven创建java项目

在cscode中创建java项目 可以通过博客完成maven的导入 建立maven项目 使用快捷键 Ctrl + Shift + P 建立一个 Maven 项目 1 Ctrl + Shift + P 打开输入框2 输入 "> java create"3 选择 maven4 选择 No Archetype5 输入 域名6 输入项目名称7 建立一个文件目录存放项目,文件名一般为项目名8 确定