您的位置:首页 > 其它

多线程入门学习-第一课(多线程并发理解)

2013-03-09 11:30 281 查看
本篇是我最近在《Thinking in java》一书多线程的概念部分的一些学习心得,由于只是重点看了这几个概念性的东西,对应后面的内容还没有深入学习,所以有什么不足的地方希望大家多多原谅,本文旨在让不了解多线程的人能够了解什么是多线程,如果有什么地方讲的不好,希望大家能多多指点,谢谢。希望对没有接触过多线程的人有点帮助。

本节要通过一个例子来简单介绍一下多线程,及使用多线程的原因。首先,我们来看几个重要的概念性的问题。说到多线程不得不提并发。那么到底什么是并发?先说一下平时我们写的程序是怎么样的,平时写的程序大都是顺序执行的,那么什么叫做顺序执行呢?举个例子来说,现在你写了个程序,这个程序很简单就100行代码,然后他从第一行顺序运行到了最后一行结束,这就是顺序执行。

在讲并发之前,我想先讲一下另外一个非常重要的概念“任务”。什么是任务?其实任务这个东西可大可小,就看你从什么角度去划分他。就说这100行代码,你可以把它看成是一个任务。当然你也可以把这个100的任务拆分成多个小任务,我们就拆分成10个小任务,假设这10个小任务都互不相干,然后从1到10给这个任务编号,那么顺序执行的程序他就会从1依次执行到10。而并发的程序也就是多线程写的程序是怎么执行的呢?从宏观上看,他有可能是10个任务同时进行,这就是所谓的并发。但事实确并不是这样的,假设只要一个处理器,也就是说同一时刻只有一个任务可以进处理。(事实上同一时刻处理器只执行一个指令才是,而一个任务是有很多指令所组成的,这里我们把这些个任务都想象成原子操作,为什么可以看出原子操作,你可以这么认为:通过特殊指令可以让处理器顺序执行完这个任务的一系列指令,也就是说每个小任务内部是顺序执行的)。

既然一个时刻只有一个任务在进行那么前面为什么又说多个任务同时进行呢?这里就又涉及到另外一个概念----阻塞。什么是阻塞?我们假设“任务1”在处理到一半由于某种原因不在执行下去了(这个原因是程序控制之外的,比如说I/O操作就是等待你从控制台输入信息),这个时候怎么办?假如是顺序执行的程序,他就会 一直这么等着,也就是说其他任务在任务1执行完之前是不会去执行的。但是并发操作就不一样了,这个时候通过某种指令告诉处理器任务1在等待了,其他的任务可以抢占这个处理器去执行自己的任务,当然只是其中一个不可能9个同时执行。因为执行速度是非常快的,宏观上看你会觉得他们是同时进行的,又或者说换一种角度来看,我们把在等待的时间也算成是执行任务的时间,那么他们也是同时进行任务的,只是cpu在处理的时候只处理一个任务。也就是说这里的并发指的是什么?这里的并发指的是多个任务可以同时进行,但每个任务的cpu处理阶段只能有一个任务在进行。

那么到底是如何进行并发操作的呢?这里就又要引入另外一个新的概念----线程。什么是线程?线程有什么用?我是这样理解:我们用线程来包装需要执行的任务,然后告诉处理器来处理我包装的任务,从微观的角度来讲,它也一个是一系列指令,这些指令告诉处理器需要执行哪些代码(当然这些代码也只是一系列指令而已)。并且这个线程一旦通知cpu去处理任务后,在这个任务结束之前他是不会再包装其他任务了。那其他任务想执行怎么办?那就再起一个线程去处理其他某个任务呗。这样子你的程序里就会存在多个线程,也就是所谓的多线程啦

从性能角度来看,假如没有阻塞,那么就没有多线程的必要,此时若是使用多线程还增加可个上下文切换(从一个任务切换到另一个任务)的时间。而当出现阻塞的时候如果不使用多线程,那么程序势必会停止那里等待,这个时候就是在浪费时间了。而假如使用了多线程,这个等待的线程就继续让他等待着,而另外一个线程可以继续执行他的工作。其实就cpu的处理而言,同一时刻其实只执行了一个任务,只是当顺序执行的程序中有出现了因为阻塞而等待的情况时,就跳过这个情况,然后继续执行下去,然后那个阻塞好了,就继续加入到cpu的抢占当中。谁抢到了,谁就执行。举个例子来说:这就好比你要烧菜,加入按部就班的烧菜要怎么做?那么我们现在做一道蛋汤。首先,第一步我要把鸡蛋搅碎,第二步:切一些配料,第三步把水放入锅中烧,第四步,等水烧开后把蛋和配料放入水中。然后等他煮好就好了。这里看起来一切都很正常,其实在烧水的时候,我们有一段等水烧开的时间,这段时间你是空闲的。这并不是一个高效的做法,假如我们先烧水,然后在烧水的这段时间搅碎鸡蛋和切配料,然后水烧开后放入水中煮,其实这样子做就相当于并发执行了你要做的事情,充分利用了空闲的时间,而你自己人就相当于执行任务的cpu。
从以上的举例中我们可以得出这样一个结论,为了能够充分利用阻塞时的空闲时间,我们可以选择多线程操作。在代码中的示例我们可以这样来考虑,这里我讲的是一种思路,其思路的宗旨是通过比较来学习理解多线程。有两个任务,任务1:非阻塞,任务2:阻塞;两种执行方法:1:顺序执行,2:并发执行。然后分别取一个任务和一种执行方法进行组合测试。我把这方法叫做排列组合法,这样可以清楚的知道各种情况下会怎么样,下面我附上简单的测试代码:

import java.util.concurrent.TimeUnit;
//倒计时
public class LiftOff implements Runnable
{
private int countDown =
10;//倒计时数
private static int taskCount =
0; //多个对象共用这个变量
private final int id = taskCount++;//标志是哪个线程在执行的这个
public LiftOff()
{
super();
}
public LiftOff(int countDown)
{
super();
this.countDown =
countDown;
}
//打印计时数
public String
status() {
return "#" + id + "(" +
( countDown > 0 ? countDown : "liftOff")
+ ")" ;
}
//用于测试不并发
public void run2()
{
while (countDown --
> 0) {
System. out.println(status());
try {
TimeUnit. MILLISECONDS.sleep(1000);
} catch (InterruptedException
e) {
e.printStackTrace();
}
}
}
//测试并发执行
public void run()
{
while (countDown --
> 0) {
System. out.println(status());
try {
TimeUnit. MILLISECONDS.sleep(100);
} catch (InterruptedException
e) {
e.printStackTrace();
}
}
}
public static void main(String[]
args) {
//并发
for (int i
= 0; i < 5; i++) {
new Thread(new LiftOff()).start();
System. out.println("diligent" );
}
// //不并发
// for( int i
= 0; i < 5; i++){
// new LiftOff().run2();
// System.out.println("diligent");
// }
}
}
下面来分析一下这个类:
这个类非常简单,其实就是一个倒计时的类。该类继承了Runnable接口,表示是一个任务,继承该接口需要实现run方法。run()内的就是一个要执行的任务,简单点说就是要在新开启的线程中跑的东西。而status方法说白了就是一个返回当前计数的方法。现在这个要执行的具体任务,也就是run方法内要执行的内(输出当前的计数直到他小于0,并且每打印一次就休眠1秒)而main方法里面有一个for循环用来执行5遍这个倒计时的任务。每次执行这个任务都重新创建一个线程Thread来执行这个任务。main其实也是跑在一个线程里面。接下来我们用极限的思想来考虑这个问题:
首先我们来罗列一下我们要分析的这个问题所涉及到的元素:1.执行这个任务的总时间我们记为t1,2.两次创建线程的时间间隔我们记为t2。然后让我们来比较一下这两段时间。第一种情况:t1无穷小,趋向于0,记t1->0,t2无穷大,记t2->∞,这就意味着这个任务执行速度会非常之快,近乎是一个原子操作了,所有这个时候系统同时最多也就存在两个线程。其实这种情况已经没有多起一个线程去执行他的必要了,因为就这么顺序执行也不会出现明显的阻塞状态。然后第二中情况:t1->∞、t2->0,这个会导致怎样的情况呢?此时执行倒计时的任务的线程会同时存在,还有一个main主线程。然后这6个线程会如何运行?首先要明确一点,那就是当同时存在这样六个线程的时候,在同一时刻必定只有一个任务在执行(这里讨论的都是单cpu的情况),可以认为这六个线程挤在那里等待执行各自的任务。然后一个任务他执行好了或者是等待了,其他的线程就会抢占资源来执行自己的任务,可以理解为抢占cpu来执行自己的任务,谁抢到谁执行,这个如果优先级都一样那么抢到资源的概率是一样的,所有到底谁执行任务是存在不确定性的。但有一点是可以确定的,那就是每个线程都有自己的任务,而这个任务是不会变的,并且即使自己的任务完成了,也不会管其他线程的任务。
然后来总结一下上面的内容:
1:有阻塞才并发
2:并发具有不确定性(那个线程抢到资源执行任务是随机的)
3:每个线程执行的任务是明确的
再补充一点就是不要为了并发而并发,能顺序执行的就不要多线程执行,启用线程本身就需要额外的开销,首先你启用一个线程需要花费时间,然后任务之前的切换执行也需要时间,还有后续将会的并发存在的一些隐患如死锁。只有当真正需要并发执行的时候再去并发,并且还要小心编写,这些在后续都会讲到。
最后总结下分析问题的小技巧:一个是上面我用到排列组合法,先从一个特例出发找出问题所在,然后把问题分解,然后提炼出分解的元素,考虑每个元素本身存在的不同情况,在排列组合出多种实际问题的情况。
另一个就是可以用极限的思想看问题,然后找出问题所在。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: