• 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);

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

  • 相关阅读:
    ZOJ 3765 Lights (zju March I)伸展树Splay
    UVA 11922 伸展树Splay 第一题
    UVALive 4794 Sharing Chocolate DP
    ZOJ 3757 Alice and Bod 模拟
    UVALive 3983 捡垃圾的机器人 DP
    UVA 10891 SUM游戏 DP
    poj 1328 Radar Installatio【贪心】
    poj 3264 Balanced Lineup【RMQ-ST查询区间最大最小值之差 +模板应用】
    【转】RMQ-ST算法详解
    poj 3083 Children of the Candy Corn 【条件约束dfs搜索 + bfs搜索】【复习搜索题目一定要看这道题目】
  • 原文地址:https://www.cnblogs.com/KangYh/p/9799799.html
Copyright © 2020-2023  润新知