Java基础之(三十七)Java多线程编程<一>
来源:互联网 发布:淘宝专业版一钻以上 编辑:程序博客网 时间:2024/05/21 06:00
说明
几乎所有的操作系统都支持同时运行多个任务,一个任务就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。
线程和进程
一个操作系统里可以有多个进程,而一个进程里可以有多个线程。
进程
几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。当一个程序进入内存运行后,即变成一个进程。进程是出于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。
一般而言,进程包含如下3个特征:
独立性:进程是系统中独立存在的实体,他可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间;
动态性:进程和程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会相互影响;
注意:并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
现代的操作系统几乎都支持同时运行多个任务:一边开着ide写代码,一边开着网页在查API,同时还在听音乐,用markdown作笔记…这些进程开上去像是在同时工作。
但是真相是:对于一个CPU而言,它在某一个时间点上只能执行一个程序,也就是说只能运行一个进程。CPU不断的在这些进程之间快速轮换执行,那么为什么感觉不到任何中断现象呢?这是因为CPU的执行速度相对我们的感觉来说实在是太快了。*如果启动的程序(进程)足够多,我们依然可以感觉程序的运行速度下降(电脑变卡)。
线程
多线程扩展了多进程的概念,使得同一个进程(注意这里是限于一个进程里!)可以同时并发处理多个任务。
线程(Thread)也被称作轻量级进程(Lightweight Process)==。线程(Thread)是进程(Process)的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流。当进程被初始化之后,主线程就被创建了。对于大多数的应用程序来说,通常仅要求有一个主线程,但我们也可以在该进程内创建多条顺序执行流,这些顺序执行流就是Thread,每条Thread也是互相独立的。
线程是进程的组成部分,一个进程可以有多个线程,一个线程必须有一个父进程。
一个线程可以拥有自己的堆、栈、自己的程序计数器(PC)和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程(Process)所拥有的全部资源。
因为多个线程共享父进程的全部资源,因此编程更加方便;但必须注意的是:必须确保一个线程不会妨碍同一进程里的其他线程!
线程可以完成一定的任务,可与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。
线程是独立运行的,它并不知道进程中是否还有其他线程的存在。线程的运行是抢占式的 ———> 当前运行的线程在任何时候都可能被挂起,以便另一个线程可以运行。
一个线程可以创建和撤销另一个线程(例如在main方法这个主线程里创建另一个线程),同一个进程(Process)的多个线程(Thread)之间可以并发执行(concurrency,多个线程之间快速切换,快到让人感觉是在同时执行)
从逻辑角度来看:多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行;但操作系统无需将多个线程看做多个独立的应用,对多线程实现调度和管理、资源分配由进程本身负责完成。
总结:一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要有一个线程(主线程)。
多线程的优势
线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中的线程之间的隔离程度要小。他们共享内存、文件句柄和其他每个进程应用的状态。
总结起来,使用多线程编程有如下几个优势:
进程间不能共享内存,但线程之间共享内存非常容易;
系统创建进程需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高;
Java给多线程编程提供了内置的支持,从而简化了多线程编程。
线程的生命周期
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。
在线程的生命周期中,它要经过
新建(new)
就绪(runnable)
运行(running)
阻塞(blocked)
死亡(dead)
五种状态。尤其是线程启动以后,它不能一直”霸占“着CPU独自运行,CPU需要在多条线程之间切换,所以线程状态也会多次在运行、阻塞之间切换。
下图显示了一个线程完整的生命周期。
线程存在于好几种状态。线程可以正在运行(running)。只要获得CPU时间它就可以运行。运行的线程可以被挂起(suspend),并临时中断它的执行。一个挂起的线程可以被恢复(resume,允许它从停止的地方继续运行。一个线程可以在等待资源时被阻塞(block)。
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
注意:
启动线程使用start()方法,而不是run()方法!
1) 调用start方法来启动线程,系统会把该run方法当成线程执行体来处理。多个线程之间可以并发执行
2)
但是如果直接调用线程对象的run方法,run方法会被立刻执行,而且在run方法返回之前其他线程无法并发执行。(变成了普通的方法调用!!!)
不要对已经启动的线程再次调用start方法,否则会引发IllegalThreadStateException
线程调度器切换线程由底层平台控制,具有一定随机性
如果希望调用子线程的strat方法立刻执行子线程,可以使用Thread.sleep(1)来让当前运行的线程(主线程)睡眠一个毫秒,这一毫秒内CPU不会空闲,它会立刻去执行一条就绪的线程。
- 运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
注意:
当一条线程开始运行后,它不可能一直处于运行状态(除非线程执行体足够短,瞬间就执行完了),线程在运行过程中需要被中断,目的是使其他线程获得执行的机会,线程调度的细节取决于底层平台所采用的策略。
系统会给每个可执行的线程一小段的时间来处理任务;当该时间段使用完,系统就会剥夺该线程所占据的资源,让其他线程获得执行的机会。在选择下一个线程时,系统会考虑线程的优先级。
就绪和运行状态之间的转换通常不受程序控制,而是由系统线程调度所导致。
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
线程会以以下三种方式之一结束,结束后处于死亡状态:
run方法执行完成,线程正常结束
线程抛出一个未捕获的Exception或Error
直接调用该线程的stop方法来结束该线程(容易导致死锁,不推荐使用!)
注意:
当主线程结束的时候,其他线程不受任何影响,并不会随之结束。一旦子线程启动起来后,它就拥有和主线程相同的地位,不会受主线程的影响(如前面所说,线程之间是相互独立的)。
为了测试某条线程是否已经死亡,可以调用线程对象的isAlive方法。
当线程处于就绪、运行、阻塞三种状态时,该方法返回true
处于新建、死亡两种状态时,返回false。
不可以对一个已经死亡的线程调用start方法,否则会引发IllegalThreadStateException(不能对已经死亡或者已经启动的线程调用start方法,只能对新建状态的线程调用)
创建一个线程
java使用Thread类代表线程,所有的线程都必须是Thread类或其子类。
每条线程的作用是:完成一定的任务,实际上就是执行一段程序流。java使用run方法来封装这样一段程序流,run方法也被成为线程执行体
大多数情况,通过实例化一个Thread对象来创建一个线程。Java定义了两种方式:
可以继承Thread类。
实现Runnable 接口;
通过继承Thread来创建线程
定义Thread类的子类,重写该类run方法。run方法的方法体代表了该线程需要完成的任务(想要它做什么,就往run方法里面写什么)。
创建线程对象
用线程对象的start方法来启动线程。
public class FirstThread extends Thread { //重写run方法 public void run() { for (int i = 0; i < 100; i++) { // 使用getName()方法来返回当前线程的名字 System.out.println(this.getName() + " " + i); } } public static void main(String[] args) { //创建线程对象 FirstThread ft = new FirstThread(); //用线程对象的start方法来启动线程。 ft.start(); for (int i = 0; i < 100; i++) { // 使用Thread类的静态方法 currentThread() 来获取当前线程 System.out.println(Thread.currentThread().getName() + " " + i); } }}
输出结果:main 0Thread-0 0main 1Thread-0 1main 2Thread-0 2main 3Thread-0 3main 4...main 38Thread-0 40Thread-0 41Thread-0 42main 39...
上面的程序只显式的启动了一条线程,但实际上有2条线程,因为还有包含main方法的主线程。主线程的线程体不是有run方法确定的,而是由main方法的方法体来确定。
上面还用到了线程类的两个方法:
Thread.currentThread():返回当前正在执行的线程对象
getName():返回调用该方法的线程的名字。
程序可以通过setName(String name)方法来为线程设置名字。默认情况下下主线程的名字为main,用户启动的多条线程名字依次为Thread-0, Thread-1…
从结果可以看出,主线程和子线程交替运行。它的运行结果是这样的:
执行了ft.start()
时,线程进入就绪状态,run方法不会立即得到执行;但是程序继续向下运行,下一行代码的for循环得到执行,因此主线程抢到CPU,输出主线程的名字main 0
,但这个时候处于绪状态的子线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。因此输出Thread-0 0
。如输出结果所示,交替运行并不代表总是挨个挨个的运行,它是没有规律的。
我们再稍微改进一下上述代码,为它再增加一条线程:
public static void main(String[] args) { for (int i = 0; i < 100; i++) { // 使用Thread类的静态方法 currentThread() 来获取当前线程 System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { new FirstThread().start(); new FirstThread().start(); } } }
输出结果:main 0...main 21...Thread-1 17Thread-0 30main 40...
上面程序Thread-0和Thread-1输出的i并不连续,这是因为i是实例属性,程序每次创建线程对象都需要创建一个FirstThread对象,Thread-0和Thread-1不能共享i。(但如果把i设成static就可以)
使用继承Thread类的方法来创建线程,多条线程之间无法共享实例变量。
通过实现Runnable接口来创建线程
- 定义实现Runnable接口的类,重写run方法
public class SecondThread implements Runnable
public void run()
- 创建Runnable实现类的对象,并以此作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象。代码如下所示:
//创建Runnable实现类的对象SecondThread st = new SecondThread();//以Runnable实现类的对象作为Thread的target来创建Thread对象new Thread(st, "NewThread");
Runnable对象仅仅作为Thread对象的Target(在创建Thread对象时作为参数传进构造方法,Runnale实现类里包含的run方法仅仅作为线程执行体。==而实际的线程对象依然是Thread类的实例,只是该Thread线程负责执行其Target的run方法而已==)
- 调用线程对象的start方法来启动该线程
下面是一个创建线程并开始让它执行的实例:
public class SecondThread implements Runnable { //run()方法同样是线程执行体 public void run() { for (int i = 0; i < 20; i++) { //当线程类实现Runnable接口时, //如果想获取当前进程,只能用Thread.currentThread()方法 System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 20; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 10) { SecondThread st = new SecondThread(); //通过new Thread(target,name)方法创建新线程 new Thread(st, "newThread1").start(); new Thread(st, "newThread2").start(); } } }}
输出结果:main 0...main 10main 11main 12main 13newThread1 0main 14newThread1 1main 15...
这时两个线程的i变量是连续的,因为程序所创建的Runnable对象只是线程的target,而多条线程可以共享同一个target,也就是说可以共享同一个target的所有实例变量。
两种方式对比
几乎所有的多线程应用都采用实现Runnable接口的方式。
- Java基础之(三十七)Java多线程编程<一>
- Java基础之(三十七)Java多线程编程<二>
- Java基础之(三十七)Java多线程编程<三>
- java多线程编程 - 基础篇 (一)
- (转载)多线程编程学习一(Java多线程的基础)
- 温故而知新之Java多线程编程(一)
- Java入门之编程基础(一)
- JAVA多线程基础(一)
- java多线程基础(一)
- JAVA多线程基础(一)
- java多线程基础(一)
- JAVA基础-多线程(一)
- Java多线程基础(一)
- java 多线程基础(一)
- Java多线程编程(一)
- Java多线程编程(一)
- Java多线程编程基础之线程对象
- Java多线程编程基础之线程对象
- iOS开发之计算动态cell的高度并缓存
- HDU3533 Escape (搜索)
- CNTK学习笔记 -- Introduction
- 程序员之所以犯错误,不是因为他们不懂,而是因为他们自以为什么都懂。
- struts.xml(二)<action>使用详解
- Java基础之(三十七)Java多线程编程<一>
- 编程之道
- CF707C(Codeforces Round #368 (Div. 2) - C)
- Java 中 == 比较的是什么?
- 如何部署JSP应用到阿里云服务器上(二)
- Linux下的find指令(文件查找)用法
- 跳转界面的工具类
- Xamarin Android提示内存溢出错误
- JZOJ4735【NOIP2016提高A组模拟8.24】最小圈 Spfa深搜判负环