java基础知识总结--多线程
Posted baoziy
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java基础知识总结--多线程相关的知识,希望对你有一定的参考价值。
1、扩展Java.lang.Thread类
1.1、进程和线程的区别:
进程:每个进程都有自己独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1~n个线程。
线程:同一类线程共享代码和数据空间,每一个线程有独立的运行栈和程序计数器,线程切换开销比较小。
进程和线程一样都分为五个阶段:创建、就绪、运行、阻塞、终止。
多进程是指操作系统能同时运行多个任务(程序)。
多线程是指在同一个程序中有多个顺序流在执行。
1.2、在Java中要想实现多线程,有两种方法:继承Thread类、实现Runable接口。
程序启动运行main的时候,Java虚拟机启动一个进程,主线程main在main()调用的时候被创建。随着调用Thread1的两个对象的start方法,另外两个线程也启动了,这样整个应用就在多线程下运行。
注意:start()方法的调用后并不是立即执行多线程代码,而是使该线程变为可运行状态(Run able),什么时候运行是由操作系统决定的。
从程序运行的结果来看,多线程程序的执行顺序是不确定的,因此,只有乱序执行的代码才是有必要设计为多线程。Thread.sleep()方法调用的目的是不让当前线程霸占该进程所获取的CPU资源,已留出一定的世家给其他的线程执行的机会。实际上所有的多线程代码执行顺序都是不确定的,每次的执行结果都是随机的。
如果对同一个对象重复调用start()方法的话,会出现java.lang.IllegalThreadStateException的异常。
2、实现Java.lang.Runable接口
Thread2类通过实现Runable接口,使得该类具有了多线程的特征。run()方法是多线程程序的一个约定,所有的多线程代码都在run()方法里面。Thread()类实际上也是实现了Runable()接口的类。
在启动多线程的时候,需要先通过Thread类的构造方法Thread(Runable target)构造对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread类的start()方法来运行的。因此不管是继承Thread类还是实现Runable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的,熟悉Thread类的API是进行多线程编程的基础。
3、Thread和Runable 的区别
如果一个类继承Thread,那么不适合资源共享。但是如果实现了Runable接口的话,则很容易实现资源共享。
这段代码Thread1继承了Thread类的情况:
从运行结果来看,不同的线程之间count是不同的,这样的情况对于售票系统来说就会有很大的问题,当然这里可以用同步来解决。
这段代码是Thread2实现了Runable接口的情况:
如果测试类和上边的创建Thread2的方法不一样:
上面的两种创建方法主要是为了说明创建完成的Thread2类对象再接着创建Thread类对象的时候,调用的构造器不同,输出的结构也是不一样的。后面那个参数可以 表示一个线程名字如果不指定的话就会生成一个默认的名字。
这里需要注意的是每个线程都是用同一个实例化对象,如果不是同一个,那效果就会上边用Thread1继承Thread的一样了。
总结:实现Runable接口比继承Thread类所具有的优势:
- 适合同一个程序代码的多个线程去处理同一个资源。
- 可以避免Java中的单继承限制
- 增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
需要特别提醒:main()方法也是一个线程,在Java中的所有的线程都是同时启动的,至于是什么时候开始执行、哪个会先执行完全看哪个线程最先得到CPU资源。在Java中每次程序运行至少启动两个线程,一个是main线程,另一个是垃圾收集线程。因为每当使用Java命令执行一个类的时候,实际上就会启动一个Java虚拟机,每一个Java虚拟机在就相当于是在操作系统中启动了一个进程。
4、线程状态的转换
4.1、新建状态:新创建了一个线程对象。
4.2、就绪状态::线程对象创建好了以后,改状态的线程位于可运行线程状态池中,变得可以运行,只需要等待获取到CPU资源。
4.3、运行状态:就绪状态的线程获取到CPU资源变成运行状态指向程序代码。
4.4、阻塞状态:因为某种原因需要放弃CPU使用权,暂停程序的运行,直到线程进入就绪状态才有机会转入运行状态。阻塞分为三种情况:
- 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
- 其他阻塞:运行的线程执行sleep()或者join()方法,或是发出了I/O请求时,JVM会把该线程置为阻塞状态。但需要注意的是当sleep()状态超时、join()等待线程结束或者超时、I/O处理完毕的时候,线程重新进入就绪状态。
4.5、死亡状态:线程执行完或者因异常退出了run()方法时,该线程的生命周期结束。
5、线程调度
5.1、调整线程的优先级:Java线程有优先级,优先级高的线程会有获得较多的运行机会。
Java线程的优先级用整数表示,取值范围是1~10,Thread类有三个静态常量:
- static int MAX_PRIORITY 线程具有最高的优先级,10
- static int MIN_PRIORITY 线程具有最低的优先级 1
- static int NORM_PRIORITY 分配给线程的默认优先级 5
Thread类的setPriority() 和getPriority()方法分别是用来设置和获取线程优先级的方法。每个线程都有一个默认的优先级。主线程的默认优先级为5,线程的优先级有继承关系,比如说A线程中创建了B线程,那么B和A具有相同的优先级。JVM提供了10个线程优先级,但是与常见的操作系统都不能很高的映射,如果希望程序能够移植到各个操作系统上,应该仅仅使用Thread类有的三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。
5.2、线程睡眠:Thread.sleep(long millis)方法是线程转到阻塞状态,millis参数设定睡眠时间,单位为毫秒,当睡眠结束时,就会自动转为就绪状态。该静态方法的移植性比较好。
5.3、线程等待:Object类中的wait()方法将会使当前的进程进入等待状态,直到其他线程调用此对象的notify()方法或者notifyAll()方法唤醒该线程。这两个唤醒方法也是Object类中的方法,行为等价于调用wait(0)方法。
5.4、线程让步:Thread.yield()方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更好优先级的线程。使用该方法的目的是让相同优先级的线程之间能适当的轮转执行,但是,实际中无法保证yield()达到让步的目的,因为让步的线程还是有可能被线程调度程序再次选中。
注意:yield()方法从未导致线程转到等待/睡眠/阻塞状态,在大多数情况下,yield()方法将会导致线程从运行状态转到可执行状态,但是有可能没有效果。
5.5、线程加入:join()方法,等待其他线程终止,在当前的线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个线程运行结束,当前线程再有阻塞转为就绪状态。
5.6、线程唤起:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程,如果所有的线程都在此对象等待,则会选择唤醒一个线程,这个选择是任意的。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。注意:Thread类中的suspend()和resume()方法已经被废除不再使用,因为有死锁倾向。
6、常用函数的说明
6.1、
Thread.sleep(long millis)方法:在指定的时间内让当前正在指向的线程休眠。
join()方法:指等待t线程终止。join()在启动线程后直接调用,他的作用是“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法以后后面的代码只能等到子线程结束了以后才能执行。
为什么要加入join()方法:
在很多情况下主线程生成并启动了子线程,但是假如子线程中有很多需要进行大量耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理其他的事物需要用到子线程的处理结果,也就是主线程需要等待子线程执行完在结束,这个时候就需要使用join()方法了。
sleep()和yield()的区别:
sleep()是当前线程进入停滞状态,所以执行完sleep()的线程在指定的时间内肯定不会被执行,但是yield()方法只是使当前的线程回到可执行状态,所以执行完yield()方法 的线程有可能在进入到可执行状态后马上又被执行。
Sleep()方法使当前运行中的线程睡眠一段时间,这段时间是不可运行状态,这段时间的长短是由程序人为设置的。yield()方法使当前的线程让出CPU占有权,但是让出的时间是不可设定的。
Sleep()方法允许较低优先级的线程获得运行机会,但是yield()方法执行时,当前线程仍然处于可运行状态,所以不可能让较低优先级的线程获得CPU占有权,在一个运行系统中,如果较高优先级的线程没有调用sleep()方法,有没有受到阻塞,那么较低优先级的线程只能等多有较高优先级的线程运行结束才有机会执行。
6.2、interrupt()方法:中断某一个线程,这种结束方式比较粗暴,如果某一个线程打开了某一个资源还没有来得及关闭也就是run()方法还没有执行完成就强制结束线程,会导致资源无法关闭。
6.3、wait()方法:
Obj.wait()和Object.notify()必须要与synchronized(obj)一起使用,也就是wait和nofity是针对已经获取了对象锁进行操作的。从语法的角度来说这两个方法必须是在synchronized(obj){…}语句块内,从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其他的线程调用对象的nofity()唤醒该线程,才能继续获取对象锁并继续执行。相应的nofity()方法就是对对象锁的唤醒。但是有一点需要注意nofity()方法调用之后并不是马上就释放对象锁而是在相应的synchronized(obj){…}语句块执行结束。自动释放以后,JVM会在wait()对象锁的线程中随机选取一个线程,赋予其对象锁,唤醒线程继续执行。sleep()方法和wait()方法二者都可以暂停当前的线程,释放CPU资源,主要的区别就是wait()在释放CPU资源的同时释放了对象锁的控制。
6.4 wait和sleep的区别:
共同点:
- 他们都是在多线程的环境下都可以在程序的调用处阻塞指定的毫秒数,并返回。
- Wait’和sleep都可以通过interrupt方法打断线程的暂停状态,从而使线程立即抛出InterruptedException,如果线程A希望立即结束线程B,则可以对线程B对应的Thread实力调用interrupt方法。如果线程B正在wait/sleep/join/。则线程B会立即抛出InterruptException,在catch中直接return即可安全的结束线程。需要注意,这个异常是线程自己从内部抛出来的,并不是interrupt方法抛出的,对某一个线程调用interrupt时,如果该线程正在执行普通的代码,那么该线程不会抛出异常,除非进入上边所述的三种状态其中之一。
不同点:
- Thread类的方法:sleep(),yield()等 , Object的方法:wait()和notify()等
- 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常,所以sleep()和wait()方法的最大区别是:sleep()睡眠时,保持对象锁,仍然占有该锁;而wait()睡眠时,释放对象锁。但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。
7、常见的名词解释
1) 主线程:JVM调用程序main()方法所产生的线程;
2) 当前线程:一般指通过Thread.currentThread()来获取的进程;
3) 后台线程:指为其他线程提供服务的线程,也称为守护线程,JVM的垃圾回收线程就是一个后台线程。用户线程和守护线程的区别在于是否等待主线程,依赖主线程结 束而结束。
4) 前台线程:是指接受后台服务的线程,其实嵌套后台线程是联系在一起的,就像傀儡和幕后操作者一样的关系。
5) 线程类的常用方法:
1) sleep(): 强迫一个线程睡眠N毫秒。
2)isAlive(): 判断一个线程是否存活。
3) join(): 等待线程终止。
4)activeCount(): 程序中活跃的线程数。
5)enumerate(): 枚举程序中的线程。
6)currentThread(): 得到当前线程。
7)isDaemon(): 一个线程是否为守护线程。
8)setDaemon(): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
9) setName(): 为线程设置一个名称。
10)wait(): 强迫一个线程等待。
11)notify(): 通知一个线程继续运行。
12)setPriority(): 设置一个线程的优先级。
以上是关于java基础知识总结--多线程的主要内容,如果未能解决你的问题,请参考以下文章