Java基础:多线程详解

Java多线程详解

多线程概述

  1. 多任务:
    例如,边吃饭边玩手机。虽然看似同时间做了很多事,但本质依然是某一时刻做一件事,只是两件事交替的很快,所以看起来像同时进行。
  2. 多线程:
    例如,一条单向的路,车很多,一辆车出事全部走不了。为了解决这个问题,开多两条同向的路,可以并排走三辆车,大家互不干扰,一条出事还有两条可以走。
    又例如,编程中,如果不使用多线程,在执行main函数时如果需要另一个函数的结果,要调用该函数并使其运行结束返回后,再执行main函数,但如果加多一个线程,跟main函数刚开始就一起跑,结果等到main函数执行到位直接获得,就大大节省了程序运行时间。
    在这里插入图片描述
  3. 多进程
    在操作系统中运行的一个程序就是一个进程,比如播放器。
    一个进程可以有多个线程,比如播放器中可以同时听到声音(一个线程),看到画面(一个线程),看到弹幕(一个线程)等等。

进程(Process) VS 线程 (Thread)

进程
  1. 进程是针对程序(Program)而言的。程序是指令和数据的有序集合,其本身不会运行,是一个静态的概念。
  2. 进程则是针对程序执行的一次执行过程,是动态的概念,是系统资源分配的单位。
  3. 一个进程可以包含一个或一个以上的线程。

线程
  1. 线程是CPU调度和执行的单位。
  2. 多线程有两种情况;第一种是真实的多线程,即多个处理器多核,每个处理器处理一个线程。第二种是模拟的多线程,一个CPU,在多个线程之间左右横跳,切换的很快达到同时执行的错觉,但本质上在一个时间点依然只有一个代码在执行,“模拟多线程” 类似 “多任务” 的情景假设。
  3. 线程是独立的执行路径。
  4. main()为主线程。
  5. 即使没主动创建线程,程序运行依然有多个线程,如主线程和垃圾回收线程。
  6. 多线程的运行顺序由调度器(CPU)安排,与操作系统相关,无法人为干预。
  7. 线程间的资源抢夺,需要加入并发控制来解决。
  8. 并发控制和CPU调度会带来额外程序开销。
  9. 每个线程在自己的内存区域交互,内存控制不当会造成数据不一致。

线程的创建



通过Thread Class

  1. 自定义类继承Tread 类
  2. 重写run()方法,编写线程执行体
  3. 在main()线程中,创建线程对象,调用start() 方法启动新线程
  4. 线程不一定立即执行,由CPU调度
  5. 不建议使用,避免OOP单继承的局限性

通过Runnable Interface

  1. 自定义类 “实现” (implements) Runnable接口
  2. 重写run() 方法
  3. 在主线程中,创建自定义类的对象,将对象丢入 Thread thread = new Thread(对象名).start() 开启执行
  4. 推荐使用,可以避免单继承的问题,方便同一个对象被多个线程类使用,但这会触发并发问题,(例如,一个火车票Runnable对象,有10张票的类属性,每个Thread类是一个买票者,每买一张票,票属性–,这时他们可能同时买第n张票,因为票属性是公共资源,在run()方法内的资源才是线程私有资源)
    在这里插入图片描述
    龟兔赛跑例子核心代码(i<=100而不是<100):
    在这里插入图片描述

通过Callable Interface

  1. 自定义类继承Callable<?>,“?”为返回类型
  2. 重写(Override)call()方法,call() 与 run() 不同,需要与Callable<?>中的“?”相同的返回类型
  3. 创建自定义类对象,一个对象需要一个提交
  4. 创建 “执行服务”,输入参数为n个线程
  5. 提交执行,n个线程需要提交n次
  6. 获取返回结果,n次提交有n个返回结果
  7. 关闭服务
  8. Callable的好处:可以定义返回值;可以抛出异常;
    在这里插入图片描述

静态代理模式

“静态代理模式” 与 “Thread” 的关系:
此处插入代理模式的原因主要是想表达,Thread类其实就是一个代理对象,Thread自身实现了Runnable接口,输入一个实现了Runnable接口的自定义类来实现多线程,底层原理就是代理模式。

  1. 真实对象和代理对象都要实现同一接口
  2. 代理对象要代理真实角色

这样做的好处:

  1. 代理对象可以做很多真实对象做不了的事情(例如结婚中的,婚庆公司帮你策划,你自己只用结婚)
  2. 真实对象专注做自己的事.

图中Marry为接口,You为真实对象,WeddingCompany为代理对象。
在这里插入图片描述

Lambda表达式

“Lambda表达式” 与 “Thread”的关系:
此处插入Lambda表达式主要是因为Runnable接口就是一个函数式接口,可以用lambda表达式简洁的实现出一个对象而不用写class类。

格式:
(parameters) -> expression
(parameters) -> statement
(parameters) -> {statements}

parameter的参数类型可以省略,要加都加,不加都不加,参数的括号在只有一个参数的时候可以省略

为什么要使用lambda表达式:

  1. 避免匿名内部类定义过多
  2. 可以让代码看起来简洁
  3. 去掉冗余的部位,只保留核心逻辑

使用lambda表达式必须先理解 “函数式接口”(functional interface)

函数式接口(Functional Interface)

定义:任何接口,只包含唯一一个抽象方法,那么他就是一个函数式接口,对于函数式接口,我们可以通过lambda表达式来创建该接口的对象。

例如:Runnable Interface
在这里插入图片描述

/*
推导lambda表达式
 */
public class TestLambda {

    //3.静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda() {
            System.out.println("I Like Lambda2");
        }
    }

    public static void main(String[] args) {
        ILike iLike = new Like();
        iLike.lambda();
        iLike = new Like2();
        iLike.lambda();
        //4.局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda() {
                System.out.println("I Like Lambda3");
            }
        }

        iLike = new Like3();
        iLike.lambda();

        //5.匿名内部类,没有类的名称,必须借助接口或者父类
        iLike = new ILike() {
            @Override
            public void lambda() {
                System.out.println("I Like Lambda4");
            }
        };
        iLike.lambda();
        
        //6.用lambda简化,简单来说就是将函数式接口直接在代码里实现其唯一的抽象方法后,跳过实现类,直接获得一个该接口的实现后的对象
        //如果是带参数的话,就在小括号内加参数(如:int a),调用时输入一个int即可,甚至int 都可以省略掉
        iLike = () -> {
            System.out.println("I Like Lambda5");
        };
        iLike.lambda();

    }
}

//1.定义一个函数式接口
interface ILike{
    void lambda();
}

//2.实现类
class Like implements ILike{
    @Override
    public void lambda() {
        System.out.println("I Like Lambda1");
    }
}

线程的五大状态

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

线程停止(flag)

  • 不推荐使用JDK的stop(),destroy()方法,已过时
  • 推荐线程自己停止下来,通过代码的条件判断
  • 建议使用一个Boolean标志位,并在Runnable类内添加一个开关方法,当main线程内条件符合时,调用该开关方法跳出run()内循环,使run()方法跑完结束并结束子线程。

线程休眠(sleep)

  • sleep( milliseconds ),指定当前线程阻塞的毫秒数
  • sleep() 存在异常 InterruptedException 需要抛出
  • sleep() 时间达到后线程进入就绪状态
  • sleep() 可以模拟网络延时,倒计时等
  • 每一个对象都有一个锁,sleep() 不会释放对象的锁
  • Thread.sleep()

线程礼让(yield)

  • 礼让线程,让当前正在执行的线程从 “运行状态” 转为 “就绪状态”,不阻塞,一旦调度器调度上,立刻开始运行
  • 礼让不一定成功,准确来说叫重新竞争,线程全部就绪状态,CPU仍然可能选择之前的线程运行,看CPU调度器的决定
  • Thread.yield()

线程强制执行(join)

  • 可以想象成清场,某线程调用该方法后,它保持运行或开始运行,命令其他正在运行的线程阻塞,等待它执行完成
  • Thread.join()

观测线程状态

  • NEW:刚刚new出来的线程,创建状态
  • RUNNABLE:在Java虚拟机中执行的线程处于此状态
  • BLOCKED:被阻塞等待监视器锁定的线程处于此状态
  • WAITING:正在等待另一个线程执行特定动作的的线程处于此状态
  • TIMED_WAITING:正在等待另一个线程执行特定时间的线程处于此状态
  • TERMINATED:已退出的线程出于此状态,死亡后的线程不可通过start再次启动

如何监测线程状态:
在这里插入图片描述
在这里插入图片描述

线程的优先级

  • Java提供线程调度器来监控所有就绪的线程,调度器按照优先级决定执行哪个线程,优先级高的大概率先获得系统资源,不是绝对的,优先级对不同操作系统意义不同。
  • 优先级用数字来表示,范围1-10,有三个常数,Thread.MIN_PRIORITY =1.
    Thread.MAX_PRIORITY=10.
    Thread.NORM_PRIORITY=5.
    默认为5
  • Thread方法getPriority()/setPriority(int x)可以获取和改变优先级
  • 优先级的设定要在start()方法调度之前

守护线程(Daemon)

  • 线程分为用户线程和守护线程
  • JVM必须等待用户线程运行完毕
  • JVM不必等待守护线程运行完毕
  • 守护线程主要做一些后台工作,包括操作日志,内存监控,垃圾回收
  • Thread.setDaemon(true)可以将线程设置为守护线程

线程的同步(Synchronized)

  • 并发:同一个对象被多个线程操作
  • 例子:网上抢火车票,夫妻同时取钱
  • 处理并发问题,我们就需要线程同步,即一种等待机制:一个对象如果有多个线程要访问,则所有线程进入该对象的等待池,形成 “队列”,当前面线程使用完毕,下一个线程开始使用。然而光有队列还是无法保证安全性,我们还需要 “锁”(想象一下公共厕所,光有队列还不够,门还要上锁)。
  • 性能和安全是补全关系,保证线程安全则一定会损失一定的性能,想要更高的性能就要牺牲一定的安全

三个不安全案例:

  1. 不安全的买票
  2. 不安全的取钱
  3. 多个线程同时对一个List添加元素,可能占用同一个位置

同步方法和同步块

如同private关键字,我们可以用一个关键字synchronized来解决同步问题,该关键字有两种用法:

  1. synchronized method (同步方法)
  2. synchronized block(同步代码块)
同步方法(Synchronized Method)
  • 同步方法(synchronized method)就在method声明中加入synchronized关键字。
  • 每个synchronized method在执行前都必须获得调用该方法的对象的锁,否则阻塞。
  • 同步方法无需指定同步监视器,因为默认是this
  • 将大方法设置为synchronized会大大影响效率,因为方法内有些代码是是只读代码,并没有做任何修改。方法内需要修改内容的代码才需要锁。因此我们引进一个新的概念,同步代码块
同步代码块(Synchronized Block)
  • 同步代码块(synchronized block)在方法内需要同步的区域加入synchronized (Obj) { }
  • 同步代码块执行完后就会释放锁
  • 此处Obj称为同步监视器,可以为任何对象,推荐使用公共资源作为同步监视器,例如夫妻取钱例子中,把夫妻共有账户锁住即可

JUC线程安全集合

JUC即java.util.concurrent API。在不安全例子中,我们讲到多个线程对同一个List操作存在线程不安全问题。JUC提供了一个CopyOnWriteArrayList的ArrayList是自带线程安全的。不需要自己增加同步方法。

线程的死锁

两个玩具车和枪,小孩A和小孩B各拿一个,他们都想要两个玩具,都不给对方,这种情况就称为死锁。

在代码中,如果一个同步代码块需要两个或两个以上的锁时,就有可能发生死锁。两个线程都拿到了其中部分锁且等待剩下的锁,同步方法和同步代码块执行完后就会释放锁

产生死锁的四个必要条件

  1. 互斥条件:一个资源每次只能被一个进程使用(单资源单线程用)
  2. 请求与保持条件:一个进程因请求资源阻塞时,不释放已获得的资源(线程不主动放资源)
  3. 不剥夺条件:进程已获得的资源,在未使用完前不能强行剥夺(线程不被动放资源)
  4. 循环等待条件:若干进程之间形成一种环形资源等待关系(资源需求形成闭环)

破除一条以上则可避免死锁发生

Lock接口(锁)

Lock接口锁是JDK 5.0开始提供的一种与线程的同步synchronized关键字相对的加锁方式

Lock接口 简介

在这里插入图片描述

Lock在代码中具体实现方式

在这里插入图片描述

Synchronized同步 与 Lock接口 的对比

在这里插入图片描述

线程的协作(生产者消费者问题)

生产者消费者模式:
这不是设计模式,是一个线程同步中的问题。
生产者和消费者共用一个仓库资源。
仓库没东西时,消费者等待,生产者生产后放入仓库,并通知消费者消费。
仓库有东西时,生产者等待,消费者消费后,通知生产者再次生产。

Synchronized同步 在这问题中,只可以解决并发问题,但是无法做到通知传递。

于是乎,Java在Object类中提供了几个解决线程间通信问题的method。

等待:

wait() 表示线程一直会等待到被其他线程唤醒,会释放锁。
wait(long timeout) 等待指定的毫秒数

唤醒:

notify() 唤醒一个处于等待的线程
notifyAll() 唤醒同一个对象上所有调用wait()方法的线程,优先级高的优先调度

无论是等待还是唤醒,只能在synchronized method或synchronized block中使用,否则会抛出IllegalMonitorStateException异常

管程法

在这里插入图片描述

信号灯法

信号灯法就是管程长度为1的管程法,通过一个flag判断是否通知其他线程启动

线程池

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值