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),这五种状态的转换关系如下图所示:

clip_image001

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()方法,该方法会把对象的等待池中的所有线程都转到对象的锁池中。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com