Java并发

Java并发编程入门

导读:感觉自己不会并发编程,因此阅读了 java 核心编程第一卷的并发编程章节,结合网上博客文章,做了一些总结。

基本概念理解

进程:一个操作系统运行多个任务,比如可以在一个操作系统上同时玩游戏、写文档、上网等。

线程:一个进程上运行多个处理流水线,给人一种并行处理的错觉。比如一个GUI应用程序可以同时处理多个按钮的监听事件,而不是互相干扰,彼此等待。线程是为了提高应用程序的使用率。

 

线程状态

Java线程的五个状态:new、runnable、running、blocked、dead。

new Thread
start
CPU时间片选中
run方法结束
sleep,wait,t2.join
1.CPU时间片截止 2.yield
1.sleep结束 2.notify 3.join线程结束
/
new
runnable
running
dead
blocked

运行线程的方法

  1. 继承 Thread 类,重写 run 方法。

     

  2. 实现 Implemented 接口,重写 run 方法。

  3. 通过 Callable 的实现类,在下文详细讲解。

 

自动挡/手动挡:Lock和Synchronized

Synchronized 是自动挡汽车,能满足大部分锁的需求,但如果另有需要,就需要手动 lock 了。

一些概念:

 

Synchronized,根据修饰位置和对象的不同,可以分为 “对象锁”, “类锁”, “方法锁”。

此 synchronized 获取的是 this 的内在锁,类别为对象锁,一次只能有一个线程访问同一对象的临界区域:

相当于以下代码(不完全是),这个 synchronized 获得的是方法和对象的内在锁:

synchronized 可以把类锁住,此处获得的是类锁,这样一次只能由一个线程访问这个类。

Synchronized 块内还能套用 Synchronized,典型的三线程循环打印ABC的题目就是这样(需要了解)。

Synchronized 的大量使用会造成很大的开销。

 

锁的详细概念

详细参考 [知乎文章] https://zhuanlan.zhihu.com/p/71156910

 

线程主要操作

  1. Thread.sleep() 方法:

    同步阻塞当前线程,进入 blocked 状态,sleep 结束后重新进入 runnable 状态。

  2. join()方法:

    其他线程调用 join 使得当前线程被阻塞,直到调用者线程死亡。

  3. wait()notifyAll() 方法:

    是 Object 类的方法,必须在已经得到锁的情况下使用,包括内在锁 (Intrinsic Lock) 和显式锁 ( 主要是 Reentrant Lock )。使用后,获得锁的物体进入阻塞状态,直到等待期结束(如果设置等待时间、否则无限等待)、被 notify 或者 interrupt

    进行银行取钱模拟实验,建立两个线程,一个线程取钱,当钱不够的时候无限等待,另一个线程存钱。

    分析:当钱不够的时候进入阻塞状态,另一个线程不断增加钱,每增加一次,调用 notifyAll() 试图唤醒阻塞的线程,当发现钱还是不够时,继续进入阻塞,反复循环,直到钱增加到指定数量时扣除。

    wait 调用者不是锁的持有者时,抛出 IllegalMonitorState 异常。

    sleep() 的比较

    1. sleep 能在任何地方调用,wait 必须在临界区域调用。
    2. 两者都要试图处理 InterruptedException

     

  4. yield()方法:

    调用者线程把状态恢复到 Runnable,但仍有可能在进入 Runnable 后又被立刻选中进入 Running 状态。

     

原子操作与Volatile

原子操作例子:a += 1,并不是原子操作,而是三个原子操作的集合:

正是因为其操作的非原子性,在CPU时间调度时,很可能在其中一个操作做完后,就切换到另一个线程了,这样就会导致数据损坏,也就有了线程安全一说。

 

Volatile 关键字的双重语义:

  1. 保证某个变量在读写操作后,对其他线程来讲,是立即可见的,其操作是直接在主存操作,不经过内存。
  2. 保证受影响部分编译后产生的指令不被重排序。

为了 I/O 一个变量设置一堆锁显然费时费力,这时 Volatile 就有用处了。

如上,不必使用 synchronized 或者 lock,数据在一次原子操作后立即更新,对所有线程可见,也就避免了数据损坏。

但是给变量加了 volatile 一定安全吗?假设有两个线程在同时操作 volatile 修饰的 a 变量,设线程 A 取得了 a 变量的值为1,在计划对其 +1 操作时,线程 B 抢先一步把它更新为 3,然后切换回 a 线程,把 1 + 1 = 2 重新写回,结果 a 的值是 2,显然不是线程安全的。

甚至对于 a += 1 的情况也不安全,他不是原子操作;然而,对于 a++ 是可行的,这是一步操作。

结论:volatile 仅对赋值、自增等原子操作才有用,否则仍然是线程不安全的。

 

线程安全集合框架

暂时不讲。

 

Callable 和 Future

Callable<V> 接口包含 call() 方法,类似 Runnablerun() 方法,但它有返回值。

Future<V> 接口的方法:

FutureTask<T> 类继承 FutureRunnableFutureRunnable 实现 RunnableFuture

FutureTask<T> 类封装一个 callable 实现类,通过 get() 方法得到 call() 的计算结果。如果计算没有结束,则开始同步阻塞等待。

通过下列封装,可以把 FutureTask 变成 Future + Runnable

 

线程池

线程池是一个装载了一堆线程的统一调度工具,好处:

  1. 如果创建太多普通的短寿命线程并一一执行,很可能把虚拟机搞炸。线程池的代码经过优化,能应对这一问题。
  2. 普通的线程执行完毕就是 dead 状态,不能恢复,线程池可以反复添加 “任务” 并执行,当其中任务结束之后,线程又变回Idle状态。要想彻底清理线程池,只需手动调用 shutdown 即可。
  3. 线程池还提供了任务周期性执行的功能(在 Scheduled Thread Pool 中)。

线程池类型有很多,个人感觉其创建属于工厂模式,都是一个名为 ThreadPoolExecutor 的类创建的,只不过每种线程池的参数不同,创建某个类别的线程池相当于创建了一种 ”预设“。

相关源码:

 

一般的,每个线程池初始时都有一些等待的线程(Idle Thread),只需把创建好的 Callable/Runnable 对象扔进去(调用 submit方法)就能自动装配成一个完整的线程并开始执行。Callable submit之后,其返回值是 Future,可以从中得到计算结果返回值。

上述代码把 future 加到 resList 是一种获得结果的方法,如果不这么做,也可以:

  1. invokeAll() 同步阻塞等待所有线程执行完毕,并返回一个结果集(List<Future<T>>)。
  2. invokeAny() 随便返回一个执行完毕的线程的计算结果(Future<T>)。