0.前言
以往我们开发的程序大多是单线程的,即一个程序只有一个入口点,只有一条从头到尾的执行线路。然而现实世界中的很多过程都具有多线索同时进行的特性。例如,我们可以一边写着代码,一边收发邮件。事实上,这些操作都是又操作系统将资源分成若干个小块,然后再分配给每个程序使用,使这些程序看起来有并行的效果。
Java语言的一大特性就是对多线程的支持。多线程是指一个程序拥有多个程序的执行点,可以按照各自的执行线索共同工作的情况。虽然它们看起来是并行着工作的,不过这只是一种错觉。因为我们的计算机只能在一个时间片做一个线程的工作。 // 如果你的计算机是多核CPU,请无视前面这句话^_^
1.进程和线程
现代操作系统都支持多任务,主要的形式包括:(1)基于进程的多任务。(2)基于线程的多任务。
1.1线程的定义
进程:就是正在执行的程序,它有着自身的指定序列,拥有独立的数据空间,会占用一定的系统资源。more
线程 :是进程中的单一连续的指定流。线程依属于进程。一个进程可以有多个线程,同一个进程的线程s会共享该进程的资源。 // 举例:银联的客户可以在标注为银联的ATM机上存取钱
1.2线程的分类
Java中的线程分为用户线程User和守护线程Daemon. // 通过setDaemon()方法指定其类型(true) ? Daemon : User
特点如下:
a.有自己独立的生命周期
b.一个用户线程User可以创造出用户线程和守护线程,而一个守护线程只能创造出守护线程。
c.用户线程可以转换成为守护线程,但只能在线程的新建态和终止态时转换。 // 关于线程的生命周期,耐心读下去。
d.守护线程是运行在系统的后台,通常做无限循环,进而服务于用户线程。其优先级低于用户线程 // 比如网游中的NPC。再比如Java中的GC(垃圾回收器)若程序中没有用户线程了,就不会有垃圾了,作为守护线程的GC就没有用武之 地了,这时JVM就会自动离开。
2.线程的状态及其生命周期
每个线程都有独立的生命周期,在它一个完整的生命周期中通常要经历下图这5-6种状态。
1.新建态 : 通过new关键字声明一个Thread类的对象 Thread t = new thread();会创建一个线程,这时系统并没有给处于新建态的线程t分配资源。
2.就绪态 : 通过调用t.start()方法会使线程t由新建态进入到就绪态(进入到就绪队列),这是t被分配到了系统资源。网上不少帖子在这时会说被调用了start()方法的线程,JVM会去调用该线程对象重写的run()方法。这说法没错,但不是立即执行,是要在就绪队列中排在第一位才能执行run()方法。附个小例子:
package thread;
public class ThreadTest {
public static void main(String[] args) {
ThreadInfo ti = new ThreadInfo();
ti.start();
System.out.println(ti.getThreadInfo());
System.out.println(ThreadInfo.getStaticInfo());
}
}
class ThreadInfo extends Thread {
private String strInfo = "define";
private static String STATIC_INFO = "define[S]";
public ThreadInfo() {
this.strInfo = "construcor";
STATIC_INFO = "construcor[S]";
}
public void run(){
try{
this.strInfo = "run";
STATIC_INFO = "run[S]";
}catch(Exception ex){
}
}
public String getThreadInfo(){
return this.strInfo;
}
public static String getStaticInfo(){
return STATIC_INFO;
}
}
执行上边的代码,控制台有时会打印:
construcor
construcor[S]
有时会打印:
construcor
run[S]
---------------------分割线-----------------------
3.运行态 : 排在就绪队列排头的线程会得到执行的机会。
运行态之后的线程对象会有以下3中结果:
1.代码执行完一个周期,如有需要改线程会再次进入就绪态,也就是所谓的就绪队列等待。关于就绪队列的算法,我 google了一下,根据OS的不同,其计算就绪队列的算法也不同,可以参见 3.线程的优先级 ,有兴趣的可以Google。
2.代码执行结束后进去终止态。
3.代码继续执行,因需要某些权限进入阻塞态。
4.阻塞态/挂起态 : 若拥有了权限,程序会回到就绪队列。两者的区别是处于阻塞态的线程对象会占用系统资源,处于刮起态的对象则不会。
5.终止态 : 代码执行后,正常结束的状态。
我们通常所说的线程的生命周期就是除了终止态以外的其他4种状态。
---------------------分割线-----------------------
说了这些,估计没人会记住这些状态以及各状态之间的关系,那么咱换个说法:
下面咱 们 拿去 银 行 办 理 业务 来模 拟线 程的状 态转换 生命周期 :
1. 到银行 按号代表要办理 业务 ,相当于 线程 的新建 态 。
2. 排在自己前面的人不多了,意味着自己 马 上可以 办业务 了,相当于排进了就绪队列,处于就绪态 。
3. 当前面没人了,叫道自己号儿的时候就可以办 理 业务了 ,相当于运行 态 。
4. 没有再需要 办 理的 业务 就可以离开 银 行了,相当于 终 止 态 。如果 还 有第二个,第三个 业务 可以 继续办 ,没有必要重新按号。
5. 前面 办业务 的人太墨迹了,一点 进 展也没有,就相当于阻塞 态 。
6. 等不了 了或 暂时银 行 办 理不了自己的 业务 ,只能 叹 着气,离开 银 行,等能 办 理 时 再跑一趟。
---------------------分割线-----------------------
3.线程的优先级
在Java中,每个线程对象都有一个线程同步锁,一个线程等待队列当然也有一个优先级。
通过setPriority()方法设置优先级。1~10 数值越大级别越高,越先执行。详见API。
线程的调度
处于就绪状态的线程首先进入就绪队列排队等候CPU资源。同一时刻在排队的线程对象会有多个。我们可以通过设置线程的优先级来让任务较紧急的线程首先享用到CPU资源;而对于优先级相同的线程,JVM会随机抽取一个线程对象来执行。这种随机性很难通过代码看出来,不过大家姑且可以理解为某种先进先出的队列。至于其内部的"调度算法",我不是很了解,大家就靠自己吧。
4.线程的创建
在Java中创建一个线程的方法有两种:1.是通过Thread类的构造器创建 2.是通过实现Runnable接口
两者的不同之处在于:通过实现Runnable接口中的抽象run方法,定义出线程的行为,并不包含线程的生命周期。而通过Thread类的构造方法,不仅可以定义出线程的行为,还可以定义出完整的线程生命周期。
即:1.继承Thread类,覆盖run()方法,定义生命周期并实现本线程特有的行为。2.实现Runnable接口,实现该接口中的run()方法,定义线程特有的行为。
5.Thread类
参见Java API
6.线程组
参见Java API
7.线程同步
7.1线程同步的概念:
前面提到了”一个进程的多个线程可以共享该进程的资源“。而当多个线程访问同一批数据(共享资源)时,为了保证共享资源在同一时间段内只能允许被一个线程访问的技术叫线程同步。
7.2实现线程同步的方法
可以将修改共享资源的方法用synchronized关键字修饰。当线程调用synchronized方法时,对象将转为”锁定“状态。这里大家可以把用synchronized修饰的同步方法想象成一个里面有锁的公用电话亭。每次只能允许一个人使用电话,并且只允许电话亭内的人给电话亭上锁。这也解释了我前两天写的一个关于《线程同步的重进入》 的帖子中说明的问题。
synchronized关键字不仅可以修饰方法,还可以修饰一个代码块,例如:synchronized(对象) {代码体;}
这样写的好处是:1.减少了锁定时间。 2.降低锁定范围。3.降低系统的开销。
8.线程死锁的问题
在《线程同步的重进入》 里面也举了一个例子,大家可以看下。
9.volatile修饰符*
多线程中有主内存和工作内存之分,在JVM中,有一个主内存。专门负责所有线程的共享数据。而每个线程都有自己私有的工作内存,主内存和工作内存分别在JVM的stack栈和heap堆上。详见《Inside JVM》
以volatile 修饰的变量,表示将非原子数据定义为原子类型。
double和long变量是非原子型的,所以就会出现啊久前几天贴出的大项目中的bug的问题。详见《inside JVM》第八章线程的8.4节Double和Long变量的非原子处理。 // 总结一句 这个volatile关键字不安全,不到不得已还是不用为妙
---------------------------------------
以上就是有关线程的部分基础知识,欢迎讨论 不正之处请指正!