Java语言进阶 #多线程 #Day17~18 #多线程 #多线程安全 #线程状态 #线程通信 #线程池 #lambda

一、多线程安全

当多个线程之间,访问了共享的数据,就会产生线程安全问题

1. Demo:卖票

我们以一个售票厅的例子举例说明:

  1. 总共100张票,作为共享数据
  2. 三个售票厅,一起售卖票,作为三个线程

1.1 发生线程安全问题

在这里插入图片描述
在这里插入图片描述


结果:
在这里插入图片描述
在这里插入图片描述

会发现:

  1. 卖出了重复的票
  2. 卖出了不存在的票

1.2 线程安全问题原理

  1. 多个线程抢夺CPU的执行权
  2. 当执行到睡眠 时,线程把抢到的CPU执行权交出
  3. 当一个线程已经判断完毕(无论如何,这部分函数体会执行),另一个线程改变了判断条件也无济于事,这时会出现条件外的值(不存在)
  4. 因为多个线程共享资源,判断依据相同,就会出现重复的情况

  • 总结: 所以,安全问题 的实质就是共享数据 时,一个线程读时,另一个线程进行写的操作,这个写操作,改变之前线程读的结果

所以,如果数据共享 ,但线程中只进行 ,是不会存在安全问题的。

  • 解决方法: 一个线程失去对CPU的控制权时,其它线程只能等待其执行完

2. 同步机制

同一时间,只能执行一件事,就叫同步
同步有三种方式:

  1. 同步代码块
  2. 同步方法
  3. 锁机制

2.1 方法1:同步代码块

同步代码块 里放可能会产生线程安全的代码(访问了共享数据的代码),表示只对这个区块的资源进行互斥访问

synchronized(同步锁){
	// 需要同步操作的代码
}

注意:

  1. 同步代码块中的对象,可以是任意对象
  2. 多个线程的锁对象 必须是同一个
  3. 锁对象作用: 把同步代码块锁住,只让一个线程在同步代码块中执行

其它部分代码不变,使用代码块包住需要锁的部分
在这里插入图片描述

2.2 同步技术原理

使用了一个锁对象 ,这个锁对象叫同步锁 ,也叫对象锁、对象监视器

  1. t0抢到了CPU的执行权,执行run()方法,遇到synchronized代码块
  2. 检查该代码块是否有锁对象
  3. 如果有,就会获取该锁对象,进入代码块中执行
  4. t1抢到了CPU的执行权,执行run()方法,遇到synchronized代码块
  5. 检查该代码块是否有锁对象,发现没有
  6. t1进入阻塞状态,直到t0归还锁对象(t0要执行完同步代码块中内容,会把锁对象归还给同步代码块

总结: 同步中的线程,没有执行完,不会释放锁。同步外的线程,没有锁,进不去同步。
同步保证了,只有一个线程在同步中执行共享数据,保证了安全
程序频繁判断锁、获取锁、释放锁 ,程序的效率会降低

2.3 方法2:同步方法

把同步代码块中的内容,放在同步方法中,调用该方法即可

修饰符 synchronized 返回值类型 函数名() {}

例: public synchronized void method() {}
在这里插入图片描述
同步方法使用的锁对象,就是Runnable的实现类对象,就是这个:
在这里插入图片描述

2.4 静态同步方法

在同步方法前加static修饰,访问的共享数据也用static修饰

// 访问的必须是静态共享
修饰符 static synchronized 返回值类型 函数名() {}

例: public static synchronized void method() {}
在这里插入图片描述
静态同步方法使用的锁对象,是本类的class属性–>class文件对象(反射)
(之所以不用this, 因为this是创建对象时产生的。静态方法的创建优先于对象创建。)

2.5 方法3:Lock锁

JDK1.5后有一个新的解决线程安全问题的方法,叫做Lock锁
Lock接口,比 synchronized更加好用

  • 我们主要使用它的两个方法void lock()获取锁void unlock()释放锁
  • 它有一个实现类ReentrankLock

步骤:

  1. 在成员位置,创建一个ReentrankLock对象
  2. 在可能出现线程安全的地方,获取锁
  3. 在这段代码结束时,释放锁

在这里插入图片描述

提示: 最好把代码放在try...catch中,并且把释放锁 放在finally中,这样无论有没有异常,都会释放锁。

3. 线程状态

Thread中有一个嵌套类(内部类)Thread.State,记录着线程的状态
在这里插入图片描述
在这里插入图片描述

3.1 无限等待状态

下面,以线程通信案例 来进行说明

  1. 创建一个线程,等待另一个线程的执行结果,调用wait();方法,进入无限等待状态
  2. 另一个线程调用notify();,退出无限等待状态

注意事项:

  1. 两个线程必须得用同步代码块包裹起来,以保证等待和唤醒,只有一个在执行
  2. 同步使用的锁对象,必须保证唯一
  3. 只有锁对象才能调用wait()notify()(这两个都是Object类中的方法)

两个方法:

  1. wait() 在其它线程调用此对象的notify()和notifyall()方法前,将导致线程等待
  2. notify()唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码

  • 代码:
    在这里插入图片描述

  • 结果:在这里插入图片描述


注意: obj.wait()如果不加参数,就是无限等待。但它可以接收一个毫秒值做参数,等到时间走完,还没有得到唤醒,会自动醒来 ,此时相当于Thread.sleep()方法

3.2 notifyAll

notify()如果有多个等待线程,随机唤醒一个
notifyAll()如果有多个等待线程,全部唤醒
在这里插入图片描述
在这里插入图片描述
可以发现,一次只唤醒一个线程(随机的)
notify()变为notifyall()会得到如下结果

在这里插入图片描述

4. 线程间通讯

线程间通讯: 多个线程在处理同一资源,但线程的任务不同
多个线程操作同一份数据,就会发生数据的抢夺,通过等待唤醒机制 可以避免这种争夺发生

注意: 被通知(notify)后,只是进入了可运行 状态,不是立即执行的(因为还在同步代码块那,还要抢锁)

4.1 Demo:服务器返回一个网页

分析:

  • 页面数据: 请求,响应
  • 服务器: 接收请求,发送响应
  • 客户端: 发送请求,接收响应

两个线程的状态是通信(互斥)状态,必须保证两个线程只有一个在执行

  1. 定义一个页面准备的类,这个页面需要文件,还需要资料渲染,还要请求响应状态在这里插入图片描述

  2. 定义一个服务器:当有请求时,如果有响应,返回响应。如果没响应,就进行响应。如果没请求,就休息等待请求。
    在这里插入图片描述

  3. 定义一个客户端:当无请求时,发送请求,当有请求时,请求响应。请求响应时,无限等待。
    在这里插入图片描述

  4. 定义一个运行用的主文件(我是用同级类的写法)
    在这里插入图片描述

  5. 结果
    在这里插入图片描述

注:

  1. 可以在服务器和客户端类中,添加死循环
  2. 现实业务中,服务器做的是沟通连接的工作,处理页面的工作交给应用框架

二、线程池

线程池: 容纳多个线程的容器,其中的线程可以复用,无需反复创建线程而消耗过多的资源

1. 线程池原理

线程池是一个容器 ,也即一个集合(LinkedList<Thread>)

步骤:

  1. 程序开始的时候,创建多个线程,放进一个集合中
  2. 取出线程:Thread t = List.remove(index)Thread t = LinkedList.removeFirst()(线程只能被一个任务使用)
  3. 放回线程:List.add(t)LinkedList.addLast(t)

JDK1.5后,JDK内置了线程池,可以直接使用。


线程池的好处:

  1. 降低资源消耗: 减少了创建和销毁线程的次数,使得线程可以复用。
  2. 提高响应速度: 因为线程在任务开始前就已经创建好了,所以在任务开始时,可以立即响应。
  3. 提高可管理性: 一个线程所占内存约为1M,可以根据系统的承受能力,来限定线程数量

2. 线程池使用

在这里插入图片描述
java.util.concurrent.Executors是线程池的工厂类,用来生成线程池

static ExecutorService newFixedThreadPool(int 线程数目)

参数:线程数目

返回值:ExecutorService接口的实现类对象(可以使用接口类型来接收,这叫多态。这是一种【面向接口编程】)

java.util.concurrent.ExecutorService线程池接口


// 用来从线程池中获取线程,调用start方法,执行线程任


submit(Runnable task)  // 提交一个Runnable任务用于执行

// 销毁线程池(不建议执行)

void shutdown()

3. 代码

  • 代码: 在这里插入图片描述
  • 结果:
    在这里插入图片描述

三、Lambda

1. 函数式编程思想

函数式编程思想: 强调做什么,而不是以某种形式做(面向对象强调通过对象来做)


  • 使用面向对象思想:在这里插入图片描述
  • 函数式思想: 外卖的核心需求是为了创建中国对象嘛?不,我们是为了将run()体内的代码让Thread()知晓。把怎么做 转向做什么 的本质上,而不在乎过程与实质 ,我们只是为了一个结果JDK1.8中加入了Lambda表达式
    在这里插入图片描述

2. Lambda标准格式

匿名内部类的好处是省去了实现类的定义 , 缺点是语法复杂


// Lambda的标准格式
(参数类型 参数名称) -> {代码体}

() : 接口中抽象方法的参数列表
-> : 传递的意思
{} : 重写接口的参数方法 

2.1 无参的示例

在这里插入图片描述

2.2 有参无返回

在这里插入图片描述

2.3 有参有返回

标准形式: 在这里插入图片描述

3. Lambda极简式

凡是可根据上下文推导的,都可以省略

  1. (参数列表) : 括号中参数列表的数据类型,可以省略不写
  2. (参数列表) : 括号中的参数只有一个,类型和括号都可省略
  3. {代码体} :如果代码体只有一行,可以省略{}、return 、分号(这三个必须一起省略,要么一起保留)

一些案例:


在这里插入图片描述
在这里插入图片描述


  • 使用Lambda表达式,必须要有一个接口,且该接口只有一个抽象方法
  • 必须具备上下文推断

注: 只有一个抽象方法的接口叫函数式接口

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章