1.并发简介

简介

1.1并发简史

线程也被称为轻量级进程,会共享进程范围内的资源,例如内存句柄和文件句柄,但每个线程都有各自的程序计数器(program counter)、栈以及局部变量等。另一方面,在同一个程序中的多个线程也可以被同时调度到多个CPU上运行
由于同一个进程中的所有线程都将共享进程的内存地址空间,因此这些线程都能访问相同的变量并在同一个堆上分配对象,这就需要实现一种比在进程间共享数据粒度更细的数据共享机制。

1.2线程的优势

如果线程使用得当,可以降低程序的开发和维护等成本,同时提升性能。线程能够将大部分的异步工作流转换成串行工作流。此外,线程还可以降低代码的复杂度,使代码更容易编写、阅读和维护。
在GUI应用程序中,线程可以提高用户界面的响应灵敏度,而在服务器应用程序中,可以提升资源利用率以及系统吞吐率。线程还可以简化JVM的实现,垃圾收集器通常在一个或多个专门的线程中运行。

1.2.1发挥多处理器的强大能力

由于基本的调度单位是线程,因此如果在程序中只有一个线程,那么最多同时只能在一个处理器上运行。在双处理器系统上,单线程的程序只能使用一半的CPU资源,而在拥有100个处理器的系统上,将有99%的资源无法使用。
另一方面,多线程程序可以同时在多个处理器上执行。同时,使用多个线程还有助于在单处理器系统上获得更高的吞吐率。如果程序是单线程的,那么当程序等待某个同步I/O操作完成时,处理器将处于空闲状态。而在多线程程序中,如果一个线程在等待I/O操作完成,另一个线程可以继续运行,使程序能够在I/O阻塞期间继续运行。

1.2.2建模的简单性

如果在程序中只包含一种类型的任务,那么比包含多种不同类型任务的程序要更易于编写,错误更少,也更容易测试。如果为模型中每种类型的任务都分配一个专门的线程,那么可以形成一种串行执行的假象,并将程序的执行逻辑与调度机制的细节,交替执行的操作,异步I/O以及资源等待等问题分离开来。
通过使用线程,可以将复杂并且异步的工作流进一步分解为一组简单并且同步的工作流,每个工作流在一个单独的线程中运行,并在特定的同步位置进行交互

1.2.3异步事件的简化处理

服务器应用程序在接受来自多个客户端的套接字连接请求时,如果为每个连接都分配其各自的线程并且使用同步I/O,那么就会降低这类程序的开发难度。
如果某个应用程序对套接字执行读操作而此时还没有数据到来,那么这个读操作将一直阻塞,直到有数据到达。在单线程应用程序中,这不仅意味着在处理请求的过程中将停顿,而且还意味着在这个线程被阻塞期间,对所有请求的处理都将停顿。为了避免这个问题,单线程服务器必须使用非阻塞I/O,这种I/O的复杂性要远远高于同步I/O,并且很容易出错。然而,如果每个请求都拥有自己的处理线程,那么在处理某个请求时发生的阻塞将不会影响其他请求的处理。

1.2.4响应更灵敏的用户界面

1.3线程带来的风险

1.3.1安全性问题

线程安全性可能是非常复杂的,在没有充足同步的情况下,多个线程中的操作执行顺序是不可预测的,甚至会产生奇怪的结果。例如:

/**
 * 常见的并发安全问题,称为竞态条件(race condition)。在多线程环境下,getValue是否会返回唯一的值,
 * 要取决于运行时对线程中操作的交替执行方式。
 */
@NotThreadSafe
public class UnsafeSequence {
    private int value;

    /** 返回一个唯一的数值 */
    public int getNext() {
        return value++;
    }
}

在这里插入图片描述

由于多个线程要共享相同的内存地址空间,并且是并发运行,因此它们可能会访问或修改其他线程正在使用的变量。当然,这是一种极大的便利,因为这种方式比其他线程间通信机制更容易实现数据共享。
但它同样也带来了巨大的风险:线程会由于无法预料的数据变化而发生错误。当多个线程同时访问和修改相同的变量时,将会在串行编程模型中引入非串行因素,而这种是很难分析的。要使多线程程序的行为可以预测,必须对共享变量的访问操作进行协同

@ThreadSafe
public class Sequence {
    @GuardedBy("this")
    private int value;

    public synchronized int getNext() {
        return value++;
    }
}

如果没有同步,那么无论是编译器、硬件还是运行时,都可以随意安排操作的执行时间和顺序,例如对寄存器或者处理器中的变量进行缓存,而这些被缓存的变量对于其他线程来说是暂时(甚至永久)不可见的。

1.3.2活跃性问题

活跃性关注于另一个目标,即某件正确的事情最终会发生。当某个操作无法继续执行下去时,就会发生活跃性问题。在串行程序中,活跃性问题的形式之一就是无意中造成的无限循环。
线程将带来其他一些活跃性问题。例如,如果线程A在等待线程B释放其持有的资源,而线程B永远都不释放该资源,那么A就会永久地等待下去。

1.3.3性能问题

性能问题包括多个方面,例如服务时间过长,响应不灵敏,吞吐率过低,资源消耗过高,或者可伸缩性较低等。在设计良好的并发应用程序中,线程能提升程序的性能,但无论如何,线程总会带来某种程序的运行时开销。
在多线程程序中,当线程调度器临时挂起活跃线程并转而运行另一个线程时,就会频繁地出现上下文切换操作(context switch),这种操作将带来极大的开销:保存和恢复执行上下文,丢失局部性,并且CPU时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓冲区中的数据无效,以及增加共享内存总线的同步流量。

1.4线程无处不在

即使在程序中没有显式地创建线程,但在框架中仍可能会创建线程,因此在这些线程中调用的代码同样必须是线程安全的
每个java应用程序都会使用线程。当JVM启动时,它将为JVM的内部任务(例如,垃圾收集、终结操作等)创建后台线程,并创建一个主线程来运行main方法。AWT和swing的用户界面框架将创建线程来管理用户界面事件。Timer将创建线程来执行延迟任务。一些组件框架,例如servlet和RMI(远程方法调用,Remote Method Invocation),都会创建线程池并调用这些线程中的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值