1 多线程
我们在之前, 学习的程序在没有跳转的前提下, 都是由上至下一次执行, 那么现在想要设计一个程序 , 边打游戏边听歌, 怎么设计, 要解决上述问题, 就得使用多进程或者多线程解决
并发与并行
-
并发
: 指两个或多个事件在同一个时间段内发生 -
并行
: 指两个或多个事件在同一时刻发生
在操作系统中, 安装了多个程序, 并发指的是在一段时间内宏观上有多个程序同时运行; 这在单CPU系统中, 每一个时刻只能有一个程序运行, 即微观上这些程序是分时的交替运行, 只不过给人的感觉是同时运行, 那是因为交替运行的时间是非常短的
而在多个CPU系统中, 则这些可以并发执行的程序便可以分配到多个处理器上(CPU), 实现多任务的并行执行, 即利用每个处理器来处理一个可以并发执行的程序, 这样多个程序便可以同时运行, ,目前电脑市场说的多核CPU, 便是多核处理器, 核越多, 并行处理的程序越多, 能大大的提高电脑运行的效率
注意 : 单核处理器的计算机是不能并行的处理多个任务的, 只能是多个任务在单个CPU并发执行; 同理, 线程也是一样的, 从宏观角度上理解线程是并行运行的, 但是从微观角度上分析确实串行执行的, 即一个线程一个线程的去运行, 当系统只有一个CPU时, 线程会以某种顺序执行多个线程, 我们把这种情况称之为线程的调度
线程与进程
进程
: 是指一个内存中运行的应用程序 , 每个进程都有一个独立的内存空间, 每个应用程序可以同时运行多个进程, 进程也是程序的一次执行过程, 是系统运行程序的基本单位; 系统运行一个程序即是一个进程从创建, 运行 到消亡的过程
线程
: 线程是进程中的一个执行单元, 负责当前进程中程序的执行, 一个进程中至少有一个线程; 一个进程中是可以有多个线程的, 这个应用程序也可以称之为多线程程序
简而言之, 一个程序运行后至少有一个进程, 一个进程中可以包含多个线程
线程调度
:
分时调度 : 所有线程轮流使用CPU的使用权, 平均分配每个线程占用CPU的时间
抢占式调度 : 优先让优先级高的线程去使用CPU, 如果线程的优先级相同, 那么会随机选择一个(线程随机性), Java使用的为抢占式调度
设置线程的优先级 : 右键 –> 设置优先级越高 –> 越先执行
抢占式调度 :
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
创建线程类
Java使用
java.lang.Thread
类代表线程, 所有的线程对象都必须是Thread类或其子类的实例; 每个线程的作用是完成一定的任务, 实际上就是执行一段程序流即一段顺序执行的代码; Java使用线程执行体来代表这段程序流
Java中通过继承Thread类创建并启动多线程的步骤如下:
- 定义Thread类的子类, 并重写该类的run方法, 该run()方法的方法体就代表了线程需要完成的任务, 因此把run() 方法称为线程执行体
- 创建Thread子类的实例, 即创建了线程对象
- 调用线程对象的start()方法来启动该线程, 去执行run()
// 自定义线程类
public class MyThread extends Thread{
@Override
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println("run: "+i);
}
}
}
// 测试类
public class Demo01Thread {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();
for (int i = 0; i < 20; i++) {
System.out.println("main: "+i);
}
}
}
// 打印的结果mian()与 run()交替执行
多线程执行的方法, 他们处于
栈的不同内存空间, 互不影响
Thread类
Thread类中有一些常用的方法
构造方法
-
public Thread()
: 分配一个新的线程对象 -
public Thread(String name)
: 分配一个指定名字的线程对象 -
public Thread(Runnable target)
: 分配一个带有指定目标的新线程对象 -
public Thread(Runnable target,String name)
:分配一个带有指定目标新的线程对象并指定名字
常用方法
-
public Strinf getName()
: 获取当前线程的名称 -
public void star()
: 导致该线程开始运行 ,JVM调用此线程的run方法 -
public void run()
: 此线程要执行的任务再此处定义代码 -
public static void sleep(long millis)
: 使当前正在执行的线程以指定的毫秒数暂停(暂停停止执行) -
public static Thread currentThread()
: 返回对当前正在执行的线程对象的引用
创建线程的第二种方法
实现线程可以通过Thread类的子类, 还可以通过Runnable接口实现线程
实现步骤如下:
- 创建一个Runnable接口的实现类
- 在实现类中重写Runnable接口的run方法, 设置线程任务
- 创建一个Runnable接口的实现类对象
- 创建一个Thread类对象, 构造方法中传递Runnable接口实现的类对象
- 调用Thread类中的start方法, 开启新的线程执行run方法
// Runnbale类的实现类
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run" +i);
}
}
}
// 测试类
public class Demo01Runnable {
public static void main(String[] args) {
// Runnable实现类的对象
Runnable r = new MyRunnable();
// 创建Thread类对象, 把Runnable实现类的对象作为参数传递到Thread类的go偶在方法中
Thread td = new Thread(r);
td.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+
"-->"+i);
}
}
}
Thread和Runnable的区别
如果一个类继承Thread, 则不适合资源共享; 但是如果实现了Runnbale接口的话, 则很容易的实现资源共享
实现Runnable接口比继承Thread类所具有的优势
- 适合多个相同的程序代码的线程去共享同一个资源
- 可以避免Java中的单继承的局限性
- 增加程序的健壮性, 实现解耦操作, 代码可以被多个线程共享, 代码和线程独立
- 线程池只能放入实现Runnable或者Callable类的线程, 不能直接放入基础Thread的类
- 在java中, 每次程序运行至少启动两个线程, 一个是main线程, 一个垃圾收集线程; 因为每当使用java命令执行一个类的时候, 实际上都会启动一个JVM, 每一个JVM其实就是在操作系统中启动了一个进程
匿名内部类实现线程的创建
实现线程的匿名内部类的方式 , 可以方便的实现每个线程执行不同的线程任务操作; 使用匿名内部类的方式实现Runnable接口, 重写Runnable接口中的run()方法
public class NoNameInnerClassThread{
public static void main(String[] args){
Runnable r = new Runnable(){
public void run(){
for(int i=0;i<20;i++){
System.out.println("name"+i);
}
}
};
new Thread(r).start();
for (int i = 0; i < 20; i++) {
System.out.println("费玉清:"+i);
}
}
}
2 线程安全
如果有多个线程在同时运行, 而这些线程可能会同时运行这段代码, 程序每次运行结果和单线程运行解雇欧式一样的, 而且其他的变量的值也和预期的一样, 就是线程安全的
线程安全问题都是由全局变量和静态变量引起的, 若每个线程中对全局变量, 静态变量只有读操作, 而无写操作, 一般来说, 这个全局变量是线程安全的; 若有多个线程同时执行写操作, 一般都需要考虑线程同步, 否则的话就可能影响线程安全
线程同步 : 当我们使用多个线程访问同一资源的时候, 且多个线程中对资源有写的操作, 就容易出现线程安全问题; 要解决上述多线程并发访问一个资源的安全性问题; 也就是解决重复票与不存在票问题, Java中提供了**同步机制(synchronized)**来解决, 包括如下
- 同步代码块
- 同步方法
- 锁机制
同步代码块
synchronized 关键字可以用于方法中的某个区块中, 表示只对这个区块的资源实行互斥访问
格式 : syschronized(同步锁)(需要同步的代码)
同步锁 : 对象的同步锁只是一个概念, 可以想象为在对象上标记了一个锁
- 锁对象可以是任意类型
- 多个线程对象要使用同一把锁
- 在任何时候, 最多只允许一个线程拥有同步锁, 谁拿到锁就进入代码块, 其他的线程只能在外等着(BLOCKED)
同步方法
使用 synchronized 关键字修饰的方法, 就叫做同步方法, 保证A线程执行该方法的时候, 其他的线程只能在外面等着
格式 : public synchronized void method(){ 可能会产生安全问题的代码 }
public class Ticket implements Runnable{
private int ticket = 100;
// 买票
@Override
public void run(){
while(true){
// 每个买票的窗口要永远开启
sellTicket();
}
}
// 锁对象, 谁调用这个方法, 谁就隐含锁对象 , 就是this
public synchronized void sellTicket(){
if(ticket>0){
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
// 获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.printl(name +"正在卖"+ ticket--);
}
}
}
// 频繁的释放锁, 可能会降低程序的效率
锁机制
java.util.concurrent.locks.ReentranLock implements Lock
机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作, 同步代码块/同步方法具有的功能Lock都有, 除此之外更强大, 更体现面向对象
Lock锁也称为同步锁, 加锁与释放锁方法化了
-
public void lock()
: 加同步锁 -
public void unlock()
: 释放同步锁
public class Ticket implements Runnable{
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
while(true){
lock.lock();
if(ticket>0){
try{
Thread.sleeep(50);
}catch(InterruptedException e){
e.printStackTrace();
}finally{
lock.unlock();
}
}
}
}
线程状态剖析
线程状态分析图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zLWfwWWk-1589289456436)(assets/线程状态剖析图.png)]
3 等待唤醒机制
线程间通信
概念 : 多个线程在处理同一个资源, 但是处理的动作 (线程的任务) 却不相同
比如 : 线程A用来生成包子的, 线程B用来吃包子的, 包子可以理解为同一资源; 线程A与线程B处理的动作, 一个是生成, 一个是消费; 那么线程A与线程B之间就存在线程通信问题
为什么要处理线程间通信
多个线程并发执行时, 在默认情况下CPU是随时切换线程的, 当我们需要多个线程来共同完成一件任务, 并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信, 以此来帮我们达到多线程共同操作一份数据
如何保证让线程通信有效有效利用资源
多个线程在处理同一个资源, 并且任务不同时, 需要线程通信来帮助解决线程之间对同一个变量的使用或操作; 就是多个线程在操作同一份数据时, 避免对同一共享变量的争夺; 也就是我们需要通过一定的手段使各个线程能有效的利用资源; 而这种手段 —— 等待唤醒机制
等待唤醒机制
什么是等待唤醒机制
这是多个线程间的一种协作机制; 谈到线程我们经常想到的是线程间的竞争, 比如去争夺锁, 但这并不是故事的全部, 线程间也会有协作机制, 就好比在公司里你和你的同事们, 你们可能存在晋升时的竞争, 但更多时候你们是一起合作完成某些任务
就是在一个线程进行了规定操作后, 就进入等待状态wait(), 等待其他线程执行完他们的指定代码过后, 再将其唤醒 notify ; 在有多个线程进行等待时, 入股偶需要, 可以使用 notifyAll() 唤醒所有等待的线程
wait/notify 是线程间的一种协作机制
等待唤醒中的方法
等待唤醒机制中的方法就是用于解决线程间的通信的问题, 使用的三个方法的含义如下:
-
wait() : 线程不再活动, 不再参与调度, 进入wait set中, 因此不会浪费CPU资源了, 也不会去竞争锁了, 这时的线程状态即是
WAITING
, 它还要等着别的线程去执行一个
特别的动作
, 也即是
通知(notify)
, 在这个对象上等待的线程从 wait set 中释放出来, 重新进入到调度队列(ready queue) 中, - notify : 则选取通知对象的wait set 中的一个线程释放; 例如, 餐馆有空位置后, 等候就餐最久的顾客最先入座
- notifyAll : 则释放通知对象的 wait set 上的全部线程
注意 :
哪怕只通知了一个等待的线程, 被通知的线程也不能立即恢复执行, 因为它当初中断的地方是在同步代码块内, 而此刻它已经不持有锁, 所以它再次尝试去获取锁(很有可能面临其他线程的竞争), 成功后才能在当初调用 wait 方法之后的地方恢复执行
总结如下:
如果能获取锁, 线程就从
WAITING
状态变成
RUNNABLE
状态; 否则, 从wait set 出来, 又进去 entry set, 线程就从
WAITING
状态又变成
BLOCKED
状态
调用wait和notify方法需要注意的细节
- wait方法与notify方法必须要有同一个锁对象调用; 因为对应的锁对象可以通过notify唤醒使用同一个锁对象调用 wait方法后的线程
- wait方法与notify方法是属于Object类的方法, 因为 锁对象可以是任意对象, 而任意对象的所属类都是继承了Object类的
- wait方法与 notify方法必须要在同步代码块或者是同步函数中使用; 因为必须要通过锁对象调用这两个方法
生产者与消费者的问题
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
包子铺线程生成包子,吃货线程消费包子
当没包子时(包子状态为false), 吃货线程等待, 包子铺线程生成包子(包子状态为true),并通知吃货线程(接触吃货的等待状态), 因为已经有包子了, 那么包子铺线程进入到等待状态; 接下来, 吃货线程能否进一步执行取决于锁的获取情况, 如果吃货取到锁, 那么执行吃包子动作, 包子吃完(包子状态为false), 并通知包子铺线程(解除包子铺的等待状态), 吃货线程进入等待, 包子铺线程能否进一步执行取决于 锁 的获取情况
分析图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AwD2wmg8-1589289456441)(assets/包子问题分析图.png)]
代码演示 :
// 包子铺资源类
public class BaoZi{
String pier;
String xianer;
boolean flag = fasle;
}
// 吃货线程类
public class ChiHuo extends Thread{
private BaoZi bz;
// 有参构造
public ChiHuo(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run(){
while(true){
synchronized (bz){
if(ba.flag == fasle){// 没包子
try{
bz.wait()
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag = false;
ba.notify();
}
}
}
}
// 包子铺线程类
public class BaoZiPu extends Thread{
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz = bz;
}
@Override
public void run(){
int count = 0;
// 做包子
while(true){
// 同步
synchronized(bz){
if (bz.flasg == true){
// 包子 存在
try{
bz.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
// 没有包子, 做包子
System.out.println(""包子铺开始做包子)
if(count%2 == 0){
// 冰皮 五仁
bz.pier = "冰皮";
bz.xianer = "五仁";
}else{
// 薄皮 牛肉大葱
bz.pier = "薄皮";
bz.xianer = "牛肉大葱";
}
count++;
ba.flag = true;
System.out.println("包子造好了:"+bz.pier+bz.xianer);
System.out.println("吃货来吃吧");
//唤醒等待线程 (吃货)
bz.notify();
}
}
}
}
// 测试类
public class Demo{
public static void main(String[] args){
// 等待唤醒案例
BaoZi bz = new BaoZi();
ChiHuo ch = new ChiHuo("吃货,bz);
BaoZiPu bzp = new BaoZiPu("包子铺",bz)
ch.start();
bzp.start()
}
}
4 线程池
线程池思想抛出
我们在使用线程的时候就去创建一个线程, 这样实现起来很方便, 但是就会有一个问题
如果并发的线程数量很多, 并且每个线程都是执行一个事件很短的任务就结束了, 这样频繁创建线程就会大大降低系统的效率, 因为频繁创建线程和销毁线程需要时间
那么有没有一种办法使得线程可以复用, 就是执行完一个任务, 并不被销毁, 而是继续执行其他的任务
在Java中可以通过线程池来达到这样的效果, 今天就来详细讲解一下Java的线程池
线程池的概念
线程池
: 其实就是一个容纳多个线程的容器, 其中的线程可以反复使用, 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源
由于线程池有很多操作都是与优化资源相关的
合理使用线程池的好处
- 降低资源消耗, 减少了创建和销毁线程的次数, 每个工作线程都可以被重复利用, 可执行多个任务
- 提高响应速度, 当任务达到时, 任务可以不需的等到线程创建就能立即执行
- 提高线程的可管理性, 可根据系统的承受能力 调整线程池中工作线程的数目, 防止因为小号过多的内存, 二八服务器累趴下(每个线程需要大约1MB内存, 线程开的越多, 消耗的内存也就越大, 最后死机)
线程池的使用
Java里面线程池的顶级接口是
java.util.concurrent.Executor
, 但是严格意义上讲
Excetuor
并不是一个线程池, 而只是一个执行线程的工具, 真正的线程池接口是
java.util.concurrent.ExecutorService
要配置一个线程池是比较复杂的, 尤其是对于线程池的原理不是很清楚的情况下, 很有可能配置的线程池不是较优的, 因此在
java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂, 生成一些常用的线程池, 官方建议使用Executors工程类来创建线程池对象
Executors类中有个创建线程池的方法如下 :
public static ExecutorService newFixedThreadPool(int nThreads)
: 返回线程池对象(创建的是有界线程池, 也就是池中的线程个数可以指定最大数量)
获取到一个线程池ExecutorService对象, 那么怎么使用呢, 在这里定义了一个使用线程池对象的方法如下 :
public Future<?> submit(Runnable task)
: 获取线程池中的某一个线程对象, 并执行
Future接口 : 用来记录线程任务执行完毕后产生的结果, 线程池创建与使用
使用线程池中线程对象的步骤
- 创建线程池对象
- 创建Runnable接口类子类对象 (task)
- 提交Runnable接口类子类对象 (take task)
- 关闭线程池
// Runnable实现类代码
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("我要一个教练");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("教练来了 :"+Thread.currentThread().getName());
System.out.println("教我游泳完, 教练回到了游泳池");
}
}
// 线程池测试类
public class ThreadPoolDemo{
public static void main(String[] args){
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2) // 创建两个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
// 自己创建线程对象的方式
// Thread t = new Thread(r); // r MyRunnable中的接口类的子类对象
// t.start(); // 调用MyRunnable中的run()
// 从线程中获取线程对象, 然后调用MyRunable中的run()
service.submit(r);
// 再从线程中获取线程对象, 调用MyRunable中的run()
service.submit(r);
// 注意 : submit方法调用结束之后, 程序并不终止, 是因为线程池控制了线程的关闭
// 将使用完的线程又归还到线程池中
// service.shutdown()
}
}
hreadPool(2) // 创建两个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
// 自己创建线程对象的方式
// Thread t = new Thread(r); // r MyRunnable中的接口类的子类对象
// t.start(); // 调用MyRunnable中的run()
// 从线程中获取线程对象, 然后调用MyRunable中的run()
service.submit(r);
// 再从线程中获取线程对象, 调用MyRunable中的run()
service.submit(r);
// 注意 : submit方法调用结束之后, 程序并不终止, 是因为线程池控制了线程的关闭
// 将使用完的线程又归还到线程池中
// service.shutdown()
}
}