java多线程、资源共享和死锁
一、进程和线程的概念和区别
进程是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元,并且进程中包含一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问,是系统进行资源分配和调度的一个独立单位。
线程是操作系统分配处理器时间的基本单元,可以有多个线程同时执行代码,且多个线程共享内存。但每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的 CPU 寄存器组和堆栈。
二、编写多线程
线程的活动都是通过线程的run()方法来实现的。在一个线程被创建并初始化后,java的运行时系统就会自动调用run()方法,正式通过run()方法才使得建立线程的目的得以实现。线程开始时,从run()开始执行,这就是线程执行的起始点。就像应用程序从main()开始一样。
通常run()方法总是某种形式的循环,使得任务一直运行下去直到不再需要。一般情况下,我们会设定跳出循环的条件,如果系统一直不能满足我们设定的跳出条件,run()会永远的运行下去。
下面来具体讲讲多线程是如何实现的:
1、通过实现一个Runnable接口
线程可以驱动任务,因此我们可以通过描述任务的方式,这可以由Runnable接口来提供。要想定义任务,只需要实现Runnable接口并编写run()方法,这就使得任务可以执行你的命令,如:
public class myThread implements Runnable{
public void run(){
while(true){
... ...
}
}
}
在使用的时候,执行要实现这个类,然后调用run()方法就是了。
2、通过继承Thread类的方式
我们可以通过建立一个Thread的子类,通过继承Thread并覆盖其run()方法来构造线程体,使其能充分按自己的吩咐行事。在这里,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。Thread 包含了一个特殊的方法,叫作 start(),它的作用是对线程进行特殊的初始化,然后调用 run()。
整个步骤包括:调用构建器来构建对象,然后用 start()配置线程,再调用 run()。如果不调用 start()——如果适当的话,可在构建器那样做——线程便永远不会启动。注意:每个线程都会“注册”自己,即使没有显示的通过句柄对其引用,在运行的过程中,垃圾回收期也不能对这个类进行回收。例子如下:
public class myThread extends Thread{
public myThread(String str){
super(str);
}
public void run(){
... ...
}
}
myThread thread = new MyThread("1");thread.start();
两种方法的比较:①直接继承Thread类:不能再从其他类继承,但编写简单,可以直接操作线程;
②通过实现Runnable接口构造线程体:可以讲CPU、代码和数据分开,形成清晰的模型;还可以继承其他类;保持程序分隔的一致性。
三、线程的状态
在一个线程的生命周期中,他总是处于某一状态中。线程的状态表示了线程正在运行的活动以及这段时间内线程能完成的任务。线程的状态有:①创建状态(new Thread)②可运行状态(Runnable)③运行状态(Running)④不可运行状态(not Runnable)⑤死亡状态(dead)⑥非法状态异常(lllegalThreadStateException).
①创建状态:他仅仅是一个空的线程对象,系统不为它分配资源;
②可运行状态:系统为这个线程分配了他所需要的资源,安排其运行并调用线程运行方法,这样就使的线程处于可运行状态。需要注意这一状态并不是运行中,以为线程实际上并未真正的运行(很多计算机都是单核的,所以同一时刻所有处于可运行状态的线程都在运行时不可能的)。
③运行中:当一个线程处于可运行状态,并且系统为他分配了他所需要的一些资源,java的运行系统调度选中一个可运行状态的线程,开始执行。
④不可运行状态:也称为堵塞状态(Blocked)。由于某种原因,系统不能执行线程的状态。这时即使处理器空置,也不能执行。具体的原因可能有以下几种:调用了sleep(),但休眠完成后线程进入可运行状态等待重新调度;为等候一个条件变量,线程调用了wait()方法,当满足这个变量后,变量所在的对象调用notify()或notifyAll()方法,唤醒线程进入可运行状态;输入输出流发生线程阻塞,则需要等待阻塞的结束;
⑤死亡状态:线程执行结束,存在两种情况:自然撤销或被停止。自然撤销是自动退出;被停止是所属应用程序(进程)停止运行。
⑥非法状态异常:但一个线程创建后,只能在对应的某个状态进行允许的操作,如果操作不当就会引起非法状态。
四、线程的调度和优先级
虚拟CPU通过使每个线程工作若干步,实现多线程同时运行的。决定实际CPU在那个时刻实际执行那个线程的方法称为线程调度模型。在java中,这个模型与平台有关。
在java中,java提供了一个线程调度器来监控线程中启动后可以进入运行状态的全部线程。线程调度器按照线程优先级的高低选择优先级高的线程执行,同时线程调度是先占式调度,即如果当前线程执行过程中,一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。先占式调度由分为:独占方式和分时方式。
1、调度方式
①独占方式:在这个方式下,当前执行线程将一直运行下去,直到执行完毕或由于某种原因主动退出放弃cpu,或者一个更高优先级的线程进入可运行状态。
②分时方式:在这种方式下,当前运行线程执行当前时间片后,如果有可运行状态的线程,系统将选中其他可运行状态的线程执行,当前线程进入可运行状态,等待下一个时间片的调度。
2、线程优先级
线程的优先级将该线程的重要度传递给了调度器。尽管CPU处理现有线程集的顺序是不确定的,但是调度器更倾向于让优先权最高的线程先运行。然而,这并不意味着优先权较低的线程将得不到运行,优先级低的线程仅仅是执行的频率较低。
线程的优先级用数字表示,范围从1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORITY。一个线程的默认优先级为5。我们可以通过getPriority()方法获得线程的优先级;也可以通过setPriority()方法设置线程的优先级:
public final void setPriority(int newPriority);
public final int getPriority();
五、线程的调度方法
在线程中提供了很多方法,通过这些方法,可以影响线程的运行状态。为了理解这些方法,我们通过一个线程状态变迁图说明:
六、有关线程的其他一些概念和方法
1、后台线程(Daemon)
后台线程(Daemon)是指程序运行时在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。因此,当所有非后台线程结束时,程序也就终止了,同时杀死了进程中所有后台线程。反过来说,只要有任何非后台线程还在运行,后台线程就不会终止。
后台线程必须在线程启动(start)之前调用setDaemon()方法,才能把它设置为后台线程。
2、同步(synchronized)
java中每个对象都对应于一个称为“互斥锁”的标志,这个标志用来保证在任何时刻,只能有一个线程访问该对象。如果系统中的资源当前没有被使用,线程可以得到“互斥锁”,即线程可以得到资源的使用权。当线程执行完毕后,他放弃“互斥锁”,如果一个线程获得“互斥锁”时,其余的线程就必须等待当前线程结束并放弃“互斥锁”。
在java中,提供了关键字synchronized来实现对象的“互斥锁”关系。当某个对象或方法用关键字synchronized修饰时,表明该对象或方法在任何一个时刻只能有一个线程访问。如果synchronized用在类的生命中,表明该类中的所有方法都是synchronized的。
3、线程组(ThreadGroup)
每个线程都是一个县城组的成员,线程组把多个线程集成为一个对象,通过线程组可以同时对其中的多个线程进行操作,如启动一个线程组中的所有线程。java线程组是由ThreadGroup实现。
类ThreadGroup用来管理一个线程组,包括线程数目,线程间的关系,线程正在执行的操作,以及线程将要启动或终止的时间等。线程组中还可以包含线程组,形成线程组和线程组之间的树状关系。
七、资源的同步与共享
前面的例子提到的线程都是独立的,而且是异步执行,也就是每个线程都包含了运行所需的一切资源,而不再需要外部资源和方法,也不关心其它线程状态和行为。但是很多时候,同时运行的线程需要共享一些数据,比如同时对一个文件读写。这就必须考虑其他线程的行为和状态,这时就需要实现同步来的预期的结果。
为了解决资源共享的问题。我们通常采用下面一些方法来解决:
①使用前面介绍的synchronized,将需要共享的资源进行标记(互斥锁)的方法,防止资源冲突;
②显示的Lock对象。在java se5的java.util.concurrent类库包含concurrent.locks中显示的互斥机制。Lock对象必须被显示的创建、锁定和释放。因此,他与内建的锁形式相比,代码缺乏有雅性。但是,对于解决某些类型的问题来说,他更加灵活。
以上是常用的集中方式,如果有需要这部分知识,需要深入研究,我这里只提供一个方向:原子性和易变性的探讨;原子类(如AtomicInteger、TtomicLong);临界区;在其他对象上同步;线程本地存储;
八、死锁问题
为了防止数据并发访问,引用线程共享的对象(条件变量)时,为使数据操作能保持一致性,应设互斥区,进行互斥操作。进入互斥去操作数据,必须先获得互斥锁的使用权。
如果程序中多个线程互相等待对方自愿,而在得到对方资源之前都不会释放自己的资源,这就造成都想得到资源而又得不到,线程就不能继续前进,这就是死锁问题。当然还会有其他死锁类型。但这主要是开发人员不小心造成的。
java技术既不能发现死锁也不能避免死锁。只能依靠程序员自己注意。但这里有一个设计规则:线程因为某个先决条件未满足而受阻,不能让其继续占有资源。如果多个对象需要互斥访问,应确定线程获得锁的一个顺序,并保证贯穿整个程序,并以相反的方向释放锁。
上一篇: Java线程学习笔记(五):线程的优先级
下一篇: c++操作符优先级总结