[Java并发编程实战] 简介

并发简史

在早期不包含操作系统的计算机中,程序都是单一的串行程序,从头至尾只能执行一个程序,并且这个程序访问这个计算机的所有资源。然而,随着技术的发展,操作系统出现了。它使得计算机程序有了进程,线程的概念,每次可以运行多个程序,并且不同的程序都在单独的进程中运行。操作系统为各个独立执行的进程分配各种资源,包括内存,文件句柄,安全证书等。不同进程之间通过系统本身的通信机制来交换数据,如:套接字,信号处理器,共享内存,信号量以及文件等。
操作系统支持多个程序同时执行,原因主要有:

  1. 资源利用率。如某个程序在等待一个耗时操作完成,那么在等待的同时可以运行另外一个程序,这样可以提高资源利用率。

  2. 公平性。如通过时间片的方式让程序轮流占用计算机资源,而不是由一个程序从头至尾运行完,再进行下一个。

  3. 便利性。编写多个程序来计算多个任务,必要时进行通信。比只编写一个程序来计算所有任务更加容易实现。

串行编程模型优势在于直观性和简单性,每次只做一件事直至完成,然后再做另外一件。然而很多情况下,这个串行模型并不理想。打个比方,我们想烧水泡茶然后看书,以串行的工作方式,我们必须等到水烧开了把茶泡好,才能去看书。而现实生活中,完全可以烧水的过程先去看书,然后等待水烧开在去泡茶。这也引出了计算机应用程序用,同步和异步的概念。正是这些原因,促使进程,线程的出现。

线程,也被称为轻量级进程。现在大多数操作系统中,都是以线程为基本的调度单位,而不是进程。一个进程可以创建多个线程,并且这些线程会共享进程范围内的资源。所以,多个线程如果没有明确的协同机制,那么他们是独立运行的。同样,这些线程都可以访问进程的变量,如果没有明确的同步机制来协同对共享数据的访问,那么当一个线程正在使用某个变量时,另外一个线程可能同时访问这个变量,造成不可预测的结果。但是,每个线程都有各自的计数器,栈以及局部变量等。

线程的优势

如果使用得当,可以降低开发,维护成本,提升性能。线程还可以降低代码复杂度,使得代码更容易编写,阅读和维护。在GUI 程序中,可以提高界面的响应速度;在服务端程序中,可以提升资源利用率和吞吐率。

  • 发挥多核处理器的强大能力。

  • 建模的简单性。将复杂且异步的工作流分解到各个线程运行,在特定的同步位置进行交互。

  • 异步事件的简化处理。某个线程的阻塞不影响其他线程的处理。

  • 响应更灵敏的用户界面。使用特定线程来处理耗时操作,而不是放在UI主线程中处理。比如 Android App 耗时事件不能在 UI 线程处理,会影响 UI 响应的流畅度。

线程的风险

Java 对线程的使用是一把双刃剑。线程的优势我们都已经知道,前提是我们能够正确的编写出安全的并发代码。然而,由于开发人员的技术不足,并发潜在风险的不易察觉,都有可能让我们的程序达不到预期的效果。所以,我们有必要了解一下并发风险这一方面的内容。

安全性问题

多个线程的执行顺序,在没有同步的情况下是不可预测的,甚至产生奇怪的结果。如下面这个序列生成类,多个线程同时获取到的值可能是相同的。

1    public class UnsafeSequene{
2        private int value;
3        //返回一个唯一的数值
4        public int getNext(){
5            return value++;
6        }
7    }

递增操作 value++,实际上他包含三个独立的操作:

  1. 读取 value 的值

  2. 将 value 加 1

  3. 将计算结果写入 value
    由于多个线程之间的操作交替执行,所以可能发生两个线程读到相同的值。如下图所示的 A 线程和 B 线程: 
    这里写图片描述这里写图片描述
    上图说明的是一种常见的并发安全问题,称为竞态条件(Race Condition)。由于多个线程共享相同的内存地址空间,并且是并发运行,因此可能会访问或修改其他线程正在使用的变量。这种方式比其他线程间通信机制更容易实现数据共享,但他同样也带来了巨大的风险:线程由于无法预料数据的变化而发生错误。

幸运的是,Java 提供了各种同步机制来协同这种访问。上面的示例代码,把它改成一个同步方法,就可以防止这种错误的发生。

1public class UnsafeSequene{
2    private int value;
3    //返回一个唯一的数值
4    public synchronized int getNext(){
5        return value++;
6    }
7}

活跃性问题

安全性的含义是,永远不发生糟糕的事情。活跃性是,某件正确的事情最终会发生。当某个操作无法继续执行下去时,就会发生活跃性问题,比如程序代码进入死循环。所线程导致的死锁,也是活跃性问题,比如线程 A 在等待线程 B 释放其持有的资源,而线程 B 永远都不释放改资源,那么,线程 A 就会永远的等待下去。

性能问题

活跃性意味着某件正确的事情最终会发生,但却不够好。这就是性能问题,因为我们通常希望正确的事尽快发生。性能问题包括:服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高等。

良好的并发程序,线程能提高性能,但无论如何,总会带来某种程度的运行时开销。
1.线程调度临时挂起活跃线程并转而运行另一个线程时,就会频繁的出现上下文切换,带来极大的开销:保存和恢复执行上下文,CPU 时间更多的花在线程调度而不是线程运行上。
2.使用同步机制,往往会抑制某些编译器优化。

线程无处不在

即使在程序中没有显示的创建线程,但在框架中仍可能会创建线程,因此在这些线程中调用的代码同样必须是线程安全的。框架通过在框架线程中调用应用程序代码将并发性引入到程序中。在代码中将不可避免地访问应用程序状态,因此所有访问这些状态的代码路径都必须是线程安全的。

下面给出的模块都将在应用程序之外的线程中调用应用程序的代码。

  1. Timer

  2. Servlet 和 JavaServer Page

  3. 远程调用方法

  4. Swing 和 AWT

本文原创首发于微信公众号 [ 林里少年 ],欢迎关注第一时间获取更新。

这里写图片描述这里写图片描述


猜你喜欢

转载自blog.51cto.com/13760461/2118862