Thinking in Java - 学习笔记 - (二十一)并发

并发的多面性

使用并发时需要解决的问题有多个,而实现并发的方式也有多种,并且在这两者之间没有明显的映射关系。
用并发解决的问题大体上可以分为“速度”和“设计可管理性
两种。

并发通常是提高运行在单处理器上的程序的性能。

这听起来有些违背直觉。在单处理器上运行的并发程序会增加上下文切换的代价。但阻塞使这个问题不同。

程序中的某个任务因为该程序控制范围之外的某些条件(通常是I/O)而导致不能继续执行,那么我们就说这个任务或线程阻塞了。

从性能的角度看,如果没有任务会阻塞,那么在单处理器机器上使用并发就没有任何意义。

在单处理器系统中的性能提高的常见示例是事件驱动的编程。实际上,使用并发最吸引人的一个原因就是要产生具有可响应的用户界面。

在Java中,通常要假定你不会获得足够的线程,从而使得可以为大型仿真中的每个元素都提供一个线程。

解决这个问题的典型方式是使用协作多线程。Java的线程机制是抢占式的,这表示调度机制会周期性地中断线程,将上下文切换到另一个线程,从而为每个线程都提供时间片。

基本的线程机制

定义任务

线程可以驱动任务,你需要一种描述任务的方式,这可以由Runnable接口来提供。实现Runnable接口并重写run方法。

Thread.yield()的调用是对线程调度器的一种建议,表示让出CPU使用权。

Thread类

Thread构造器接受一个Runnable对象,调用Thread对象的start()方法为该线程执行必需的初始化操作,然后调用run方法,以启动任务。

在main方法中调用Thread的start方法,会产生一个新的线程,与main线程”同时“执行

任何线程都可以启动另一个线程,使得程序好像没有顺序地运行。

可以使用Executor类,使用线程池技术。

共享受限资源

在Java中,递增不是原子性的操作。如果不保护任务,即使单一的递增也不是安全的。

使用线程时有一个基本问题需要注意:你永远都不知道一个线程何时在运行。

为了防止两个任务访问相同的资源,一种方法是加锁。

基本上所有的并发模式在解决线程冲突的问题的时候,都是采用序列化访问共享资源的方案。因为锁语句产生了一种互相排斥的效果,所以这种机制常常称为互斥量(mutex)。

使用synchronized

synchronized关键字:对于某个特定对象来说,其所有synchronized方法共享同一个锁。

一个任务可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者又调用了同一对象上的另一个方法,应付发生这种情况。JVM负责跟踪对象被加锁的次数。如果一个对象被解锁(即锁被完全释放),其计数变为0。在任务第一次给对象加锁的时候,计数变为1。只有首先获得了任务才能允许继续获取多个锁。
针对每个类,也有一个锁(作为类的Class对象的一部分),所以synchronized static方法可以在类的范围内防止对static数据的并发访问。

每个访问临界资源(一次仅允许一个进程使用的共享资源)都必须被同步。

使用显式的Lock对象

用显式的Lock对象,可以使用finally子句,从而有机会做一些清理工作。

显式的Lock对象在加锁和释放锁方面,相对于内建的synchronized锁来说,还赋予了更加细粒度的控制力。例如遍历链接列表中的节点的节节传递的加锁机制(也称为锁耦合)。

原子性与易变性

volatile保证可视性,但是当一个字段的值依赖于它之前的值时,volatile不能很好地完成任务。

因为在Java中自增不是原子操作。i++时会先get i的值,再取常数1,对i加1,再把值put给i。

原子类

Java引入了原子性变量类。
使用CAS(compare and swap)机制。在涉及调优时大有用武之地。

临界区

有时,你只是希望防止多个线程线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式分离出来的代码段被称为临界区(critical section),它也使用synchronized关键字建立。这里,synchronized被用来指定某个对象,此对象的锁被服务业对花括号内的代码进行同步控制:

    synchronized(syncObject) {
        // This code can be accessed
        // by only one task at a time
    }

这也被称为同步控制块;在进入此代码前,必须得到syncObject对象的锁。

通过使用同步控制块,而不是对整个方法进行同步控制,可以使多个任务访问对象的时间性能得到显著提高。

synchronized关键字不属于方法特征签名的组成部分,所以可以在覆盖方法的时候加上去。

在其他对象上同步

synchronized块必须给定一个在其上进行同步的对象,并且最合理的方式是,使用其方法正在被调用的当前对象:synchronized(this)

有时必须在另一个对象上同步,但是如果要这么做,就必须确保所有相关的任务都是在同一个对象上同步的。

线程本地存储

防止任务在共享资源上产生冲突的第二种方式是根除对变量的共享。线程本地存储是一种自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。

创建和管理线程本地存储可以由java.lang.ThreadLocal类来实现。

ThreadLocal对象通常当作静态域存储。

终结任务

线程状态

一个线程可以处于以下四种状态之一:

  1. 新建(new):当线程被创建时,它只会短暂地处于这种状态。此时它已经分配了必需的系统资源,并执行了初始化。此刻线程已经有资格获得CPU时间了,之后调度器将把这个线程转变为可运行状态或阻塞状态。

  2. 就绪(Runnable):在这种状态下,只要把调度器把时间片分配给线程,线程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度器能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。

  3. 阻塞(Blocked):线程能够运行,胆有某个条件阻止它的运行。当线程处于阻塞状态时,调度器将忽略线程,不会分配给线程任何CPU时间。直到线程重新进入了就绪状态,它才有可能执行操作。

  4. 死亡(Dead):牌死亡或终止状态的线程将不再是可调度的,并且再也不会得到CPU时间,它的任务已结束,或不再是可运行的。任务死亡的通常方式是从run()方法返回,但是任务的线程还可以被中断。

进入阻塞状态

一个任务进入阻塞状态,可能有如下原因:

  • 通过调用sleep(milliseconds)使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行。

  • 通过调用wait()使线程挂起。直到线程得到了notify()notifyAll()消息,线程都会进入就绪状态。

  • 任务在等待某个输入/输出/完成。

  • 任务试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个任务已经获取了这个锁。

中断

能够中断对sleep()的调用(或者任何要求抛出InterruptedException的调用)。但是,不能中断正在试图获取synchronized锁或者试图执行I/O操作的线程。

线程之间的协作

wait()和notifyAll()

wait()使你可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制能力。通常,这种条件将由另一个任务来改变。

wait()会在等待外部世界产生变化的时候将任务扶起,并且只有在notify()或notifyAll()发生时,这个任务才会被唤醒并去检查所诞生的变化。因此,wait()提供了一种在任务之间对活动同步的方式。

调用sleep()时候锁并没有被释放,调用yield()也属于这种情况。

另一方面,不一个任务在方法里遇到了对wait()的调用的时候,线程的执行被扶起,对象上的锁被释放。因为wait()将释放锁,这就意味着另一个任务可以获得这个锁,因此在该对象(现在是未锁定的)中的其他synchronized方法可以在wait()期间被调用。这一点至关重要,因为这些其他的方法通常将会产生改变,而这种改变正是使被扶起的任务重新唤醒所感兴趣的变化。因此,当你调用wait()方法时,就是在声明:“我已经刚刚做完能做的所有事情,因此我要在这里等待,但是我希望其他的synchronized操作在条件适合的情况下能够执行。”

sleep()不同的是,对于wait()而言:

  1. wait()期间对象锁是释放的

  2. 可以通过notify()notifyAll(),或者指令时间到期(如果有毫秒数作为参数),人wait()中恢复执行。

调用wait()notify()notifyAll()的任务在调用这些方法前必须“拥有”(获取)对象的锁。

使用notify()而不是notifyAll()是一种优化。使用notify()时,在众多等待同一个锁的任务中只有一个会被唤醒,因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务。

notifyAll()因某个特定锁而被调用时,只有等待这个锁的任务都会被唤醒。

死锁

哲学家就餐事件
通过破坏循环等待,打破死锁。

进阶读物

Java Concurrency in Practice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值