Java 多线程

Posted 康先森

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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以上是关于Java 多线程的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程与并发库高级应用-工具类介绍

多线程 Thread 线程同步 synchronized

Java多线程具体解释

自己开发的在线视频下载工具,基于Java多线程

什么是JAVA的多线程?

多个用户访问同一段代码