进程和多线程的概念及线程的优点
通过查看“Windows任务管理器”中的列表,完全可以将运行在内存中的exe文件理解成进程,进程是受操作系统管理的基本运行单元。
那什么是线程呢?线程可以理解成是在进程中独立运行的子任务。比如,QQ.exe运行时就有很多的子任务在同时运行。再如,好友视频线程、下载文件线程、传输数据线程、发送表情线程等,这些不同的任务或者说功能都可以同时运行,其中每一项任务完全可以理解成是“线程”在工作,传文件、听音乐、发送图片表情等功能都有对应的线程在后台默默地运行。
这样做有什么优点呢?更具体来讲,使用多线程有什么优点呢?其实如果读者有使用“多任务操作系统”的经验,比如Windows系列,那么它的方便性大家应该都有体会:使用多任务操作系统Windows后,可以最大限度地利用CPU的空闲时间来处理其他的任务,比如一边让操作系统处理正在由打印机打印的数据,一边使用Word编辑文档。而CPU在这些任务之间不停地切换,由于切换的速度非常快,给使用者的感受就是这些任务似乎在同时运行。所以使用多线程技术后,可以在同一时间内运行更多不同种类的任务。
创建线程
- 继承Thread类
说明在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
线程是一个子任务,CPU以不确定的方式,或者说是以随机的时间来调用线程中的run方法,所以就会出现先打印“运行结束!”后输出“MyThread”这样的结果了。
如果多次调用start()方法,则会出现异常Exception in thread “main” java.lang.IllegalThreadStateException。
start()方法和run()方法的区别
Thread.java类中的start()方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。
另外还需要注意一下,执行start()方法的顺序不代表线程启动的顺序。
- 实现Runnable接口
如果欲创建的线程类已经有一个父类了,这时就不能再继承自Thread类了,因为Java不支持多继承,所以就需要实现Runnable接口来应对这样的情况。
有两个构造函数Thread(Runnable target)和Thread(Runnable target,String name)可以传递Runnable接口,说明构造函数支持传入一个Runnable接口的对象。
构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
非线程安全问题
在某些JVM中,i–的操作要分成如下3步:
1)取得原有i值。
2)计算i-1。
3)对i进行赋值。
在这3个步骤中,如果有多个线程同时访问,那么一定会出现非线程安全问题。
非线程安全主要是指多个线程对同一个对象中的同一个实例变量进行操作时会出现值被更改、值不同步的情况,进而影响程序的执行流程。
通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。
留意i–与System.out.println()的异常
虽然println()方法在内部是同步的,但i–的操作却是在进入println()之前发生的,所以有发生非线程安全问题的概率
currentThread()方法
currentThread()方法可返回代码段正在被哪个线程调用的信息。
MyThread.java类的构造函数是被main线程调用的,而run方法是被名称为Thread-0的线程调用的,run方法是自动调用的方法。
更改代码如下:
isAlive()方法
方法isAlive()的功能是判断当前的线程是否处于活动状态。
另外,在使用isAlive()方法时,如果将线程对象以构造参数的方式传递给Thread对象进行start()启动时,运行的结果和前面示例是有差异的。造成这样的差异的原因还是来自于Thread.currentThread()和this的差异。
sleep()方法
方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
停止线程
在Java中有以下3种方法可以终止正在运行的线程:
1)使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。
2)使用stop方法强行终止线程,但是不推荐使用这个方法,因为stop和suspend及resume一样,都是作废过期的方法,使用它们可能产生不可预料的结果。
3)使用interrupt方法中断线程。
判断线程是否是停止状态
调用interrupt()方法仅仅是在当前线程中打了一个停止的标记,并不是真的停止线程。
在介绍如何停止线程的知识点前,先来看一下如何判断线程的状态是不是停止的。在Java的SDK中,Thread.java类里提供了两种方法。
1)this.interrupted():测试当前线程是否已经中断。
测试当前线程是否已经中断,当前线程是指运行this.interrupted()方法的线程。
来判断thread对象所代表的线程是否停止,但从控制台打印的结果来看,线程并未停止,这也就证明了interrupted()方法的解释:测试当前线程是否已经中断。这个“当前线程”是main,它从未中断过,所以打印的结果是两个false。
更改为如下代码:
从上述的结果来看,方法interrupted()的确判断出当前线程是否是停止状态。但为什么第2个布尔值是false呢?查看一下官方帮助文档中对interrupted方法的解释:测试当前线程是否已经中断。线程的中断状态由该方法清除。换句话说,如果连续两次调用该方法,则第二次调用将返回false
2)this.isInterrupted():测试线程是否已经中断。
从结果中可以看到,方法isInterrupted()并未清除状态标志,所以打印了两个true。
最后,再来看一下这两个方法的解释。
1)this.interrupted():测试当前线程(运行interrupted方法的线程)是否已经是中断状态,执行后具有将状态标志置清除为false的功能。
2)this.isInterrupted():测试线程Thread对象(调用interrupt的线程)是否已经是中断状态,但不清除状态标志。
停止线程具体方法
-
异常法
-
先调用sleep再调用interrupt
-
先调用interrupt再调用sleep
-
使用stop方法暴力停止
-
将方法interrupt()与return结合使用也能实现停止线程的效果。
暂停线程
暂停线程意味着此线程还可以恢复运行。在Java多线程中,可以使用suspend()方法暂停线程,使用resume()方法恢复线程的执行。
在使用suspend与resume方法时,如果使用不当,极易造成公共的同步对象的独占,使得其他线程无法访问公共同步对象。
在使用suspend与resume方法时也容易出现因为线程的暂停而导致数据不同步的情况。
yield方法
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU执行时间。但放弃的时间不确定,有可能刚刚放弃,马上又获得CPU时间片。
线程优先级
在操作系统中,线程可以划分优先级,优先级较高的线程得到的CPU资源较多,也就是CPU优先执行优先级较高的线程对象中的任务。设置线程优先级有助于帮“线程规划器”确定在下一次选择哪一个线程来优先执行。
在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw newIllegalArgumentException()。
在Java中,线程的优先级具有继承性,比如A线程启动B线程,则B线程的优先级与A是一样的。
线程优先级和代码顺序无关,线程优先级最高的也不一定最先执行,也具有随机性
守护线程
在Java线程中有两种线程,一种是用户线程,另一种是守护线程。
守护线程是一种特殊的线程,它的特性有“陪伴”的含义,当进程中不存在非守护线程了,则守护线程自动销毁。典型的守护线程就是垃圾回收线程,当进程中没有非守护线程了,则垃圾回收线程也就没有存在的必要了,自动销毁。用个比较通俗的比喻来解释一下“守护线程”:任何一个守护线程都是整个JVM中所有非守护线程的“保姆”,只要当前JVM实例中存在任何一个非守护线程没有结束,守护线程就在工作,只有当最后一个非守护线程结束时,守护线程才随着JVM一同结束工作。Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是GC(垃圾回收器),它就是一个很称职的守护者。