一、线程的引入和实现
在传统的操作系统中,作为拥有资源的基本单位和独立调度、分派的基本单位都是进程,也因此导致线程在创建、撤销和切换中都会占用OS较大的时空资源。但是也正因如此,在OS中所设置的进程,数目不宜过多,切换频率不宜过快,这也就限制了OS并发程度的进一步提高。所有后来人们提出了线程的概念,为了进一步提高OS的并发程度和系统吞吐量。线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以作为CPU的基本调度单位。
在Java语言中提供了在不同硬件和OS下对线程操作的统一处理,每个已经执行start()且还未结束的java.lang.Thread类的实例都代表了一个线程。如果查看Thread类的源码会发现,它所有的关键方法都是native方法。也就意味着此方法使用平台相关的技术实现的,比如由C++语言编写的。线程的实现由3种方式:使用内核线程实现、使用用户线程实现和前两者混合实现。而JVM采用的第一种实现方式,系统内核线程实现,这种线程由底层系统内核来完成线程切换和调度。
二、线程调度和状态转换
线程调度是指系统为线程分配CPU使用权的过程。主要调度方式有两种,协同式调度(Cooperative Threads-Scheduling)和抢占式调度(PreemptiveThreads-Scheduling)。协同式多线程系统中,线程的执行时间由线程自己控制,线程把自己执行完了,再通知系统切换到其他线程。优点就是,实现简单,多线程顺序执行,也没有什么线程同步问题。缺点也很明显,执行时间对系统来说不可控制,也相当不稳定如果一个线程阻塞了,可能导致整个系统崩溃。抢占式多线程系统,每个线程的执行时间有系统分配,线程的切换调度不由线程本身决定(Java 中可以用Thread.yield()让出执行权,但是要获取执行权,线程本身没有办法,只能等待系统调度)。Java采用的第二种抢占式,虽然由系统调用,但是Java也可以给OS提出建议,比如给某个线程多点执行时间,通过设置线程的优先级完成。Java中,共设置了10个线程优先级,优先级越高越容易被OS选中执行。不过,由于Java的线程是映射到OS的原生线程上的,所以线程调度最终还是由OS 决定,在Windows OS中共设置了7个优先级,所以会造成优先级相同的情况,比如Java中的优先级4和5映射到Windows上可能是同一个优先级。Thread类提供了三个常量MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分别是1、5、10,优先级跨度大还有点作用。
Java中共有5种线程状态,任意时刻,一个线程只能处于一种状态。
- 新建(New):创建后尚未启动的线程处于此状态。
- 运行(Runnable):此状态包含了OS线程中的Running和Ready两个状态,也就是说,处于此状态的线程可能正在运行,也有可能正在等待CPU。
- 等待(Waiting):此状态的线程不会被分配CPU执行时间,需要被其他线程显示的唤醒或由系统唤醒,调用没有设置Timeout的wait()方法的线程必须调用notify()唤醒。而调用Thread.sleep(timeout)、Object.wait(timeout)、Thread.join(timeout)等方法,timeout时间结束后由系统自动唤醒。
- 阻塞(Blocked):阻塞态和等待态的区别是:阻塞状态是在等待一个对象锁,知道其他线程放弃这个锁,而等待状态则在一段时间后,由系统或唤醒动作唤醒。在程序等待进入临界区(同步区域)的时候,线程会进入此状态。
- 结束(Terminated):又称dead线程。已终止的线程进入此状态。
线程状态的转换图如下:
三、Java中创建线程的方法
创建一个线程有两种方法:直接继承Thread类和实现Runnable接口。具体用法如下:
//一个线程输出大写字母 ,一个线程输出小写字母
public class ThreadTest {
public static void main(String[] args) {
//创建线程时,传一个Runnable的实现类,就会执行重写的run方法
//线程创建后,并不会直接执行,只有调用start方法后,才会被系统执行
new Thread(new PrintUpper()).start();
//PrintLower继承自Thread
//创建对象就是创建了一个线程
new PrintLower().start();
}
}
/**
* 打印大写字母
* 当只需要覆写run方法时,优先选择实现Runnable接口
* 因为接口更灵活,一个类可以实现多个接口
* @author guoke
*
*/
class PrintUpper implements Runnable{
//线程运行时,系统会自动调用run方法
@Override
public void run() {
for(char c='A';c<='Z';c++){
System.out.print(c);
/**
* 打印一个字母,睡眠10ms
* 睡眠期间,线程会放弃cpu
* 睡眠结束后进入等待队列.重新获得cpu后继续执行
* 本方法必须try-catch
*/
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 打印小写字母
* 继承自Thread类,子类本身就是一个线程,创建对象就是创建了一个线程
* 优点:创建线程简单
* 缺点:java是单继承,有局限性
*/
class PrintLower extends Thread{
@Override
public void run() {
for(char c='a';c<='z';c++){
System.out.print(c);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
四、线程同步
线程同步是指在多线程环境下,当多线程并发执行时,多个线程能不出差错的访问共享数据。Java多线程的同步依靠的是对象锁机制synchronized关键字的背后就是利用了封锁来实现对共享资源的互斥访问。下面以经典的懒汉式单例模式为例:
public class SingletonDemo {
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
Single s3=Single.getInstance2();
System.out.println(s3);
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
Single s4=Single.getInstance2();
System.out.println(s4);
}
});
t1.start();
t2.start();
}
}
class Single{
private static Single s=null;
private Single(){}
//方法 1
public static synchronized Single getInstance(){
if(s==null){
// 1
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
s=new Single();
}
return s;
}
被sychronized修饰的方法,意味着如果线程t1先执行了getInstance()方法,在执行期间,其他任何线程都不能在执行此方法。如果不是sychronized方法,当t1执行到代码1处时睡眠100毫秒,此时线程t2执行if(s==null)为true也进入了代码1处,执行完此方法执行了
s=new Single()创建了一个Single对象,然后线程t1被唤醒同样执行了s=new Single()又创建了一个对象。这时就会出现2个Single对象,也就不是单例模式了,所以不加synchronized修饰,此单例模式不是线程安全的。
最后对synchronized关键字做几点说明:
第一:synchronized用来标识一个普通方法时,表示一个线程要执行该方法,必须取得该方法所在的对象的锁。
第二:synchronized用来标识一个静态方法时,表示一个线程要执行该方法,必须获得该方法所在的类的类锁,也就是Class对象的锁。
第三:synchronized修饰一个代码块。synchronized(obj) { //code.... }。表示一个线程要执行该代码块,必须获得obj的锁。