码迷,mamicode.com
首页 > 编程语言 > 详细

Java 多线程

时间:2018-11-13 21:46:29      阅读:150      评论:0      收藏:0      [点我收藏+]

标签:偶数   网页   err   竞争   编译   注意   count   文件   方法体   

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():唤醒所有等待的线程。

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

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

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

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

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

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

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

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

问题分析

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

这里面引入两个概念:

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

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

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

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

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

我们希望是这样的:

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

②A执行到了wait()方法,A释放锁,进入了T的等待池。

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

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

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

(我不清楚一个方法正常执行完run方法之后进入什么池,求证后修改。)

技术分享图片
 1 class Res       //共享资源
 2 {
 3     int count = 0;
 4     boolean isEmpty = true;//资源是否为空
 5 }
 6 
 7 class Producer implements Runnable
 8 {
 9     private Res res;
10 
11     public Producer(Res res)
12     {
13         this.res = res;
14     }
15 
16     @Override
17     public void run()
18     {
19         while (true)
20         {
21             try
22             {
23                 while (!res.isEmpty)//如果不是空的,就让生产者等待
24                     this.wait();
25 
26                 res.isEmpty = false;
27                 if (res.count % 2 == 0)//偶数加1
28                 {
29                     res.count++;
30                     System.out.println(Thread.currentThread().getName() + "生产了:" + res.count);
31                 }
32                 else
33                 {
34                     res.count += 3;//奇数加3
35                     System.out.println(Thread.currentThread().getName() + "生产了:" + res.count);
36                 }
37 
38                 this.notifyAll();//唤醒所有的消费者
39             } catch (Exception e)
40             {
41             }
42 
43         }
44 
45     }
46 }
47 
48 class Consumer implements Runnable
49 {
50     private Res res;
51 
52     public Consumer(Res res)
53     {
54         this.res = res;
55 
56     }
57 
58     @Override
59     public void run()
60     {
61         while (true)
62         {
63             try
64             {
65                 while (res.isEmpty)
66                     this.wait();
67                 System.out.println(Thread.currentThread().getName() + "消费了:" + res.count);
68                 res.isEmpty = true;
69                 this.notifyAll();
70             } catch (Exception e)//因为涉及不到异常的处理,所以笼统的用Exception。
71             {
72             }
73         }
74 
75 
76     }
77 }
78 
79 class synchronizedDemo
80 {
81     public static void main(String[] args)
82     {
83         Res res = new Res();
84         Thread thread1 = new Thread(new Consumer(res));
85         Thread thread2 = new Thread(new Producer(res));
86         Thread thread3 = new Thread(new Consumer(res));
87         Thread thread4 = new Thread(new Producer(res));
88         thread1.start();
89         thread2.start();
90         thread3.start();
91         thread4.start();
92 
93     }
94 }
消费者-生产者示例代码一

 

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

 技术分享图片

 

Java 多线程

标签:偶数   网页   err   竞争   编译   注意   count   文件   方法体   

原文地址:https://www.cnblogs.com/KangYh/p/9799799.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!