多线程的了解(上)

多线程的了解(上)
计算机操作系统的发展历史
可以从百度百科上了解一下计算机操作系统发展史,顺便了解一下多个单核CPU与多核单CPU的区别:
多核CPU和多个CPU的区别,知乎上有这部分的讨论:
> 选择多个单核CPU,那么每一个CPU都需要有较为独立的电路支持,有自己的Cache,而他们之间通过板上的总线进行通信。假如在这样的架构上,我们要跑一个多线程的程序(常见典型情况),不考虑超线程,那么每一个线程就要跑在一个独立的CPU上,线程间的所有协作都要走总线,而共享的数据更是有可能要在好几个Cache里同时存在。这样的话,总线开销相比较而言是很大的,怎么办?那么多Cache,即使我们不心疼存储能力的浪费,一致性怎么保证?如果真正做出来,还要在主板上占多块地盘,给布局布线带来更大的挑战,怎么搞定?
>
> 如果我们选择多核单CPU,那么我们只需要一套芯片组,一套存储,多核之间通过芯片内部总线进行通信,共享使用内存。在这样的架构上,如果我们跑一个多线程的程序,那么线程间通信将比上一种情形更快。如果最终实现出来,对板上空间的占用较小,布局布线的压力也较小。
>
> 看起来,多核单CPU完胜嘛。可是,如果需要同时跑多个大程序怎么办?每个程序都需要用很多内存怎么办?假设俩大程序,每一个程序都好多线程还几乎用满cache,它们分时使用CPU,那在程序间切换的时候,光指令和数据的替换就要费多大事情啊!
>
> 所以呢,大部分一般咱们使用的电脑,都是单CPU多核的;少部分高端人士需要更强的多任务并发能力,就会搞一个多核多颗CPU的机子,高端的服务器一般都是多颗多核,甚至还高频率。

为什么使用多线程?
为什么使用多线程?
单线程有很多的局限性,例如:磁盘读取文件可能存在阻塞,一个程序等待另一个程序的执行结果,之间都会有很多CPU空闲等待……
资源利用率更好
程序设计在某些情况下更简单
程序响应更快
多线程代价
一个单线程的应用到一个多线程的应用并不仅仅带来好处,它也会有一些代价。不要仅仅为了使用多线程而使用多线程。而应该明确在使用多线程时能多来的好处比所付出的代价大的时候,才使用多线程。
设计更复杂
上下文切换的开销(切换线程需要:1.存储原线程本地数据,程序指针等,2.载入另一个线程的本地数据,程序指针等)
增加资源的消耗(多线程需要更多的内存、操作系统的资源来管理线程)
并发编程模型
并发模型与分布式系统之间的相似性
本文所描述的并发模型类似于分布式系统中使用的很多体系结构。在并发系统中线程之间可以相互通信。在分布式系统中进程之间也可以相互通信(进程有可能在不同的机器中)。线程和进程之间具有很多相似的特性。这也就是为什么很多并发模型通常类似于各种分布式系统架构。
当然,分布式系统在处理网络失效、远程主机或进程宕掉等方面也面临着额外的挑战。但是运行在巨型服务器上的并发系统也可能遇到类似的问题,比如一块CPU失效、一块网卡失效或一个磁盘损坏等情况。虽然出现失效的概率可能很低,但是在理论上仍然有可能发生。
由于并发模型类似于分布式系统架构,因此它们通常可以互相借鉴思想。例如,为工作者们(线程)分配作业的模型一般与分布式系统中的负载均衡系统比较相似。同样,它们在日志记录、失效转移、幂等性等错误处理技术上也具有相似性。
[ 注:幂等性,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同 ]
并行工作者模式
并行工作者模式-Master-Worker,传入的作业会被分配到不同的工作者上
优点:
容易理解,只需添加更多的工作者来提高系统的并行度。
缺点:
共享状态可能会比较复杂(每个工作者可能会访问当共享内存、数据库等)
无状态的工作者(工作者无法保证状态,内部数据可能其他工作者已经修改)
任务顺序是不确定的(每个工作者完成任务的时间无法得到确认)
流水线模式
流水线技术的Pipelining,关于流水线的计算机体系详解基于汇编。
优点:
无需共享的状态 (线程内部使用内存、数据库数据,好像是单个线程在工作)
有状态的(没有其他线程可以修改它们的数据,工作者可以变成有状态的)
合理的作业顺序(某种程度上是有可能保证作业的顺序的)
缺点:
作业的执行往往分布到多个工作者上,并因此分布到项目中的多个类上,这样导致在追踪某个作业到底被什么代码执行时变得困难。
函数式并行
函数式并行的基本思想是采用函数调用实现程序,函数可以看作是”代理人(agents)“或者”actor“,函数之间可以像流水线模型(AKA 反应器或者事件驱动系统)那样互相发送消息。
创建、运行多线程
继承 Thread
public class MyThread extends Thread {
public void run(){
System.out.println("MyThread running");
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myTread.start();
}
匿名Thread的子类
Thread thread = new Thread(){
public void run(){
System.out.println("Thread Running");
}
};
thread.start();
实现 Runnable 接口
public class MyRunnable implements Runnable {
public void run(){
System.out.println("MyRunnable running");
}
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
匿名Runnable子类
Runnable myRunnable = new Runnable(){
public void run(){
System.out.println("Runnable running");
}
}
Thread thread = new Thread(myRunnable);
thread.start();
常见错误:调用run()**方法而非start()方法**
创建并运行一个线程所犯的常见错误,调用线程的run()方法而非start()方法
Thread newThread = new Thread(MyRunnable());
newThread.run(); //should be start();
起初你并不会感觉到有什么不妥,因为run()方法的确如你所愿的被调用了。但是只执行了一次,相当于成员方法执行一次。
但是,事实上,run()方法并非是由刚创建的新线程所执行的,而是被创建新线程的当前线程所执行了。也就是被执行上面两行代码的线程所执行的。想要让创建的新线程执行run()方法,必须调用新线程的start方法。
竞态条件与临界区
在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源。例如:同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件。更准确的说,就是一个或者多个线程,对这些资源做了写操作,如果资源没有改变,多线程读取资源是安全的。
竞态条件: 当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作 临界区

线程安全与共享资源
允许被多个线程同时执行的代码 称作线程安全的代码。线程安全的代码 不包含竞态条件。当多个线程同时更新共享资源时会引发竞态条件。因此,了解Java线程 执行时共享了什么资源很重要。
局部变量:
存储在线程自己的栈中,局部变量永远也不会被多个线程共享。所以,基础类型的局部变量是线程安全的。
局部的对象引用:**
对象的局部引用和基础类型的局部变量不太一样。尽管引用本身没有被共享,但引用所指的对象并没有存储在线程的栈内。所有的对象都存在共享堆中。
如果在某个方法中创建的对象不会逃逸出(译者注:即该对象不会被其它方法获得,也不会被非局部变量引用到)该方法,那么它就是线程安全的。
实际上,哪怕将这个对象作为参数传给其它方法,只要别的线程获取不到这个对象,那它仍是线程安全的。当然,如果别的线程获取到了这个对象,那它就不再是线程安全的了。
对象成员:
对象成员存储在堆上。如果两个线程同时更新同一个对象的同一个成员,那这个代码就不是线程安全的
线程控制逃逸规则:
如果一个资源的创建,使用,销毁都在同一个线程内完成,
【多线程的了解(上)】且永远不会脱离该线程的控制,则该资源的使用就是线程安全的。

    推荐阅读