zoukankan      html  css  js  c++  java
  • Java 多线程

    文章较长,给大家提供索引:

    1.多线程的概念

    2.我们为什么要应用多线程?

    3.多线程的定义方式

    4.多线程的运行状态

    5.多线程的安全问题与同步

    6.死锁

    7.等待(wait())、唤醒(notify()、notifyAll())、休眠(sleep())

    8.消费者-生产者问题(consumer-producter)

    9.守护线程

    10.join方法

    11.线程组

    12.优先级


    1.多线程的概念

    首先明确两个概念:进程与线程

    进程:一个进程对应了一个应用程序。进程是某个数据集合的一次执行过程,也是操作系统进行资源分配和保护的基本单位。比如我们打开QQ,QQ在系统中就是一个进程,我们打开任务管理器,每一个大项就是一个进程。

    线程:线程是进程的具体执行场景,一个进程可以包含多个线程。最简单的例子,我们用Chrome浏览器打开多个网页,在任务管理器里面可以看见一个Chrome进程包含了多个线程,每个线程就是我们具体的使用场景(网页)。

    进程与进程间的内存是独立的,也就是说,每个进程都有自己的一块专属空间。但是线程间会共享堆内存与方法区(栈内存每个进程都有一个)。

    并行和并发:

    并行:多个CPU同时干一个事儿,或者是多台电脑,是真正的同时。

    并发:CPU在多个任务间进行快速切换,切换规则根据CPU的调度算法指定。因为CPU执行速度太快,我们看上去像是在同时运行。

    2.我们为什么要应用多线程?

    多线程可以提高应用程序的利用率。事实上,所有的多线程都可以通过单线程写出来。

    3.多线程的定义方式

    第一种:通过继承Thread类。

     1 class Demo extends Thread
     2 {
     3     public void run()
     4     {
     5         for (int i = 0; i < 10; i++)
     6             System.out.print("a+"+i+" ");
     7     }
     8 }
     9 
    10 class ThreadDemo
    11 {
    12     public static void main(String args[])
    13     {
    14         
    15         Demo d = new Demo();
    16         d.start();
    17         for(int i=0;i<10;i++)
    18         {
    19             System.out.print("b+"+i+" ");
    20         }
    21     }
    22 }

      运行结果: 

       我们发现两个输出是交替运行的,说明多线程具有随机性。

    具体步骤:

    1.定义一个类,继承Thread;

    2.覆盖Thread类中的run()方法;

    run()方法里面写你想多线程执行的代码。

    3.调用线程的start()方法。

    start()两个作用:启动线程,调用run()方法。

    向下面这样的是不行的:

     

     1 class Demo extends Thread
     2 {
     3     public void run()
     4     {
     5         for (int i = 0; i < 10; i++)
     6             System.out.print("a+"+i+" ");
     7     }
     8 }
     9 
    10 class ThreadDemo
    11 {
    12     public static void main(String args[])
    13     {
    14 
    15         Demo d = new Demo();
    16         d.run();
    17         for(int i=0;i<10;i++)
    18         {
    19             System.out.print("b+"+i+" ");
    20         }
    21     }
    22 }

     

    对比输出我们发现,如果只调用run()就跟一般的对象建立与方法调用无二了。

    所以要调用start()而不是去自己调用run()。

     第二种:实现Runnable接口

     1 class Demo implements Runnable
     2 {
     3     public void run()
     4     {
     5         System.out.println("Thread Running!");
     6     }
     7 }
     8 
     9 class ThreadDemo
    10 {
    11     public static void main(String args[])
    12     {
    13 
    14       Demo d=new Demo();
    15       Thread t=new Thread(d);
    16       t.run();
    17     }
    18 }

    步骤:

    1.定义类实现Runnable接口;

    2.覆盖Runnable接口中的run()方法,将线程要运行的代码存放在该run方法中;

    3.通过Thread类建立线程对象。

    4.将Runnable接口的子类对象作为实际参数传递给Thread类的构造函数。

    为什么要将Runnable接口的子类对象作为实际参数传递给Thread的构造函数?

    因为,自定义的run方法所属的对象是Runnable接口的子类对象,所以要让线程去指定对象的run方法的话,就必须明确该run方法所属对象。

     5.调用Thread类的start方法开启线程并调用Runnable接口子类的run()方法

    一般来说,我们是推荐Runnable方式来定义的。因为可以避免单继承的局限性。

    4.线程的运行状态

    不多说,直接上图。

     

     

     

    一个线程被创建(new())之后,会进入准备(start())状态。然后CPU通过调度使线程进入运行(run())状态,也就是说,线程被执行有且只有一个机会,就是进入start()状态,才有机会执行。

     5.线程的安全问题,同步

    我们知道,CPU的运行、切换速度是极快的。这样就会产生一个问题:两个线程操作同一个数据(共享数据)时,A线程向里面存放数据,B从里面取出数据,A的数据还没放完B就取走了。这样就导致取出的数据重复。又或者,A存数据时B还没来得及取,A就继续向里面存了,这样导致少取了几个数据。这两种情况都会导致数据错乱,所以我们必须去解决这个问题。Java为我们提供了同步(synchronized)来解决问题。

     不加同步,我们进行下面的操作:

     1 class Res
     2 {
     3     private String name;
     4     private String sex;
     5 
     6     public String getName()
     7     {
     8         return name;
     9     }
    10 
    11     public String getSex()
    12     {
    13         return sex;
    14     }
    15 
    16     public void setName(String name)
    17     {
    18         this.name = name;
    19     }
    20 
    21     public void setSex(String sex)
    22     {
    23         this.sex = sex;
    24     }
    25 }
    26 
    27 class In implements Runnable
    28 {
    29     private Res res;
    30     private boolean flag;
    31 
    32     public In(Res res)
    33     {
    34         this.res = res;
    35     }
    36 
    37     @Override
    38     public void run()
    39     {
    40         while (true)
    41         {
    42             if (flag)
    43             {
    44                 res.setName("小红!!!");
    45                 res.setSex("女!!!!");
    46             }
    47             else
    48             {
    49                 res.setName("小明");
    50                 res.setSex("男");
    51             }
    52             flag = !flag;
    53         }
    54 
    55 
    56     }
    57 }
    58 
    59 class Out implements Runnable
    60 {
    61     private Res res;
    62 
    63     public Out(Res res)
    64     {
    65         this.res = res;
    66     }
    67 
    68     @Override
    69     public void run()
    70     {
    71         while (true)
    72         {
    73             System.out.println("姓名是:" + res.getName());
    74             System.out.println("性别是:" + res.getSex());
    75         }
    76 
    77     }
    78 }
    79 
    80 class synchronizedDemo
    81 {
    82     public static void main(String[] args)
    83     {
    84         Res res = new Res();
    85         Thread thread1 = new Thread(new In(res));
    86         Thread thread2 = new Thread(new Out(res));
    87         thread1.start();
    88         thread2.start();
    89     }
    90 }
    同步示例代码1

    代码很简单,就是一个存数据一个取数据。

    当我们运行之后:

    也很好理解,原因就像上面说过的那样,数据错乱了。

    同步:在一个线程运行的时候,只能等该线程运行完毕之后才能让其他线程参与操作,这就保证了线程对数据操作的唯一性。

    方式1:同步代码块:

     我们在循环外加上:

     1 public void run()
     2 {
     3     while (true)
     4     {
     5         synchronized (res)//修改代码
     6         {
     7             if (flag)
     8             {
     9                 res.setName("小红!!!");
    10                 res.setSex("女!!!!");
    11             }
    12             else
    13             {
    14                 res.setName("小明");
    15                 res.setSex("男");
    16             }
    17             flag = !flag;
    18         }
    19     }
    20 }
    21 //只保留了修改的代码
    22 @Override
    23 public void run()//修改代码
    24 {
    25     while (true)
    26     {
    27         synchronized (res)
    28         {
    29             System.out.println("姓名是:" + res.getName());
    30             System.out.println("性别是:" + res.getSex());
    31         }
    32     }
    33 }
    同步示例代码2

    这样我们的打印结果就是正确的了。

    原理:

    synchronized相当于一个锁,每一个线程进去之后会持有该锁。只有这个线程将同步代码块内的代码执行完毕之后,这个锁才会被释放。在执行过程中,如有其他线程也要去执行被同步的代码,因为该锁已经被占有,所以就不会去执行里面的代码而是进入了锁定(block())状态。只有锁被释放,这些被锁定的线程才回去争抢执行资格。可以很形象的类比高铁上的厕所,因为只有一个位置,所以一次只能进一个人。这个人进去之后为防流氓会把门锁上(持有锁),只有等他解决完才能开门(释放锁)。然后门口等待的人就可以争抢这个位置了。

    但是我们要注意三点:

    1.一定是多线程环境。

    2.多线程涉及到了共享数据且进行了修改。

    3.同步的锁必须唯一。

    也就是说,我们这里面传入的是res而不是this,就因为this不唯一。res我们只建立了一个对象当然是唯一的。其实,我们传入In.class,Out.class等等都可以,唯一就行。

    方式2:同步函数

    上面的方式是在需要被同步的函数外面套了一层同步代码块,我们还有另一种方式来实现,就是同步函数。

     实现也很简单,就是在函数的返回值前面加上synchronized关键字就可以了,这个函数就是同步函数了。效果与同步代码块无二。

    1 public synchronized void add(){}

    但是如果是静态函数呢?

    我们知道,静态函数不依赖对象的存在而存在,所以用对象作为静态函数的同步代码块的锁是错误的且没有意义的。这时候,我们就要使用类(.class)作为锁,因为在编译的时候字节码文件是唯一的。

    6.死锁

    死锁也很好理解:你持有我的锁,我持有你的锁,两个锁都在等待对方锁的释放。死锁的结果就是程序停滞,无法继续。

    死锁的出现,一般是不正确的同步嵌套。

     1 class DeadLock implements Runnable
     2 {
     3     @Override
     4     public void run()
     5     {
     6         synchronized (Lock.object1)
     7         {
     8             System.out.println("123");
     9             synchronized (Lock.object2)
    10             {
    11                 System.out.println("456");
    12             }
    13         }
    14     }
    15 }
    16 class Lock
    17 {
    18     static Object object1 = new Object();
    19     static Object object2 = new Object();
    20 
    21 }
    22 class sychronizedDemo
    23 {
    24     public static void main(String[] args)
    25     {
    26         Thread thread = new Thread(new DeadLock());
    27         thread.start();
    28     }
    29 }
    死锁

    7.等待(wait())、唤醒(notify()、notifyAll())、休眠(sleep())

    在线程的控制中有四个方法比较关键:

    wait():使一个线程进入等待阻塞状态(blocked),此时这个线程放弃了CPU的执行权,只有被唤醒才能重新参与争夺。这个方法必须在synchronzed内。

    notify():唤醒处于等待阻塞的线程。

    notifyAll():唤醒所有等待的线程。

    (以上三个方法都会抛出异常。具体的异常这里不提,只需要知道如果使用了这些方法,或者try或者throws)

    需要注意的是,这三个方法都是属于Objec类中的方法,也就说所有对象都具有这几个方法。这么做的用意是?

    因为这些方法在操作同步中的线程是,都必须标示出他们所操作的线程持有的锁。只有同一个锁上的处于等待中的线程,才可以被同一个锁上的notify()唤醒。不可以对不同锁中的不同线程进行唤醒。但是,如果所有线程都在这个锁上等待,这时notify会随机唤醒其中一个线程。

     sleep():使线程进入休眠状态。需要注意的是,在休眠状态的线程并没有交出执行权。我们可以指定休眠时间,比如sleep(10)。

     了解了这四个方法,我们就可以解决下面的经典问题了——

    8.消费者-生产者问题(consumer-producter)

    首先先简单的构建一个场景--

    在一个采矿场,有两拨工人——一波负责挖煤,一波负责将煤运出去。挖煤的人会将挖好的煤放入一个大桶,然后运煤的会从桶里面将煤运走。假设两拨工人的效率是一样的,也就是挖和踩的速度是一样的。我们希望,桶内不要存煤,换句话说,挖了一块放在桶里,立刻就有人将桶内的煤运走。

    这个问题,就涉及到了我们的生产者-消费者模型。我们会用三个方法来解决这个问题。

    问题分析

    我们知道,不同线程会去争夺CPU的执行权。也就是说,如果第一次挖煤的抢到了执行权,然后下一次是挖煤还是运煤是不确定的。我们希望的是,当我们挖煤的时候,运煤的不去干扰我们,等我们把煤挖完之后,运煤的安心运煤同样不让挖煤的干扰,这样两个动作交替运行,就会有和谐的结果。

    这里面引入两个概念:

    等待池:假设一个线程执行了wait()方法,那么这个线程就会释放掉自己的锁(因为这个线程既然执行了wait方法,那么它一定实在synchornized内,也就是说这个线程一定持有锁),然后进入等待池中。等待池中的线程不会去竞争执行权。

    同步锁池:在一个线程获得对象的锁的时候,其他线程就在同步锁池中等待。在这个线程释放掉自己的锁之后,就进入了等待池中。notify()方法会(随机)唤醒等待池中的一个方法进入到同步锁池中进行竞争,而notityAll()会唤醒所有的线程。

    关于这俩概念,我这里有个比喻可能会帮助理解。

    在古代,想进入官场(执行方法体)出人头地就必须去科举考试,谁第一谁当官(获取到锁,也就是持有锁)。而众所周知,有这个想法的人很多,大家都在考试(竞争CPU执行权),大家都在考场进行考试(同步锁池)。但是官场阴暗,现任官员被罢官(执行了对象的wait()方法),这个官员就去了深山老林(等待池)。这时候,有人对他进行了鼓舞(notify()),他很兴奋,但是想当官也是需要重新考的,于是和大家一起考试(进入了同步锁池)。当然,官位不能空缺,在他隐居深山的时候,自然有人通过竞争当了官。

    介绍完概念,我们回到问题上。首先把问题简化,假设只有挖煤人A,运煤人B,一个挖一个运,效率相同。

    解决方式1:

    我们希望是这样的:

    ①A执行T对象的同步方法,此时对象持有T对象的锁,B在T的锁池中等待。

    ②A执行到了wait()方法,A释放锁,进入了T的等待池,此时A进入了同步锁池,与其他的线程一通参与竞争。

    ③在锁池中的B拿到锁,执行它自己的同步方法。

    ④B执行到了notify(),唤醒了在等待池中的A并将其移动到了T对象的锁池中等待获取锁。

    ⑤B执行完了同步方法,释放锁,A获取锁,继续①。

      1 class Res       //共享资源
      2 {
      3     int age = 0;
      4     String name;
      5     boolean isEmpty = true;//资源是否为空
      6 
      7     public synchronized void In(String name, int age)//生产方法
      8     {
      9         try
     10         {
     11             while (!isEmpty)//如果资源非空
     12             {
     13                 this.wait();//等待
     14             }
     15             this.name = name;
     16             this.age = age;
     17             isEmpty = false;//生产完毕,资源非空
     18             this.notifyAll();
     19         } catch (Exception e)
     20         {
     21             e.printStackTrace();
     22         }
     23     }
     24 
     25     public synchronized void Out()//消费方法
     26     {
     27         try
     28         {
     29             while (isEmpty)//资源为空
     30             {
     31                 this.wait();//等待
     32             }
     33             System.out.println("姓名:" + name + "年龄:" + age);
     34             isEmpty = true;
     35             this.notifyAll();
     36         } catch (Exception e)
     37         {
     38             e.printStackTrace();
     39         }
     40     }
     41 }
     42 
     43 
     44 class Producer implements Runnable
     45 {
     46     private Res res;
     47     private int i = 0;
     48 
     49     public Producer(Res res)
     50     {
     51         this.res = res;
     52     }
     53 
     54     @Override
     55     public void run()
     56     {
     57         while (true)
     58         {
     59             if (i % 2 == 0)
     60                 res.In("小红", 10);
     61             else
     62                 res.In("老王", 70);
     63             i++;
     64         }
     65 
     66     }
     67 }
     68 
     69 class Consumer implements Runnable
     70 {
     71     private Res res;
     72 
     73     public Consumer(Res res)
     74     {
     75         this.res = res;
     76 
     77     }
     78 
     79     @Override
     80     public void run()
     81     {
     82         while (true)
     83         {
     84             res.Out();
     85         }
     86 
     87 
     88     }
     89 }
     90 
     91 class synchronizedDemo
     92 {
     93     public static void main(String[] args)
     94     {
     95         Res res = new Res();//分别创建了两个生产者两个消费者,更能突出现象
     96         Thread thread1 = new Thread(new Consumer(res));
     97         Thread thread2 = new Thread(new Producer(res));
     98         Thread thread3 = new Thread(new Consumer(res));
     99         Thread thread4 = new Thread(new Producer(res));
    100         thread1.start();
    101         thread2.start();
    102         thread3.start();
    103         thread4.start();
    104 
    105     }
    106 }
    生产者消费者1

    上面的代码就解决了问题——

     

    稍加说明:

    同步函数的锁是this,也就是当前对象。因为在main函数中我们只创建了一个Res对象,所以自始至终两个函数(In,Out)用的是同一个锁。

    解决方式2:

    在JDK升级到5.0之后,java为我们提供了一个新的解决方式:Lock包。

    以往我们是使用sychronized关键字来隐式的建立锁、释放锁的(我们从没手动干涉过锁的建立,也没手动释放锁(wait是个例外,其实它也不算是正常释放锁,因为wait之后线程进入了等待池,而正常情况下应该进入锁池))。Java为我们提供了手动创建锁和释放锁的方式,并将我们上述的三个方法(wait,notify,notifyAll)与锁挂上了钩。下面详细说明。

    我们将上面的“生产者消费者1”进行Lock的修改:

      1 import java.util.concurrent.locks.Condition;
      2 import java.util.concurrent.locks.Lock;
      3 import java.util.concurrent.locks.ReentrantLock;
      4 
      5 class Res       //共享资源
      6 {
      7     int age = 0;
      8     String name;
      9     boolean isEmpty = true;//资源是否为空
     10     Lock lock = new ReentrantLock();
     11     private Condition conditionOfConusmer=lock.newCondition();
     12     private Condition conditionOfProducer=lock.newCondition();
     13     public void In(String name, int age)//生产方法
     14     {
     15         lock.lock();
     16         try
     17         {
     18             while (!isEmpty)//如果资源非空
     19             {
     20                 conditionOfProducer.await();//等待
     21             }
     22             this.name = name;
     23             this.age = age;
     24             isEmpty = false;//生产完毕,资源非空
     25             conditionOfConusmer.signal();
     26         } catch (Exception e)
     27         {
     28             e.printStackTrace();
     29         }finally
     30         {
     31             lock.unlock();
     32         }
     33     }
     34 
     35     public synchronized void Out()//消费方法
     36     {
     37         lock.lock();
     38         try
     39         {
     40             while (isEmpty)//资源为空
     41             {
     42                 conditionOfConusmer.await();//等待
     43             }
     44             System.out.println("姓名:" + name + "  年龄:" + age);
     45             isEmpty = true;
     46             conditionOfProducer.signal();
     47         } catch (Exception e)
     48         {
     49             e.printStackTrace();
     50         }finally
     51         {
     52             lock.unlock();
     53         }
     54     }
     55 }
     56 
     57 
     58 class Producer implements Runnable
     59 {
     60     private Res res;
     61     private int i = 0;
     62 
     63     public Producer(Res res)
     64     {
     65         this.res = res;
     66     }
     67 
     68     @Override
     69     public void run()
     70     {
     71         while (true)
     72         {
     73             if (i % 2 == 0)
     74                 res.In("小红", 10);
     75             else
     76                 res.In("老王", 70);
     77             i++;
     78         }
     79 
     80     }
     81 }
     82 
     83 class Consumer implements Runnable
     84 {
     85     private Res res;
     86 
     87     public Consumer(Res res)
     88     {
     89         this.res = res;
     90 
     91     }
     92 
     93     @Override
     94     public void run()
     95     {
     96         while (true)
     97         {
     98             res.Out();
     99         }
    100 
    101 
    102     }
    103 }
    104 
    105 class synchronizedDemo
    106 {
    107     public static void main(String[] args)
    108     {
    109         Res res = new Res();//分别创建了两个生产者两个消费者,更能突出现象
    110         Thread thread1 = new Thread(new Consumer(res));
    111         Thread thread2 = new Thread(new Producer(res));
    112         Thread thread3 = new Thread(new Consumer(res));
    113         Thread thread4 = new Thread(new Producer(res));
    114         thread1.start();
    115         thread2.start();
    116         thread3.start();
    117         thread4.start();
    118 
    119     }
    120 }
    生产者消费者2

    我们将生产消费方法的sychronized关键字去掉,使用了Lock类提供的方法。

    Lock是一个接口,我们使用的是他的以实现子类ReentrantLock来实例化对象。

    1 Lock lock = new ReentrantLock();

    这样我们就拿到了一个可以自己操作的锁对象。然后在函数首使用lock.lock()方法来获取锁。在finally中去释放锁lock.unlock()。(因为如果发生异常,程序会停掉,但是此时的线程仍然持有锁。所以我们无论是否发生异常,释放锁是一定会做的,也就是放在finally中)。

    Condition也是一个接口。Condition将以前对于的Object的方法(wait,notify,notifyAll)分成了截然不同的对象,这些对象就可以去与不同的锁搭配使用。而且一个锁可以对应多个Condition,而Object的方法只能对应一个锁。3

    换言之,一个锁就对应这一个阻塞队列,Condition可以操作多个队列。

    使用方法:

    1 Condition conditionr=lock.newCondition();

    其中lock是你指定的锁。也就是说,之前的wait等方法,只是对于一个锁而言的,我们不能去操作别的锁(如果跨锁操作,就意味着有两个以上的锁,这样就会涉及死锁倾向)。现在我们用Condition替换,就能去操作指定的锁上的线程:

    condition.await();   对应wait();

    condition.singal();  对应notify();

    condition.signalAll();  对应notifyAll();

    拿刚才的程序来讲;

    1 conditionOfConusmer.signal();

    这句话就很清晰的在生产者中去唤醒了消费者的线程。以前是用notifyAll(),不管本方他方全部唤醒,然后去循环判断标记。与指定方向的唤醒比较,显然后者更清晰效率更高。

    9.守护线程

    守护线程见名知意,这个线程会自创建以来一直运行,直到主线程结束,主线程结束,所有守护线程全部结束。

    1 Thread thread1 = new Thread();
    2 thread1.setDaemon(true);

    这样就可以将一个线程设置为守护线程。

    10.join方法

    当A线程执行到了B线程的join方法时,A就会等待,直到B线程执行完毕,A才会执行。

    join可以用来临时加入线程执行。

    11.线程组

    线程组很好理解,谁开启的线程,这个线程就属于那个线程组。

    比如在main函数里面开启thread1,thread2,那么这俩就属于main组。

    我们可以用thread.toString来详细线程的名称、优先级、线程组等详细信息。

    12.优先级

    优先级,一个线程的优先级越高,它被CPU执行的机会就越大。

    我们用setPriority(int newPriority)来设置优先级(0-10)。默认的优先级是5。

    但是用数字来表示优先级不是很明显,5与6的优先级我们可能并不能感觉到明显差别。所以,我们定义了三个常量,来表示三个级别的优先级:MIN_PRIORITY、MAX_PRIOROTY、NORM_PRIORITY。

    1 thread1.setPriority(MAX_PRIORITY);

    至此,多线程部分就总结完毕。对于初学者,这些内容已经足够了。才疏学浅,有错误欢迎指正。倾心总结,希望捧场。

  • 相关阅读:
    TdxGaugeControl
    TdxSkinController
    delphi TdxMemData 使用
    深入方法(16)- 方法的顺序
    深入方法(15)- 调用其他单元的函数
    深入方法(14)- 在TForm1 类内声明的方法
    深入方法(13)- 在 interface 区声明的方法
    深入方法(12)- implementation 区中的方法
    深入方法(11)- 参数前缀
    深入方法(10)- 默认参数
  • 原文地址:https://www.cnblogs.com/KangYh/p/9799799.html
Copyright © 2011-2022 走看看