Java学习总结之第十三章 多线程
在实现多线程时,Java语言提供了三种实现方式:
l 继承Thread类
l 实现Runnable接口
l 使用Timer和TimerTask组合
一、继承Thread类
1. 如果一个类继承了Thread类,则该类就具备了多线程的能力,则该类则可以以多线程的方式进行执行。示例代码如下:
public class FirstThread extends Thread{ public static void main(String[] args) { //初始化线程 FirstThread ft = new FirstThread(); //启动线程 ft.start(); try{ for(int i=0;i<10;++i){ //延时1秒 Thread.sleep(1000); System.out.println("main:"+i); } }catch(Exception e){} } public void run(){ try{ for(int i=0;i<10;++i){ //延时1秒 Thread.sleep(1000); System.out.println("run"+i); } }catch(Exception e){} } } |
2. 线程的代码必须书写在run方法内部或者在run方法内部进行调用。
3. 可以把线程以单独类的形式出现。一个类具备了多线程的能力以后,可以在程序中需要的位置进行启动,而不仅仅是在main方法内部启动。
4. 当自定义线程中的run方法执行完成以后,则自定义线程将自然死亡。而对于系统线程来说,只有当main方法执行结束,而且启动的其它线程都结束以后,才会结束。当系统线程执行结束以后,则程序的执行才真正结束。
5. 在Thread子类中不应该随意覆盖start()方法,假如一定要覆盖start()方法,那么应该先调用super.start()方法。
6. 当自定义线程中的run方法执行完成以后,则自定义线程将自然死亡。所以一个线程只能被启动一次,否则会抛出java.lang.IllegalThreadStateException异常。
二、实现Runnable接口
一个类如果需要具备多线程的能力,也可以通过实现java.lang.Runnable接口进行实现。示例代码如下:
//MyRunnable.java public class MyRunnable implements Runnable{ public void run(){ try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println("run:" + i); } }catch(Exception e){} } } //Test.java public class Test { public static void main(String[] args) { MyRunnable mr = new MyRunnable(); Thread t = new Thread(mr); t.start(); try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println("main:" + i); } }catch(Exception e){} } } |
三、使用Timer和TimerTask组合
1. 在这种实现方式中,Timer类实现的是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。而TimerTask类是一个抽象类,该类实现了Runnable接口,该类具备多线程的能力。
2. 在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
3. 在实际使用时,一个Timer可以启动任意多个TimerTask实现的线程,但是多个线程之间会存在阻塞。所以如果多个线程之间如果需要完全独立运行的话,最好还是一个Timer启动一个TimerTask实现。
4. 以下是示例代码:
//MyTimerTask.java import java.util.TimerTask; public class MyTimerTask extends TimerTask{ String s; public MyTimerTask(String s){ this.s = s; } public void run(){ try{ for(int i = 0;i < 10;i++){ Thread.sleep(1000); System.out.println(s + i); } }catch(Exception e){} } } //Test.java import java.util.Timer; public class Test { public static void main(String[] args) { //创建Timer Timer t = new Timer(); //创建TimerTask MyTimerTask mtt1 = new MyTimerTask("线程1:"); //启动线程 t.schedule(mtt1, 0); } } |
5. Timer类中启动线程还包含两个scheduleAtFixedRate方法,这其作用是实现重复启动线程时的精确延时。
四、线程的状态转换
1. Java中的线程有五种基本状态:新建状态(New),就绪状态(Runnable),运行状态(Running),阻塞状态(Blocked)和死亡状态(Dead),这五种状态的转换关系如下图所示:
2. 阻塞状态是指线程因某些原因放弃CPU,暂时停止运行。分为以下三种:
a) 位于对象等待池中的阻塞状态(Blocked in object’s wait pool):当线程处于运行状态时,如果执行了某个对象的wait()方法,Java虚拟机就会把线程放到这个对象的等待池中。
b) 位于对象锁池中的阻塞状态(Blocked in object’s lock pool):当线程处于运行状态,试图获得某个对象的同步锁时,如果该对象的同步锁已经被其他线程占用,Java虚拟机会把这个线程放到这个对象的锁池中。
c) 其他阻塞状态(Otherwise Blocked):当前线程执行了sleep()方法,或者调用了其他线程的join()方法,或者发出了I/O请求时,就会进入这个状态。
五、线程调度
1. Java虚拟机采用抢占式调度模型,线程的调度不是分时的,同时启动多个线程后,不能保证各个线程轮流获得均等的CPU时间片。
2. 如果希望明确地让一个线程给另外一个线程运行的机会,可以采取以下方法之一:
a) 调整各个线程的优先级。
b) 让处于运行状态的线程调用Thread.sleep()方法。
c) 让处于运行状态的线程调用Thread.yield()方法。
d) 让处于运行状态的线程调用另一个线程的join()方法。
3. Thread类的setPriority(int)和getPriority()方法分别用来设置优先级和读取优先级。优先级用整数表示,取值范围是1~10,Thread类有以下3个静态常量:
a) MAX_PRIORITY:取值为10,表示最高优先级。
b) MIN_PRIORITY:取值为1,表示最低优先级。
c) NORM_PRIORITY:取值为5,表示默认的优先级。
4. 主线程的默认优先级为Thread.NORM_PRIORITY。如果线程A创建了线程B,那么线程B和线程A具有相同的优先级。
5. 线程睡眠:Thread.sleep()——当一个线程在运行中执行了sleep()方法时,它就放弃CPU,转到阻塞状态。当线程结束睡眠后,首先转到就绪状态。如果线程在睡眠时被中断,就会收到一个InterruptException异常。
6. 线程让步:Thread.yield()——当线程在运行中执行了Thread类的yield()静态方法,如果此时具有相同优先级的其他线程处于就绪状态,那么yield()方法将把当前运行的线程放到可运行池中并使另一个线程运行,如果没有相同优先级的可运行线程,则yield()方法什么也不做。
7. sleep()方法和yield()方法都是Thread类的静态方法,都会使当前处于运行状态的线程放弃CPU,把运行机会让给别的线程,两者的区别在于:
a) sleep()方法会给其他线程运行机会,而不考虑其他线程的优先级,因此会给较低优先级线程一个运行机会;yield()方法只会给相同优先级或者更高优先级的线程一个运行的机会。
b) 当线程执行了sleep(long millis)方法后,将转到阻塞状态,参数millis指定睡眠时间;当线程执行了yield()方法后,将转到就绪状态。
c) sleep()方法声明抛出InterruptException异常,而yield()方法没有声明抛出任何异常。
d) sleep()方法比yield()方法具有更好的可移植性,不能依靠yield()方法来提高程序的并发性能。
8. 等待其他线程结束:join()——当前线程可以调用另一个线程的join()方法,当时运行的线程将转到阻塞状态,直到另一个线程运行结束,它才会恢复运行。
六、获得当前线程对象的引用及其他
1. Thread类的currentThread()静态方法返回当前线程对象的引用。
2. Thread类的getName()实例方法返回线程的名字。
3. Thread类的setName()实例方法可以显示地设置线程的名字。
七、后台线程
1. 后台线程是指为其他线程提供服务的线程,也称为守护线程。
2. 后台线程与前台线程相伴相随,只有所有的前台线程都结束生命周期,后台线程才会结束生命周期。只要有一个前台线程还没有运行结束,后台线程就不会结束生命周期。
3. 主线程在默认情况下是前台线程,由前台线程创建的线程在默认情况下也是前台线程。
4. 调用Thread类的setDaemon(true)方法,就能把一个线程设置为后台线程。Thread类的isDaemon()方法用来判断一个线程是否是后台线程。
5. 使用后台线程,要注意以下几点:
a) Java虚拟机所能保证的是,当所有后台线程都运行结束时,假如后台线程还在运行,Java虚拟机就会终止。此外,后台线程是否一定在前台线程的后面结束生命周期,还取决于程序的实现。
b) 只有在线程启动前(即调用start()方法前),才能把线程设置为后台线程。如果线程启动后再调用这个线程的setDaemon()方法,就会导致IllegalThreadStateException异常。
c) 由前台线程创建的线程在默认情况下仍然是前台线程,由后台线程创建的线程在默认情况下仍然是后台线程。
八、线程的同步
1. 原子操作由相关的一组操作完成,这些操作可能会操纵与其他线程共享的资源。一个线程在执行原子操作的期间,必须采取措施使得其他线程不能操纵共享资源。
2. 为了保证每个线程都能正常地执行原子操作,Java引入了同步机制,具体做法是在代表原子操作的程序代码前加上synchronized标记,这样的代码被称为同步代码块。
3. 每个Java对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。
a) 假如这个锁已经被其他线程占用,Java虚拟机就会把这个线程放到对象的锁池中,这个线程进入阻塞状态。在对象的锁池中可能会有许多等待锁的线程,等到其他线程释放了锁,Java虚拟机会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
b) 假如这个锁没有被其他线程占用,线程就会获得这把锁,开始执行同步代码块。在一般情况下,线程只有在执行完同步代码块后才会释放锁。
4. 如果一个方法中的所有代码都属于同步代码,则可以直接在方法前用synchronized修饰。
5. 当一个线程执行一个对象的同步代码块时,其他线程仍然可以执行对象的非同步代码块。
6. 在静态方法前也可以使用synchronized修饰符。
7. 当一个线程开始执行同步代码块时,并不意味着必须以不中断的方式运行,进入同步代码块的线程也可以执行Thread.sleep()或者执行Thread.yield()方法,此时它并没有释放锁,只是把运行机会(即CPU)让给了其他线程。
8. synchronized声明不会被继承。
9. 在以下情况下,持有锁的线程会释放锁:
a) 执行完同步代码块,就会释放锁。
b) 在执行同步代码块的过程中,遇到异常而导致线程终止,锁也会被释放。
c) 在执行同步代码块的过程中,执行了锁所属对象的wait()方法,这个线程也会自觉释放锁,进入对象的等待池。
10. 除了以上情况外,只要持有锁的线程还没有执行完同步代码块,就不会释放锁。因此在以下情况下,线程不会释放锁:
a) 在执行同步代码块的过程中,执行了Thread.sleep()方法,当前线程放弃CPU,开始睡眠,在睡眠中不会释放锁。
b) 在执行同步代码块的过程中,执行了Thread.yield()方法,当前线程释放CPU,但不会释放锁。
c) 在执行同步代码块的过程中,其他线程执行了当前线程对象的suspend()方法,当前线程被暂停,但不会释放锁。Thread类的suspend()方法已经被废弃。
11. 避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B和C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,再访问B和C。
九、线程通信
1. java.lang.Object类中提供了两个用于线程通信的方法:
a) wait():执行该方法的线程会释放对象的锁,Java虚拟机把该线程放到该对象的等待池中。该线程等待其他线程将它唤醒。
b) notify():执行该方法的线程唤醒在对象的等待池中等待的一个线程,Java虚拟机从对象的等待池中随机选择一个线程,把它转到对象的锁池中。如果对象的等待池中没有任何线程,那么notify()方法什么也不做。
2. Object类还有一个notifyAll()方法,该方法会把对象的等待池中的所有线程都转到对象的锁池中。