并发
Peng's Blog 只记录和技术相关的东西

并发

2017-09-20

内容整理自《Java编程思想》第四版,第21章 并发

java.util.concurrent包

我的理解,主要就是多线程。

并发的多面性

用并发解决的问题大体上可以分为两种:“速度” 和 “设计可管理性”

更快的执行

并发是利用多处理器编程的基本工具。

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

上面这句话听起来可能违背直觉,但其实并不是。因为这样可以节省上下文切换(从一个任务切换到另外一个任务)的代价。

这里面还需要考虑的是阻塞的问题,当一个任务被阻塞了,程序中的其它任务理论上应该是可以继续执行的才对。

在单处理器系统中的性能提高的常见示例是:事件驱动的编程。

改进代码设计

这里面提到了,并发的主要应用场景是游戏的地图、场景等刷新。还有就是用户窗口程序等。

并发是需要代价的,包含复杂性代价。但跟它收获的好处相比,这点代价显得微不足道。

通常,线程使你能够创建更加松耦合的设计(我的理解是,每个任务是一个线程)

基本的线程机制

并发编程使我们可以将程序划分为多个分离的、独立运行的任务。通过使用多线程机制,这些独立任务中的每一个都将由执行线程来驱动。

一个线程就是子安进程中的一个单一的顺序控制流。

定义任务

线程可以驱动任务,因此你需要一种描述任务的方式。Runnable接口,重写里面的 run 方法。

要实现线程行为,你必须显式地将一个任务附着到线程上。

Thread类

这里面主要讲了两种东西:

继承Thread类,然后重写里面的run方法。

构造Thread类的时候,传进去一个Runnable对象。

最后再调用Thread的start方法。这样就开启一个线程了。

使用Executor

这是java.util.concurrent包里面的 执行器。

这个东西主要就是帮你管理Thread对象,从而简化了并发编程。

Executor在客户端和任务执行之间提供了一个间接层,与客户端直接执行任务不同,这个中介对象将执行任务。相当于代理模式吧,我觉得。

里面的线程是异步执行的。

从任务中产生返回值

Runnable是执行工作的独立任务,但是它不反悔任何值。

当你希望任务在完成时能够返回一个值的时候,那么你可以选择Callable接口来代替Runnable接口。重写里面的call方法。

接收的话是用:Future f = pool.submit(这里是Callable对象);

上面的 pool 是:ExecutorService pool = Executor.newFixedThreadPoll(xx); //当然也可以是其它线程池

最后使用 f.get(); 方法,接收。

休眠

sleep(); 这将使任务中止给定的时间。这会阻塞线程

yield(); 中止,但不阻塞线程,线程变回runable状态。

优先级

Thread线程

读取优先级:getPriority();

修改优先级:setPriority();

让步

yield();

后台线程(守护线程)

所谓的后台(daemon)线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。

我的理解,这TM就是守护线程好吗

加入一个线程

一个线程可以在其他线程之上调用 join() 方法,其效果是等待一段时间直到第二个线程结束才继续执行。

如果某个线程在另一个线程 t 上调用 t.join() ,此线程将被挂起,直到目标线程 t 结束之后才恢复。(即t.isAlive()返回false)

共享受限资源

既然讲到多线程,那么肯定会涉及到资源共享的问题。我的理解,这部分主要就是讲线程安全那些东西了。

不正确地访问资源

.. 主要就是多线程访问某个共享资源,这可能会出现脏读等现象。

解决共享资源竞争

防止访问冲突的方法就是:当资源被一个任务使用时,对其进行加锁。第一个访问某项资源的任务必须锁定这项资源,使其它任务在其被解锁之前,无法访问它。

基本上所有的并发模式在解决线程冲突问题的时候,都是采用序列化访问共享资源的方式。这意味着每次只能有一个线程访问资源。其实主要就是加锁。因为锁语句产生了一种互相排斥的效果,所以这种机制常常被称为互斥量(mutex)。

Java中的解决方法目前主要是:synchronized。

要控制对共享资源的访问,首先需要把它包装进一个对象。然后把所有要访问这个资源的方法全都定义成synchronized。

对对象加锁的话,可以防止对static数据的并发访问。

其次还有一定,在使用并发时,需要将域设置为private。否则synchronized关键字就不能防止其它任务直接访问域,这样就可能会产生冲突。

什么时候需要用同步

Brian同步规则:如果你正在写一个变量,它可能接下来将被另一个线程读取,或者正在读取一个上一次已经被另一个线程写过的变量,那么你必须使用同步。并且,读写线程都必须用相同的锁来进行同步。

原子性和易变性

原子操作是不能被线程调度机制中断的操作。

这里提到了 volatile 关键字,说这个关键字可能使得线程对资源的操作是可见的。但如果一个域的值依赖于它之前的值时,volatile就无法工作了。 比如: i++,这个在Java中不是原子性的,但在C++里面是的。

原子类

Java 5引入的 AtomicInteger、AtomicLong、AtomicRefernce等。

临界区

概念:有时你可能只是希望防止多个线程同时访问方法内部的部分代码而不是防止访问整个方法。通过这种方式 分离处理的代码段被称为 临界区(critical section)

synchronized(syncObject){

​ // …..

}

临界区也叫做同步代码块。

在其它对象上同步

class DualSynch {
  private Object syncObject = new Object();
  public synchronized void f() {
    for(int i = 0; i < 5; i++) {
      print("f()");
      Thread.yield();
    }
  }
  public void g() {
    synchronized(syncObject) {
      for(int i = 0; i < 5; i++) {
        print("g()");
        Thread.yield();
      }
    }
  }
}
public class SyncObject {
  public static void main(String[] args) {
    final DualSynch ds = new DualSynch();
    new Thread() {
      public void run() {
        ds.f();
      }
    }.start();
    ds.g();
  }
}

这个demo说明了:两个任务可以同时进入同一个对象,只要对象上的方法是在不同的锁上同步的即可。

线程本地存储

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

防止任务在共享资源上产生冲突,除了之前的加锁。还一种方式就是根除对变量的共享。线程本地存储可以为使用相同变量的每个不同的线程都创建不同的存储。

终结任务

阻塞

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

  • 通过调用sleep()使任务进入休眠状态,在这种情况下,任务在指定的时间内不会运行;
  • 你通过调用wait()使线程挂起。直到线程得到了notify()或者notifyAll()唤醒,线程才会进入到Runnable(就绪)状态;
  • 任务在等待某个输入/输出完成;
  • 任务试图在某个对象上调用其同步控制方法,但是拿不到锁

中断

在Runnable.run()方法的中间打断它。

在Java 5之后,一般不太建议直接对Thread调用interrupt()方法,而是用concurrent包里面的Executor里面的一些东西,比如shutdownNow()和cancel()之类的。

其它

在ReentrantLock上阻塞的任务具备可以被中断的能力

检查中断

interrupted():检查中断状态。

这个方法不仅可以告诉你interrupt()方法是否被调用过,而且还可以清除中断状态。

线程之间的协作

前面都是讲的避免两个线程之间互相影响,但有时候又需要线程间协作。以使得多个任务可以一起工作去解决问题。比如要先烧水,然后才能泡咖啡,这两个线程就是彼此协作的。而且线程写作一般有个顺序问题。

现在的问题就不是线程彼此间的干涉,而是彼此间的协调了。

wait()和notifyAll()

有两种形式的wait():

  • 带参数的wait()
    • 意思是在此期间暂停
    • 期间释放锁(sleep()不释放)
    • 可以通过 notify()、notifyAll()、或者时间到期,然后就恢复了
  • 不带参数的wait()
    • 线程将无限等待下去,直到线程接收到 notify() 或者 notifyAll()

notify()和notifyAll()

调用notifyAll()比只调用notify()要更加安全;

但使用notify()而不是notifyAll()其实是一种优化;在众多等待同一个锁的任务中,只有一个会被唤醒。因此如果你希望使用notify(),就必须保证被唤醒的是恰当的任务。这里主要是指任务被唤醒等待的条件

生产者与消费者

.. 这个太熟悉了,略

生产者-消费者与队列

推荐 java.util.concurrent.BlockingQueue。

任务间使用管道进行输入/输出

通过I/O在线程间通信通常非常有用,提供线程功能的类库以“管道”的形式对线程间的I/O提供了支持。在Java I/O类库中对应的就是 PipedWriter(允许任务向管道写)和PipedReader(允许不同任务从同一个管道中读)。

这个是在 BlockingQueue 之前出现的,所以可以用BlockingQueue做到同样的效果。

死锁

哲学家就餐问题… 五个哲学家围在一起,每人中间放一根筷子,当有个哲学家要吃饭,就需要同时得到左边的和右边的筷子。如果有一个没有,那么就需要等。。。这时候可能就一直等下去。

死锁的四个条件:

  • 互斥(即资源中至少有一个是不能共享的,或者说同时访问的)
  • 占有等待
  • 不能抢占
  • 循环等待

新类库中的构件

举了八个例子.. 只用过 ScheduledExecutor 。

中间还有点东西,因为我看过《Java并发编程实战》所以比较熟,就略过了。


Comments

评论功能暂停使用,如需跟作者讨论请联系底部的GitHub