线程的基础知识
http://blog.csdn.net/lonelyroamer/article/details/7993637
什么是线程
2、线程的主要用途
①、利用它可以完成重复性的工作(如实现动画、声音等的播放)。
②、从事一次性较费时的初始化工作(如网络连接、声音数据文件的加载)。
③、并发执行的运行效果(一个进程多个线程)以实现更复杂的功能
我的理解就是,不影响整个程序的进程,程序接着往下走,线程默默的把自己这部分工作再做完。有点片面。
3、多线程(多个线程同时运行)程序的主要优点
①、可以减轻系统性能方面的瓶颈,因为可以并行操作;
②、提高CPU的处理器的效率,在多线程中,通过优先级管理,可以使重要的程序优先操作,提高了任务管理的灵活性;另一方面,在多CPU系统中,可以把不同的线程在不同的CPU中执行,真正做到同时处理多任务。
注意:①、在继承Thread的方式中,可以使用getName()方法,来获得当前线程的名字,这是因为在Thread类中,有这个方法。可是在实现Runnable方式中,却不可以使用this.getName(),因为Runnable接口没有这个方法(可以看出来,因为我们没有提示我们需要重写这个方法),所以只能通过Thread的静态方法Thread.currentThread()取得当前的Thread对象,在调用getName()方法,来取得当前线程的名字。
②、对Java来说,run()方法没有任何特别之处。像main()方法一样,它只是新线程知道调用的方法名称(和签名)。因此,在Runnable上或者Thread上调用run方法是合法的。但并不启动新的线程。只有调用start()方法才会启动新线程。
4、两种方式的对比
采用实现Runnable接口方式的多线程具有优势,一般都会使用这种方式:
1、线程类只是实现了Runnable接口,还可以继承其他类。
2、在这种方式下,可以多个线程共享一个Runnable target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好的体现了面向对象的思想。
线程的生命周期
1、新建状态
用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态(runnable)。
注意:不能对已经启动的线程再次调用start()方法,否则会出现java.lang.IllegalThreadStateException异常。2、就绪状态
处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。
3、运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
当发生如下情况是,线程会从运行状态变为阻塞状态:
①、线程调用sleep方法主动放弃所占用的系统资源
②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞
③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
④、线程在等待某个通知(notify)
⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。
当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
4、阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。
在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。
5、死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
二、线程状态的控制
Java提供了一些便捷的方法用于会线程状态的控制。
interrupt()中断线程。.join()等待该线程终止。
join(long millis) 等待该线程终止的时间最长为 millis 毫秒。
join(long millis, int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。
setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
setPriority(int newPriority) 更改线程的优先级。
setPriority(int newPriority) 更改线程的优先级。
sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
sleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
yield() 暂停当前正在执行的线程对象,并执行其他线程。
1、线程睡眠——sleep
如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep方法
注意如下几点问题
①、sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。看下面的例子:
[cpp] view plaincopy
public class Test1 {
public static void main(String[] args) throws InterruptedException {
System.out.println(Thread.currentThread().getName());
MyThread myThread=new MyThread();
myThread.start();
myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
}
②、Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
2、线程让步——yield
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。
关于sleep()方法和yield()方的区别如下:
①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
3、线程合并——join线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。从上面的方法的列表可以看到,它有3个重载的方法:void join() 当前线程等该加入该线程后面,等待该线程终止。 void join(long millis) 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 void join(long millis,int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 例子:
[java] view plaincopy
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
thread.start();
thread.join(1);//将主线程加入到子线程后面,不过如果子线程在1毫秒时间内没执行完,则主线程便不再等待它执行完,进入就绪状态,等待cpu调度
for(int i=0;i<30;i++){
System.out.println(Thread.currentThread().getName() + "线程第" + i + "次执行!");
}
}
}
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}
在这个例子中,在主线程中调用thread.join(); 就是将主线程加入到thread子线程后面等待执行。不过有时间限制,为1毫秒。
4、线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
MAX_PRIORITY =10
MIN_PRIORITY =1
NORM_PRIORITY =5
例子:
[java] view plaincopy
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}
从结果可以看到 ,一般情况下,高级线程更显执行完毕。
注意一点:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。
5、守护线程
守护线程与普通线程写法上基本么啥区别,调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。
setDaemon方法的详细说明:public final void setDaemon(boolean on)将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: on - 如果为 true,则将该线程标记为守护线程。 抛出: IllegalThreadStateException - 如果该线程处于活动状态。 SecurityException - 如果当前线程无法修改该线程。
[java] view plaincopy
/**
* Java线程:线程的调度-守护线程
*/
public class Test {
public static void main(String[] args) {
Thread t1 = new MyCommon();
Thread t2 = new Thread(new MyDaemon());
t2.setDaemon(true); //设置为守护线程
t2.start();
t1.start();
}
}
class MyCommon extends Thread {
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("线程1第" + i + "次执行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class MyDaemon implements Runnable {
public void run() {
for (long i = 0; i < 9999999L; i++) {
System.out.println("后台线程第" + i + "次执行!");
try {
Thread.sleep(7);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} 执行结果:
[java] view plain
后台线程第0次执行!
线程1第0次执行!
线程1第1次执行!
后台线程第1次执行!
后台线程第2次执行!
线程1第2次执行!
线程1第3次执行!
后台线程第3次执行!
线程1第4次执行!
后台线程第4次执行!
后台线程第5次执行!
后台线程第6次执行!
后台线程第7次执行!
从上面的执行结果可以看出:前台线程是保证执行完毕的,后台线程还没有执行完毕就退出了。
实际上:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。
守护线程的用途:
守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。Java的垃圾回收也是一个守护线程。守护线
的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,
不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了
6、如何结束一个线程
Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法。
1、正常执行完run方法,然后结束掉
2、控制循环条件和判断条件的标识符来结束掉线程
3、使用interrupt结束一个线程。
诚然,使用第2中方法的标识符来结束一个线程,是一个不错的方法,但是如果,该线程是处于sleep、wait、join的状态的时候,while循环就不会执行,那么我们的标识符就无用武之地了,当然也不能再通过它来结束处于这3种状态的线程了。
可以使用interrupt这个巧妙的方式结束掉这个线程。
我们看看sleep、wait、join方法的声明:
[java] view plaincopy
public final void wait() throws InterruptedException
[java] view plaincopy
public static native void sleep(long millis) throws InterruptedException
[java] view plaincopy
public final void join() throws InterruptedException
可以看到,这三者有一个共同点,都抛出了一个InterruptedException的异常。在什么时候会产生这样一个异常呢?
每个Thread都有一个中断状状态,默认为false。可以通过Thread对象的isInterrupted()方法来判断该线程的中断状态。可以通过Thread对象的interrupt()方法将中断状态设置为true。
当一个线程处于sleep、wait、join这三种状态之一的时候,如果此时他的中断状态为true,那么它就会抛出一个InterruptedException的异常,并将中断状态重新设置为false。
看下面的简单的例子:
[java] view plaincopy
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
thread.start();
}
}
class MyThread extends Thread {
int i=1;
@Override
public void run() {
while (true) {
System.out.println(i);
System.out.println(this.isInterrupted());
try {
System.out.println("我马上去sleep了");
Thread.sleep(2000);
this.interrupt();
} catch (InterruptedException e) {
System.out.println("异常捕获了"+this.isInterrupted());
return;
}
i++;
}
}
}
测试结果:
[java] view plaincopy
1
false
我马上去sleep了
2
true
我马上去sleep了
异常捕获了false
可以看到,首先执行第一次while循环,在第一次循环中,睡眠2秒,然后将中断状态设置为true。当进入到第二次循环的时候,中断状态就是第一次设置的true,当它再次进入sleep的时候,马上就抛出了InterruptedException异常,然后被我们捕获了。然后中断状态又被重新自动设置为false了(从最后一条输出可以看出来)。
所以,我们可以使用interrupt方法结束一个线程。具体使用如下:
[java] view plaincopy
public class Test1 {
public static void main(String[] args) throws InterruptedException {
MyThread thread=new MyThread();
thread.start();
Thread.sleep(3000);
thread.interrupt();
}
}
class MyThread extends Thread {
int i=0;
@Override
public void run() {
while (true) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("中断异常被捕获了");
return;
}
i++;
}
}
}
多测试几次,会发现一般有两种执行结果:
[java] view plaincopy
0
1
2
中断异常被捕获了
或者
[java] view plaincopy
0
1
2
3
中断异常被捕获了
这两种结果恰恰说明了 只要一个线程的中断状态一旦为true,只要它进入sleep等状态,或者处于sleep状态,立马回抛出InterruptedException异常。
第一种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程正处于sleep状态,立马抛出InterruptedException异常。
第一种情况,是当主线程从3秒睡眠状态醒来之后,调用了子线程的interrupt方法,此时子线程还没有处于sleep状态。然后再第3次while循环的时候,在此进入sleep状态,立马抛出InterruptedException异常。
线程的同步
由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。为了避免这样的事情发生,我们要保证线程同步互斥,所谓同步互斥就是:并发执行的多个线程在某一时间内只允许一个线程在执行以访问共享数据。
二、同步互斥锁
同步锁的原理:Java中每个对象都有一个内置同步锁。Java中可以使用synchronized关键字来取得一个对象的同步锁。synchronized的使用方式,是在一段代码块中,加上synchronized(object){ ... }
例如,有一个show方法,里面有synchronized的代码段:
[java] view plaincopy
public void show() {
synchronized(object){
......
}
}
这其中的object可以使任何对象,表示当前线程取得该对象的锁。一个对象只有一个锁,所以其他任何线程都不能访问该对象的所有由synchronized包括的代码段,直到该线程释放掉这个对象的同步锁(释放锁是指持锁线程退出了synchronized同步方法或代码块)。
注意:synchronized使用方式有几个要注意的地方(还是以上面的show方法举例):
①、取得同步锁的对象为this,即当前类对象,这是使用的最多的一种方式
[java]
public void show() {
synchronized(this){
......
}
}
②、将synchronized加到方法上,这叫做同步方法,相当于第一种方式的缩写
[java] view plaincopy
public synchronized void show() {
}
③、静态方法的同步
[java] view plaincopy
public static synchronized void show() {
}
相当于[java] view plain
public static void show() {
synchronized(当前类名.class)
}
相当于取得类对象的同步锁,注意它和取得一个对象的同步锁不一样synchronized关键字使用要注意以下几点:
1)、只能同步方法和代码块,而不能同步变量和类。只要保护好类中数据的安全访问和设置就可以了,不需要对类使用synchronized关键字,所以Java不允许这么做。并且想要同步数据,只需要对成员变量私有化,然后同步方法即可,不需要对成员变量使用synchronized,java也禁止这么做。
2)、每个对象只有一个同步锁;当提到同步时,应该清楚在什么上同步?也就是说,在哪个对象上同步?上面的代码中run方法使用synchronized (account)代码块,因为两个线程访问的都是同一个Account对象,所以能够锁定。但是如果是其他的一个无关的对象,就没用了。比如说synchronized (new Date())代码块,一样没有效果。
3)、不必同步类中所有的方法,类可以同时拥有同步和非同步方法。
4)、如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。5)、如果线程拥有同步和非同步方法,则非同步方法可以被多个线程自由访问而不受锁的限制。6)、线程睡眠时,它所持的任何同步锁都不会释放。7)、线程可以获得多个同步锁。比如,在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的同步同步锁。8)、同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块。
9)、编写线程安全的代码会使系统的总体效率会降低,要适量使用
一个线程取得了同步锁,那么在什么时候才会释放掉呢?
1、同步方法或代码块正常结束
2、使用return或 break终止了执行,或者跑出了未处理的异常。
3、当线程执行同步方法或代码块时,程序执行了同步锁对象的wait()方法。
三、死锁
死锁:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不能正常运行。简单的说就是:线程死锁时,第一个线程等待第二个线程释放资源,而同时第二个线程又在等待第一个线程释放资源。这里举一个通俗的例子:如在人行道上两个人迎面相遇,为了给对方让道,两人同时向一侧迈出一步,双方无法通过,又同时向另一侧迈出一步,这样还是无法通过。假设这种情况一直持续下去,这样就会发生死锁现象。 导致死锁的根源在于不适当地运用“synchronized”关键词来管理线程对特定对象的访问。“synchronized”关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。
一个死锁的造成很简单,比如有两个对象A 和 B 。第一个线程锁住了A,然后休眠1秒,轮到第二个线程执行,第二个线程锁住了B,然后也休眠1秒,然后有轮到第一个线程执行。第一个线程又企图锁住B,可是B已经被第二个线程锁定了,所以第一个线程进入阻塞状态,又切换到第二个线程执行。第二个线程又企图锁住A,可是A已经被第一个线程锁定了,所以第二个线程也进入阻塞状态。就这样,死锁造成了。
举个例子:
[java] view plaincopy
package com.tao.test;
public class DeadLock2 {
public static void main(String[] args) {
Object object1=new Object();
Object object2=new Object();
new Thread(new T(object1,object2)).start();
new Thread(new T(object2,object1)).start();
}
}
class T implements Runnable{
private Object object1;
private Object object2;
public T(Object object1,Object object2) {
this.object1=object1;
this.object2=object2;
}
public void run() {
synchronized (object1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2) {
System.out.println("无法执行到这一步");
}
}
};
}
上面的就是个死锁。第一个线程首先锁住了object1,然后休眠。接着第二个线程锁住了object2,然后休眠。在第一个线程企图在锁住object2,进入阻塞。然后第二个线程企图在锁住object1,进入阻塞。死锁了。
四、线程的协调运行
关于线程的协调运行,经典的例子就是生产者和消费者的问题。比如有生产者不断的生产馒头,放入一个篮子里,而消费者不断的从篮子里拿馒头吃。并且,当篮子满的时候,生产者通知消费者来吃馒头,并且自己等待不在生产馒头。当篮子没满的的时候,由消费者通知生产者生产馒头。这样不断的循环。
要完成上面的功能,光靠我们前面的同步等知识,是不能完成的。而是要用到线程间的协调运行。顶级父类Object中有3种方法来控制线程的协调运行。
notify、notifyAll、wait。其中wait有3个重载的方法。
这三个方法必须由同步监视器对象(即线程获得的锁对象)来调用,这可分为两种情况:
1、对于使用synchronized修饰的同步代码块,因为当前的类对象(this)就是同步监视器,所以可以再同步方法中直接调用这三个方法。
2、对于使用synchronized修饰的同步代码块,同步监视器是synchronized后括号的对象,所以必须使用该对象调用这三个方法。
wait(): 导致当前线程等待,直到其他线程调用该同步监视器的notify()方法或notifyAll()方法来唤醒该线程。wait()方法有三种形式:无时间参数的wait(一直等待,直到其他线程通知),带毫秒参数的wait和带毫秒、微秒参数的wait(这两种方法都是等待指定时间后自动苏醒)。调用wait()方法的当前线程会释放对该同步监视器的锁定。
notify(): 唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择幻想其中一个线程。选择是任意性。只有当前线程放弃对该同步监视器的锁定后(使用wait()方法),才可以执行被唤醒的其他线程。
notifyAll():唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。因为使用wait、notify和notifyAll三个方法一定是在同步代码块中使用的,所以一定要明白下面几点:
1、如果两个线程是因为都要得到同一个对象的锁,而导致其中一个线程进入阻塞状态。那么只有等获得锁的线程执行完毕,或者它执行了该锁对象的wait方法,阻塞的线程才会有机会得到锁,继续执行同步代码块。
2、使用wait方法进入等待状态的线程,会释放掉锁。并且只有其他线程调用notify或者notifyAll方法,才会被唤醒。要明白,线程因为锁阻塞和等待是不同的,因为锁进入阻塞状态,会在其他线程释放锁的时候,得到锁在执行。而等待状态必须要靠别人唤醒,并且唤醒了也不一定会立刻执行,有可能因为notifyAll方法使得很多线程被唤醒,多个线程等待同一个锁,而进入阻塞状态。还可能是调用notify的线程依然没有释放掉锁,只有等他执行完了,其他线程才能去争夺这个锁。
看下面的例子:
[java] view plaincopy
package com.tao.test;
public class ThreadA {
public static void main(String[] args) {
RunnableTest myRunnanle=new RunnableTest();
new Thread(myRunnanle).start();
synchronized (myRunnanle) {
try {
System.out.println("第一步");
myRunnanle.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第四步");
}
}
}
class RunnableTest implements Runnable {
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
System.out.println("第二步");
notify();
System.out.println("第三步");
}
}
}
有两个线程,主线程和我们自己新建的子线程。一步步的分析程序的执行:1、因为子线程启动后,调用了sleep,所以主线程先进入同步代码块,而子线程之后因为没有锁,会进入阻塞状态。
2、主线程的同步代码块执行,打印第一句话,然后调用wait方法,进入等待状态。因为进入了等待状态,所以释放掉了锁,所以子线程可以获得锁,开始执行。
3、子线程执行,打印第二句话,然后调用notify方法,将主线程唤醒。可是子线程并没有结束,依然持有锁,所以主线程不得不进入阻塞状态,等待这个锁。
4、子线程打印第三句话,然后线程正常运行结束,释放掉锁。然后主线程得到了锁,从阻塞进入运行状态,打印第四句话。
5、完毕
在看一个关于上面提到的生产者和消费者的例子:
首先,是生产物品的Mode,这里以馒头举例:
[java] view plaincopy
// 馒头的实例
class ManTou {
private int id;// 馒头的id
public ManTou(int id) {
this.id = id;
}
public String toString(){
return "ManTou"+id;
}
}
共享对象,生产者生产的馒头放入其中,消费者从里面拿出馒头,这里以篮子举例:[java] view plaincopy
// 篮子的实例,用来放馒头
class BasketBall {
private int index = 0;// 表示装到第几个了馒头
private ManTou[] manTous = new ManTou[6];// 可以放6个馒头
// 放进去一个馒头
public synchronized void push(ManTou manTou) {
while(index==manTous.length){
try {
System.out.println("篮子满了!");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+"生产"+manTou.toString());
this.notify();
manTous[index] = manTou;
index++;
}
// 拿一个馒头
public synchronized ManTou pop() {
while (index==0) {
try {
System.out.println("篮子空了!");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
ManTou manTou=manTous[--index];
System.out.println(Thread.currentThread().getName()+"吃了"+manTou.toString());
this.notify();
return manTou;
}
}
生产者:
// 生产者,生产馒头
class Producer implements Runnable {
private BasketBall basketBall;
public Producer(BasketBall basketBall) {
this.basketBall = basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou = new ManTou(i);// 生产馒头
basketBall.push(manTou);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
[java] view plaincopy
// 消费者,拿馒头吃
class Consumer implements Runnable {
private BasketBall basketBall;
public Consumer(BasketBall basketBall) {
this.basketBall = basketBall;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
ManTou manTou=basketBall.pop();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试:
[java] view plaincopy
public class ProducerConsumer {
public static void main(String[] args) {
BasketBall basketBall=new BasketBall();
new Thread(new Producer(basketBall)).start();
new Thread(new Consumer(basketBall)).start();
}
}
线程池
1、概述
系统启动一个新线程的成本是比较高的,因为它涉及到与操作系统的交互。在这种情况下,使用线程池可以很好的提供性能,尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
与数据库连接池类似的是,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法,当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
下面来看一下Java并发包下如何创建线程池。1. 创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。ExecutorService threadPool = Executors.newFixedThreadPool(3);// 创建可容纳3个线程的线程池2. 创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。ExecutorService threadPool = Executors.newCachedThreadPool();// 线程池的大小会根据执行的任务数动态分配3. 创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。ExecutorService threadPool = Executors.newSingleThreadExecutor();// 创建单个线程的线程池,如果当前线程在执行任务时突然中断,则会创建一个新的线程替代它继续执行任4. 创建一个可安排在给定延迟后运行命令或者定期地执行的线程池。ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(3);// 效果类似于Timer定时器每种线程池都有不同的使用场景,下面看一下这四种线程池使用起来有什么不同。
2、ExecutorService类
可以看到上面的4个方法中,前面3个方法的返回值都是一个ExecutorService对象。该ExecutorService对象就代表着一个尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务),程序只要将一个Runnable对象或Callable对象提交给该线程池即可,该线程就会尽快的执行该任务。
ExecutorService有几个重要的方法:
execute(Runnable command) 在未来某个时间执行给定的命令。(继承自Executor)
isShutdown() 如果此执行程序已关闭,则返回 true。
isTerminated() 如果关闭后所有任务都已完成,则返回 true。
shutdown() 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
shutdownNow() 试图停止所有正在执行的活动任务,暂停处理正在等待的任务,并返回等待执行的任务列表。
submit(Callable<T> task) 提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。
submit(Runnable task) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
submit(Runnable task, T result) 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
3、ScheduleExecutorService类
ScheduleExecutorService代表可在指定延迟或周期性执行线程任务的线程池。
ScheduleExecutorService类是ExecutorService类的子类。所以,它里面也有直接提交任务的submit方法,并且新增了一些延迟任务处理的方法:
schedule(Callable<V> callable, long delay, TimeUnit unit) 创建并执行在给定延迟后启用的 ScheduledFuture。
schedule(Runnable command, long delay, TimeUnit unit) 创建并执行在给定延迟后启用的一次性操作。
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,后续操作具有给定的周期;也就是将在 initialDelay 后开始执行,然后在 initialDelay+period 后执行,接着在 initialDelay + 2 * period 后执行,依此类推。
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) 创建并执行一个在给定初始延迟后首次启用的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
例子
public class PoolTest {
public static void main(String[] args) {
//创建一个固定大小为5的线程池
ExecutorService pool=Executors.newFixedThreadPool(3);
for(int i=0;i<5;i++){
pool.submit(new MyThread());
}
pool.shutdown();
}
}
class MyThread extends Thread{
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"正在执行。。。");
}
}
输出结果:
[java] view plaincopy
pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-1正在执行。。。
可以看到虽然我们呢创建了7个MyThread线程对象,但是由于受线程池的大小限制,只是开启了5个线程,这样就减少了并发线程的数量。
如果换成单任务
输出结果: ExecutorService pool=Executors.newSingleThreadExecutor();
[java] view plaincopy
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。
pool-1-thread-1正在执行。。。 可以看到,线程池只开启了一个线程。
创建可变尺寸的线程池:ExecutorService pool=Executors.newCachedThreadPool();
看输出结果:
[java] view plaincopy
pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-5正在执行。。。 可以看到,我们没有限制线程池的大小,但是它会根据需求而创建线程。
延迟线程池
public class PoolTest {
public static void main(String[] args) {
ScheduledExecutorService pool=Executors.newScheduledThreadPool(6);
for(int i=0;i<4;i++){
pool.submit(new MyThread());
}
pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS);
pool.schedule(new MyThread(), 1000, TimeUnit.MILLISECONDS);
pool.shutdown();
}
}
输出结果:
[java] view plaincopy
pool-1-thread-1正在执行。。。
pool-1-thread-3正在执行。。。
pool-1-thread-2正在执行。。。
pool-1-thread-4正在执行。。。
pool-1-thread-6正在执行。。。
pool-1-thread-1正在执行。。。
可以明显看到,最后两个线程不是立即执行,而是延迟了1秒在执行的。
其他线程知识
Timer和TimerTask可以做为实现线程的第三种方式,前两中方式分别是继承自Thread类和实现Runnable接口。 Timer是一种线程设施,用于安排以后在后台线程中执行的任务。可安排任务执行一次,或者定期重复执行,可以看成一个定时器,可以调度TimerTask。TimerTask是一个抽象类,实现了Runnable接口,所以具备了多线程的能力。 一个Timer可以调度任意多个TimerTask,它会将TimerTask存储在一个队列中,顺序调度,如果想两个TimerTask并发执行,则需要创建两个Timer。schedule是Timer调度任务的方法,Timer重构了四个schedule方法,具体可以查看JDK API。 自JDK5之后,可以用ScheduledThreadPoolExecutor来替代Timer。所以这里不做过多描述。
Callable接口类似于Runnable,从名字就可以看出来了,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到,岂不美哉!
下面来看另一种方式使用Callable和Future,通过ExecutorService的submit方法执行Callable,并返回Future,代码如下:
public class CallableAndFuture {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<Integer> future = threadPool.submit(new Callable<Integer>() {
public Integer call() throws Exception {
return new Random().nextInt(100);
}
});
try {
Thread.sleep(5000);// 可能做一些事情
System.out.println(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
在Java中,如果需要设定代码执行的最长时间,即超时,可以用Java线程池ExecutorService类配合Future接口来实现。 ExecutorService继承自Executor,它的目的是为我们管理Thread对象,从而简化并发编程,Executor使我们无需显示的去管理线程的生命周期,是JDK 5之后启动任务的首选方式。 执行多个带返回值的任务,并取得多个返回值。 Future接口是Java标准API的一部分,在java.util.concurrent包中。Future接口是Java线程Future模式的实现,可以来进行异步计算。 Future模式可以这样来描述:我有一个任务,提交给了Future,Future替我完成这个任务。期间我自己可以去做任何想做的事情。一段时间之后,我就便可以从Future那儿取出结果。就相当于下了一张订货单,一段时间后可以拿着提订单来提货,这期间可以干别的任何事情。其中Future接口就是订货单,真正处理订单的是Executor类,它根据Future接口的要求来生产产品。 Future接口提供方法来检测任务是否被执行完,等待任务执行完获得结果,也可以设置任务执行的超时时间。这个设置超时的方法就是实现Java程序执行超时的关键。 Future接口是一个泛型接口,严格的格式应该是Future<V>,其中V代表了Future执行的任务返回值的类型。 Future接口的方法介绍如下:
boolean cancel(boolean mayInterruptIfRunning) 取消任务的执行。参数指定是否立即中断任务执行,或者等等任务结束
boolean isCancelled() 任务是否已经取消,任务正常完成前将其取消,则返回 true
isDone() 任务是否已经完成。需要注意的是如果任务正常终止、异常或取消,都将返回true V
get() throws InterruptedException, ExecutionException 等待任务执行结束,然后获得V类型的结果。InterruptedException 线程被中断异常, ExecutionException任务执行异常,如果任务被取消,还会抛出 CancellationException V
get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException 同上面的get功能一样,多了设置超时时间。参数timeout指定超时时间,uint指定时间的单位,在枚举类TimeUnit中有相关的定义。如果计算超时,将抛出TimeoutException
Future的实现类有java.util.concurrent.FutureTask<V>即 javax.swing.SwingWorker<T,V>。通常使用FutureTask来处理我们的任务。FutureTask类同时又实现了Runnable接口,所以可以直接提交给Executor执行。使用FutureTask实现超时执行的代码如下:ExecutorService executor = Executors.newSingleThreadExecutor();FutureTask<String> future = new FutureTask<String>(new Callable<String>() {//使用Callable接口作为构造参数 public String call() { //真正的任务在这里执行,这里的返回值类型为String,可以为任意类型 }});executor.execute(future);//在这里可以做别的任何事情try {
//取得结果,设置超时执行时间为5秒。同样可以用future.get(),不设置执行超时时间取得结果 result = future.get(5000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { futureTask.cancel(true);} catch (ExecutionException e) { futureTask.cancel(true);} catch (TimeoutException e) { futureTask.cancel(true);} finally { executor.shutdown();} 不直接构造Future对象,也可以使用ExecutorService.submit方法来获得Future对象,submit方法即支持以Callable接口类型,也支持Runnable接口作为参数,具有很大的灵活性。使用示例如下:
ExecutorService executor = Executors.newSingleThreadExecutor();FutureTask<String> future = executor.submit( new Callable<String>() {//使用Callable接口作为构造参数 public String call() { //真正的任务在这里执行,这里的返回值类型为String,可以为任意类型 }});//在这里可以做别的任何事情//同上面取得结果的代码