黑马程序员——多线程


 

多线程

一、进程和线程概述

  每个正在系统上运行的程序都是一个进程。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文。
  线程是程序中一个单一的顺序控制流程.在单个程序中同时运行多个线程完成不同的工作,称为多线程.
  线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文.多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定. 线程的运行中需要使用计算机的内存资源和CPU。

  多线程的好处:解决了多部分代码同时运行的问题。

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

  JVM启动时启动了多条线程,至少有两个线程可以分析的出来:

    1.执行main函数的线程,该线程的任务代码都定义在main函数中。

    2.负责垃圾回收的线程。

  在单线程程序中,只有上一句代码执行完,下一句代码才有执行的机会。创建线程的目的就是为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行,而运行的指定代码就是这个执行路径的任务。jvm创建的主线程的任务都定义在了主函数中。而自定义的线程,它的任务在run()方法中?

二、创建线程的两种方式:继承Thread类和实现Runnable接口

  1、继承Thread类

  Thread类用于描述线程,线程是需要任务的。所以Thread类也有对任务的描述。这个任务就是通过Thread类中的run方法来体现。也就是说,run方法就是封装自定义线程运行任务的函数,run方法中定义的就是线程要运行的任务代码。开启线程是为了运行指定代码,所以只有继承Thread类,并复写run方法,将运行的代码定义在run方法中即可。

  继承Thread类创建线程的步骤

    1.定义一个类继承Thread类。

    2.覆盖Thread类中的run方法。

    3.直接创建Thread的子类对象创建线程。

    4.调用start方法开启线程并调用线程的任务run方法执行。

  创建线程的代码示例:

 1 class ThreadDemo extends Thread//继承Thread类
 2 {
 3     private String name;
 4     ThreadDemo(String name)
 5     {
 6         this.name=name;
 7     }
 8     public void run()//覆盖run方法
 9     {
10         for(int i=0;i<5;i++)
11         {
12             System.out.println("i am "+name+", Hello World!");
13         }
14     }
15 }
16 class MyThread 
17 {
18     public static void main(String[] args) 
19     {
20         //创建两个线程,展示多线程的效果
21         ThreadDemo td1=new ThreadDemo("1号");
22         ThreadDemo td2=new ThreadDemo("2号");
23         td1.start();//注意不能携程td1.run(),run是只调用run方法,是单线程的,start是启动线程,是多线程的方式
24         td2.start();
25     }
26 }

  运行结果为:

  

2、实现Runnable接口

  通过实现Runnable接口创建线程的步骤为:

    1.定义类实现Runnable接口。

    2.覆盖接口中的run方法,将线程的任务代码封装到run方法中。

    3.通过Thread类创建线程对象,并将Runnable接口的子类对象作为Thread类的构造函数的参数进行传递。

    4.调用线程对象的start方法开启线程。

  通过实现Runnable接口创建线程的方式的好处:

    1.将线程的任务从线程的子类中分离出来,进行了单独的封装,按照面向对象的思想将任务封装成对象。

    2.避免了Java单继承的局限性。所以,创建线程的第二种方式较为常用。

同样也通过程序来看一下具体怎么通过实现Runnable接口来创建线程

class RunnableDemo implements Runnable
{
    private String name;
    RunnableDemo(String name)
    {
        this.name=name;
    }
    public void run()
    {
        for(int i=0;i<5;i++)
        {
            System.out.println("我是Runnable "+name+", Hello World!");
        }
    }
}
class MyThread 
{
    public static void main(String[] args) 
    {
        //通过将Runnable作为参数传递给Thread的构造函数来创建线程
        Thread t1=new Thread(new RunnableDemo("1号"));
        Thread t2=new Thread(new RunnableDemo("2号"));
        t1.start();
        t2.start();
    }
}

  运行结果为:

  

三、线程安全

  线程安全产生的原因:

    1.多个线程在操作共享的数据。

    2.操作共享数据的线程代码有多条。

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

  线程安全的解决方案

    就是将多条操作共享数据的线程代码封装起来,当有线程在执行这些代码的时候,其他线程不可以参与运算。必须要当前线程把这些代码都执行完毕后,其他线程才可以参与运算。

    在java中,用同步代码块就可以解决这个问题。

    同步代码块的格式:synchronized(对象){需要被同步的代码;}

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

  下面我们用卖票小程序来说明一下同步和非同步的程序的区别:

  首先我们写一个非同步的卖票小程序:

 1 //非同步的卖票函数
 2 class Ticket implements Runnable
 3 {
 4     private int ticket=100;
 5     public void run()
 6     {
 7         while(true)
 8         {
 9             if(ticket>0)
10             {
11                 try{Thread.sleep(10);}catch(Exception e){}
12                 System.out.println(Thread.currentThread().getName()+"...sale..."+ticket--);
13             }
14             else
15             {
16                 break;
17             }
18         }
19     }
20 
21 }
22 class MySynchronized 
23 {
24     public static void main(String[] args) 
25     {
26         Ticket t=new Ticket();
27         Thread t1=new Thread(t);
28         Thread t2=new Thread(t);
29         Thread t3=new Thread(t);
30         Thread t4=new Thread(t);
31         t1.start();
32         t2.start();
33         t3.start();
34         t4.start();
35     }
36 }

运行的结果为:

  可以很明显的看到卖出去的票出现了-1和0.这是不正常的。出现上图安全问题的原因在于Thread-0通过了if判断后,在执行到“num--”语句之前,num此时仍等于1。CPU切换到Thread-1、Thread-3之后,这些线程依然可以通过if判断,从而执行“num--”的操作,因而出现了0、-1的情况。

  接下来我们通过同步的方法将上面的程序改进一下避免出现-1和0的情况。

  

 1 //同步的卖票函数
 2 class Ticket implements Runnable
 3 {
 4     private int ticket=100;
 5     Object o=new Object();
 6     public void run()
 7     {
 8         while(true)
 9         {
10             synchronized(o)
11             {
12                 if(ticket>0)
13                 {
14                     try{Thread.sleep(10);}catch(Exception e){}
15                     System.out.println(Thread.currentThread().getName()+"...sale..."+ticket--);
16                 }
17                 else
18                 {
19                     break;
20                 }
21             }
22         }
23     }
24 
25 }
26 class MySynchronized 
27 {
28     public static void main(String[] args) 
29     {
30         Ticket t=new Ticket();
31         Thread t1=new Thread(t);
32         Thread t2=new Thread(t);
33         Thread t3=new Thread(t);
34         Thread t4=new Thread(t);
35         t1.start();
36         t2.start();
37         t3.start();
38         t4.start();
39     }
40 }

运行结果如下:

  

  上图显示安全问题已被解决,原因在于Object对象相当于是一把锁,只有抢到锁的线程,才能进入同步代码块向下执行。因此,当num=1时,CPU切换到某个线程后,如上图的Thread-3线程,其他线程将无法通过同步代码块继而进行if判断语句,只有等到Thread-3线程执行完“num--”操作(此后num的值为0),并且跳出同步代码块后,才能抢到锁。其他线程即使抢到锁,然而,此时num值已为0,也就无法通过if语句判断,从而无法再执行“num--”的操作了,也就不会出现0、-1、-2等情况了。

  安全问题的另一种解决方法是同步函数,格式:在函数上加上synchronized修饰符即可。

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

  静态的同步函数使用的锁是该函数所属字节码文件对象,可以用getClass方法获取,也可以用当前类名.class表示。

  在前面的博文中提到过,单例模式中的懒汉式单例设计模式在多线程下是存在线程安全问题的。这里我们通过同步的方式优化一下,让懒汉式单例设计模式变成线程安全的。

 1 class Single
 2 {
 3     private static Single s=null;
 4     private Single(){}
 5     public static Single getSingle()
 6     {
 7         if(s==null)//外层判断提高效率
 8         {
 9             synchronized(Single.class)
10             {
11                 if(s==null)//内存判断确保安全
12                     s=new Single();
13             }
14         }
15         return  s;
16     }
17     void show()
18     {
19         System.out.println("我是单例!");
20     }
21 }
22 class MySingle 
23 {
24     public static void main(String[] args) 
25     {
26         Single.getSingle().show();
27     }
28 }

运行结果为:

  懒汉式单例设计模式可以通过同步解决多线程安全问题,但若直接使用同步函数,则效率较低,因为每次都需要获取同步锁之后再判断。所以添加内层和外层两层判断解决效率问题。

死锁问题

  多线程在运行的时候可以极大的提高程序的运行效率,但是也可能会引发死锁。所谓死锁: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

  最常见的死锁情景之一是同步嵌套

  下面用一个简单的死锁代码来展示一下死锁的现象。

 1 class MyLock
 2 {
 3     //创建两个锁对象
 4     public static final Object objectA = new Object();
 5     public static final Object objectB = new Object();
 6 }
 7 class LockDemo implements Runnable
 8 {
 9     private boolean flag;//
10     LockDemo(boolean flag)
11     {
12         this.flag=flag;//构造函数中的flag用来区别运行不同线程的内容
13     }
14     public void run()
15     {
16         if(flag)
17         {
18             synchronized(MyLock.objectA)
19             {
20                 System.out.println(Thread.currentThread().getName()+"获取了objectA,请求objectB。。。");
21                 try{Thread.sleep(100);}catch(Exception e){}
22                 synchronized(MyLock.objectB)
23                 {
24                     System.out.println(Thread.currentThread().getName()+"获取了objectA,也获取了objectB。。。");
25                 }
26             }
27         }
28         else
29         {
30             synchronized(MyLock.objectB)
31             {
32                 System.out.println(Thread.currentThread().getName()+"获取了objectB,请求objectA。。。");
33                 try{Thread.sleep(100);}catch(Exception e){}
34                 synchronized(MyLock.objectA)
35                 {
36                     System.out.println(Thread.currentThread().getName()+"获取了objectB,也获取了objectA。。。");
37                 }
38             }
39         }
40     }
41 }
42 class circleLock 
43 {
44     public static void main(String[] args) 
45     {
46         Thread t1=new Thread(new LockDemo(true));
47         Thread t2=new Thread(new LockDemo(false));
48         t1.start();
49         t2.start();
50     }
51 }

运行结果如下:

  如上图所示,程序没有完成,进入了死锁状态。我们在写多线程的时候需要尽量避免死锁,也就是需要避免线程间相互等待的情况。

四、线程间通讯

  在多线程处理同一资源,但是任务却不同,这时候就需要线程间通讯。

  等待/唤醒机制涉及的方法:

    1、wait():让线程处于冻结状态,被wait的线程会被存储到线程池中。

    2、notify():唤醒线程池中的一个线程

    3、notifyAll():唤醒线程池中的所有线程。

  这些方法都必须定义在同步中,并且必须要明确到底操作的是哪个锁上的线程!

  wait和sleep的区别?

    wait可以指定时间也可以不指定,sleep必须指定时间。

    wait()释放cpu执行权,也释放锁。sleep释放执行权,但是不释放锁。

  注意,方法wait、notify、notifyAll定义在Object方法中,因为锁可以是任意对象,任意对象的调用方式一定是定义在Object类中

  下面以经典的生产者、消费者的问题来说明一下,线程之间的通讯。

 1 //经典的多生产者和多消费者的问题,生产者生产资源,消费者消费资源,
 2 //首先定义他俩共用资源类
 3 class Resource
 4 {
 5     private String name;
 6     private int count=0;
 7     private boolean flag=false;//用于判断生产和消费的状态true--可以生产,false--可以消费
 8     public synchronized void set(String name)
 9     {
10         while(!flag)
11         {
12             try{wait();}catch(InterruptedException e){e.printStackTrace();}
13         }
14         this.name=name;
15         count++;
16         System.out.println(Thread.currentThread().getName()+"生产了"+name+count);
17         flag=false;
18         notifyAll();//如果用notify(),只换醒一个线程,如果唤醒的还是生产者线程,程序就死锁了。
19     }
20     public synchronized void out()
21     {
22         while(flag)
23         {
24             try{wait();}catch(InterruptedException e){e.printStackTrace();}
25         }
26         this.name=name;
27         System.out.println(Thread.currentThread().getName()+"----消费了 "+name+count);
28         flag=true;
29         notifyAll();
30     }
31 }
32 
33 //接下来定义生产者类和消费者类
34 class Producer implements Runnable
35 {
36     private Resource r;
37     Producer(Resource r)
38     {
39         this.r=r;
40     }
41     public void run()
42     {
43         while(true)
44         {
45             r.set("货物");
46         }
47     }
48 }
49 class Consumer implements Runnable
50 {
51     private Resource r;
52     Consumer(Resource r)
53     {
54         this.r=r;
55     }
56     public void run()
57     {
58         while(true)
59         {
60             r.out();
61         }
62     }
63 }
64 class ProduceComsumerDemo 
65 {
66     public static void main(String[] args) 
67     {
68         Resource r=new Resource();
69         Producer pro=new Producer(r);
70         Consumer con=new Consumer(r);
71         //消费者和生产者各两个
72         Thread t0=new Thread(pro);
73         Thread t1=new Thread(pro);
74         Thread t2=new Thread(con);
75         Thread t3=new Thread(con);
76 
77         t0.start();
78         t1.start();
79         t2.start();
80         t3.start();
81     }
82 }

运行结果为:

五、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来修改上面的生产者,消费者问题。

  1 //经典的多生产者和多消费者的问题,生产者生产资源,消费者消费资源,
  2 //使用jdk1.5的新特性来实现生产者和消费者的问题
  3 import java.util.concurrent.locks.*;
  4 //首先定义共用资源类
  5 class Resource
  6 {
  7     private String name;
  8     private int count=0;
  9     private boolean flag=false;//用于判断生产和消费的状态true--可以生产,false--可以消费
 10     //创建一个锁对象
 11     Lock lock = new ReentrantLock();
 12     //通过已有的锁获取锁上的监视器对象
 13     Condition perducerCon =lock.newCondition();
 14     Condition consumerCon =lock.newCondition();
 15     public void set(String name)
 16     {
 17         lock.lock();
 18         try
 19         {
 20             while(!flag)
 21             {
 22                 try{perducerCon.await();}catch(InterruptedException e){e.printStackTrace();}
 23             }
 24             this.name=name;
 25             count++;
 26             System.out.println(Thread.currentThread().getName()+"生产了"+name+count);
 27             flag=false;
 28             consumerCon.signal();//由于有两类监视器,这里只需要通知consumer监视器下的线程运行就行了
 29         }
 30         finally 
 31         {
 32             lock.unlock();
 33         }
 34     }
 35     public synchronized void out()
 36     {
 37         lock.lock();
 38         try
 39         {
 40             while(flag)
 41             {
 42                 try{consumerCon.await();}catch(InterruptedException e){e.printStackTrace();}
 43             }
 44             this.name=name;
 45             System.out.println(Thread.currentThread().getName()+"----消费了 "+name+count);
 46             flag=true;
 47             perducerCon.signal();
 48             }
 49         finally 
 50         {
 51             lock.unlock();
 52         }
 53     }
 54 }
 55 
 56 //接下来定义生产者类和消费者类
 57 class Producer implements Runnable
 58 {
 59     private Resource r;
 60     Producer(Resource r)
 61     {
 62         this.r=r;
 63     }
 64     public void run()
 65     {
 66         while(true)
 67         {
 68             r.set("货物");
 69         }
 70     }
 71 }
 72 class Consumer implements Runnable
 73 {
 74     private Resource r;
 75     Consumer(Resource r)
 76     {
 77         this.r=r;
 78     }
 79     public void run()
 80     {
 81         while(true)
 82         {
 83             r.out();
 84         }
 85     }
 86 }
 87 class ProduceComsumerDemo 
 88 {
 89     public static void main(String[] args) 
 90     {
 91         Resource r=new Resource();
 92         Producer pro=new Producer(r);
 93         Consumer con=new Consumer(r);
 94         //消费者和生产者各两个
 95         Thread t0=new Thread(pro);
 96         Thread t1=new Thread(pro);
 97         Thread t2=new Thread(con);
 98         Thread t3=new Thread(con);
 99 
100         t0.start();
101         t1.start();
102         t2.start();
103         t3.start();
104     }
105 }

运行结果与上面的同步代码块的一致。

六、多线程其他问题

  怎么控制线程的任务结束呢?任务中都会有循环结构,只要控制住循环就可以结束任务。也可以使用stop方法停止线程,不过已经过时,不再使用。

  但是如果线程处于了冻结状态,无法读取标记,如何结束呢?

    可以使用interrupt()方法将线程从冻结状态强制恢复到运行状态中来,让线程具备CPU的执行资格。强制动作会发生InterruptedException,一定要记得处理。

  setDaemon方法:

    将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。

  join方法:  

    public final void join()throws InterruptedException
    等待该线程终止
 setPriority方法:
    public final void setPriority(int newPriority)

     更改线程的优先级。

     首先调用线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException

     在其他情况下,线程优先级被设定为指定的 newPriority 和该线程的线程组的最大允许优先级相比较小的一个。  

   toString()方法:

    

  yield方法:

    

  多线程就介绍到这里,多线程的内容比较难理解,需要自己动手多写代码,在写代码中感受,明白为什么要这么写,这么写有什么好处。

原文地址:https://www.cnblogs.com/dengzhenyu/p/4832069.html