Java多线程与JUC

前言:多线程相关概念理解

大家应该都听过多线程这个概念,那到底是什么呢?下面辨析一些多线程里的概念,让大家对多线程理解更深一点,我也算是整理我的知识。

1.并发与并行

  • 并发:两个或多个事件在用一个时间段内发生

  • 并行:两个或多个事件在同一时刻发生(同时发生)

在这里插入图片描述
在我们的计算机中,如果你的CPU是多核的,这个核的数量就是处理任务的线程的数量,比如你是双核的,那你的计算机便能并行处理两个任务。如果你只有单核,但又有多个任务或者双核有两个以上的任务,这时候怎么办呢?这时候就是并发处理任务了。
在这里插入图片描述

并发处理时,我们的CPU会在多个线程间反复横跳,一下子执行A线程,一下子执行B线程。由于CPU操作的时间是毫秒甚至是纳秒级别的,所以对我们来说可以忽略不记,可以理解把他们当作是同时发生的,但对于某一个时间点,都只有一个线程在工作。

2.进程与线程

关于多线程,对于线程与进程也是要理解一下:

  • 程序:是指令和数据的的有序集合,其自身没有任何执行的概念,是一个静态的概念。
  • 进程:表示程序执行一次的过程,是一个动态的概念,也是系统分配资源的单位。
  • 线程:一个进程里有许多个线程,但至少也要有一个线程,不然进程便失去意义了。线程是CPU执行和调用的单位。

3.线程调度

在并发执行的时候,有多个线程要去抢夺CPU的执行权,那谁决定它们的执行权呢?线程调度器决定。它会随机选择一个线程去执行,当然前提是线程的优先级都一样,下面我们会讲到有一些更高优先级的线程(VIP线程)它们会先被线程调度器调用。

4.多线程

在我们学习多线程之前,我们使用的一直是单线程。我们都知道,程序是从main方法开始执行的,我们一直使用的单线程也就是main线程,也叫主线程。多线程也就是同时开启多个线程去并发的工作。

比如我们观看一个视频,它就是多线程同时工作的。声音、图形、字幕都是不同的线程,它们并发执行组成一部完整影片。
在这里插入图片描述
从上图可以得知,如果我们没有使用线程类,那就是主线程一条直线执行完;如果我们有了一个线程类,但是我们是直接调用它的run方法,这样也仍然是单线程作业;只有我们调用它的start方法去开启线程,才能实现多线程操作。run()start()下面会讲到。

有一点需要注意:在开启多线程的时候,都是从主线程作为入口先执行主线程的。比如下面的代码,在开启多线程之前,一定先执行主线程里的东西。

public class A {
   
    public static void main(String[] args) {
   
        System.out.println("无论如何爷先执行");
        new Thread(new Receive(5555)).start();
        System.out.println("这里就不一定先执行了,看线程调度大哥的心情");
    }
}

开启多线程的三种方式

1.继承Thread类

  1. 自定义线程类继承Thread
  2. 重写run方法,编写线程逻辑体
  3. 创建线程对象,调用start()方法开启线程

在这里插入图片描述

2.实现Runnable接口

  1. 自定义类实现Runnable接口
  2. 实现run()方法, 编写线程逻辑体
  3. 创建线程对象,把自定义类作为参数传入,调用start()方法开启线程
    在这里插入图片描述

这时候你可能就有疑问了?咦为啥还要有这种方式呢,第一种不够吗?确实不够,使用实现Runnable接口来开启线程优势是很明显的:

  • 避免单继承的局限:一个类只能继承一个类(一个人只能有一个亲爹),类继承了Thread类就不能继承其他的类,实现了Runnable接口,还可以继承其他的类,实现其他的接口。可以是线程逻辑更加灵活。
  • 增强了程序的扩展性,降低了程序的耦合性(解耦):将线程的逻辑和线程的操作分开了,Thread类中还有许多用来操作线程的方法,这里将执行逻辑与操作方法分离开,体现了面向对象的思想。
  • 适合多个相同程序代码的线程去处理同一个资源:比如有多个线程它们的执行逻辑都是一样的,如果使用runnable接口就不必写多个重复的线程类了。

其实还有一点,大家可能都没有发现,就是这种实现runnable接口的方式其实是一种静态代理的方式。Thread是中介对象,Runnable接口是真实对象。

启动采用静态代理的模式:

  1. 创建真实角色

  2. 创建代理对象 (Thread 持有 真实角色 引用)

  3. 代理对象.start 启动

3.实现Callable接口

  1. 自定义线程类实现callable接口,需要定义返回值类型
  2. 重写call方法,定义线程逻辑,需要抛出异常
  3. 创建自定义类对象(目标对象)
  4. 创建线程池,产生线程,这种方式不能直接new出一个线程
  5. 利用线程池的sumit方法,将目标对象作为参数传入,返回一个Future,这个类后面会讲解到
  6. 利用Futrue的get方法获取线程执行结果
  7. 关闭线程池

在这里插入图片描述
你可能又有疑问了,第二种又不够了?确实不够,因为第三种的好处是显而易见的,但是我们日常使用中较多还是使用第二种,第三种的使用还是较少的,我们它创建过程就可以看出第三种的优势:

  • 拥有返回值
  • 能够抛出异常

Thread类

通过第二种方式创建线程的时候,我们已经知道实现runnable接口其实是一种静态代理模式,Thread类是代理对象。那它到底能做什么呢?

我们可以把它和Runnable按代理模式的角度来理解:Runnable接口是真实角色(被代理的对象),里面规定了任务的逻辑,这里也就是线程的逻辑;Thread是它的代理对象,所以它也实现了Runnable接口,拥有编写线程逻辑的功能,同时它里面也定义了许多方法用来操作线程,实现了对Runnable的增强。

总结一下:Runnable实现线程逻辑,Thread可以理解成工具类,提供给我们操作线程的方法。

1.知识引入:线程的状态

在这里插入图片描述
线程重新建到死亡一共有上图的几种状态,了解它们对我们下面学习Thread的方法有很大好处。实际上,Thread的方法就是让线程的状态改变。

  • 新生状态(NEW)
    在程序中用构造方法(new操作符)创建一个新线程时,如new Thread(),该线程就是创建状态,此时它已经有了相应的内存空间和其它资源,但是还没有开始执行。

  • 就绪状态(READY)

    新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态(runnable)。

    由于还没有分配CPU,线程将进入线程队列排队,等待 CPU 服务,这表明它已经具备了运行条件。当系统挑选一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态。

    系统挑选的动作称之为“CPU调度"。一旦获得CPU线程就进入运行状态并自动调用自己的run方法

  • 运行状态(RUNNING)

    当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的 run()方法

    run()方法定义了该线程的操作和功能。运行状态中的线程执行自己的run方法中代码。直到调用其他方法或者发生阻塞而终止。

  • 死亡状态(TERMINATED)

    线程调用 stop()方法、destory()方法或 run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力,即一个线程死亡后不能再重写调用start()方法。

上面那些状态可以只是有个理解就好,下面的阻塞状态和等待超时等待状态则是大家必须掌握的,这是一些很重要的线程状态。

如果你要将线程状态分的大一点的话,等待状态和超时等待状态都会归为阻塞状态,这里把它分的小一点。网上很多博客将这些归结在一起,统称为阻塞状态,我们看他们博客时要懂得更具语境判断是哪种状态。

下图的阻塞状态就是将等待状态和超时等待状态也包括进去了的。

在这里插入图片描述

  • 阻塞状态(BLOCKED)

    阻塞状态特指一些线程在争取锁的过程中失败,然后进入的一种状态。当线程被阻塞时,JVM会把它们放在锁池队列中。锁池队列中的线程会等待锁的释放,然后它们会再次去竞争这个锁,获得锁的则会进入就绪状态,等待线程调度的翻牌。

    对于锁池的加深理解:

    1. 当前线程想调用对象A的同步方法时,发现对象A的锁被别的线程占有,此时当前线程进入锁池状态。简言之,锁池里面放的都是想争夺对象锁的线程。
    2. 当一个线程1被另外一个线程2唤醒时,1线程进入锁池状态,去争夺对象锁。
    3. 锁池是在同步的环境下才有的概念,一个对象对应一个锁池。
  • 等待状态(WAITING)

    等待状态也叫无限等待状态,当一个线程在执行过程中调用了wait()方法,它便会进入等待状态,进入JVM中的等待队列中(waitting queue),这种等待状态是无限时间的,直到它被notify()或者notifyAll()唤醒或者它被打断进入死亡状态.

    被唤醒的线程重新进入就绪状态,等待线程调度而不是直接进入运行状态。此时线程会释放掉锁。

    如果有用同步方法或者用锁锁住,这时线程就没有马上进入就绪状态,而是会进入锁池等待获取锁。

    对于等待队列的加深理解:

    1. 等待队列中存放的线程是被同一个锁阻塞的线程,不是所有锁阻塞的线程都在同一个等待队列里。
    2. 一定要注意是同一个锁阻塞才会出现在同一个队列里!!!
    3. 可以理解为每一个锁配有一个专门的等待队列,用来放被它阻塞的线程,不同锁的等待队列里面的线程不一样。
  • 超时等待状态(TIME_WAITING)

    这个状态可以看出是上一个状态的补充,它不仅能用唤醒方法唤醒,也能等超过规定的等待时间后,自己进入就绪状态。

    它也同等待状态一样,醒来都是进入就绪状态,且在沉睡过程释放锁。如果有用同步方法或者用锁锁住,就会进入锁池。

对于等待和超时等待状态,要注意一些方法,比如join()sleep(),它们有些特殊,它们虽然也是会进入等待和超时状态,但它们不会进入等待队列。下面会讲解到。对于线程的状态,这里有一篇博客,大家也可以去看下:

https://blog.csdn.net/xingjing1226/article/details/81977129?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522159540187219195264564454%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=159540187219195264564454&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v3~pc_rank_v2-1-81977129.first_rank_ecpm_v3_pc_rank_v2&utm_term=%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%AD%89%E5%BE%85%E7%8A%B6%E6%80%81&spm=1018.2118.3001.4187


2.普通方法:不改变线程状态

  • void run()

    首先是run方法,Runnable接口里面就只有一个run方法,表示线程的逻辑。Thread代理Runnable,所以也实现了这个方法,我们使用第一种方式创建线程的时候就是重写了这个方法,所以两种创建线程的方式都是重写了run()方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值