并发编程-初级之认识并发编程

1.并发领域可以处理的问题

  1. 分工
  2. 同步:分好工之后,就可以具体执行。任务之间是有依赖的,一个任务结束之后将去去通知后续的任务。java里面 Executor、Fork/Join、Future本质上都
    是分工方法,但同时也能解决线程协作的问题。

例如,用 Future 可以发起一个异步调用,主线程通过 get() 方法取结果时,主线程就会等待,当异步执行的结果返回时,get() 方法就自动返回了。
我之前用的是CountDownLatch,先创建多个线程实现分工,再用CountDownLatch来一起协

  1. 互斥:这里要说的就是线程安全的问题。导致这种多个线程访问一个变量的时候的不确定性的源头是由于可见性问题,有序性问题,原子性问题
    而当我们讲到互斥其实我们是在说***同一时刻,只允许一个线程访问共享 变量***。
    而实现互斥的核心就是锁。

2.可见性,原子性和有序性问题:并发编程bug源头

1.什么是可见性

一个线程对共享变量的修改另一个线程能立刻看到,我们称为可见性。

1.1 缓存问题导致的可见性问题
1.2 线程切换带来的原子性问题

1.3 编译优化带来的有序性问题

3.Java内存模型:java如何解决可见性和有序性问题

解决这种问题合理方案:按需禁用缓存,编译优化

1.按需禁用缓存

1.1 volatile关键字

volatile 关键字并不是 Java 语言的特产,古老的 C 语言里也有,它最原始的意义就是禁用
CPU 缓存。

在java 1.5后 对volite使用Happens-Before增强。

Happens-Before规则(java-可见性)

大意:前一个操作的结果对后续的操作是可见的。
Happens-Before 约束了
编译器的优化行为,虽允许编译器优化,但是要求编译器优化后一定遵守 HappensBefore 规则

一共有六项

1.程序的顺序性规则

1 // 以下代码来源于【参考 1】 2 class VolatileExample {3 int x = 0;
4 volatile boolean v = false;
5 public void writer() {6 x = 42;
7 v = true;
8 }
9 public void reader() {10 if (v == true) {11 // 这里 x 会是多少呢?42// 1.5之前这里x=0
12 }
13 }
14 }

2.volatile规则

3.传递性:这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A HappensBefore C。

从图中,我们可以看到:

1. “x=42” Happens-Before 写变量 “v=true” ,这是规则 1 的内容;

2. 写变量“v=true” Happens-Before 读变量 “v=true”,这是规则 2 的内容 。
再根据这个传递性规则,我们得到结果:“x=42” Happens-Before 读变
量“v=true”。这意味着什么呢?
如果线程 B 读到了“v=true”,那么线程 A 设置的“x=42”对线程 B 是可见的。也就是
说,线程 B 能看到 “x == 42” ,有没有一种恍然大悟的感觉?这就是 1.5 版本对
volatile 语义的增强,这个增强意义重大,1.5 版本的并发工具包(java.util.concurrent)

ps:这个规则也解释了为什么之前我们看到的x是等于42而不是等于0
4.管程中锁的规则

管程:
一种通用的同步原语,在
Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

1 synchronized (this) { // 此处自动加锁
2 // x 是共享变量, 初始值 =10
3 if (this.x < 12) {4 this.x = 12;
5 }
6 } // 此处自动解锁

所以结合规则 4——管程中锁的规则,可以这样理解:假设 x 的初始值是 10,线程 A 执行
完代码块后 x 的值会变成 12(执行完自动释放锁),线程 B 进入代码块时,能够看到线程
A 对 x 的写操作,也就是线程 B 能够看到 x==12。这个也是符合我们直觉的,应该不难理
解。

5.线程start()规则

这条是关于线程启动的。它是指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启
动子线程 B 前的操作。

1 Thread B = new Thread(()->{2 // 主线程调用 B.start() 之前
3 // 所有对共享变量的修改,此处皆可见
4 // 此例中,var==77
5 });
6 // 此处对共享变量 var 修改
7 var = 77;
8 // 主线程启动子线程
9 B.start();

6.线程join原则

这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B
的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看
到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意
操作 Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。

1 Thread B = new Thread(()->{2 // 此处对共享变量 var 修改
3 var = 66;
4 });
5 // 例如此处对共享变量修改,
6 // 则这个修改结果对线程 B 可见
7 // 主线程启动子线程
8 B.start();
9 B.join()
10 // 子线程所有对共享变量的修改
11 // 在主线程调用 B.join() 之后皆可见
12 // 此例中,var==66

所以以前我们的单例模式,其实会导致空指针异常 因为New对象是三步 - 1.开内存 2.堆上初始化一个对象 3.在吧地址给那个栈 但是java会进行优化 2,3步会交换 所以 很多线程访问的时候 可能会拿到instance!= null 然后他们就拿着这个对象走了 但其实这个对象是没有内存的 会出现空指针异常

public class DoubleCheckLock {private static Instance instance;public static Instance getInstance(){// 第一次检查if(instance==null){// 第一次检查为null再进行加锁,降低同步带来的性能开销synchronized (DoubleCheckLock.class){// 第二次检查if(instance==null){// 问题出在此处instance=new Instance();}}}return instance;}
}

所以解决方案就是 Instance 加上volatile关键字 防止java的优化

final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化。


4.互斥锁

解决原子性问题

本质:互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定减少持有锁的时间

最原始粗暴的锁模型

在上面的锁在我们看来是非常简单粗暴的。在现实世界里,锁和要保护的资源是有对应关系的,比如我家的锁锁我家的门。
所以引进了下面一种模型

首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的
资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们
还需在进出临界区时添上加锁操作和解锁操作。另外,在锁 LR 和受保护资源之间,我特地
用一条线做了关联,这个关联关系非常重要。很多并发 Bug 的出现都是因为把它忽略了,
然后就出现了类似锁自家门来保护他家资产的事情,这样的 Bug 非常不好诊断,因为潜意
识里我们认为已经正确加锁了。

图画出来了,但是这个图在java里面是怎样实现呢?
我们接着往下面看。

1.synchronized :可以用来修饰方法和代码块

1  class X {2 // 修饰非静态方法
3 synchronized void foo() {4 // 临界区
5 }
6 // 修饰静态方法
7 synchronized static void bar() {8 // 临界区
9 }
10 // 修饰代码块
11 Object obj = new Object();
12 void baz() {13 synchronized(obj) {14 // 临界区
15 }
16 }
17 }

用synchronized可以自动lock()和unLock(),而在修饰代码块的时候,锁定了一个Obj,那他修饰方法呢?

当修饰静态方法的时候,锁定的是当前类的 Class 对象,在上面的例子中就
是 Class X;
当修饰非静态方法的时候,锁定的是当前实例对象 this。

锁和受保护资源的关系

资源->锁 多->一

ps:当多个锁锁住同一个资源(方法或者是什么)那么就会线程不安全

如何用一把锁保护多个资源

1.保护没有关联关系的多个资源 -上不同的锁
2.保护有关联关系的多个资源

评估性能的指标:
1.吞吐量
2.延迟
3.并发量:

评估性能的指标

吞吐量 延迟 并发量
单位时间内能处理的请求数量 发出请求到收到响应的时间 同时处理的请求数量

5.管程

1.什么是管程
管程,对应的英文是 Monitor,很多 Java 领域的同学都喜欢将其翻译成“监视器”,这是
直译。操作系统领域一般都翻译成“管程”,这个是意译,而我自己也更倾向于使用“管
程”。
是Java并发使用的技术,而其体现在synchronized 关
键字及 wait()、notify()、notifyAll()。

2.作用: 指的是管理共享变量以及对共享变量的操作过程,让他们支持并发

3.广泛使用的管程模型:MESA

3.1 互斥:
管程解决互斥问题的思路很简单,就是将共享变量及其对共享变量的操作统一封装起来。在
下图中,管程 X 将共享变量 queue 这个队列和相关的操作入队 enq()、出队 deq() 都封装
起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、
deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。
在管程模型里,对于共享变量的操作是被封装的。图中最外层的框就代表封装。框的上面只有一个入口,并且在入口旁边有一个等待队列,当多个线程同时进入时。只允许一个线程进入。其他则在队伍中等待。

条件变量
每个条件变量对应一个条件变量等待队列。比如说有一个条件变量 A,当执行线程 T1 时发现不满足条件变量 A,T1 就会进入条件变量 A 的等待队列中。就像去看医生,医生让你先去排个 X 光,就要去拍 X 光的地方排队。

public class BlockedQueue<T>{final Lock lock =new ReentrantLock();// 条件变量:队列不满  final Condition notFull =lock.newCondition();// 条件变量:队列不空  final Condition notEmpty =lock.newCondition();// 入队void enq(T x) {lock.lock();try {while (队列已满){// 等待队列不满 notFull.await();}  // 省略入队操作...//入队后,通知可出队notEmpty.signal();}finally {lock.unlock();}}// 出队void deq(){lock.lock();try {while (队列已空){// 等待队列不空notEmpty.await();}// 省略出队操作...//出队后,通知可入队notFull.signal();}finally {lock.unlock();}  }
}

3.2 synchronized 单条件变量的管程模型
Java内置的管程方案(synchronized)只支持一个条件变量
而如果要支持多个 可以引用Java 的Sdk


6.线程的生命周期

  1. NEW(初始化状态)
  2. RUNNABLE(可运行 / 运行状态)
  3. BLOCKED(阻塞状态)
  4. WAITING(无时限等待)
  5. TIMED_WAITING(有时限等待)
  6. TERMINATED(终止状态)

只要线程处于BLOCKED、WAITING、TIMED_WAITING那么这个线程就永远没有 CPU 的使用权,这个时候被如果被Interrute()就会报异常

如何从New切换到 WAITING
调用start()

如何从Runnable切换到 WAITING

  1. Object.wait()
  2. Thread.join()
  3. LockSupport.park()

如何从Runnable切换到 TIMED_WAITING

1.调用带超时参数的 Thread.sleep(long millis) 方法;
2.带超时参数的 Object.wait(long timeout)
3.带超时参数的 Thread.join(long millis

如何从Runnable切换到 WAITING
1.程序执行完
2.stop() 超级不建议使用
3.interrupt() 正确使用

TIMED_WAITING 和 WAITING 状态的区别,仅仅是触发条件多了超时参数

7.使用线程的正确姿势 降低延迟,提高吞吐量

7.1 创建最优数量线程

单核CPU
I/O 密集型计算 1 +(I/O 耗时 / CPU 耗时)

CPU 密集型计算 CPU 核数 +1
多核CPU
CPU 核数 * [ 1 +(I/O 耗时 / CPU 耗时)]

8.局部变量的安全性

局部变量是安全的,因为他只存在于栈中,且每个方法都有自己的调用栈。

线程封闭:方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个响当当的名字叫做线程封闭,比较官方的解释是:仅在单线程内访问数据。

比如数据库连接Conn,因为JDBC规范并没有要求必须是线程安全的,数据库连接池通过线程封闭技术,保证一个Conn一旦被一个线程获取后,在这个线程关闭之前不会把这个Con分给其他线程

9.Java面向对象思想与并发编程的融合之旅

9.1 封装共享变量

对于这些不会发生变化的共享变量,建议你用 final 关键字来

9.2 注意If判断

在我们的指定并发访问策略是利用原子类。所以我们要特别注竞态条件判断
反应在代码里面


public class SafeWM {// 库存上限private final AtomicLong upper =new AtomicLong(0);// 库存下限private final AtomicLong lower =new AtomicLong(0);// 设置库存上限void setUpper(long v){// 检查参数合法性if (v < lower.get()) {throw new IllegalArgumentException();}upper.set(v);}// 设置库存下限void setLower(long v){// 检查参数合法性if (v > upper.get()) {throw new IllegalArgumentException();}lower.set(v);}// 省略其他业务代码
}

在这个类中,我们用了原子类AtomicLong来保证我们存取的值的安全性。
我们假设库存的下限和上限分别是 (2,10),线程 A 调用 setUpper(5) 将上限设置为 5,线
程 B 调用 setLower(7) 将下限设置为 7,如果线程 A 和线程 B 完全同时执行,你会发现线
程 A 能够通过参数校验,因为这个时候,下限还没有被线程 B 设置,还是 2,而 5>2;线
程 B 也能够通过参数校验,因为这个时候,上限还没有被线程 A 设置,还是 10,而
7<10。当线程 A 和线程 B 都通过参数校验后,就把库存的下限和上限设置成 (7, 5) 了,显
然此时的结果是不符合库存下限要小于库存上限这个约束条件的。
这个时候光只用原子类是不行的。

9.4 制定并发访问策略

三个方案

1.避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。
我的理解可能就是ThreadLocal

2.不变模式:java运用少 不解释

3.管程及其他同步工具:java并发包

三原则

1.优先使用成熟的工具类,而不是自己造轮子

2.迫不得已才使用低级的同步原理
这里主要指的是 synchronized、Lock、
Semaphore 等,这些虽然感觉简单,但实际上并没那么简单,一定要小心使用。

3.避免过早优化:并发程序首先要保证安全,出现性能瓶颈时再优化


总结

思考题:

1.下面的代码用 synchronized 修饰代码块来尝试解决并发问题,你觉得这个使用方式正确
吗?有哪些问题呢?能解决可见性和原子性问题吗?

class SafeCalc {long value = 0L;long get() {synchronized (new Object()) {return value;}}void addOne() {synchronized (new Object()) {value += 1;}}
}

我的答案:不正确。图中的资源是Value,但是在两个方法中用了不同的锁去请求方法,所以会出现线程不安全。

官方答案:,每次调用方法 get()、addOne() 都创建了不同的锁,相当于无锁。这里需要
你再次加深一下记忆,“一个合理的受保护资源与锁之间的关联关系应该是 N:1”。只有
共享一把锁才能起到互斥的作用。

2.加粗:这个问题我错了

class Account {// 账户余额 private Integer balance;// 账户密码private String password;// 取款void withdraw(Integer amt) {synchronized(balance) {if (this.balance > amt){this.balance -= amt;}}} // 更改密码void updatePassword(String pw){synchronized(password) {this.password = pw;}}
}

我的答案:这个对象多个线程用的如果是同一个,那么就可行。

官方答案错!!!!
1.锁可能会变
2.Interger和Sting类型不适合做锁,因为他们在JVM可能被重用。
那么你的锁可能别其他代码使用,如果其他代码 synchronized(你的锁),而且不释放,那你的程序就永远拿不
到锁,这是隐藏的风险

通过这两个反例,我们可以总结出这样一个基本的原则:锁,应是私有的、不可变的、不可
重用的。我们经常看到别人家的锁,都长成下面示例代码这样

// 普通对象锁
private final Object lock = new Object();
// 静态对象锁
private static final Objectlock = new Object();

撒花~~~~

并发编程-初级之认识并发编程相关推荐

  1. 实验4 MapReduce编程初级实践【java编程实现】

    1.编程实现文件合并和去重操作 对于两个输入文件,即文件A和文件B,请编写MapReduce程序,对两个文件进行合并,并剔除其中重复的内容,得到一个新的输出文件C.下面是输入文件和输出文件的一个样例供 ...

  2. 《Java并发编程入门与高并发面试》or 《Java并发编程与高并发解决方案》笔记

    <Java并发编程入门与高并发面试>or <Java并发编程与高并发解决方案>笔记 参考文章: (1)<Java并发编程入门与高并发面试>or <Java并发 ...

  3. 多线程编程:阻塞、并发队列的使用总结

    最近,一直在跟设计的任务调度模块周旋,目前终于完成了第一阶段的调试.今天,我想借助博客园平台把最近在设计过程中,使用队列和集合的一些基础知识给大家总结一下,方便大家以后直接copy.本文都是一些没有技 ...

  4. 学习ASP.NET Core Razor 编程系列十八——并发解决方案

    原文:学习ASP.NET Core Razor 编程系列十八--并发解决方案 学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP. ...

  5. java 并发模型总类_java并发编程系列-内存模型基础

    java线程之间的通信对程序开发人员是完全透明的,内存的可见性问题很容易困扰很多开发人员.本篇博文将揭开java内存模型的神秘面纱,来看看内存模型到底是怎样的. 并发编程模型的分类 并发编程中需要处理 ...

  6. java并发编程代码示例_java并发编程之同步器代码示例

    java并发编程之同步器代码示例 发布时间:2020-09-08 16:53:41 来源:脚本之家 阅读:58 作者:Blessing_H 同步器是一些使线程能够等待另一个线程的对象,允许它们协调动作 ...

  7. 多线程编程学习笔记——使用并发集合(三)

    接上文 多线程编程学习笔记--使用并发集合(一) 接上文 多线程编程学习笔记--使用并发集合(二) 四.   使用ConcurrentBag创建一个可扩展的爬虫 本示例在多个独立的即可生产任务又可消费 ...

  8. solr 高并发_你真的了解并发编程吗?

    ​并发编程是提升程序性能的有效手段.不过,你是否真的了解并发编程...... 1.并发编程 Bug 的根源是什么? 2.volatile 实质上是解决什么问题? 3.什么是Happens-Before ...

  9. Java并发编程的艺术-Java并发编程基础

    第4章 Java并发编程基础 ​ Java从诞生开始就明智地选择了内置对多线程的支持,这使得Java语言相比同一时期的其他语言具有明显的优势.线程作为操作系统调度的最小单元,多个线程能够同时执行,这将 ...

最新文章

  1. 构建之法阅读笔记02
  2. 昆虫大脑帮助AI解决导航难题
  3. P3385 【模板】负环
  4. 蓝桥杯java第八届第七题--日期问题
  5. 「后端小伙伴来学前端了」Vue中Props 实现组件通信TodoList案例
  6. UVA 10600 ACM Contest and Blackout (次小生成树)
  7. thymeleaf模板引擎shiro集成框架
  8. java打印输出 怎么隔开_java日志分开打印
  9. 新闻媒体是怎样使用计算机的,计算机技术在新闻上的应用
  10. 设计模式7大结构型模式
  11. 各种边缘检测算子特点比较(canny)
  12. SQL Server和Oracle数据库索引介绍
  13. MTK camera驱动浅析(1)
  14. Android开发+Java基础视频教程——第一讲
  15. 浙江计算机三级在线试题及答案,计算机三级试题及答案
  16. cad有没有网页版_AutoCAD
  17. VC模拟鼠标的两种方式(SendMessage、mouse_event)
  18. 360安全软件设置白名单
  19. 上海计算机一级和四六级,上海市关于英语四六级考试的改革通知
  20. H3C路由器多出口NQA+TRACK实现冗余

热门文章

  1. gaussian用法 matlab_matlab中的twomodegauss函数-双峰高斯函数
  2. JAVA强制类型转换总结
  3. JDK软件安装+环境变量配置图文详解(Win10环境)
  4. Pytorch中nn.Module中的self.register_buffer解释
  5. mysql: “Warning: Using a password on the command line interface can be insecure.“ 解决方法
  6. 计算机公式与函数试题,计算机国考样题EXCEL之公式与函数的应用一
  7. sublime text3--js智能提示插件以及其他常用插件
  8. 《卸甲笔记》-单行函数对比之二
  9. GitLab-CI 基础介绍
  10. 大众创业热度不减,好机友项目强势来袭