并发编程实战(一)

并发编程的三个核心问题:

  1. 分工 : 高效的拆解任务分给线程
  2. 同步 : 线程之间的协作
  3. 互斥 : 保证同一时刻只允许一个线程访问共享资源

这个其实不难理解,做个简单的比喻,我们团队做一个项目的时候肯定是先分配任务(分工),然后等到任务完成进行合并对接(同步),在开发过程中,使用版本控制工具访问,一个代码只能被一个人修改,否则会报错,需要meger(互斥).

学习攻略:

  1. 跳出来,看全景
  2. 钻进去,看本质

核心: 分工(拆分) - 同步(一个线程执行完成如何通知后续任务的线程开始工作) - 互斥(同一时刻,只允许一个线程访问共享变量)

全景:
全景

本质 : 知其然知其所以然,有理论做基础.技术的本质是背后的理论模型

并发编程为啥好难?

我从我的角度看,一个是并发编程的API不是很了解,第二个就是出现了问题不会解决,如果说还有,那就是是在不知道并发编程是用来干啥的?有什么用?

每一中技术的出现都有他出现的必然性,对于并发来说无疑是提高性能,那单线程为啥就不能提高性能,原因就在于CPU,内存和IO设备三者的速度差异太大,举个例子来说: CPU一天,内存一年,IO一百年; 而木桶理论告诉我们程序的性能是由短板决定,所以只要合理的平衡三者的速度差异,就可以提高性能.

并发编程问题的源头
  1. 缓存导致的可见性: 对于单CPU来说,缓存是可见的,也就是说多个线程同时操作,CPU会从内存读取数据,线程更新数据到CPU,CPU写入内存,线程和CPU进行交互,这个操作每个线程之间是可见的.
    但是对于多CPU来说,多个线程操作不同的CPU,不同的CPU操作同一个内存,这会导致操作的不可见性,就出现了问题.(说下可见性的概念: 一个线程对共享变量的修改,另一个线程能够立刻看到,这就是可见性)
  2. 线程切换带来的原子性问题: 原子性是一个或多个操作在CPU执行的过程中不被中断的特性. 那为什么会中断呢?原因就在于提高性能,就和现在的计算机一样,是分时间片来进行任务切换,同时听歌和敲代码,看似是同时发生,其实不是,知识任务之间切换的非常快,做到了看似同时进行.
    在高级程序中,一个看似简单的操作可能需要多条CPU指令来完成,不如说count += 1;CPU指令至少三个,从内存中拿到count值到寄存器,在寄存器中进行加一操作,将结果写入内存,这个过程中可能会发生任务间的切换,比如说另一个线程在写入内存前有进行了一次++操作,这个时候结果就不是想要的结果了,可能例子不合适,但是这个意思就是这个. 而原子性就是保证高级语言层面保证操作的原子性.
  3. 编译优化的有序性问题: 有序性指的是程序按照代码的先后顺序执行. 看起来没问题,本来就应该这样,其实不然,在JVM的知识中有一个叫重排序,就是编译器为了优化性能,有时会改变程序中语句的先后顺序,大部分情况下编译器调整后的顺序是不会影响程序的最终结果,不过也有特殊情况,如下:
public class Singleton {static Singleton instance;static Singleton getInstance(){if (instance == null) {synchronized(Singleton.class) {if (instance == null)instance = new Singleton();}}return instance;}
}

上面是经典的双重检查创建单例对象,在我们的印象中new的操作应该是: 分配内存,在内存上初始化对象,地址赋值. 实际上优化后是: 分配内存,地址赋值,初始化. 优化后的顺序就会出现问题,地址赋值后发生了线程切换,这时候其他线程读取到了对象不为null,但是实际上只有地址,这个时候访问成员变量就会出现空指针异常,这个就是编译优化可能会出现的问题.

也就是说,很多的并发Bug是由可见性,原子性,有序性的原理造成的,从这三个方面去考虑,可以理解诊断很大部分一部分Bug. 缓存导致可见性问题,线程切换带来的原子性,编译优化带来的有序性,本质都是提高程序性能,但是在带来性能的时候可能也会出现其他问题,所以在运用一项技术的时候一定要清楚它带来的问题是什么,以及如何实现.

Java内存模型: 解决可见性和有序性问题

可见性的原因是缓存,有序性的原因是编译优化,那解决的最直接的办法就是禁用缓存和编译优化,但是有缓存和编译优化的目的是提高程序性能,禁用了程序的性能如何保证? 合理的方案是按需禁用缓存和编译优化,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法,具体的,这些方法包括volatile,synchronized和final三个关键字,以及六项Happens-Before规则

volatile的困惑

volatile关键字用来声明变量,告诉编译器这个变量的读写不能使用CPU缓存,必须从内存中读写.

// 以下代码来源于【参考 1】
class VolatileExample {int x = 0;volatile boolean v = false;public void writer() {x = 42;v = true;}public void reader() {if (v == true) {// 这里 x 会是多少呢?}}
}

上面的代码x的值是多少呢?直觉上应该是42,但是在jdk1.5之前,可能的值是0或者42,1.5之后就是42,为什么?原因是变量x可能被CPU缓存而导致可见性问题,也就是x=42可能不被v=true可见,那Java的内存模型在1.5版本之后是如何解决的呢? 就是Happens-before规则.

Happens-Before规则

Happens-before指的是前一个操作的结果对后续操作是可见的,具体如下.

1. 程序的顺序性规则

这个规则说的是在一个线程中,按照程序顺序,前面的操作Happens-Before于后续的任意操作. 简单理解就是: 程序前面对于某个变量的修改一定是对后续操作可见的.也就是前面的代码x=42对于v=true是可见的.

2. volatile变量规则

这条规则指的是对一个volatile变量的写操作,Happens-Before于后续对这个volatile变量的读操作,即volatile变量的写操作对于读操作是可见的.

3. 传递性

这条规则指的是A Happens-Before C,且B Happens-Before C,那么A Happens-Before C,如下图:
传递性

这样就很明显了,x=42 Happens-Before v=true,写v=true Happens-Before 读v=true,那也就是说x=42 Happens Before 读v=true,这样下来,其他线程就可以看到x=42这个操作了.

4. 管程中锁的规则

这个规则是指对一个锁的解锁Happens-Before与后续对这个锁的加锁. 管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现.管程中的锁在Java中是隐式实现的,也就是进入同步块之前,会自动加锁,而在代码块执行完后自动释放锁,加锁以及解锁都是编译器帮我们实现的.

synchronized (this) { // 此处自动加锁// x 是共享变量, 初始值 =10if (this.x < 12) {this.x = 12; }  
} // 此处自动解锁
5. 线程start()规则

这个是线程启动的,指的是主线程A启动子线程B,子线程B能够看到主线程在启动子线程B前的操作.

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

这条规则是关于线程等待的.它是指主席爱能成A通过调用子线程B的join方法,子线程B执行完成之后,主线程可以看到子线程中的操作.这里指的是对共享变量的操作.

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

final修饰变量是告诉编译器: 这个变量生而不变,可以可劲儿优化.在 1.5 以后 Java 内存模型对 final 类型变量的重排进行了约束。现在只要我们提供正确构造函数没有“逸出”,就不会出问题了。下面的例子,在构造函数里将this赋值给全局变量global.obj,这就是逸出(逸出就是对象还没有构造完成,就被发布出去),线程global.obj读取到x有可能读到0.

// 以下代码来源于【参考 1】
final int x;
// 错误的构造函数
public FinalFieldExample() { x = 3;y = 4;// 此处就是讲 this 逸出,global.obj = this;
}

在 Java 语言里面,Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。

互斥锁: 解决原子性问题

前面看了Java的内存模型,解决了可见性和编译优化的重排序问题,哪还有一个原子性如何解决?答案就是使用互斥锁实现.

先探究源头,long在32位机器上操作可能出现Bug,原因是线程的切换,那只要保证同一时刻只有一个线程执行,就可以了,这就是互斥.

互斥锁模型:
互斥

Java中如何实现这种互斥锁呢?

Java语言提供的锁技术: synchronized

java中的synchronized关键字就是锁的一种实现,synchronized关键字可以用来修饰方法,也可以用来修饰代码块,如下:

class X {// 修饰非静态方法synchronized void foo() {// 临界区}// 修饰静态方法synchronized static void bar() {// 临界区}// 修饰代码块Object obj = new Object();void baz() {synchronized(obj) {// 临界区}}
}  

先说一下那个加锁和释放锁,synchronized并没有显示的进行这一操作,而是编译器会在synchronized修饰的方法或代码块前后自动加锁lock()和解锁unlock(),不需要编程人员手动加锁和释放锁(省的忘记,程序员很忙的).

synchronized锁的规则是什么: 当修饰静态方法的时候,锁定的是当前的类对象. 修饰非静态方法和代码块的时候,锁定的是当前的对象this.如下

class X {// 修饰静态方法synchronized(X.class) static void bar() {// 临界区}
}class X {// 修饰非静态方法synchronized(this) void foo() {// 临界区}
}
案例深入理解:

下面的代码可以解决多线程问题吗?

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

答案是并不可以,原因是虽然对addOne进行了加锁操作(对一个锁的解锁Happens-Before于后续对这个锁的加锁),保证了后续addOne的操作的共享变量是可以看到前面addOne操作后的共享变量的值,但是get方法却没有,多个线程get方法可能获取到的值相同,addOne()之后就会乱套,所以并不能解决.那下面的代码可以解决问题吗?

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

这种是可以解决多线程问题,也就是可以解决多个线程操作同一个对象的并发问题.那如果要解决多个线程操作不同对象的并发问题呢?

锁和受保护资源的关系

受保护资源和锁之间的关联关系是N:1的关系.也就是说一个锁可以保护多个受保护的资源,这个就是现实生活中的包场,但是我觉得这个也要分情况,多个受保护的资源和锁之间一定要有关系,不然锁不起作用就麻烦了,举个例子来说就是自己家门的锁肯定保护自己东西,不能用自己家门的锁去保护别人家的东西.

下面的例子:

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

分析如图:
static lock

所以说addOne对value的修改对临界区get()没有可见性保证,会导致并发问题.将get方法也改为静态的就可以解决了.

synchronized 是 Java 在语言层面提供的互斥原语,其实 Java 里面还有很多其他类型的锁,但作为互斥锁,原理都是相通的:锁,一定有一个要锁定的对象,至于这个锁定的对象要保护的资源以及在哪里加锁 / 解锁,就属于设计层面的事情了。

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

受保护的资源和锁之间合理的关联关系应该是N:1的关系.使用一把锁保护多个资源也是分情况的,在于多个资源之间存不存在关系,这是要分情况讨论的.

保护没有关联关系的多个资源

举个例子来说明,Account类有两个成员变量,分别是账户余额balance和账户密码password. 取款和查看余额会访问balance,创建一个final对象balLock来作为balance的锁;更改密码和查看密码会操作password,创建一个final对象pwLock来作为password的锁.不同的资源用不同的锁保护.代码示例如下:

class Account {// 锁:保护账户余额private final Object balLock= new Object();// 账户余额  private Integer balance;// 锁:保护账户密码private final Object pwLock= new Object();// 账户密码private String password;// 取款void withdraw(Integer amt) {synchronized(balLock) {if (this.balance > amt){this.balance -= amt;}}} // 查看余额Integer getBalance() {synchronized(balLock) {return balance;}}// 更改密码void updatePassword(String pw){synchronized(pwLock) {this.password = pw;}} // 查看密码String getPassword() {synchronized(pwLock) {return password;}}
}

那还有没有其他的解决方案? 可以使用this来进行加锁,但是这种情况性能会很差,因为password和balance使用同一把锁,操作也就串行了,使用两把锁,password和balance的操作是可以并行的,用不同的锁对受保护资源进行精细化关系,能够提升性能.这个叫细粒度锁

保护有关联关系的多个资源

如果多个资源之间有关联关系,那就比较复杂,经典的转账问题.看下面代码可能发生并发问题吗?

class Account {private int balance;// 转账synchronized void transfer(Account target, int amt){if (this.balance > amt) {this.balance -= amt;target.balance += amt;}} 
}

开起来没问题,其实不然,只对当前对象进行了加锁,那目标对象的访问呢?也就是说当前的对象是无法保护target.balance的.
unsafe

上面的案例两个人之间的转账或许没有问题,但是涉及三个人呢?
example

这个时候B的余额可能为100,也可能为300,看哪个执行在后了.那应该如何解决这种有关联的资源呢,找公共的锁就可以,也就是要锁能覆盖所有受保护资源,解决方案其实不少,如下

class Account {private Object lock;private int balance;private Account();// 创建 Account 时传入同一个 lock 对象public Account(Object lock) {this.lock = lock;} // 转账void transfer(Account target, int amt){// 此处检查所有对象共享的锁synchronized(lock) {if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}}
}

这个解决方案缺点在于需要传入共享的lock,还有一种方案

class Account {private int balance;// 转账void transfer(Account target, int amt){synchronized(Account.class) {if (this.balance > amt) {this.balance -= amt;target.balance += amt;}}} 
}

这个是不是很简单.
safe

上图展示了如何使用共享的锁来保护不同对象的临界区.

解决原子性问题,是要保证中间状态对外不可见.

转载于:https://www.cnblogs.com/wadmwz/p/10504164.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/264590.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java pdf打印_Java 打印PDF文档

本文将介绍如何在Java应用程序中打印PDF文档。一般有以下三种常见打印方式&#xff1a;静默打印显示打印对话框打印打印PDF时自定义纸张大小使用工具&#xff1a;Free Spire.PDF for Java创建运行环境&#xff1a;方式 1&#xff1a;通过官网下载组件&#xff0c;解压后将lib文…

微信支付配置参数:支付授权目录、回调支付URL

一、开通微信支付的首要条件是&#xff1a;认证服务号或政府媒体类认证订阅号&#xff08;一般认证订阅号无法申请微信支付&#xff09;二、微信支付分为老版支付和新版支付&#xff0c;除了较早期申请的用户为老版支付&#xff0c;现均为新版微信支付。三、公众平台微信支付开…

[导入]关于阶乘的两个常见算法及一个相关面试题

摘要: 本文首先给出了阶乘的数学定义&#xff0c;然后说明了它的两种简单算法&#xff0c;最后提供了一个很有意思的与阶乘相关的面试题的思路。 阅读全文Anders Cui 2008-05-19 12:12 发表评论文章来源:http://www.cnblogs.com/anderslly/archive/2008/05/19/factorial-algor…

autoresetevent java_[原创]AutoResetEvent, ManualResetEvent的Java模拟

AutoResetEvent, ManualResetEvent是C#中常用的线程同步方法&#xff0c;在Java中可以模拟&#xff0c;AutoResetEvent使用Semaphore&#xff0c;增加的是许可证数量&#xff0c;程序里只有一个许可证&#xff0c;那么当这个许可被使用后&#xff0c;就会自动锁定。相反&#x…

用Jfree实现条形柱状图表,java代码实现

用Jfree实现条形柱状图表&#xff0c;java代码实现。可经经常使用于报表的制作&#xff0c;代码自己主动生成后能够自由查看。能够自由配置图表的各个属性&#xff0c;用来达到自己的要求和目的package test1;import org.jfree.chart.*; import org.jfree.chart.plot.*; import…

bzoj 2160: 拉拉队排练

Description 艾利斯顿商学院篮球队要参加一年一度的市篮球比赛了。拉拉队是篮球比赛的一个看点&#xff0c;好的拉拉队往往能帮助球队增加士气&#xff0c;赢得最终的比赛。所以作为拉拉队队长的楚雨荨同学知道&#xff0c;帮助篮球队训练好拉拉队有多么的重要。拉拉队的选拔工…

java long double精度丢失_long long类型转double类型部分精度丢失问题

我最近做了一道题&#xff0c;一个64位(unsigned __int64)范围内的数输出其除以1000的值&#xff0c;并按四舍五入保留小数点后三位。我刚开始直接写WA&#xff0c;结果发现当数比较大的时候&#xff0c;结果后几位精度总会丢失&#xff0c;只好手动模拟了一个&#xff0c;水过…

从服务器上自动更新系统补丁

对于经常重系统的用户或公司,每次安装系统后,必须得从微软网站上面下载补丁,这样很浪费时间. 如何从自己服务器上自动更新系统补丁,方法如下: 服务器端 服务器端需要安装一个更新服务器软件,如:SUS (下载地址http://www.onlinedown.net/soft/35844.htm) 客户端设置 开始 -- 运行…

搭建于 Cubieboard 之上的超小型实时监控平台 - mjpg篇

2019独角兽企业重金招聘Python工程师标准>>> 运行于 Cubieboard开发板 之上的个人笔记博客 http://cb.e-fly.org:81/archives/system-camera-monitor-mjpg-streamer.html 原文作者&#xff1a;Cannikin原文链接&#xff1a;http://forum.cubietech.com/forum ... p…

架构-浅谈MySQL数据库优化

主从复制博文&#xff1a;http://lizhenliang.blog.51cto.com/7876557/1290431 读写分离博文&#xff1a;http://lizhenliang.blog.51cto.com/7876557/1305083 MySQL-MMM博文&#xff1a;http://lizhenliang.blog.51cto.com/7876557/1354576 &#xff08;一&#xff09;数据库部…

java base64解码出错_Java Base64解码错误及解决方法

问题提出&#xff1a;自己在做一个小网站充当练手&#xff0c;但是前端图片经过base64加密后传往后端在解码。但是一直都有问题&#xff0c;请大神赐教public static string base64toimg(string src) throws ioexception {string uuid uuid.randomuuid().tostring();stringbui…

PPT图片内嵌文字效果

【摘要】在报纸杂志上我们经常看到&#xff0c;有些图片中可以嵌入文字&#xff0c;如下图所示的效果&#xff1a;今天我们一起来学习一下这种效果是怎样生成的。 【正文】以下的操作步骤为PowerPoint 2013版本。 一 插入图片并编辑图片在【插入-联机图片】中搜索需要的图片。…

[天地君亲若追问 枉为知音百年羞]2008.06.07 晃荡在芳华

洞房悄悄静悠悠&#xff0c;花烛高烧暖心头&#xff0c; 喜气阵阵难抑止&#xff0c;这姻缘百折千磨方成啊就。 三月来&#xff0c;屡托刘兄把亲求&#xff0c;每遭见拒愿难酬&#xff0c; 从此我诗书五经无心看&#xff0c;三餐茶饭懒下喉&#xff0c; 日卧书斋愁脉脉&#xf…

基于mykernel完成多进程的简单内核

学号351 原创作品转载请注明出处 https://github.com/mengning/linuxkernel/ mykernel简介 mykernel是由孟宁老师建立的一个用于开发您自己的操作系统内核的平台&#xff0c;基于Linux Kernel 3.9.4 source code mykernel的源代码 https://github.com/mengning/mykernel &…

iOS开发:通过经纬度获得城市、省份等信息

iOS系统自带定位&#xff0c;用CLLocationManager就可以轻松的实现定位的操作&#xff0c;获得的是一组经纬度&#xff0c;当然&#xff0c;也可以根据给出的经纬度获取相应的省份、城市、街道等信息&#xff0c;下面就看一个根据经纬度获得城市的demo&#xff1a;因为获取经纬…

.net内存回收与Dispose﹐Close﹐Finalize方法

一. net的对象使用一般分为三种情况﹕ 1.创建对象 2.使用对象 3.释放对象 二.创建对象 1.创建对象实际分为两个步骤﹕变量类型宣告和初始化对象 2.变量类型宣告(declare),如﹕ FileStream fs这行代码会在当前的变量作用域空间(栈或堆)里建立一个叫做fs的变量﹐至少四个字节吧(因…

19、Qt线程(四):继承QRunnable类

一、功能说明 1、通过继承QRunnable&#xff0c;重写run的方式实现多线程 2、点击“开始”按钮启动子线程&#xff1b; 二、项目创建 1、新建Qt Widgets Application应用&#xff0c;名称为Runnable&#xff0c;基类选择QMainWindow&#xff1b; 2、MainWindow.ui中放入一…

cocos2dx java 调用lua_cocos2dx之C++调用Lua

1&#xff0e;引入头文件#include "cocos2d.h"#include "CCLuaEngine.h"USING_NS_CC;using namespace std;extern "C"{#include "lua.h"#include "lualib.h"#include "lauxlib.h"}2导入Lua文件如果是cocos2dx的Lu…

93号涨0.86元售6.2元/升 20日油价正式上调

93号涨0.86元售6.2元/升 20日油价正式上调 据悉&#xff0c;自6月20日起汽油、柴油价格每吨提高1000元&#xff0c;航空煤油价格每吨提高1500元。 上调后全国汽油、柴油平均零售价分别为&#xff1a;97号汽油6.6元/升&#xff0c;93号汽油6.2元/升&#xff0c;90号汽油5.8元/升…

eclipse 新建java无scr_解决eclipse中没有js代码提示的问题

自学js&#xff0c;发现eclipse中不管js文件、html文件、jsp文件没有都没js代码的提示&#xff0c;对于js代码也不报错&#xff0c;有时候就因为单词敲错却查了很久没查出来&#xff0c;很烦很难受。在网上找了很多方法&#xff0c;都没有解决&#xff0c;特别是有个在javascri…