分类目录归档:多线程

线程安全

定义线程安全是一件相当费劲的事。简单在谷歌上一搜,就出现大量的定义,比如:
1. 线程安全代码就是当多个线程同时执行时也能正常工作的代码
2. 如果以多个线程同时执行仍能保证正确的方式来管理共享的数据结构,那么我们就说代码块是线程安全的
还有一些类似的定义。
你不觉得上述定义等于啥也没说吗,甚至让你更加困惑。我们不能排除掉这些定义,毕竟它们不是错误的。但是事实上它们没有提供任何实际帮助和观点。如何区别线程安全类和非线程安全类?“安全”到底意味着什么?

线程安全中的正确性

所有合理的多线程定义都是“正确性”的概念,因此理解线程安全的前提就是这个“正确性”。

正确性意味着类符合它的规范

当特定的操作作用于类时,一个好的类规范将会包含任意时刻类的状态以及后置条件。由于我们常常不能给出完整的类规范,我们怎么可能知道它们是正确的呢?我们不知道,但即使这样,一旦我们确信“这些代码好使”,我们仍然会使用它们。这种 “代码自信” 跟我们想要的正确性很接近(This “code confidence” is about as close as many of us get to correctness.)
上面已经乐观地将“正确性”定义为可被识别的东西,现在让我们以一种直接的方式来定义线程安全:当被多个线程访问仍然保持正确的行为,那么,我们说这个类是线程安全的。

一个类是安全的,就是当它被多个线程访问,且无论运行环境如何调度,交替执行这些线,它都能保持正确的行为,并且调用方代码无需额外的同步或其他条件。

如果这种宽泛的“正确性”让你很烦躁,你可以将线程安全理解为相比于单线程环境,并发环境并没有给类本身带来更多破坏。线程安全类封装了所需的同步,因此客户端无需再提供。

示例:无状态的Servlet

线程安全类的一个很好示例就是java servlets,它没有任何字段及对其他类的引用等,它们是无状态的

public class StatelessFactorizer implements Servlet{
  public void service(ServletRequest req,ServletResponse resp){
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = factor(i);
    encodeIntoResponse(resp,factors);
  }
}

特定计算的瞬时状态仅存在于本地变量,而本地变量仅存储在执行线程的栈里,因此它只对执行线程可见。当一个线程访问StatelessFactorizer时,不影响另一个线程的访问,因为两个线程没有任何共享的状态,就像他们访问不同的实例。因为一个线程访问一个无状态类不影响另一个线程操作的正确性,所以无状态类是线程安全的。
以上就是关于这个很小但很重要的线程安全相关的概念
快乐学习!!


译者注:以下为一些精彩评论:
Ramakrishna:
如果参数类型是基本类型,那么每个线程在自己的栈里维护这些变量,如果一个方法引用另一个对象作为参数,那么每个线程仍然在自己线程栈中维护这些对象的状态吗?
Rajesh:
对象的状态总是保存在堆上而非栈,如果你在方法内部引用其他对象,在这种情况下每个线程都会有自己的一份拷贝,因此是线程安全的。

并发的演变

Anant,我的读者之一,问了一个嘎嘎好的问题:是否可以详细说明多线程包括java 8中的相应改进的相关主题(从入门高阶)。他想要的就是从简单的Runnable接口到java 8最新特性的多线程演变过程,我今天就满足他。
我费了九牛二虎之力终于收集到了以下信息,如果哪里不太完善,请愉快的通知我(反正我也不改)。

JDK中的多线程概念

JDK1.x版本里只有很少的类出现。具体的接口/类如下:

  • java.lang.Thread
  • java.lang.ThreadGroup
  • java.lang.Runnable
  • java.lang.Process
  • java.lang.ThreadDeath
  • 以及一些异常类,例如
  1. java.lang.IllegalMonitorStateException
  2. java.lang.IllegalStateException
  3. java.lang.IllegalThreadStateException

除此之外还有一丢丢的同步集合,例如java.util.Hashtable
JDK 1.2和1.3 在多线程这块没什么显著改变(如果有什么出入请更正)
JDK 1.4,只有一点JVM层面关于单次(single call)调用挂起(suspend)/恢复(resume)多线程的改动
JDK 1.5 算是一个大的改动,它融入了多线程并发的各种辅助类:Executor,semaphore,mutex,barrier,latches,concurrent collections 以及 blocking queues,所有的这些都被包含在了这个版本中,java多线程应用云的巨大改变正是孕育于此。

完整的变更列表看这里:http://docs.oracle.com/javase/1.5.0/docs/guide/concurrency/overview.html

JDK 1.6 相比于API的升级更多的是平台的修复,因此在这个版本中出现了一些新的API
JDK 1.7 增加对ForkJoinPool的支持,ForkJoinPool通过实现工作-窃取(work-stealing)技术来打到最大吞吐量,Phaser类也被引入
JDK 1.8 你懂的 Lambda的引入,然而在并发这块仍然没有太大改动,只有2个接口和4个类引入到了java.util.concurrent包,比如CompletableFutureCompletionException
在java 8 中,为了增加基于新添加的stream功能和lambda表达式的聚合操作,集合框架经历了一次大的修订,因此大量方法被几乎全部引入到了Collection类中,也包括并发集合。

完整的变更列表看这里:http://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html

参考:

  • https://www.cs.princeton.edu/courses/archive/fall97/cs461/jdkdocs/relnotes/intro.html
  • http://programmers.stackexchange.com/questions/147205/what-were-the-core-api-packages-of-java-1-0
  • http://docs.oracle.com/javase/1.5.0/docs/guide/concurrency/overview.html
  • http://docs.oracle.com/javase/7/docs/technotes/guides/concurrency/changes7.html
  • http://www.oracle.com/technetwork/java/javase/jdk7-relnotes-418459.html
  • http://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html

希望上述列表能帮助你理解多线程特征的JDK版本指导
译者注:网友在评论区内的补充
1.7
TransferQueue, ConcurrentLinkedDeque, and ThreadLocalRandom

  • https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/TransferQueue.html
  • https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadLocalRandom.html
  • https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ConcurrentLinkedDeque.html

并发和并行

并发意味着在重叠的时间周期内,多个任务以无序的方式启动,运行及完成。并行则是多个任务或者某一任务的不同部分以字面意思同时运行,比如在一个多核的处理上运行。记住,并行和并发不是一件事。
让我们深入的了解一下我所说的并发和并行。

并发

并发本质上应用于我们所讨论的最少两个任务或多任务。当一个应用可以几乎同时地执行两个任务,我们说它是并发程序。虽然两个任务看上去同时运行,但实际上可能并不是。它们利用操作系统CPU时间片的特性,运行任务的一部分,然后进入等待状态,当第一个任务进入等待状态,CPU被分配给第二个任务来完成它的特定部分。
基于优先级的操作系统,分配CPU和其他计算资源例如内存,轮流执行所有的任务,从而让这些任务有机会完成。对于最终用户来说,看起来所有的任务在并行运行。这就叫做并发。

并行

并行并不一定需要存在两个任务,借助于多核架构CPU,它将每个核分配给一个任务或子任务,从而实现物理地同时运行任务的多个部分或多个任务。
实际上,并行依赖多处理单元的硬件支持。而用单核CPU,你可能能得到并发,但是不可能得到并行。

并发与并行的不同

现在,让我们列出两个最明显的不同:
并发两个任务在重叠的时间周期内启动,运行及完成
并行是任务真实地同时运行,例如在多核处理器上。
并发是各独立执行过程的组合,而并行是同时执行的(可能有关)的计算。
并发是一次能处理多件事情,而并行是一次多个事情。
如果一个应用程序是并发但不是并行,那么意味着它虽然可以处理多个任务,但是在某一个时间点任务并不是同时运行
如果一个应用程序是并行但不是并发,那么意味着它在多核的CPU中同时执行某一任务的多个子任务。
如果一个应用程序既不是并行也不是并发,那么意味着在每个时间点顺序执行一个任务
如果一个应用程序既是并行又是并发,那么意味着它利用多核同时执行多个任务
以上就是并发和并行,在java多线程中一个非常重要的概念


译者注:
根据作者的表式,并行和并发的核心区别就是并行是真的有多核/CPU在运行多个任务,而并发则是某个时刻只有一个任务在执行。作者在文章里还区别了任务和子任务,感觉不是很有必要,这只是划分粒度的不同,你认为的子任务在别人眼里可能就是任务。引入任务和子任务反而引起更多的困惑。大家可以参考这片知乎文章:https://www.zhihu.com/question/33515481 比较同意观点:
李遥:

不要把事情说得那么复杂:
并发就是指代码逻辑上可以并行,有并行的潜力,但是不一定当前是真的以物理并行的方式运行
并发指的是代码的性质,并行指的是物理运行状态

黑板客

并发是指同时有很多事要做,你可以串行处理也可以并行处理。
并行是指同时做多件事。
因此并发和并行是相关的,但是是不同的两个概念

Java比较和交换示例-CAS算法

最好的java5新特性之一就是在类中引入原子操作,例如AtomicIntegerAtomicLong等等。针对一些基本操作,这些类帮你简化了多线程代码的复杂性,例如对多线程共享的数据进行增加和减少操作。这些类在内部依赖一个名叫CAS(compare and swap)的算法。在这篇文章中,我将深入的讨论这个概念。

1.乐观锁与悲观锁

对于传统的锁机制,例如在java中使用synchronized,它是一种悲观的锁或多线程技术。它首先就要保证没有其他线程干扰特定的操作(例如:给一个对象加锁)然后只允许你(译者注:当前线程)访问其实例或方法。

就像说“先把门关上,否则其他的骗子会乱动你的东西”

虽然上述方法安全有效,但是在性能方面却挖了个大坑。原因就是等待线程除了获得机会去运行受保护的操作外什么也干不了。
有一种更有效的方法,它天生乐观.在该方法里,你处理一个更新操作,并希望能不受干扰地完成它。该方法依靠冲突检测来决定是否有来自其他部分的干扰,当更新失败时能够重试(与否)。

乐观方法正应了那句老话:获得原谅容易,获得许可难。在这里“容易”意味着“更有效”
译者注:作者采用类比,将乐观锁比作原谅,因为乐观锁就是假设当前没有其他线程的干扰。然后还是不太理解这句老话跟本文主旨的联系

Java多线程

Java多线程是一个复杂的话题,里面涉及到多种术语,编程模型以及一些操作系统层面的细节。要学好多线程除了数量掌握好这些理论知识,更重要的在于练习,编程世界唯一的捷径就是不断练习。

结合自己多年的编程经验,以及参考国内外一些著名的文章和书籍,我想试着带大家进入多线程那有趣而又富有挑战性的世界。其实写这些文章需要勇气,因为稍微不注意,或者由于自己的水平有限,就会误导读者,反而带来不好的影响,所以我想先以翻译国外一些高质量的文章开始,保证理论知识的准确性,让大家正确入门,然后再添加各种实战。千里之行始于足下,只要我们都能坚持下去,相信每个人都会有收获。好了,让我们开始吧。

简单来说,并发是一种能能并行运行多个程序,或同一程序不同部分的能力。利用底层操作系统和硬件锁蕴含的能力,并发可以使程序获得更高的性能和吞吐量。例如,现在的计算机拥有多个CPU以及一个CPU包含多核,程序可以利用多核来服务某一处理的特定部分。在java程序里,结合多线程来达到并行处理及并发。

什么让java拥有并发能力

首当其中的就是java.lang.Thread类,它是java中所有并发概念的基础,其次就是用java.lang.Runnable来抽象了线程类的行为。还有其他的一些用于构建高级应用类,这些类在java1.5时被引入,存在于java.util.concurrent中。

更多:java并发的演变

Java并发真的如此简单吗

上述描述让我们觉得并发确实不错,而且容易实现。然而并不是,它需要你很好的理解各种基本概念以及清楚的了解你应用程序的目标。
相比于单线程,并发线程拥有更复杂的设计。那些访问共享资源的代码在被多线程执行时需要格外注意,不正确的线程同步所引发的问题极难检测,重现以及修复。这些错误往往发生在更高的环境,例如生产环境,有时根本无法在较低环境中复现。
除此之外,运行这些程序需要更多的资源,所以请确保你拥有充足的资源。

Java并发系列

在一篇博文中覆盖所有的并发主题是不太可能的,所以在以下文章中将分开讨论不同的概念。你可以浏览这些主题,如果有什么问题和建议可以留言。
Java并发基础

  • 什么是线程安全
  • 对象级别锁和类级别锁
  • 比较和交换算法(CAS)
  • wait(),notify()以及notifyAll()

对比

  • 实现Runnable与继承Thread
  • 锁与监视器
  • yield()与join()
  • sleep()与wait()

Executro框架

  • Executor框架系列
  • ScheduledThreadPoolExecutor示例
  • FixedSizeThreadPoolExecutor示例
  • ThreadPoolExecutor示例
  • ThreadPoolExecutor + Callable + Future示例
  • 使用ThreadPoolExecutor和信号量限制任务提交率示例
  • BlockingQueue示例
  • UnCatchExceptionHandler示例

高级注意

  • ForkJoinPool示例
  • CountdownLatch示例
  • 使用信号量控制并发访问
  • BinarySemaphore
  • java.util.concurrent.locks.Lock
  • java.util.concurrent.ThreadFactory
  • ThreadLocal变量
  • 线程间通讯

并发集合

  • ConcurrentHashMap示例
  • ConcurrentLinkedDeque示例

其他

  • 创建和解决死锁