黑马程序员JAVA笔记5--多线程

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

多线程

一、多线程的概念

(1)进程、线程、多进程的概念
    进程:正在进行中的程序(直译)。
    线程:进程中一个负责程序执行的控制单元(执行路径)。

    注释:
    1、一个进程中可以有多个执行路径,称之为多线程。
    2、一个进程中至少要有一个线程。
    3、开启多个线程是为了同时运行多部分代码,每一个线程都有自己运行的内容,这个内容可以称为线程要执行的任务。

    多线程的好处:解决了多部分代码同时运行的问题。
    多线程的弊端:线程太多,会导致效率的降低。
    其实,多个应用程序同时执行都是CPU在做着快速的切换完成的。这个切换是随机的。CPU的切换是需要花费时间的,从而导致了效率的降低。

    JVM启动时启动了多条线程,至少有两个线程可以分析的出来:
    1. 执行main函数的线程,该线程的任务代码都定义在main函数中。
    2. 负责垃圾回收的线程。


(2)创建线程方式一:继承Thread类
    1. 定义一个类继承Thread类。
    2. 覆盖Thread类中的run方法。
    3. 直接创建Thread的子类对象创建线程。
    4. 调用start方法开启线程并调用线程的任务run方法执行。

    创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。

    jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在哪儿呢?
    Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。

    开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。

(3)创建线程方式二:实现Runnable接口

1. 定义类实现Runnable接口。
    2. 覆盖接口中的run方法,将线程的任务代码封装到run方法中。
    3. 通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。为什么?因为线程的任务都封装在Runnable接口子类对象的run方法中。所以要在线程对象创建时就必须明确要运行的任务。
    4. 调用线程对象的start方法开启线程。

    实现Runnable接口的好处:
    1. 将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。
    2. 避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。

示例:

1. //准备扩展Demo类的功能,让其中的内容可以作为线程的任务执行。
2. //通过接口的形式完成。
3. class Demo implements Runnable{
4.       public void run(){
5.             show();
6.       }
7.       public void show(){
8.              for(int x = 0; x < 20; x++){
9.                   System.out.println(Thread.currentThread().getName() + "..." + x);
10.              }
11.       }
12. }
13. 
14. class ThreadDemo{
15.        public static void main(String[] args){
16.             Demo d = new Demo();
17.             Thread t1 = new Thread(d);
18.             Thread t2 = new Thread(d);
19.             t1.start();
20.             t2.start();
21.        }
22. }

Thread类、Runnable接口内部源码关系模拟代码:

1. class Thread{
2.       private Runnable r ;
3.       Thread(){
4.       }
5.       Thread(Runnable r){
6.              this.r = r;
7.       }
8. 
9.        public void run(){
10.              if(r !=null)
11.                    r.run();
12.       }
13.        public void start(){
14.             run();
15.       }
16. }
17. 
18. class ThreadImpl implements Runnable{
19.        public void run(){
20.             System.out.println("runnable run" );
21.       }
22. }
23. 
24. class ThreadDemo4{
25.        public static void main(String[] args){
26.             ThreadImpl i = new ThreadImpl();
27.             Thread t = new Thread(i);
28.             t.start();
29.       }
30. }
31. 
32. class SubThread extends Thread{
33.        public void run(){
34.             System.out.println("hahah" );
35.       }
36. }
37. 
38. class ThreadDemo5{
39.        public static void main(String[] args){
40.             SubThread s = new SubThread();
41.             s.start();
42.       }
43. }


二、线程安全问题

(1)线程安全问题产生的原因

需求:模拟4个线程同时卖100张票。

代码:

1. class Ticket implements Runnable{
2.        private int num = 100;
3. 
4.        public void run(){
5.              while(true ){
6.                    if(num > 0){
7.                          try{
8.                               Thread. sleep(10);
9.                         } catch(InterruptedException e){
10.                               e.printStackTrace();
11.                         }
12.                         System.out.println(Thread.currentThread().getName() + "...sale..." + num--);
13.                   }
14.             }
15.       }
16. }
17. 
18. class TicketDemo{
19.        public static void main(String[] args){
20.             Ticket t = new Ticket();
21.             Thread t1 = new Thread(t);
22.             Thread t2 = new Thread(t);
23.             Thread t3 = new Thread(t);
24.             Thread t4 = new Thread(t);
25. 
26.             t1.start();
27.             t2.start();
28.             t3.start();
29.             t4.start();
30.       }
31. }

原因分析:
    出现上图安全问题的原因在于Thread-0通过了if判断后,在执行到“num--”语句之前,num此时仍等于1。
    CPU切换到Thread-1、Thread-2、Thread-3之后,这些线程依然可以通过if判断,从而执行“num--”的操作,因而出现了0、-1、-2的情况。

 

线程安全问题产生的原因:
    1. 多个线程在操作共享的数据。
    2. 操作共享数据的线程代码有多条。
    当一个线程在执行操作共享数据的多条代码过程中,其他线程参与了运算,就会导致线程安全问题的产生。

(2)线程安全问题的解决方案
    思路:
    就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。
    必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

    在java中,用同步代码块就可以解决这个问题。
    同步代码块的格式:
    synchronized(对象){
           需要被同步的代码;
    } 

    同步的好处:解决了线程的安全问题。
    同步的弊端:当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。
    同步的前提:必须有多个线程并使用同一个锁。
注释:

    同步函数和同步代码块的区别:
    1. 同步函数的锁是固定的this。
    2. 同步代码块的锁是任意的对象。
    建议使用同步代码块。

 

    由于同步函数的锁是固定的this,同步代码块的锁是任意的对象,那么如果同步函数和同步代码块都使用this作为锁,就可以实现同步。

 

(3)多线程下的单例模式

    饿汉式:

1. class Single{
2.        private static final Single s = new Single();
3.        private Single(){}
4.        public static Single getInstance(){
5.              return s ;
6.       }
7. }

注释:
    饿汉式不存在安全问题,因为不存在多个线程共同操作数据的情况。

    懒汉式:

1. class Single{
2.       private static Single s = null;
3.        private Single(){} 
4.        public static Single getInstance(){
5.              if(s ==null){
6.                    synchronized(Single.class){
7.                          if(s == null)
8.                                s = new Single();
9.                   }
10.             }
11.             return s ;
12.       }
13. }

注释:

    懒汉式存在安全问题,可以使用同步函数解决。

    但若直接使用同步函数,则效率较低,因为每次都需要判断。

    原因在于任何一个线程在执行到第一个if判断语句时,如果Single对象已经创建,则直接获取即可,而不用判断是否能够获取锁,相对于上面使用同步函数的方法就提升了效率。如果当前线程发现Single对象尚未创建,则再判断是否能够获取锁。

    1. 如果能够获取锁,那么就通过第二个if判断语句判断是否需要创建Single对象。因为可能当此线程获取到锁之前,已经有一个线程创建完Single对象,并且放弃了锁。此时它便没有必要再去创建,可以直接跳出同步代码块,放弃锁,获取Single对象即可。如果有必要,则再创建。

    2. 如果不能获取到锁,则等待,直至能够获取到锁为止,再按步骤一执行。

 

(4)死锁示例

    死锁常见情景之一:同步的嵌套。
    示例1:

1. class Ticket implements Runnable{
2.        private static int num = 100;
3.        Object obj = new Object();
4.        boolean flag = true;
5. 
6.        public void run(){
7.              if(flag ){
8.                    while(true ){
9.                          synchronized(obj ){
10.                               show();
11.                         }
12.                   }
13.             } else
14.                    while(true )
15.                         show();
16.       }
17. 
18.        public synchronized void show(){
19.              synchronized(obj ){
20.                    if(num > 0){
21.                          try{
22.                               Thread. sleep(10);
23.                         } catch(InterruptedException e){
24.                               e.printStackTrace();
25.                         }
26.                         System.out.println(Thread.currentThread().getName() + "...function..." + num--);
27.                   }
28.             }
29.       }
30. }
31. 
32. class DeadLockDemo{
33.        public static void main(String[] args){
34.             Ticket t = new Ticket();
35.             Thread t1 = new Thread(t);
36.             Thread t2 = new Thread(t);
37.             
38.             t1.start();
39.              try{
40.                   Thread. sleep(10);
41.             } catch(InterruptedException e){
42.                   e.printStackTrace();
43.             }
44.             t. flag = false ;
45.             t2.start();
46.       }
47. } 

原因分析:
    由上图可以看到程序已经被锁死,无法向下执行。
    由下图代码可以看到,run方法中的同步代码块需要获取obj对象锁,才能执行代码块中的show方法。
    而执行show方法则必须获取this对象锁,然后才能执行其中的同步代码块。
    当线程t1获取到obj对象锁执行同步代码块,线程t2获取到this对象锁执行show方法。同步代码块中的show方法因无法获取到this对象锁无法执行,show方法中的同步代码块因无法获取到obj对象锁无法执行,就会产生死锁。

 

三、线程间通信

(1)线程间通信涉及的方法

    多个线程在处理统一资源,但是任务却不同,这时候就需要线程间通信。
    等待/唤醒机制涉及的方法:
    1. wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。
    2. notify():唤醒线程池中的一个线程(任何一个都有可能)。
    3. notifyAll():唤醒线程池中的所有线程。

    注释:
    1、这些方法都必须定义在同步中,因为这些方法是用于操作线程状态的方法。
    2、必须要明确到底操作的是哪个锁上的线程!
    3、wait和sleep区别?
         1)wait可以指定时间也可以不指定。sleep必须指定时间。
         2)在同步中时,对CPU的执行权和锁的处理不同。
         wait:释放执行权,释放锁。
         sleep:释放执行权,不释放锁。

    为什么操作线程的方法wait、notify、notifyAll定义在了object类中,因为这些方法是监视器的方法,监视器其实就是锁。
锁可以是任意的对象,任意的对象调用的方式一定在object类中。

生产者-消费者问题:

1. class Resource{
2.        private String name ;
3.        private String sex ;
4.        private boolean flag = false;
5. 
6.        public synchronized void set(String name,String sex){
7.              if(flag )
8.                    try{
9.                          this.wait();
10.                   } catch(InterruptedException e){
11.                         e.printStackTrace();
12.                   }
13.              this.name = name;
14.              this.sex = sex;
15.              flag = true ;
16.              this.notify();
17.       }
18.       
19.       public synchronized void out(){
20.              if(!flag )
21.                    try{
22.                          this.wait();
23.                   } catch(InterruptedException e){
24.                         e.printStackTrace();
25.                   }
26.              System. out.println(name + "..." + sex);
27.              flag = false ;
28.              this.notify();
29.      } 
30. }
31. 
32. //输入
33. class Input implements Runnable{
34.       Resource r;
35.       Input(Resource r){
36.              this.r = r;
37.       }
38. 
39.       public void run(){
40.              int x = 0;
41.              while(true ){
42.                    if(x == 0){
43.                          r.set( "mike","男" );
44.                   } else{
45.                          r.set( "lili","女" );
46.                   }
47.                   x = (x + 1)%2;
48.             }
49.       }
50. }
51. 
52. //输出
53. class Output implements Runnable{
54.       Resource r;
55. 
56.       Output(Resource r){
57.              this.r = r;
58.       }
59. 
60.        public void run(){
61.              while(true ){
62.                    r.out();
63.             }
64.       }
65. }
66. 
67. class ResourceDemo {
68.        public static void main(String[] args){
69.              //创建资源
70.             Resource r = new Resource();
71.              //创建任务
72.             Input in = new Input(r);
73.             Output out = new Output(r);
74.              //创建线程,执行路径
75.             Thread t1 = new Thread(in);
76.             Thread t2 = new Thread(out);
77.              //开启线程
78.             t1.start();
79.             t2.start();
80.       }
81. }

    多生产者-多消费者问题:

1. class Resource{
2.        private String name ;
3.        private int count = 1;
4.        private boolean flag = false;
5. 
6.        public synchronized void set(String name){
7.              if(flag )
8.                    try{
9.                         wait();
10.                   } catch(InterruptedException e){
11.                         e.printStackTrace();
12.                   }
13.              this.name = name + count;
14.              count++;
15.              System.out.println(Thread.currentThread().getName() + "...生产者..." + this. name);
16.              flag = true ;
17.             notify();
18.        }
19. 
20.        public synchronized void out(){
21.             if(!flag )
22.                    try{
23.                         wait();
24.                   } catch(InterruptedException e){
25.                         e.printStackTrace();
26.                   }
27.             flag = false ;
28.             notify();
29.             System.out.println(Thread.currentThread().getName() + "...消费者..." + this. name);
30.       }
31. }
32. 
33. class Producer implements Runnable{
34.       private Resource r ;
35.       Producer(Resource r){
36.              this.r = r;
37.       }
38.        public void run(){
39.              while(true ){
40.                    r.set( "烤鸭");
41.             }
42.       }
43. }
44. 
45. class Consumer implements Runnable{
46.       private Resource r ;
47.       Consumer(Resource r){
48.              this.r = r;
49.       }
50.        public void run(){
51.              while(true ){
52.                    r.out();
53.             }
54.       }
55. }
56. 
57. class ProducerConsumerDemo {
58.        public static void main(String[] args){
59.             Resource r = new Resource();
60.             Producer pro = new Producer(r);
61.             Consumer con = new Consumer(r);
62. 
63.             Thread t0 = new Thread(pro);
64.             Thread t1 = new Thread(pro);
65.             Thread t2 = new Thread(con);
66.             Thread t3 = new Thread(con);
67.             t0.start();
68.             t1.start();
69.             t2.start();
70.             t3.start();
71.       }
72. } 

运行结果:以上代码存在安全问题。  

    原因分析:

    得到以上结果的过程分析如下:

    1. 线程Thread-0获取到CPU执行权及锁,生产了烤鸭3298,将flag设置为true。然后,Thread-0又重新获取到CPU执行权,由于flag为true,故执行wait方法,阻塞。Thread-1接着获取到CPU执行权,由于flag为true,故执行wait方法,也阻塞。

    2. 线程Thread-3获取到CPU执行权及锁,消费了烤鸭3298,将flag设置为false。然后,线程Thread-0被唤醒,但是并没有获取到锁,而是线程Thread-3接着获取到CPU执行权及锁,然而此时flag为false,所以Thread-3阻塞。下面线程Thread-2接着获取到CPU执行权及锁,然而此时flag为false,所以Thread-2也阻塞。

    3. 线程Thread-0获取到CPU执行权及锁,不需要if语句判断,直接生产烤鸭3299,然后又唤醒线程Thread-1获取到CPU执行权及锁,不需要if语句判断,直接生产烤鸭3300。从而造成了烤鸭3299还没有被消费,就直接生产了烤鸭3300的情况。

 

    由于if判断标记,只有一次,会导致不该运行的线程运行了,出现了数据错误的情况。故修改成while判断标记,线程获取CPU执行权及锁后,将重新判断是否具备运行条件。

    notify方法只能唤醒一个线程,如果本方唤醒了本方,没有意义。而且while判断标记+notify会导致死锁。notifyAll解决了本方线程一定会唤醒对方线程的问题。

 

注释:

    while判断标记+notify会导致死锁的示例:

    如果将上面的代码中的if判断标记修改成wile判断标记,就会出现死锁的现象,前2步与原来是一致的。第3步如下:

    3. 线程Thread-0获取到CPU执行权及锁,通过了while语句判断,直接生产烤鸭3299,将flag设置为true。然后又唤醒线程Thread-1获取到CPU执行权及锁,没有通过while语句判断,阻塞。线程Thread-0又获取到CPU执行权及锁,通不过while语句判断,也阻塞,此时Thread-0、1、2、3都阻塞,故死锁。

 

(2)JDK1.5新特性

    同步代码块就是对于锁的操作是隐式的。
    JDK1.5以后将同步和锁封装成了对象,并将操作锁的隐式方式定义到了该对象中,将隐式动作变成了显示动作。

    Lock接口:出现替代了同步代码块或者同步函数,将同步的隐式操作变成显示锁操作。同时更为灵活,可以一个锁上加上多组监视器。
    lock():获取锁。
    unlock():释放锁,为了防止异常出现,导致锁无法被关闭,所以锁的关闭动作要放在finally中。

    Condition接口:出现替代了Object中的wait、notify、notifyAll方法。将这些监视器方法单独进行了封装,变成Condition监视器对象,可以任意锁进行组合。
    Condition接口中的await方法对应于Object中的wait方法。
    Condition接口中的signal方法对应于Object中的notify方法。
    Condition接口中的signalAll方法对应于Object中的notifyAll方法。

 

    使用一个Lock、一个Condition修改上面的多生产者-多消费者问题。

 

(3)停止线程  

    怎么控制线程的任务结束呢?

    任务中都会有循环结构,只要控制住循环就可以结束任务。
    控制循环通常就用定义标记来完成。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值