简介

从创建以来,JAVA就支持核心的并发概念如线程和锁。这篇文章会帮助从事多线程编程的JAVA开发人员理解核心的并发概念以及如何使用它们。

(博主将在其中加上自己的理解以及自己想出的例子作为补充)

概念

原子性:原子操作是指该系列操作要么全部执行,要么全部不执行,因此不存在部分执行的状态。
可见性:一个线程能够看见另一个线程所带来的改变。

竞争情况

当多个线程在一个共享的资源上执行一组操作时,会产生竞争。根据各个线程执行操作的顺序可能产生多个不同结果。下面的代码不是线程安全的,value可能会被初始化多次,因为check-then-act型(先判断是否为null,然后初始化)的惰性初始化并非原子性操作

1
2
3
4
5
6
7
8
class  <T> {
private volatile T value;
T get() {
if (value == null)
value = initialize();
return value;
}
}

数据冲突

当两个或多个线程在没有同步的情况下试图访问同一个非final变量时,会产生数据冲突。不使用同步可能使数据的改变对别的线程不可见,从而可能读取过期的数据,并导致如无限循环,数据结构损坏和不准确的计算等后果。下面这段代码可能会导致无限循环,因为读者线程可能永远都没有看到写入者线程做出的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Waiter implements Runnable {
private boolean shouldFinish;
void finish() { shouldFinish = true; }
public void run() {
long iteration = 0;
while (!shouldFinish) {
iteration++;
}
System.out.println("Finished after: " + iteration);
}
}
class DataRace {
public static void main(String[] args) throws InterruptedException {
Waiter waiter = new Waiter();
Thread waiterThread = new Thread(waiter);
waiterThread.start();
waiter.finish();
waiterThread.join();
}
}

JAVA内存模型:happens-before关系

JAVA内存模型是根据读写字段等操作来定义的,并在控制器上进行同步。操作根据happens-before关联排序,这解释了一个线程何时能够看到另一个线程操作的结果,以及是什么构成了一个同步良好的程序。

happens-before关联有以下属性:

  • Thread#start的方法在线程的所有操作之前执行
  • 在释放当前控制器之后,后序的请求才可以获取控制器。(Releasing a monitor happens before any subsequent acquisition of the same monitor.)
  • 写入volatile变量的操作在所有后序读取该变量的操作之前执行。
  • 写入final型变量的操作在发布该对象的引用之前执行
  • 线程的所有操作在从Thread#join方法返回之前执行

上图中,Action XAction Y之前执行,因此线程1Action X以前执行的所有操作对线程2Action Y之后的所有操作可见。

标注的同步功能

synchronized关键字

synchronized关键字用来防止不同的线程同时进入一段代码。它确保了你的操作的原子性,因为你只有获得了这段代码的锁才能进入这段代码,使得该锁所保护的数据可以在独占模式下操作。除此以外,它还确保了别的线程在获得了同样的锁之后,能够观察到之前线程的操作。

1
2
3
4
5
6
7
8
9
10
class AtomicOperation {
private int counter0;
private int counter1;
void increment() {
synchronized (this) {
counter0++;
counter1++;
}
}
}

synchronized关键字也可以在方法层上声明。

静态方法:将持有该方法的类作为加锁对象
非静态方法:加锁this指针

锁是可重入的。所以如果一个线程已经持有了该锁,它可以一直访问该锁下的任何内容:

1
2
3
4
5
6
7
8
9
10
11
12
class Reentrantcy {
synchronized void doAll() {
doFirst();
doSecond();
}
synchronized void doFirst() {
System.out.println("First operation is successful.");
}
synchronized void doSecond() {
System.out.println("Second operation is successful.");
}
}

争用程度影响如何获得控制器:

初始化:刚刚创建,没有被获取
biased:锁下的代码只被一个线程执行,不会产生冲突
thin:控制器被几个线程无冲突的获取。使用CAS(compare and swap)来管理这个锁
fat:产生冲突。JVM请求操作系统互斥,并让操作系统调度程序处理线程停放和唤醒。

wait/notify

wait/notify/notifyAll方法在Object类中声明。wait方法用来将线程状态改变为WAITING或是TIMED_WAITING(如果传入了超时时间值)。要想唤醒一个线程,下列的操作都可以实现:

  • 另一个线程调用notify方法,唤醒在控制器上等待的任意的一个线程
  • 另一个线程调用notifyAll方法,唤醒在该控制器上等待的所有线程
  • Thread#interrupt方法被调用,在这种情况下,会抛出InterruptedException

最常用的一个模式是一个条件性循环:

1
2
3
4
5
6
7
8
9
10
11
12
class ConditionLoop {
private boolean condition;
synchronized void waitForCondition() throws InterruptedException {
while (!condition) {
wait();
}
}
synchronized void satisfyCondition() {
condition = true;
notifyAll();
}
}
  • 记住,要想使用对象上的wait/notify/notifyAll方法,你首先需要获取对象的锁
  • 总是在一个条件性循环中等待,从而解决如果另一个线程在wait开始之前满足条件并且调用了notifyAll而导致的顺序问题。而且它还防止线程由于伪唤起继续执行。
  • 时刻确保你在调用notify/notifyAll之前已经满足了等待条件。如果不这样的话,将只会发出一个唤醒通知,但是在该等待条件上的线程永远无法跳出其等待循环。

博主备注:这里解释一下为何建议将wait放在条件性循环中、假设现在有一个线程,并没有将wait放入条件性循环中,代码如下:大专栏  猫头鹰的深夜翻译:核心JAVA并发一r/>

1
2
3
4
5
6
7
8
9
10
11
12
13
class UnconditionLoop{
private boolean condition; synchronized void waitForCondition() throws InterruptedException{ wait();
} synchronized void satisfyCondition(){
condition = true;
notifyAll();
}
}

假设现在有两个线程分别同时调用waitForConditionsatisfyCondition(),而调用satisfyCondition的方法先调用完成,并且发出了notifyAll通知。鉴于waitForCondition方法根本没有进入wait方法,因此它就错过了这个解挂信号,从而永远无法被唤醒。

这时你可能会想,那就使用if判断一下条件呗,如果条件还没满足,就进入挂起状态,一旦接收到信号,就可以直接执行后序程序。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UnconditionLoop{
private boolean condition; private boolean condition2; synchronized void waitForCondition() throws InterruptedException{ if(!condition){
wait();
}
}
synchronized void waitForCondition2() throws InterruptedException{ if(!condition2){
wait();
}
}
synchronized void satisfyCondition(){
condition = true;
notifyAll();
} synchronized void satisfyCondition2(){
condition2 = true;
notifyAll();
}
}

那让我们再假设这个 方法中还存在另一个condition,并且也有其对应的等待和唤醒方法。假设这时satisfyConsition2被满足并发出nofityAll唤醒所有等待的线程,那么waitForConditionwaitForCondition2都将会被唤醒继续执行。而waitForCondition的条件并没有被满足!

因此在条件中循环等待信号是有必要的。


volatile关键字

volatile关键字解决了可见性问题,并且使值的更改原子化,因为这里存在一个happens-before关联:对volatile值的更改会在所有后续读取该值的操作之前执行。因此,它确保了后序所有的读取操作能够看到之前的更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VolatileFlag implements Runnable {
private volatile boolean shouldStop;
public void run() {
while (!shouldStop) {
//do smth
}
System.out.println("Stopped.");
}
void stop() {
shouldStop = true;
}
public static void main(String[] args) throws InterruptedException {
VolatileFlag flag = new VolatileFlag();
Thread thread = new Thread(flag);
thread.start();
flag.stop();
thread.join();
}
}

Atomics

java.util.concurrent.atomic包中包含了一组支持在单一值上进行多种原子性操作的类,从而从加锁中解脱出来。

使用AtomicXXX类,可以实现原子性的check-then-act操作:

1
2
3
4
5
6
7
8
class CheckThenAct {
private final AtomicReference<String> value = new AtomicReference<>();
void initialize() {
if (value.compareAndSet(null, "Initialized value")) {
System.out.println("Initialized only once.");
}
}
}

AtomicIntegerAtomicLong都用increment/decrement操作:

1
2
3
4
5
6
7
class Increment {
private final AtomicInteger state = new AtomicInteger();
void advance() {
int oldState = state.getAndIncrement();
System.out.println("Advanced: '" + oldState + "' -> '" + (oldState + 1) + "'.");
}
}

如果你想要创建一个计数器,但是并不需要原子性的读操作,可以使用LongAdder替代AtomicLong/AtomicIntegerLongAdder在多个单元格中维护该值,并在需要时对这些值同时递增,从而在高并发的情况下性能更好。

ThreadLocal

在线程中包含数据并且不需要锁定的一种方法是使用ThreadLocal存储。从概念上将,ThreadLocal就好像是在每个线程中都有自己版本的变量。ThreadLocal常用来存储只属于线程自己的值,比如当前的事务以及其它资源。而且,它还能用来维护单个线程专有的计数器,统计或是ID生成器。

1
2
3
4
5
6
7
8
9
10
11
12
class TransactionManager {
private final ThreadLocal<Transaction> currentTransaction
= ThreadLocal.withInitial(NullTransaction::new);
Transaction currentTransaction() {
Transaction current = currentTransaction.get();
if (current.isNull()) {
current = new TransactionImpl();
currentTransaction.set(current);
}
return current;
}
}

原文链接: https://dzone.com/refcardz/core-java-concurrency?chapter=1

猫头鹰的深夜翻译:核心JAVA并发一的更多相关文章

  1. 【Java并发核心三】CountDownLatch、CyclicBarrier及Phaser

    个人感觉,看书学习还是需要“不求甚解”,因为一旦太过于计较小的得失,就容易钻牛角尖,学习进度也慢.我们完全可以先学一个大概,等到真正用到的时候再把那些细节丰富起来,就更有针对性. 所以,针对java并 ...

  2. 【java并发核心一】Semaphore 的使用思路

    最近在看一本书<Java并发编程 核心方法与框架>,打算一边学习一边把学习的经验记下来,所粘贴的代码都是我运行过的,大家一起学习,欢迎吐槽. 估计也没多少人看我的博客,哈哈,那么我还是会记 ...

  3. 【转】Java 并发编程:核心理论

    并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可靠的多线程并发程序.本系 ...

  4. Java并发编程核心知识体系精讲

    第1章 开宗明义[不看错过一个亿]本章一连串设问:为什么学并发编程?学并发编程痛点?谁适合学习本课?本课程包含内容和亮点?首先4大个理由告诉你为什么要学,其实源于JD岗位要求就不得不服了.其次5个痛点 ...

  5. (转)Java并发编程:核心理论

    原文链接:https://www.cnblogs.com/paddix/p/5374810.html Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及 ...

  6. Java 并发编程:核心理论(一)

    前言......... 并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密,这样才能写出高效.安全.可 ...

  7. Java并发编程-核心问题(1)

    一.常见问题 从小的方面讲, 并发编程最常见的问题就是可见性.原子性和有序性问题. 从大的方面讲, 并发编程最常见的问题就是安全性问题.活跃性问题和性能问题. 下面主要从微观上分析问题. 二.可见性问 ...

  8. Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

  9. [转]Java并发的四种风味:Thread、Executor、ForkJoin和Actor

    这篇文章讨论了Java应用中并行处理的多种方法.从自己管理Java线程,到各种更好几的解决方法,Executor服务.ForkJoin 框架以及计算中的Actor模型. Java并发编程的4种风格:T ...

随机推荐

  1. 在mybatis框架中,延迟加载与连表查询的差异

    1.引子 mybatis的延迟加载,主要应用于一个实体类中有复杂数据类型的属性,包括一对一和一对多的关系(在xml中用collection.association标签标识).这个种属性往往还对应着另一 ...

  2. 题解【[BJOI2015]树的同构】

    切了省选题+紫题,来写个题解 这题其实挺水,才120行代码 该题写了我一天(上午1.5h,晚上10min = 一天) hash,对于节点A,\[hashval[A] = \{hashval[i]\ti ...

  3. Python基础学习五

    Python基础学习五 迭代 for x in 变量: 其中变量可以是字符串.列表.字典.集合. 当迭代字典时,通过字典的内置函数value()可以迭代出值:通过字典的内置函数items()可以迭代出 ...

  4. dubbo的重试原则

    验证思路.使用超时来验证重试次数 XML 注解

  5. python 并发执行

    并发执行, 精简代码. 适用python2 和python3 # -*- encoding:utf-8 -*- from threading import Thread from multiproce ...

  6. Angular ng-container ng-template 用法

    ng-container本身不创建任何html代码,相当于一个容器. <ng-container *ngFor="let item of dataSource;let i=index& ...

  7. 联想的amd电脑,Debian8.8开机后亮度值始终最大,尝试过各种方法,始终无法解决,最后debian8.8在安装开源驱动后,成功调节

    安装ATI显卡驱动(开源)(方法步骤来自Debian WiKi) A.先升级可用的包 # aptitude upgrade B.安装下面3个包 # apt-get install firmware-l ...

  8. 用hash存数组|得地址|取地址

    #!/usr/bin/perl -w use strict; my %hash = %{&collect};my $arr_ad=$hash{'a'};print "$arr_ad\ ...

  9. HTTP编码

    HTTP编码 不仅仅URL需要编码,HTTP header也需要编码,HTTP body 无特殊要求 一般采用百分号编码:比如一个字节的ascii码值是 0x89 那使用百分号编码之后 输出是 %89 ...

  10. 能够伪装为 win 10 的 kali 体验与中文设置

    前言 作为习惯性捣鼓各类操作系统,时长也会使用 Kali 系统,之前看到有新的版本发行 传闻这个版本和之前的版本在系统界面和壁纸上都做了更新,还能一键设置 win 10 的系统界面 对此决定下载体验一 ...