黑马程序员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 

原文地址:https://www.cnblogs.com/jbelial/p/2964472.html