黑马程序员_Java基础:多线程总结

时间:2023-02-20 10:36:07

  ------- android培训、java培训、期待与您交流! ----------

一、多线程的概念

  进程和线程经常会被人混淆,那是因为对它们的概念不明确。就拿我们平时使用的操作系统来说,它是多任务的操作系统,而多线程就是实现多任务的一种方式。
  进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。
   线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如平时下载的软件迅雷进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。
   说起多线程,给很多人得印象就是“同时”执行。但是多线程在并发执行的时候,其实是在线程之间快速轮换执行的,只不过轮换的速度很快,所以有“同时”执行的效果。

 

二、多线程的优缺点

  优点:
  (1)资料利用率更好。
  (2)部分程序设计更加简单。
  (3)程序响应更快。
  (4)简化异步时间的处理(服务器)。

  缺点:
  (1)部分程序设计更复杂。
  (2)增加cpu上下文切换开销。
  (3)增加资源消耗。

 

三、多线程的创建和使用

  在java中提供了对象线程这类事物的描述,该类是Thread。线程的常用创建和启动方式有两种:

  1.继承Thread类,子类覆盖父类中的run方法,把需要多线程执行的代码存放在run方法中。然后创建子类的对象同时线程也会被创建起来。最后通过子类调用start方法开启线程。

具体如下面代码:

 1 class Demo extends Thread {  //继承Thread类。
 2     public void run() {    //覆盖run方法。
 3         for(int x=0; x<60; x++)
 4             System.out.println("线程1----"+x);
 5     }
 6 }
 7 
 8 public class ThreadDemo {
 9     public static void main(String[] args) {
10         Demo d = new Demo();//创建好一个线程。
11         d.start();    //开启线程并执行该线程的run方法。
12         //d.run();    //注意,这里仅仅是对象调用方法。而线程创建了,并没有运行。
13         
14         for(int x=0; x<60; x++)
15             System.out.println("主线程--"+x);    
16     }
17 }

 

  2.实现Runnable接口,子类覆盖接口中的run方法,把需要多线程执行的代码存放在run方法中。然后通过Thread创建线程,并将实现了Runnable接口的子类对象作为参数传递给Thread类的构造函数。最后Thread类对象调用Start方法开启线程。

具体如下代码:

 1 class Demo_2 implements Runnable {
 2     private  int x = 100;
 3     public void run() {
 4         while(true) {
 5             if(x>0)
 6 //                Thread.currentThread().getName()获取当前线程的名称。
 7                 System.out.println(Thread.currentThread().getName()+"....x : "+ x--);
 8         }
 9     }
10 }
11 
12 public class  ThreadDemo_2 {
13     public static void main(String[] args) {
14         Demo_2 d = new Demo_2();
15 
16         Thread t1 = new Thread(d);//创建了一个线程;
17         Thread t2 = new Thread(d);//创建了一个线程;
18         Thread t3 = new Thread(d);//创建了一个线程;
19         Thread t4 = new Thread(d);//创建了一个线程;
20         t1.start();
21         t2.start();
22         t3.start();
23         t4.start();
24     }
25 }

 

  以上两种创建方式的区别:
  (1)继承Thread:线程代码存放Thread子类run方法中。
  (2)实现Runnable:线程代码存在接口的子类的run方法,使用起来更加灵活,可以避免单继承局限性,因为java中的对象是单继承多实现的。

  另外通过以上的运行可以发现,运行结果每一次都不同,这是因为线程需要获取cpu的执行权时才能执行。在多个线程中,cpu执行到谁,谁就运行。我们可以把形象的把多个线程在抢夺cpu的资源,这也是多线程随机性的体现。需要明确的一点是,在某一个时刻,只能有一个线程在运行(多核除外)。

 

四、线程的状态转换

  下面是线程状态转换示意图:

  黑马程序员_Java基础:多线程总结

  从上图可以看出,一个线程从执行到结束,会经历几种状态,下面对这几种状态进行解释。

  1.创建线程:也可以说叫new状态,就是把线程建立起来。
  2.阻塞状态:当线程start开启后,不一定马上进入运行状态,而是可能先进入阻塞状态,阻塞状态表示程序准备就绪,等待系统的调度。也可以说是具有运行权,但没有执行权。
  3.运行状态:线程接收到系统的调度,执行起来。也就是说线程具有执行权。
  4.冻结状态:当线程处于处于sleep,wait等状态时,即是冻结状态,表示线程还是活的,但是没有运行权。另外当一个线程休眠或者被唤醒结束时,不一定马上进入运行状态,而是可能先进入阻塞状态,等待系统调度,当被调度具有执行权时,再进入运行状态。
  5.消亡状态:就是线程结束。

  当我们对线程各种状态有了基本了解后,设计多线程时才能更加效率准确。

 

五、多线程的同步

  当多个线程间通讯,也就是多个线程的同时操作同一资源时,如果一个线程正在访问修改一些数据的同时,另一个线程也在修改这些数据,那么前一个线程的修改操作后就得到错误数据,造成安全隐患。

  解决办法:
  当有多条对共享数据进行操作的语句时,要让一个线程都执行完,而在执行过程中,其他线程不可以参与执行,也就是实现同步。

  实现同步的方法:
  java中对于实现同步提供了解决方法。

  1.synchronized同步代码块: 

  使用同步代码块的前提,是具有两个或以上的线程,而且多个线程必须使用同一把锁。当线程持有锁时可以在同步中执行,而没有持有锁的线程即使获取cpu的执行权,也无法执行同步代码块里的代码。同时,也因为同步的特性,当多个线程判断锁时,有较为消耗资源的缺点。以下是同步代码块的应用: 

 1 /*
 2 多个窗口销售1000张票,票号不重复。
 3  */
 4 class Ticket implements Runnable {
 5     private  int tick = 1000;
 6     Object obj = new Object();
 7     public void run() {
 8         while(true) {
 9             synchronized(obj) {    //同步代码块,需要传递一个对象参数,可以把对象看成是锁。
10                 //此处两条语句操作tick,多个线程执行时,可能出现安全隐患,需要同步
11                 if(tick>0)
12                     System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--);
13             }
14         }
15     }
16 }
17 
18 
19 public class  TicketDemo {
20     public static void main(String[] args) {
21 
22         Ticket t = new Ticket();
23 
24         Thread t1 = new Thread(t);
25         Thread t2 = new Thread(t);
26         Thread t3 = new Thread(t);
27         Thread t4 = new Thread(t);
28         t1.start();
29         t2.start();
30         t3.start();
31         t4.start();
32     }
33 } 

从结果可以看出没有错票,实现同步。

 

  2.synchronized同步函数:

  synchronized同步函数和synchronized同步代码块的功能是一样的,不同的是在同步函数中,synchronized修饰的是函数。

  另外要注意,synchronized同步函数的锁对象有两种情况:
  (1)当synchronized修饰的函数不是静态函数时,那么synchronized对应的锁对象是this,即是调用方法的对象。看以下代码验证:

 1 /*
 2 非静态同步函数用的是哪一个锁呢?
 3 非静态函数需要被对象调用。那么函数都有一个所属对象引用。就是this。
 4 所以非静态同步函数使用的锁是this。
 5 
 6 通过该程序进行验证。
 7 使用两个线程来买票。
 8 一个线程在同步代码块中。
 9 一个线程在同步函数中。
10 都在执行买票动作。
11 */
12 class Ticket implements Runnable {
13     private  int tick = 1000;
14     Object obj = new Object();
15     boolean flag = true;    //定义标记,不同标记执行不同的代码。
16     public  void run() {
17         if(flag) {
18             while(true) {
19                 synchronized(this) {
20                     if(tick>0) {
21                         //让线程停顿,方便验证结果。
22                         try{Thread.sleep(10);}catch(Exception e){}
23                         System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
24                     }
25                 }
26             }
27         }
28         else
29             while(true)
30                 show();
31     }
32     public synchronized void show(){  //非静态同步代函数
33         if(tick>0) {
34             try{Thread.sleep(10);}catch(Exception e){}
35             System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
36         }
37     }
38 }
39 
40 
41 public class  ThisLockDemo {
42     public static void main(String[] args) {
43         Ticket t = new Ticket();
44 
45         Thread t1 = new Thread(t);
46         Thread t2 = new Thread(t);
47         t1.start();
48         try{Thread.sleep(10);}catch(Exception e){}    //这里让线程停顿,方便验证同步。
49         t.flag = false;    //通过修改标记,t2线程执行不同的语句。
50         t2.start();
51     }
52 }

从结果可以看出没有错票,实现同步。


  (2)当synchronized修饰的函数是静态函数时,那么synchronized对应的锁对象是函数所在类的字节码对象(xxx.class),因为静态方法是属于类,不属于对象。看以下代码验证:

 1 /*
 2 如果同步函数被静态修饰后,使用的锁是什么呢?
 3 
 4 通过验证,发现不在是this。因为静态方法中也不可以定义this。
 5 静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。
 6 
 7 类名.class  该对象的类型是Class
 8 静态的同步方法,使用的锁是该方法所在类的字节码文件对象。 类名.class
 9 */
10 class Ticket implements Runnable {
11     private static  int tick = 1000;
12     //Object obj = new Object();
13     boolean flag = true;
14     public  void run() {
15         if(flag) {
16             while(true) {
17                 synchronized(Ticket.class) {
18                     if(tick>0) {
19                         try{Thread.sleep(10);}catch(Exception e){}
20                         System.out.println(Thread.currentThread().getName()+"....code : "+ tick--);
21                     }
22                 }
23             }
24         }
25         else
26             while(true)
27                 show();
28     }
29     public static synchronized void show() {  //静态同步代函数
30         if(tick>0) {
31             try{Thread.sleep(10);}catch(Exception e){}
32             System.out.println(Thread.currentThread().getName()+"....show.... : "+ tick--);
33         }
34     }
35 }
36 
37 
38 class  StaticMethodDemo {
39     public static void main(String[] args) {
40         Ticket t = new Ticket();
41 
42         Thread t1 = new Thread(t);
43         Thread t2 = new Thread(t);
44         t1.start();
45         try{Thread.sleep(10);}catch(Exception e){}
46         t.flag = false;
47         t2.start();
48     }
49 }

从结果可以看出没有错票,实现同步。

 

  在synchronized同步代码块和同步函数中,可以使用wait(),notify(),notifyAll()来控制线程,这三个方法的使用前提就是必须使用在同步中,而且使用它们的时候要标识它们所属的同步的锁,它们的作用是:
  ①wait():释放cpu执行权,释放锁。也就是说,当线程wait后就处于冻结状态。
  ②notify():唤醒第一个wait的线程。
  ③notify():唤醒全部等待的线程。

  使用这三种方法可以实现等待唤醒机制,以下是使用的实例代码:

 1 /*
 2 等待唤醒机制。
 3 也就是常见的生产者消费者问题,举个例子,生产者没生产一个商品,就要被消费者消费。
 4 
 5 1,当多个生产者消费者出现时。
 6 需要让获取执行权的线程判断标记。
 7 通过while完成。
 8 
 9 2,需要将对方的线程唤醒。
10 仅仅notify,是不可以的。
11 因为有可能出现只唤醒本方,导致所有线程都在等待。
12 所以要通过notifyAll形式来完成。
13 
14 
15 对于多个生产者和消费者。
16 为什么要定义while判断标记。
17 原因:让被唤醒的线程再一次判断标记。
18 */
19 
20 class Resource {
21     private String name;
22     private int count = 1;
23     private boolean flag = false;
24 
25     public synchronized void set(String name) {
26         while(flag)
27             try{this.wait();}catch(Exception e){}  //当线程执行到这里会进入等待状态,释放锁。
28         this.name = name+"--"+count++;
29         //注意下面输出语句name前必须加this,否则会直接访问局部变量中的name。
30         System.out.println(Thread.currentThread().getName()+"生产者:"+this.name);
31         flag = true;
32         this.notifyAll();  //唤醒所有等待的线程。
33     }
34 
35     public synchronized void out() {
36         while(!flag)
37             try{this.wait();}catch(Exception e){}
38         System.out.println(Thread.currentThread().getName()+"消费者:--"+this.name);
39         flag = false;
40         this.notifyAll();
41     }
42 }
43 
44 class Producer implements Runnable {
45     private Resource r;
46 
47     Producer(Resource r) {
48         this.r = r;
49     }
50 
51     public void run() {
52         while(true)
53             r.set("+商品+");
54     }
55 }
56 
57 
58 class Consumer implements Runnable {
59     private Resource r;
60     Consumer(Resource r) {
61         this.r = r;
62     }
63 
64     public void run() {
65         while(true)
66             r.out();
67     }
68 }
69 
70 
71 class ProducerConsumerDemo {
72     public static void main(String[] args) {
73         Resource r = new Resource();
74 
75         Producer pro = new Producer(r);
76         Consumer con = new Consumer(r);
77 
78         Thread t1 = new Thread(pro);
79         Thread t2 = new Thread(pro);
80         Thread t3 = new Thread(con);
81         Thread t4 = new Thread(con);
82 
83         t1.start();
84         t2.start();
85         t3.start();
86         t4.start();    
87     }
88 }

运行结果是不停的生产和消费,但生产和消费的数目总是一样的。说明程序通过标记成功控制每生产一个商品就消费一个,并且实现了多线程的同步。

 

  同时,等待唤醒机制要避免死锁的出现。所谓死锁就是同步中嵌套同步,然后多线程在执行时相互取得不同的锁后不释放,造成程序一致等待。请看下面例子:

 1 class Test implements Runnable {
 2     private boolean flag;
 3     Test(boolean flag) {
 4         this.flag = flag;
 5     }
 6 
 7     public void run() {
 8         if(flag) {
 9             while(true) {
10                 synchronized(MyLock.locka) {
11                     System.out.println(Thread.currentThread().getName()+"...if locka ");
12                     synchronized(MyLock.lockb) {
13                         System.out.println(Thread.currentThread().getName()+"..if lockb");                    
14                     }
15                 }
16             }
17         } else {
18             while(true) {
19                 synchronized(MyLock.lockb) {
20                     System.out.println(Thread.currentThread().getName()+"..else lockb");
21                     synchronized(MyLock.locka) {
22                         System.out.println(Thread.currentThread().getName()+".....else locka");
23                     }
24                 }
25             }
26         }
27     }
28 }
29 
30 
31 class MyLock {
32     static Object locka = new Object();
33     static Object lockb = new Object();
34 }
35 
36 class  DeadLockTest {
37     public static void main(String[] args)  {
38         Thread t1 = new Thread(new Test(true));
39         Thread t2 = new Thread(new Test(false));
40         t1.start();
41         t2.start();
42     }
43 }

运行结果是程序一直等待不动。

 

  另外需要注意的,wait()和sleep()的区别:
  ①wait()是Object类中的方法;sleep()是Thread类中的方法。
  ②它们两个都可以让线程冻结,wait()需要被唤醒;而sleep在冻结一定时间后会自动唤醒。
  ③wait()释放cpu执行权的同时,会释放锁;而sleep()只释放执行权,不会释放锁。

 

  (3)在JDk1.5中提供了更加灵活的多线程同步方法:Lock。相应地在使用Lock同步时,也可以使用Condition中的await(),signal();signalAll();来实现多线程等待唤醒机制。以下是具体使用例子:

  1 import java.util.concurrent.locks.*;
  2 
  3 /*
  4 JDk1.5中提供了多线程升级解决方案。
  5 将同步隐式锁Syncronized替换成显式Lock操作。
  6 将Object中的wait,notify,notifyAll,替换成Condition对象。
  7 该对象可以通过Lock锁进行获取。
  8 该示例中,实现了本方只唤醒对方的操作。
  9 
 10 Lock:替换了synchronized
 11     lock;
 12     unlock;
 13     newCondition();
 14 
 15 Condition:替代了Object中方法wait notify notifyAll
 16     await();
 17     signal();
 18     signalAll();
 19 */
 20 class Resource {
 21     private String name;
 22     private int count = 1;
 23     private boolean flag = false;
 24     private Lock lock  = new ReentrantLock();    //创建同步锁。
 25     private Condition condition_pro = lock.newCondition();    //通过Lock建立不同的条件。
 26     private Condition condition_con = lock.newCondition();
 27 
 28     public void set(String name)throws InterruptedException {
 29         try {
 30             lock.lock();
 31             while (flag)
 32                 condition_pro.await();
 33             this.name = name+"--"+count++;
 34             System.out.println(Thread.currentThread().getName()+"生产者:"+this.name);
 35             flag = true;
 36             condition_con.signal();    //实现只唤醒消费者。
 37 
 38         } finally {    //利用finally特性保证最后能释放锁。
 39             lock.unlock();
 40         }
 41     }
 42 
 43     public void out()throws InterruptedException {
 44         try {
 45             lock.lock();
 46             while(!flag)
 47                 condition_con.await();
 48             System.out.println(Thread.currentThread().getName()+"----消费者:"+this.name);
 49             flag = false;
 50             condition_pro.signal();     //实现只唤醒生产者。    
 51         } finally {    //利用finally特性保证最后能释放锁。
 52             lock.unlock();
 53         }
 54     }
 55 }
 56 
 57 
 58 class Producer implements Runnable {
 59     private Resource r;
 60     Producer(Resource r) {
 61         this.r = r;
 62     }
 63 
 64     public void run() {
 65         try {
 66             while(true)
 67                 r.set("+商品+");            
 68         } catch (InterruptedException e){
 69 
 70         }
 71 
 72     }
 73 }
 74 
 75 
 76 class Consumer implements Runnable {
 77     private Resource r;
 78     Consumer(Resource r) {
 79         this.r = r;
 80     }
 81 
 82     public void run() {
 83         try {
 84             while(true)
 85                 r.out();
 86         } catch (InterruptedException e) {
 87 
 88         }
 89     }
 90 }
 91 
 92 
 93 class ProducerConsumerDemo2 {
 94     public static void main(String[] args) {
 95         Resource r = new Resource();
 96 
 97         Producer pro = new Producer(r);
 98         Consumer con = new Consumer(r);
 99 
100         Thread t1 = new Thread(pro);
101         Thread t2 = new Thread(pro);
102         Thread t3 = new Thread(con);
103         Thread t4 = new Thread(con);
104     
105         t1.start();
106         t2.start();
107         t3.start();
108         t4.start();
109     }
110 }

运行结果是不停的生产和消费,但生产和消费的数目总是一样的。说明程序通过标记成功控制每生产一个商品就消费一个,并且实现了多线程的同步。

 

六、多线程的停止

  多线程的stop()已过时,所以想要多线程停止的方法只有通过run方法结束。多线程的代码通常都是摆在循环结构中的,所以只要控制循环,就可以让run方法结束,也就是线程结束。

  但是,如果线程处于冻结状态(等待,睡眠),读不到循环的标记,那么程序就无法停止。所以在没有指定方式让冻结的线程恢复到运行状态中时,可以使用Thread类中的interrupt()方法强行让线程恢复到运行状态中,这样线程就能继续执行读取到循环标记,从而停止线程。以下是例子:

 1 class StopThread implements Runnable {
 2     private boolean flag = true;
 3     public synchronized void run() {
 4         while(flag) {
 5             try {
 6                 wait();
 7             }
 8             catch (InterruptedException e) {
 9                 //注意这里接收到的InterruptedException是t1.interrupt()和t2.interrupt()抛出的异常。
10                 System.out.println(Thread.currentThread().getName()+".........Exception");
11                 changeFlag();        //最后记住要改标记才能让线程恢复运行后停止。
12             }
13             
14             System.out.println(Thread.currentThread().getName()+"...run");
15         }
16     }
17 
18     public void changeFlag() {
19         flag = false;
20     }
21 
22 }
23 
24 class StopThreadDemo {
25     public static void main(String[] args) {
26         StopThread st = new StopThread();
27 
28         Thread t1 = new Thread(st);
29         Thread t2 = new Thread(st);
30     
31         t1.start();
32         t2.start();
33 
34         int num = 0;        
35         while(true) {
36             if(num++ == 60) {
37 //                st.changeFlag();
38                 t1.interrupt();        //清除线程中断状态,但是会抛出InterruptedException异常。
39                 t2.interrupt();
40                 break;
41             }
42             System.out.println(Thread.currentThread().getName()+"..."+num);
43         }
44         System.out.println("over");
45     }
46 }

运行结果是程序顺利结束。

 

七、线程中的其他常见方法

  1.join:
  当A线程执行到了B线程的.join()方法时,A机会等待。等B线程都执行完,A线程才会执行。
  join可以用来临时加入线程执行。

  2.setDaemon(boolean):
  将线程标记为后台程序(守护线程),后台线程和前台线程一样开启,一样抢执行权运行。
  只有在结束时有区别。当前台线程都运行结束后,后台程序会自动结束。

  3.setPriority:
  更改线程优先级别:级别为(1~10),线程默认优先级别为5。
  Thread.MAX_PRIORITY = 10;
  Thread.NORM_PRIORITY = 5;
  Thread.MIN_PRIORITY = 1。

  4.yield:
  暂停正在执行的线程,并执行其他线程。

 

  以下是它们的使用例子:

 1 class Demo implements Runnable {
 2     public void run() {
 3         for (int x= 0; x<70; x++){
 4             System.out.println(Thread.currentThread().getName()+"..."+x);
 5             Thread.yield();
 6         }
 7     }
 8 }
 9 
10 class JoinDemo {
11     public static void main(String[] args)throws Exception {
12         Demo d = new Demo();
13         
14         Thread t1 = new Thread(d);
15         Thread t2 = new Thread(d);
16 //        t1.setDaemon(true);        //设置为守护线程,一旦前台所有线程结束,守护线程会自动结束。
17 //        t1.setPriority(Thread.MAX_PRIORITY);    //设置线程优先级别,级别越大,线程得到cpu执行权概率就越大。
18         t1.start();
19         t1.join();
20         t2.start();
21     
22         for (int x=0; x<80; x++)
23             System.out.println("main..."+x);
24         System.out.println("over");
25     }
26 }

运行结果是t1的0线程全部要执行完后,再把执行主线程main执行完,最后才执行t2的1线程。

 

  多线程总结完毕。