文章目录
一、多线程安全
当多个线程之间,访问了共享的数据,就会产生线程安全问题
1. Demo:卖票
我们以一个售票厅的例子举例说明:
- 总共100张票,作为共享数据
- 三个售票厅,一起售卖票,作为三个线程
1.1 发生线程安全问题
结果:
会发现:
- 卖出了重复的票
- 卖出了不存在的票
1.2 线程安全问题原理
- 多个线程抢夺CPU的执行权
- 当执行到睡眠 时,线程把抢到的CPU执行权交出
- 当一个线程已经判断完毕(无论如何,这部分函数体会执行),另一个线程改变了判断条件也无济于事,这时会出现条件外的值(不存在)
- 因为多个线程共享资源,判断依据相同,就会出现重复的情况
- 总结: 所以,安全问题 的实质就是共享数据 时,一个线程读时,另一个线程进行写的操作,这个写操作,改变之前线程读的结果
所以,如果数据共享 ,但线程中只进行读 ,是不会存在安全问题的。
- 解决方法: 一个线程失去对
CPU
的控制权时,其它线程只能等待其执行完
2. 同步机制
同一时间,只能执行一件事,就叫同步
同步有三种方式:
- 同步代码块
- 同步方法
- 锁机制
2.1 方法1:同步代码块
同步代码块 里放可能会产生线程安全的代码(访问了共享数据的代码),表示只对这个区块的资源进行互斥访问
synchronized(同步锁){
// 需要同步操作的代码
}
注意:
- 同步代码块中的对象,可以是任意对象
- 多个线程的锁对象 必须是同一个
- 锁对象作用: 把同步代码块锁住,只让一个线程在同步代码块中执行
其它部分代码不变,使用代码块包住需要锁的部分
2.2 同步技术原理
使用了一个锁对象 ,这个锁对象叫同步锁 ,也叫对象锁、对象监视器
t0
抢到了CPU
的执行权,执行run()
方法,遇到synchronized
代码块- 检查该代码块是否有锁对象
- 如果有,就会获取该锁对象,进入代码块中执行
t1
抢到了CPU
的执行权,执行run()
方法,遇到synchronized
代码块- 检查该代码块是否有锁对象,发现没有
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
步骤:
- 在成员位置,创建一个
ReentrankLock
对象 - 在可能出现线程安全的地方,获取锁
- 在这段代码结束时,释放锁
提示: 最好把代码放在try...catch
中,并且把释放锁 放在finally
中,这样无论有没有异常,都会释放锁。
3. 线程状态
Thread
中有一个嵌套类(内部类)Thread.State
,记录着线程的状态
3.1 无限等待状态
下面,以线程通信案例 来进行说明
- 创建一个线程,等待另一个线程的执行结果,调用
wait();
方法,进入无限等待状态 - 另一个线程调用
notify();
,退出无限等待状态
注意事项:
- 两个线程必须得用同步代码块包裹起来,以保证等待和唤醒,只有一个在执行
- 同步使用的锁对象,必须保证唯一
- 只有锁对象才能调用
wait()
和notify()
(这两个都是Object类
中的方法)
两个方法:
- wait() 在其它线程调用此对象的notify()和notifyall()方法前,将导致线程等待
- notify()唤醒在此对象监视器上等待的单个线程,会继续执行wait方法之后的代码
-
代码:
-
结果:
注意: obj.wait()
如果不加参数,就是无限等待。但它可以接收一个毫秒值做参数,等到时间走完,还没有得到唤醒,会自动醒来 ,此时相当于Thread.sleep()
方法
3.2 notifyAll
notify()
如果有多个等待线程,随机唤醒一个
notifyAll()
如果有多个等待线程,全部唤醒
可以发现,一次只唤醒一个线程(随机的)
把notify()
变为notifyall()
会得到如下结果
4. 线程间通讯
线程间通讯: 多个线程在处理同一资源,但线程的任务不同
多个线程操作同一份数据,就会发生数据的抢夺,通过等待唤醒机制 可以避免这种争夺发生
注意: 被通知(notify)后,只是进入了可运行 状态,不是立即执行的(因为还在同步代码块那,还要抢锁)
4.1 Demo:服务器返回一个网页
分析:
- 页面数据: 请求,响应
- 服务器: 接收请求,发送响应
- 客户端: 发送请求,接收响应
两个线程的状态是通信(互斥)状态,必须保证两个线程只有一个在执行
-
定义一个页面准备的类,这个页面需要文件,还需要资料渲染,还要请求响应状态
-
定义一个服务器:当有请求时,如果有响应,返回响应。如果没响应,就进行响应。如果没请求,就休息等待请求。
-
定义一个客户端:当无请求时,发送请求,当有请求时,请求响应。请求响应时,无限等待。
-
定义一个运行用的主文件(我是用同级类的写法)
-
结果
注:
- 可以在服务器和客户端类中,添加死循环
- 现实业务中,服务器做的是沟通连接的工作,处理页面的工作交给应用框架
二、线程池
线程池: 容纳多个线程的容器,其中的线程可以复用,无需反复创建线程而消耗过多的资源
1. 线程池原理
线程池是一个容器 ,也即一个集合(LinkedList<Thread>
)
步骤:
- 程序开始的时候,创建多个线程,放进一个集合中
- 取出线程:
Thread t = List.remove(index)
、Thread t = LinkedList.removeFirst()
(线程只能被一个任务使用) - 放回线程:
List.add(t)
、LinkedList.addLast(t)
JDK1.5
后,JDK
内置了线程池
,可以直接使用。
线程池的好处:
- 降低资源消耗: 减少了创建和销毁线程的次数,使得线程可以复用。
- 提高响应速度: 因为线程在任务开始前就已经创建好了,所以在任务开始时,可以立即响应。
- 提高可管理性: 一个线程所占内存约为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极简式
凡是可根据上下文推导的,都可以省略
- (参数列表) : 括号中参数列表的数据类型,可以省略不写
- (参数列表) : 括号中的参数只有一个,类型和括号都可省略
- {代码体} :如果代码体只有一行,可以省略{}、return 、分号(这三个必须一起省略,要么一起保留)
一些案例:
- 使用Lambda表达式,必须要有一个接口,且该接口只有一个抽象方法
- 必须具备上下文推断
注: 只有一个抽象方法的接口叫函数式接口