一、并发简史
早期的计算机中不包含操作系统,它们从头到尾只执行一个程序,并且这个程序能访问计算机中所有的资源。在这种裸机环境中,不仅很难编写和运行程序,而且每次只能运行一个程序,这对于昂贵并且稀有的计算机资源来说也是一种浪费。为此,现代计算机中加入了操作系统来支持多个程序同时执行。这主要基于以下考虑:
资源利用率:在某些情况下,程序必须等待某个外部操作完成,例如输入操作或输出操作等,而在等待时程序无法执行其他任何工作。因此,如果在等待的同时可以运行另一个程序,那么无疑将提高资源的利用率。
公平性:不同的用户和程序对于计算机上的资源有着相同的使用权。一种高效的运行方式是通过粗粒度的时间分片使这些用户和程序能共享计算机资源,而不是由一个程序从头运行到尾,然后再启动下一个程序。
便利性:通常来说,在计算多个任务时,应该编写多个程序,每个程序执行一个任务并在必要时相互通信,这比只编写一个程序来计算所有任务更容易实现。
二、进程和线程的概念
进程
进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。我们可以理解为一个应用就是一个进程,比如说QQ。
线程
是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。像QQ.exe运行的时候就有很多子任务在运行,比如聊天线程、好友视频线程、下载文件线程等等。
三、为什么要使用多线程
如果使用得当,线程可以有效地降低程序的开发和维护等成本,同时提升复杂应用程序的性能。具体说,线程的优势有:
1、发挥多处理器的强大能力
现在,多处理器系统正日益盛行,并且价格不断降低,即时在低端服务器和中断桌面系统中,通常也会采用多个处理器,这种趋势还在进一步加快,因为通过提高时钟频率来提升性能已变得越来越困难,处理器生产厂商都开始转而在单个芯片上放置多个处理器核。试想,如果只有单个线程,双核处理器系统上程序只能使用一半的CPU资源,拥有100个处理器的系统上将有99%的资源无法使用。多线程程序则可以同时在多个处理器上执行,如果设计正确,多线程程序可以通过提高处理器资源的利用率来提升系统吞吐率。
2、在单处理器系统上获得更高的吞吐率
如果程序是单线程的,那么当程序等待某个同步I/O操作完成时,处理器将处于空闲状态。而在多线程程序中,如果一个线程在等待I/O操作完成,另一个线程可以继续运行,使得程序能在I/O阻塞期间继续运行。
3、建模的简单性
通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互。我们可以通过一些现有框架来实现上述目标,例如Servlet和RMI,框架负责解决一些细节问题,例如请求管理、线程创建、负载平衡,并在正确的时候将请求分发给正确的应用程序组件。编写Servlet的开发人员不需要了解多少请求在同一时刻要被处理,也不需要了解套接字的输入流或输出流是否被阻塞,当调用Servlet的service方法来响应Web请求时,可以以同步的方式来处理这个请求,就好像它是一个单线程程序。
4、异步事件的简化处理
服务器应用程序在接受多个来自远程客户端的套接字连接请求时,如果为每个连接都分配其各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。如果某个应用程序对套接字执行读操作而此时还没有数据到来,那么这个读操作将一直阻塞,直到有数据到达。在单线程应用程序中,这不仅意味着在处理请求的过程中将停顿,而且还意味着在这个线程被阻塞期间,对所有请求的处理都将停顿。为了避免这个问题,单线程服务器应用程序必须使用非阻塞I/O,但是这种I/O的复杂性要远远高于同步I/O,并且很容易出错。然而,如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。
四、线程带来的风险
任何事物都具有两面性,多线程也不例外,多线程用好了非常爽,用不好让就痛苦非常。下面就来了解一下多线程存在的一些风险:
1、安全问题
线程安全问题是非常复杂的,在没有充足的同步情况下,多个线程的操作执行顺序是不可预测的,这会产生非常奇怪的结果。如下代码,在单线程环境没有问题,但是在多线程环境,就会出现不可预料的结果
public class UnsafeSequence {
private int value;
/** 返回一个唯一的数值**/
public int getValue() {
return value++;// 该操作包含三个子操作:从内存中读取value到线程的工作内存、在线程的的工作内存将value+1、并将结果写回主内存
}
}
由于,value++涉及到三个子操作,而CPU在为每个线程分配时间片去执行时,如果不能在该时间片内一起完成这三个子操作,那么就可能被其他线程捷足先登,修改了原来的的value值,而等CPU再分配给这个线程的时间片后,该线程去执行剩下的操作,就可能覆盖已经修改的value值,导致数据不一致。
2、性能问题
可能你会疑惑,不是说多线程是提升服务性能的么?怎么又会有性能问题?这是因为在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另外一个线程,就会频繁的出现上下文切换(Context Switch)操作,这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多的花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,是内存缓存区中的数据无效,以及增加共享内存总线的同步流量。
3、活跃性问题
在开发并发代码时一定要注意,线程安全性是可不被破坏的。安全性不仅对于多线程很重要,对单线程同样重要。安全性的含义是“永远不要发生糟糕的事情”,而活跃性的含义是“某件正确的事情最终会发生”。当某个操作无法继续执行下去时,就会发生活跃性问题,比如线程A等待线程B释放资源,而线程B永远不会释放资源,那么A就会永久等待下去。就是我们常说的死锁问题。
五、线程状态
虚拟机中的线程状态有六种,定义在Thread.State中:
1、新建状态NEW
new了但是没有启动的线程的状态。比如"Thread t = new Thread()",t就是一个处于NEW状态的线程
2、可运行状态RUNNABLE
new出来线程,调用start()方法即处于RUNNABLE状态了。处于RUNNABLE状态的线程可能正在Java虚拟机中运行,也可能正在等待处理器的资源,因为一个线程必须获得CPU的资源后,才可以运行其run()方法中的内容,否则排队等待
3、阻塞BLOCKED
如果某一线程正在等待监视器锁,以便进入一个同步的块/方法,那么这个线程的状态就是阻塞BLOCKED
4、等待WAITING
某一线程因为调用不带超时的Object的wait()方法、不带超时的Thread的join()方法、LockSupport的park()方法,就会处于等待WAITING状态
5、超时等待TIMED_WAITING
某一线程因为调用带有指定正等待时间的Object的wait()方法、Thread的join()方法、Thread的sleep()方法、LockSupport的parkNanos()方法、LockSupport的parkUntil()方法,就会处于超时等待TIMED_WAITING状态
6、终止状态TERMINATED
线程调用终止或者run()方法执行结束后,线程即处于终止状态。处于终止状态的线程不具备继续运行的能力