黑马程序员-JAVA基础-多线程(上)
单线程的程序只有一个顺序流;而多线程的程序则可以包括多个顺序执行流,并且多个顺序流之间互不干扰。就像单线程程序如同只雇佣了一个服务员的餐厅,他只有做完一件事情后才可以做下面一件事情;而多线程程序则是雇佣了多名服务员的餐厅,他们可以同时进行着多件事情。
JAVA多线程编程的相关知识:创建、启动线程、控制线程、以及多线程的同步操作。
1.概述:
进程是指正在运行中的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或叫一个执行单元。
线程是指进程中能够独立执行的控制单元。线程控制着进程的执行。一个进程可以同时运行多个不同的线程。
两者的区别:
一个程序运行后至少有一个进程,一个进程里可以包含一个或多个线程。
每个进程都需要操作系统为其分配独立的内存地址空间,而同一进程中的所有线程在同一块地址空间中工作。这些线程可以共享进程的状态和资源。
2.创建和启动线程:
创建线程有两种方式:
1、继承 java.lang.Thread 类。
2、实现Runnable 接口。
2.1 继承Thread 类创建线程类:
步骤如下 :
1、定义Thread 类的子类,并重写该线程的run() 方法。
2、创建Thread 类的子类的实例,即创建线程对象。
3、调用线程的start方法来启动该线程。
1 //定义Thread 类的子类 2 class ThreadText extends Thread { 3 // 重写run()方法 4 public void run() 5 { 6 for (int a = 0 ; a <10 ; a ++) 7 { 8 System.out.println( currentThread().getName() + ":" + a); 9 } 10 } 11 } 12 public class StratThread { 13 14 public static void main(String args[]) 15 { 16 // 创建Thread 类的子类的实例 17 ThreadText t = new ThreadText() ; 18 // 调用线程的start()方法来启动该线程 19 t.start() ; 20 21 // 主线程调用用户线程的对象run()方法。 22 t.run() ; 23 } 24 }
为什么要重写Thread 类的run() 方法?
Thread 类定义了一个功能,就是用于存储线程要运行的代码,该存储功能就是 run() 方法。即 该run() 方法的方法体就是代表了线程需要完成的任务。所以,我们也把run() 方法称为线程执行体。
为什么要调用 start() 方法来启动线程,而不是run() 方法?
因为调用start() 方法来启动线程,系统会把run() 方法当成线程执行体来处理,而如果直接调用线程对象的run() 方法,则系统会把线程对象当成一个普通的对象,而run() 方法也是一个普通方法,而不是线程执行体。
在上面的代码中第19行和第22行分别调用了start() 和 run() 方法。通过运行的结果如下:
main:0 Thread-0:0 main:1 Thread-0:1 main:2 Thread-0:2 main:3 Thread-0:3 main:4 Thread-0:4 main:5 Thread-0:5 main:6 Thread-0:6 main:7 Thread-0:7 Thread-0:8 Thread-0:9 main:8 main:9
通过运行结果可以看到两个线程交替运行:t 和 main 线程(当运行JAVA程序时,JVM 首先会创建并启动主线程,主线程从main() 开始运行)。所以,当调用 Thread 类的子类调用start() 方法是启动该线程;而调用run() 方法时则只是让主线程运行其run() 方法中的代码,并没有启动新的线程。
注意:局部变量在每一个线程中都是独立的一份。
在上面程序第8行用还用到了Thread 类的两个方法:
> static Thread currentThread () : 该方法是Thread 类的静态方法,该方法返回当前正在执行的线程对象。
> String getName() : 该方法是Thread 的实例方法,该方法返回调用该方法的线程的名字。
2.2 实现Runnable 接口创建线程类:
1、定义 Runnable 接口的实现类,并重写该接口的run方法。
2、创建 Runnable 实现类的实例,并以此实例作为Thread 的target 来创建Thread 对象,该Thread 对象才是真正的线程对象。
3、调用线程对象的 start() 方法来启动该线程。
1 //定义Runnable 接口的实现类 2 class RunnableText implements Runnable 3 { 4 // 重写接口的run方法。 5 public void run() 6 { 7 for (int a = 0 ; a <10 ; a ++) 8 { 9 System.out.println( Thread.currentThread().getName() + ":" + a); 10 } 11 } 12 } 13 14 public class ThreadTextRunnable { 15 16 public static void main(String args[]) 17 { 18 // 创建Runnable 实现类的实例 19 RunnableText t = new RunnableText() ; 20 21 // 通过new Thread(Runnable target ) 创建新线程 22 Thread thread = new Thread(t) ; 23 Thread thread1 = new Thread(t) ; 24 25 // 启动线程。 26 thread.start() ; 27 thread1.start() ; 28 } 29 }
为什么要将Runnable 实现类的实例传递给Thread 的构造函数?
因为,自定的run() 方法所属是 Runnable 接口的实现类的实例。所以,要让线程去指定对象的run() 方法,就必须明确该 run() 方法所属的对象。
2.3 两种方式的区别:
2.3.1 采用实现 Runnable 接口方式的多线程
1、线程类只是实现了 Runnable 接口。所以,还可以继承其他的类。
2、在这种方式下,可以多个线程共享同一个target 对象,所以适合多个相同线程处理同一分资源的情况。
2.3.2 采用继承 Thread 类方式的多线程
1、因为线程继承了 Thread 类,所以不能继承其他父类。
3.线程的运行状态
当线程被创建并启动后,并不是已启动就进入执行状态,也不是一直处于执行状态。在线程的生命周期中要经历如下集中状态:新建、就绪、运行、阻塞、和死亡五种状态。
3.1 新建(New)和就绪(Runnable) 状态:
当程序使用 new 关键字创建一个线程之后,该线程就处于新建状态,这时候它仅仅由JVM为其分配了内存。
当线程对象调用 start() 方法之后,该线程就处于就绪状态。这个状态的线程处于有运行资格,却没有运行权利。如何有运行权利则取决于JVM里线程调度器。
3.2 就绪(Runnable)、运行(Running)和阻塞(Blocked)状态:
如果就绪状态获得了运行权利,则开始执行 run() 方法 的线程执行体,则该线程处于运行状态。
当一条线程开始运行时,他不可能一直处于运行状态(除非它的线程执行体足够短,瞬间就执行结束了)。所以当其他的线程抢到CPU的执行权利时,运行状态则重新进入就绪状态。但是当运行状态的线程发生如下情况时,则会进入阻塞状态(放弃所占用的资源,即没有执行资格):
1、线程调用sleep 方法。当sleep(time) 的时间到了 ,线程又会进入就绪状态。
2、线程试图获得一个同步监视器,但该同步监视器正在被其他线程所持有。
3、线程在等待某个通知(notify、notifyAll),即被wait 挂起。
3.3 线程死亡(Dead)
线程结束后就处于死亡状态:
1、run() 方法执行完成,线程正常结束。
2、线程异常。
3、直接调用该线程的stop() 方法来结束该线程。
注意:不要对处于死亡状态的线程调用 start() 方法,程序只能对新建状态的线程调用 start() 方法。同时对新建状态的线程两次调用start() 方法也是错误的。
4.控制线程
4.1 join 线程
当A 线程执行到了 B 线程的join() 方法时,A 线程就会等待,等 B 线程执行完,A 才会执行。即A 线程 等待B 线程终止。
1 // 定义Runnable 接口的实现类 2 public class JoinText implements Runnable { 3 // 重写run() 方法 4 public void run() 5 { 6 for ( int i = 0; i <= 10 ; ++ i) 7 System.out.println(Thread.currentThread().getName()+ "..." + i); 8 } 9 public static void main(String args[]) throws InterruptedException 10 { 11 // 创建Runnable 接口实现类的实例 12 JoinText t = new JoinText() ; 13 // 通过 Thread(Runnable target) 创建新线程 14 Thread thread = new Thread(t) ; 15 Thread thread1 = new Thread(t) ; 16 // 启动线程 17 thread.start() ; 18 // 只有等thread 线程执行结束,main 线程才会向下执行; 19 thread.join() ; 20 System.out.println("thread1 线程将要启动"); 21 thread1.start() ; 22 } 23 }
在上面程序中,thread 线程调用了join() 方法。所以main 线程会等 thread 线程执行结束才会向下执行,所以运行的结果如下:
Thread-0...0 Thread-0...1 Thread-0...2 Thread-0...3 Thread-0...4 Thread-0...5 Thread-0...6 Thread-0...7 Thread-0...8 Thread-0...9 Thread-0...10 thread1 线程将要启动 Thread-1...0 Thread-1...1 Thread-1...2 Thread-1...3 Thread-1...4 Thread-1...5 Thread-1...6 Thread-1...7 Thread-1...8 Thread-1...9 Thread-1...10
join 的方法有三种重载形式:
void join() : 等待该线程终止。
void join(long millis) : 等待该线程中指的时间最长为 millis 毫秒。
void join(long millis , int nanos) 等待该线程终止的时间最长为 millis
毫秒 + nanos
纳秒。
4.2 守护线程(后台线程)
Daemon Thread ,JVM 的垃圾回收机制就是典型的 后台线程。
调用 Thread 对象 setDaemon(true) 方法可以指定线程设置成后台线程。该方法必须在启动线程钱调用,否则会引发 IllegalThreadStateException 异常。
后台线程特点:如果所有前台线程都死亡,后台线程会自动死亡。
1 public class DaemonText implements Runnable { 2 public void run() 3 { 4 for (int i = 0 ; i < 1000 ; i ++) 5 { 6 System.out.println(Thread.currentThread().getName()+"..."+i); 7 } 8 } 9 public static void main(String args[]) 10 { 11 DaemonText t = new DaemonText() ; 12 Thread thread = new Thread(t) ; 13 14 // 将次线程设置为后台线程 15 thread.setDaemon(true) ; 16 // 启动后台线程 17 thread.start() ; 18 19 for(int i = 0 ; i < 10; i ++) 20 { 21 System.out.println(Thread.currentThread().getName()+"......"+i); 22 } 23 // 程序执行到此,前台线程结束。 24 // 后台线程也随之结束。 25 } 26 }
因为thread 线程被设置成了 后台线程。所以,当主线程运行完后 ,JVM 会主动退出,因而后台线程也被结束。
4.3 线程睡眠:sleep
当当前线程调用 sleep 方法进入阻塞状态后,在其sleep 时间段内,该线程不会获得执行机会,即使系统中没有其他线程了,直到sleep 的时间到了。所以,调用sleep 能让线程暂短暂停。
4.4 线程让步:yield
和sleep 类似,也可以让当前执行的线程暂停,但他不会让线程进入阻塞状态。而是,取消当前线程的执行权,使当前线程进入就绪状态。就是相当于让系统的线程调度器重新调度一次。
4.5 改变线程的优先级
每个线程执行时都具有一定的优先级,优先级高的则获得多的运行机会,优先级低的则获得较少的运行机会。
Thread 提供了 setPriority(int newPriority) 和 getPriority() 方法来设置和返回指定线程的优先级。设置优先级的整数在1~10之间,也可以使用Thread 类的三个静态常量:
> MAX_PRIORITY : 其值是 10
> MIN_PRIORITY : 其值是 1
> NORM_PRIORITY: 其值是 5