并发编程是Java语言的重要特性之一,也是作为Java开发无法不迈过去的坎。
并发大家都不陌生,有些东西总感觉自己会,但要实际应用起来的时候总难下笔。本着温故知新,整理下之前的学习笔记,从新梳理并发的整体架构。这里简单介绍下并发涉及到的概念。
并发概要
计算机处理器的运算速度和它的存储和通信速度差距较大,大量的时间花在磁盘IO和网络IO中。如果不希望计算机的大部分时间处于等待状态,必须使用一些手段将处理器的运算能力释放出来。
在计算机中加入操作系统能同时处理多个程序,每个程序都在单独的进程中运行:操作系统为每个程序单独分配各种资源(内存、文件句柄、安全证书等),进程之间可以进行粗粒度的数据交换(套接字、信号处理器、共享内存、信号量、文件)。
进程与线程的区别
进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。
线程是程序中一个单一的顺序控制流程。有时被称为轻量进程(Lightweight Process,LWP)。
通常在一个进程中可以包含若干个线程,它们可以利用进程所拥有的资源。在引入线程的操作系统中,通常都是把进程作为分配资源的基本单位,而把线程作为独立运行和独立调度的基本单位。 由于线程比进程更小,基本上不拥有系统资源,故对它的调度所付出的开销就会小得多,能更高效的提高系统内多个程序间并发执行的程度,从而显著提高系统资源的利用率和吞吐量。
线程的优势
如果使用得当,线程可以有效的降低程序的开发成本和维护成本,提交复杂程序的性能。
- 发挥多处理器的强大能力:通过提高时钟频率来提升性能变得非常困难,增加 CUP 内处理器核心数。基本的调度单位是线程,一个线程只能在一个处理器上执行。一个线程发生IO等待操作,另一个线程可以继续执行。
- 更好的响应时间:例如一个复杂的业务,一笔订单的创建,包括插入订单、生成订单、发送订单消息等,可以使用多线程,只要插入订单成功就返回给用户,其他的步骤在后台使用多线程处理,缩短响应时间、提升用户体验。
- 建模的简单性:需要完成多种类型的任务,为模型中每种类型的任务都分配一个专门的线程,那么可以形成一种串行执行的假象。通过线程将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,因为它模仿了人类的工作方式,便于理解。
线程带来的风险
-
安全问题:不同的线程对同一个数据进行处理,导致结果异常。
private int value = 9; public int getNext(){ return value++; }
value++看起来是一个单独的操作,但是它包含三个独立的操作:读取 value,将 value+1,写入 value。
多线程可能同时对 value 进行读操作,读取的是相同的值 9,然后都加一后返回相同的数值 10。
这里程序的正确性取决于多线程的交替执行时序,发生了竞态条件(Race Condition)。
上面的代码将方法 getNext() 修改为同步方法可以解决问题,在 Java 中可以加上 synchronize 关键字。
或者使用Java现有的线程安全类 AtomicLong 的 incrementAndGet() 方法。 -
活跃性问题:程序无法继续进行,例如死锁、卡死、死循环。
-
性能问题:线程调度会挂起活跃的线程并转而运行另一个线程,这时会发生上下文切换,这种操作开销很大;且使用多线程引入同步机制,同步机制往往抑制编译器优化,使内存的数据无效,增加了共享总线的同步流量。
什么是线程安全
当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不需要任何额外的同步或者协作,这个类都能表现出正确的行为,那么这个类就是线程安全的。
什么是原子操作
假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么B全部执行完,要么完全不执行B,那么A和B对彼此来说就是原子的。
对象的状态是指存储在状态变量中的数据,无状态对象一定是线程安全的。
原子操作是指,对于访问同一个状态的所有操作(包括该操作本身)来说,这个操作是一个以原子方式执行的操作。
要保证状态的一致性,需要在单个原子操作中更新所有相关的状态变量。
加锁机制
Java提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。
synchronize () {
//访问或者修改由锁保护的共享状态
}
Java内置锁是一种互斥锁,这意味着最多只有一个线程能持有这种锁。当线程A尝试获取一个由线程B持有的锁时,线程A必须等待或者阻塞,直到线程B释放这个锁。如果B永远不释放,那么A也将永远地等待下去。
内置锁是可以重入的,重入的一种实现方式是,为每一个锁关联一个计数值和一个所有者线程。
什么是可见性
某个线程正在使用对象的状态而另一个线程同时在修改这个状态,可见性可以保证当一个线程修改了对象状态之后,其他线程立即可以看到发生的状态变化。
Java提供了一种稍弱的同步机制,volatile 变量,用来确保将变量的更新操作通知到其他线程。
加锁既保证了原子性又保证了可见性,而 volatile 变量只能确保可见性。
同步块的可见性由“对一个变量执行 unlock 操作前,必须把此变量同步回主内存”。
final 关键字的可见性指:被 final 修饰的字段在构造器中一旦初始化完成,并且构造器没有把 this 指针传递出去,那么其他线程就能看到 final 字段的值。
发布与溢出
发布一个对象指,使对象能够在当前作用域之外的代码中使用。
溢出指,某个不应该发布的对象被发布。
下面这个例子就发生了this引用溢出,ThisEscape 发布 EventListener 时,也隐含地发布了 ThisEscape 实例本身,因为在Java中内部类包含外部实例的隐含引用。
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
...
}
参考书籍:《JAVA并发编程实战》