小学徒成长系列—线程
现在很多面向对象语言中都有了操作线程这个重要的功能,线程能够使复杂的一部代码变得更简单,大大的降低了复杂系统的开发,随着现在处理器数量的发展,充分的利用线程,就能够更好的发挥多处理器系统的强大能力。
1.基本概念
1.1线程和进程的关系
在刚开始的时候,操作系统(OS)为了使多个程序能并发执行,从而提高资源利用率和系统吞吐量,从而提出了进程,后来为了减少时空的开销,提高并发性和CPU的利用率,就提出了线程这个概念。
在OS中,其实线程包含于进程内的,一个程序只有一个进程,但是却可以有很多的线程,如图:
从图中,我们也可以很好的知道,进程是操作系统中资源分配的基本单位,而线程只拥有运行时必不可少的资源,线程本身基本不拥有系统的资源,而且,因为隶属 于进程,所以线程可以访问隶属进程的资源,同一进程的线程间可以共享所属进程的资源,这也就出现了后面我们必须采用措施处理线程间的同步问题。在运行期间,线程 才是操作系统的调度和分派的基本单位。同时,操作系统在创建、撤销及切换线程的时候,开销会比进程小。
1.2线程和进程的基本运行状态
其实,线程拥有的基本运行状态和进程一样,分别是以下3种:
1>执行状态,表示线程或者进程正获得处理机而运行;
2>就绪状态,表示线程或者进程已经具备了各种执行条件,一旦获得CPU便可执行的状态;
3>阻塞状态,表示线程在执行中因为某时间而受阻,出于暂停执行时的状态。
它们的状态转换图具体如下:
1.3并发和并行
我们前面提到了并发这个词,平时口中也经常提到并发和并行这两个词,很多人经常把他们混淆来使用,但是,其实这两个词并不是等同意义的,下面我们来看下他们的解释:
1>并发:是指两个或多个事件在同一时间间隔内发生;
2>并行:是指两个或多个事件在同一时刻发生;
1.4同步和异步
在操作系统中,对同步和异步是这样描述的:
1>异步:多进(线)程按各自独立的,不可预知的速度向前推进;
2>同步:多个相关的进(线)程在执行次序上进行协调使之共享资源相互合作。
或许直接这样子看概念会有点生涩难以理解,那么就举一个最经典的例子(生产者与消费者)来共同学习下吧。
假设我们现在有一个生产进程和一个消费进程:
1>生产进程在生产产品并置于缓存(可以理解为货仓)的期间,消费进程不能进入缓存取出产品,直到生产产品生产完产品释放它对缓存的掌控权之后,消费进程才可以进入缓存取出产品,并且必须等待消费进程不再取出产品并且释放了他对缓存的掌控权之后,生产进程才可以再次进入缓存进行生产。这个就是同步。
2>生产进程将产品置于缓存之后并不关心消费进程是否取走产品就继续生产,也就是说在生产进程生产产品至于缓存的过程中,消费者进程也可以进入缓存取走产品。这个就是异步。
2.Java中的线程Thread
在Jdk中,已经结合系统的内核,给我们封装好了线程的控制API,我们只要创建一个线程对象,熟悉它的API,调用对应的函数,就可以把我们想要对线程进行的操作传达给操作系统,由操作系统来帮我们完成,这就大大降低了学习者的学习难度,同时也大大方便了开发人员,提高了开发效率。
2.1线程的实现方式
在Java中,我们有两种线程的实现方式
1>直接继承Thread类,重写父类的run()方法,eg :
1 package com.thread.first; 2 3 public class MyThread extends Thread { 4 5 //重写父类的run()方法 6 @Override 7 public void run() { 8 this.setName("The Thread To Print Number"); 9 for (int i = 0; i < 10; i++) { 10 System.out.println(this + " " + i); 11 } 12 } 13 14 public static void main(String[] args) { 15 //注意,线程开始执行的方法是start()而不是run()方法 16 //具体区别后面会讲 17 new MyThread().start(); 18 for (int i = 0; i < 10; i++) { 19 System.out.println("Main Thread : " + i); 20 } 21 } 22 }
因为线程在CPU中是并发执行交替执行的,所以执行结果并不唯一,以下只是其中一种输出结果而已,如果出现跟下图不一样的结果,也不用担心,只要代码无误,能正常运行,就对了:
2>定义线程类并实现Runnable接口。因为Java是单继承了,如果继承了Thread类就不能继承其他的类了,而接口却是可以实现多个的,同时这种方式还能够为多个线程提供共享的数据,所以在实际开发中,我们都比较推荐使用这种方式定义线程类,eg:
1 package com.thread.second; 2 3 public class MyRunnable implements Runnable { 4 5 @Override 6 public void run() { 7 //获取当前线程并且设置线程的名字 8 Thread.currentThread().setName(("The Thread To Print Number")); 9 for (int i = 0; i < 10; i++) { 10 System.out.println(Thread.currentThread() + " " + i); 11 } 12 } 13 }
1 package com.thread.second; 2 3 public class TestThread { 4 public static void main(String[] args) { 5 //注意将创建自定义的线程类对象并传进Thread类的构造方法中 6 new Thread(new MyRunnable()).start(); 7 8 for (int i = 0; i < 10; i++) { 9 System.out.println("Main Thread : " + i); 10 } 11 12 } 13 }
运行结果这次我就不贴出来啦,大家自己运行一下就知道的。
下面我们一起来看一下Runnable这个接口,其实它的源代码也很简单,就只有一个用来定义线程的运行体的run()方法:
2.2线程的基本控制方法
2.2.1开始执行线程 start()
其实这个,在前面我们的代码测试中已经用过了,我就不再另外给出一个代码啦,我们就直接讲一下run()方法和start()方法见的区别吧。
当我们创建了一个线程对象之后,想要通过该对象开启线程,我们就要调用start()方法,而不是run()方法,因为通过run()方法调用的只是方法的执行,还是属于当前线程的,也就是说依旧是单线程,我们通过一个简单的程序看看吧,eg:
1>直接调用run()方法:
1 package com.thread.first; 2 3 public class TestThread extends Thread{ 4 5 @Override 6 public void run() { 7 //输出当前线程的名字 8 System.out.println("in run menthod : " + Thread.currentThread().getName()); 9 } 10 11 public static void main(String[] args) { 12 //创建一个线程对象并且运行它的run方法 13 new TestThread().run(); 14 //输出当前线程的名字 15 System.out.println("in main method : " + Thread.currentThread().getName()); 16 } 17 }
执行结果:
根据执行结果,我们发现,通过调用run()方法并没有开启新的线程,也就是说依旧是单线程。下面我们再看看通过调用start()方法的结果:
2>直接调用start()方法:
//直接把上面一段代码中的run方法修改成调用start()方法 //创建一个线程对象并且运行它的start()方法 new TestThread().start();
执行结果:
根据执行结果,我们发现,通过调用start()方法才是可以开启新的线程,也就是说实现线程并发的,具体为什么呢?我们再来看看Thread类中的部分源代码吧。
1 public class Thread implements Runnable { 2 //Runnable接口 3 private Runnable target; 4 5 //这只是Thread的其中一个构造方法, 6 //其他的构造方法由于篇幅问题省略了 7 public Thread(Runnable target) { 8 init(null, target, "Thread-" + nextThreadNum(), 0); 9 } 10 11 //Thread通过实现Runnable接口重写的方法 12 @Override 13 public void run() { 14 if (target != null) { 15 target.run(); 16 } 17 } 18 public synchronized void start() { 19 20 //..... 21 //前面省略了一部分代码 22 boolean started = false; 23 try { 24 start0(); //调用该方法启动线程 25 started = true; //设置线程开启状态为真 26 } finally { 27 try { 28 if (!started) { 29 group.threadStartFailed(this); 30 } 31 } catch (Throwable ignore) { 32 /* do nothing. If start0 threw a Throwable then 33 it will be passed up the call stack */ 34 } 35 } 36 } 37 38 //通过JNI调用开启线程 39 private native void start0(); 40 }
通过上述源代码我们知道,run()方法中并没有开启线程,而是属于一个普通的方法正常调用而已,而start()方法才有通过JNI调用线程。
2.2.2 判断线程生死 isAlive()
如果线程还存在,则返回true, 否则返回false;
1 package com.thread.first; 2 3 public class TestThread extends Thread{ 4 5 @Override 6 public void run() { 7 //输出当前线程的名字 8 System.out.println("in run menthod : " + Thread.currentThread().getName()); 9 } 10 11 public static void main(String[] args) { 12 //创建一个线程对象 13 TestThread tt = new TestThread(); 14 //线程还没有开启,下面语句将会输出false 15 System.out.println("线程还或者吗? " + tt.isAlive());; 16 tt.start(); 17 //线程已经开启,下面语句将会输出true 18 System.out.println("线程还或者吗? " + tt.isAlive());; 19 } 20 }
当然啦,对线程的操作,Java是不可能直接做到的,他也是通过JNI本地调用进行操作的,源代码如下图:
2.2.3 线程的优先级 getPriority() & setPriority()
通过该方法可以设置线程的优先级,一般情况下,优先级越高的线程获得CPU调度的时间片将会越长。java线程的优先级值默认为5,设置范围为1-10.
因为Java的线程是被映射到系统的原声线程上来实现的,所以线程调度最终还是由操作系统说了算的,虽然很多线程都提供线程优先级的概念,但是并不剪得能与java线程的优先级一一对应,如Solaries中有231种优先级,Windows中就只有7中,并不一定能与java线程的优先级设置一一对应,因此,建议还是直接使用java本身自带的三个MIN_PRIORITY(1)、NORM_PRIORITY(5)、MAX_PRIORITY(10)来设置优先级比较好,eg:
1 package com.thread.first; 2 3 public class TestThread extends Thread{ 4 5 @Override 6 public void run() { 7 Thread currentThread = Thread.currentThread(); 8 //输出当前线程的名字、优先等级 9 for (int i = 0; i < 50; i++) { 10 System.out.println("name : " + currentThread.getName() 11 + "\t priopity : " + currentThread.getPriority() + " " + i); 12 } 13 } 14 15 public static void main(String[] args) { 16 //创建优先级为5一个线程对象 17 TestThread ttNorm = new TestThread(); 18 ttNorm.setPriority(NORM_PRIORITY); 19 ttNorm.start(); 20 21 //创建优先级为1一个线程对象 22 TestThread ttMin = new TestThread(); 23 ttMin.setPriority(MIN_PRIORITY); 24 ttMin.setPriority(MIN_PRIORITY); 25 ttMin.start(); 26 27 //创建优先级为10一个线程对象 28 TestThread ttMax = new TestThread(); 29 ttMax.setPriority(MAX_PRIORITY); 30 ttMax.start(); 31 32 } 33 }
2.2.4线程睡眠时间sleep()
讲当前线程指定睡眠毫秒数
1 @Override 2 public void run() { 3 Thread currentThread = Thread.currentThread(); 4 try { 5 Thread.sleep(1000); //让线程休眠1秒钟 6 } catch (InterruptedException e) { 7 e.printStackTrace(); 8 } 9 }
2.2.5线程合并join()
合并某线程A的这个方法,将该线程与当前线程B合并,即等待该线程A执行完毕后再继续执行线程B,如图:
下面我们再来看看代码的实现,eg:
1 package com.thread.first; 2 3 public class TestThread extends Thread{ 4 5 @Override 6 public void run() { 7 Thread currentThread = Thread.currentThread(); 8 //输出当前线程的名字、优先等级 9 for (int i = 0; i < 50; i++) { 10 System.out.println("name : " + currentThread.getName() 11 + "\t priopity : " + currentThread.getPriority() + " " + i); 12 } 13 } 14 15 public static void main(String[] args) throws Exception { 16 //创建优先级为5一个线程对象 17 TestThread ttNorm = new TestThread(); 18 ttNorm.setPriority(NORM_PRIORITY); 19 ttNorm.start(); 20 ttNorm.join(); //合并线程 21 Thread currentThread = Thread.currentThread(); 22 for (int i = 0; i < 50; i++) { 23 System.out.println("name : " + currentThread.getName() 24 + "\t priopity : " + currentThread.getPriority() + " " + i); 25 } 26 } 27 }
2.2.6 yield()
让出CPU,当前线程进入就绪队列等待调度,测试代码如下:
1 package com.thread.first; 2 3 public class TestYield { 4 public static void main(String[] args) { 5 MyThread3 t1 = new MyThread3("t1"); 6 MyThread3 t2 = new MyThread3("t2"); 7 t1.start(); 8 t2.start(); 9 } 10 } 11 12 class MyThread3 extends Thread { 13 MyThread3(String s) { 14 super(s); 15 } 16 17 public void run() { 18 for (int i = 1; i <= 100; i++) { 19 System.out.println(getName() + ": " + i); 20 if (i % 10 == 0) { 21 yield(); 22 } 23 } 24 } 25 }
运行结果我们会发现,t1线程和t2线程格子输出10个数字后就会进入等待队列把CPU让给了对方,所以他们轮流输出数字。
2.2.7线程暂停与唤醒 wait() & notify() & notifyAll()
当前线程进入等待池(wait pool),wait方法通过参数可以指定等待的时长,如果没有指定参数,默认一直等待直到使用notify()方法通知该线程或者notifyAll()方法通知才会继续执行下去。需要注意的是,根据java文档的说法,调用这三个方法的对象的前提是当前线程是此对象监视器的所有者,换句话说,这执行这三个方法的对象必须被synchronized锁定,同时这三个方法在同步块中执行,否则会报以下错误:
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at com.thread.second.TestWait.main(TestWait.java:23)
根据官方的说明就是:当前线程不是此对象监视器的所有者。下面我给出一个运用的例子吧,eg:
1 package com.thread.second; 2 3 public class TestWait extends Thread { 4 public TestWait(String name) { 5 super(name); 6 } 7 8 @Override 9 public void run() { 10 for (int i = 0; i < 30; i++) { 11 System.out.println(Thread.currentThread().getName() + " : " + i); 12 } 13 } 14 15 public static void main(String[] args) throws InterruptedException { 16 Thread t1 = new TestWait("t1"); 17 Thread t2 = new TestWait("t2"); 18 19 t1.start(); 20 21 synchronized (t1) { 22 t1.wait(10); 23 } 24 t2.start(); 25 26 synchronized (t1) { 27 t1.notify(); 28 } 29 } 30 31 }
2.3 sleep() VS wait()
sleep()方法和wait()方法都可以使得线程暂停等待,那么究竟它们有什么共同点呢?有什么区别呢?
共同点:
1>都是在多线程的环境下,都能够使得线程暂停等待一定的时间;
2>都可以使用interrupt()打断线程的等待状态,但是会使得线程对象立即抛出InterruptedException错误
区别:
1>来源不同,sleep()是在Thread类中定义的,而wait()是Object类定义的;
2>正如2.2.7中所说的,wait()方法只能在同步块中使用,而sleep()可以在任何地方使用;
3>使用sleep()方法的时候必须捕捉异常,而wait()方法不用
4>调用sleep()方法并不会释放锁,睡眠期间,其他线程也无法访问这个对象;而wait()方法会释放锁(暂时的释放),等待期间,其他线程可以访问这个对象,关于这里的锁 ,并不是指synchronized,而是对象监视器,详细大家可以直接参考这个网址上说的:http://www.iteye.com/topic/310577
[本文为原创,转载请注明出处:http://www.cnblogs.com/xiaoxuetu]