前言
在开发中我们经常使用线程来优化程序,提高系统执行效率,今天我们就来简单概述一下Java开发过程中需要了解的多线程知识点。首先,整理出一张图概括了Java多线程的体系:
一、进程与线程
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。线程是程序中一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。
进程和线程的关系可以用下图来描述:
二、同步与异步
对于一次方法的调用来说,同步方法调用一旦开始,就必须等待该方法的调用返回,后续的方法才可以继续执行;异步的话,方法调用一旦开始,就可以立即返回,调用者可以执行后续的方法,这里的异步方法通常会在另一个线程里真实的执行,而不会妨碍当前线程的执行。
三、并行与并发
并发和并行是两个相对容易比较混淆的概念。他都可以表示在同一时间范围内有两个或多个任务同时在执行,但其在任务调度的时候还是有区别的,首先看下图:
并发任务执行过程:
从上图中可以看到,两个任务在执行的时候,并发是没有时间上的重叠的,两个任务是交替执行的,由于切换的非常快,对于外界调用者来说相当于同一时刻多个任务一起执行了;而并行可以看到时间上是由重叠的,也就是说并行才是真正意义上的同一时刻可以有多个任务同时执行。
四、线程的状态
线程从创建、运行到结束总是处于下面五个状态之一:新建状态、就绪状态、运行状态、阻塞状态及死亡状态。
1、新建状态(New):
当用new操作符创建一个线程时,例如new Thread(r),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码。
2、就绪状态(Runnable)
一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态,处于就绪状态的线程并不一定立即运行run()方法,线程还必须同其他线程竞争CPU时间,只有获得CPU时间才可以运行线程。因为在单CPU的计算机系统中,不可能同时运行多个线程,一个时刻仅有一个线程处于运行状态。因此此时可能有多个线程处于就绪状态,对多个处于就绪状态的线程是由Java运行时系统的线程调度程序(thread scheduler)来调度的。
3、运行状态(Running)
当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法。
4、 阻塞状态(Blocked)
线程运行过程中,可能由于各种原因进入阻塞状态: 1)线程通过调用sleep方法进入睡眠状态; 2)线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者; 3)线程试图得到一个锁,而该锁正被其他线程持有; 4)线程在等待某个触发条件; …… 所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。
5、 死亡状态(Dead)
有两个原因会导致线程死亡: 1) run方法正常退出而自然死亡; 2) 一个未捕获的异常终止了run方法而使线程猝死。 为了确定线程在当前是否存活(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。
现在我们用一张图来说明它们之间的状态:
五、创建线程的三种方式
(1)继承Thread类创建线程类
1、定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务.因此把run()方法称为线程执行体。
2、创建Thread子类的实例,即创建了线程对象。
3、调用线程对象的start()方法来启动该线程。
方法的方法体就是主线程的线程执行体。
可以看到Thread-0和Thread-1两个线程的输出的i变量不连续 注意:i变量是FirstThread的实例变量,而不是局部变量,但是因为程序每次创建线程对象都需要创建一个FirstThread对象,所以Thread-0和Thread-1不能共享该实例变量。
使用继承Thread类的方法来创建线程类时,多个线程之间是无法共享线程类的实例变量。
(2) 实现Runnable接口创建线程类
1、定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
2、创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
3、调用线程对象的start()方法来启动该线程。
深入浅出:Java多线程编程实战(一)
当线程类实现Runnable接口时,如果想获取当前线程,只能用Thread.currentThread()方法可以看到两个子线程的i变量是连续的这是因为采用Runnable接口的方式创建的多个线程可以共享线程类的实例变量.是因为:程序创建的Runnable对象只是线程的target,而多个线程可以共享一个target,所以多个线程可以共享一个线程类(实际上应该是线程的target类)的实例变量。
(3)使用Callable和Future创建线程
通过实现Runnable接口创建多线程时,Thread类的作用就是把run()方法包装成线程执行体.从方法可以声明抛出的异常。
但是Callable接口并不是Runnable接口的子接口,所以Callable对象不能直接作为Thread的target.而且call()方法还有一个返回值,call()方法并不是直接调用的,它是作为线程执行体被调用的.好在方法的返回值,并为Future接口提供了一个FutureTask实现类,该实现类既实现了Future接口,并实现了Runnable接口—-可以作为Thread类的target。
在Future接口里定义了几个公共方法来控制它关联的Callable任务。
Callable接口有泛型限制,并且Callable接口里的泛型形参类型与call()方法返回值类型相同.而且Callable接口是函数式接口,可以用Lambda表达式创建Callable对象。
创建并启动具有返回值的线程的步骤如下:
1、创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,且该call()方法有返回值,再创建Callable实现类的实例。
2、使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
3、使用FutureTask对象作为Thread对象的target创建并启动新线程。
4、调用FutureTask对象的get()方法来获得子线程执行结束后的返回值。
(4)创建线程的三种方式对比
采用实现Runnable、Callable接口的方式创建多线程的优缺点:
1、线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。
2、多个线程可以共享同一个target对象,非常适合多个相同线程来处理同一份资源的情况,较好的体现了面向对象的思想。
3、需要访问当前线程,则必须使用Thread.currentThread()方法。
采用继承Thread类的方式创建多线程的优缺点:
1、因为该线程已经继承了Thread类,所以不能在继承其他父类。
2、编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
总结
之后还会更新多线程相关知识点和案例剖析,学习毕竟是一个循序渐进的过程,需要时间精力,更需要技巧和规律。天赋往往存在少数人身上,绝大多数人只能不断摸索,有的人花费大量时间还一无所获,有些人却既能轻松工作,又享受了生活。
其中的原因不得而知:站在巨人的肩膀上必定是要比站在地上攀爬的人快一步的。
而现在就有一个平台可以提供给你学习,让你在实践中积累经验掌握原理。主要方向是JAVA架构师。在这里你可以学习Java工程化、高性能及分布式、深入浅出。性能调优、Spring,MyBatis,Netty源码分析和大数据等知识点。
如果你想拿高薪、想突破瓶颈、想在竞争中占据优势、想进BAT但又担心面试不过的,想要了解详情的可以加入Java后端技术群:819940388,或关注微信公众号:Java资讯库,回复“架构”,免费的大型互联网Java面试视频分享给大家。
最后,分享跟大家分享汪国真的一句诗“既然选择了远方,便只顾风雨兼程”,学习我们不再只体验过程,也需要关注结果。
今天的文章深入浅出:Java多线程编程实战(一)分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/30450.html