Java并发编程之常用概念

来源:互联网 发布:string数组中添加元素 编辑:程序博客网 时间:2024/06/15 10:28

本文转载自《奔跑的猪博客》

可见性

所谓的可见性,就是在多线程环境下,一个线程的改动能够被其他线程看见。通常在多线程环境下,每个线程都有自己的线程空间,线程创建的时候,会将主线程变量拷贝一份到线程空间中,线程中进行的更改会立即在线程空间中反映出来,但是对于其它线程并不能看见该线程做的改动。

失效数据

当一个线程对数据修改之后没有及时将修改同步到其他线程,这个数据就是失效的数据。

同步

此时需要进行同步,将线程空间中的改动同步到主线程中,因此为了实现可见性,同步是必不可少的操作。

常用的具有同步功能的java关键字或者类:

  • synchronized代码块
  • 继承了Lock的锁,例如ReentrantLock
  • volatile关键字
  • 原子变量
  • 同步原语,例如compareAndSwapObject

加锁与可见性

上节提到了同步的重要性,然而在多线程环境中,不加限制的同步会导致程序错误。

例如主线程有一个变量x=1;线程A对其进行加一操作之后x=2,同时线程B也进行了加一操作,按理说此时x应该为3,然而由于线程B的加一操作是在线程A进行同步操作之前发生的,线程B返回的x值也等于2,此时程序发生了错误。

为了避免这种情况的发生,为了保证线程的安全,需要采取一定的措施。普遍的做法有两种:

(1)加锁

加锁的意思是,当多个线程要修改同一个对象的内容时,要求同一个时间只能有一个线程对该对象进行操作,当一个线程获得了锁,其他线程就需要阻塞。这样保证了数据修改的一致性,就不会发生错误了。

(2)循环cas

循环cas是一种无锁(无阻塞)的线程安全方式,常用同步原语compareAndSwap来实现,具体会在后面介绍无阻塞队列中讲解。

原子性

原子性又可以称为不可分割性,指出一系列操作要么一起完成,要么都不完成,不会中间结束。原子性无论在事务还是程序运行上都是一个很重要的特性。在单线程程序中一段代码编译之后的执行顺序和程序员编码的顺序并不是一样的,这是编译器优化的结果。虽然顺序不一样,但优化的时候还是会保证程序的顺序一致性,也就是不会出错。然而在多线程程序中,对共享变量的操作顺序很难保证,如下代码所示,两个原子变量单独的操作是原子的,但是不能保证两个变量NumberA和NumberB的复合操作也是原子的。假设两个线程同时执行doSet方法,一个线程执行doSet(1),另一个线程执行doSet(2),那么最后的结果可以是:
1、NumberA=1,NumberB=1;
2、NumberA=2,NumberB=2;
3、NumberA=1,NumberB=2;
4、NumberA=2,NumberB=1;
假如能够保证doSet方法的原子性,那么结果只可能是:
1、NumberA=1,NumberB=1;
2、NumberA=2,NumberB=2;

AtomicReference<Integer> NumberA=new AtomicReference<Integer>();AtomicReference<Integer> NumberB=new AtomicReference<Integer>();public void doSet(int i){    NumberA.set(i);    NumberB.set(i);}

在很多场景下,原子性的要求是必要的,例如我们需要NumberA和NumberB同时变化。为了实现原子性,有两种方法:加锁、原语操作

加锁的方式来实现的原子性,同一时间只有一个线程在修改某个对象,那么就能够保证原子性。例如synchronized代码块中的代码就具有原子性,编译器在编译优化的时候就不会将synchronized代码块内的代码和代码块外的代码交换顺序,这就保证了顺序一致性。

原语操作中包含某些简单的原子操作,例如incrementAndGet包含了读取-修改-写入操作。

活跃性

加锁能保证线程安全,但是不恰当的加锁方式会导致活跃性的降低甚至活跃性的僵死。

死锁

(1)顺序死锁

最简单的一种死锁形式是有两个锁A,B,线程1持有锁A等待锁B,线程2持有锁B等待锁A,这种情况下两个线程会一直等待下去。

上述情况更加普遍化就是两个线程试图以不同的顺序来获得相同的锁,如果锁链形成了一个环状就会出现死锁。数据库系统中有监测锁的依赖关系,监测到环状的锁链就会强制释放锁从而解除死锁。然而JVM没有那么强大,需要我们在编码的时候避免顺序死锁。

(2)资源死锁

饥饿

饥饿是在线程无法访问它需要的资源时而不能继续执行的情况,引发饥饿最常见的资源就是CPU时钟周期,例如Java应用程序对线程的优先级使用不当,或者持有锁的线程执行一些无法结束的结构。可以通过Thread.sleep或者Thread.yield来克服优先级调整和响应性问题。

活锁

活锁是犹豫错误处理机制或者回滚机制导致的,典型的现象就是一个任务不停地失败,不停地回滚,导致其他线程不能前进。还有一个典型的现象,例如两个非常有礼貌的路人互相让路,结果都没前进,还有就是局域网拥塞。
解决方法就是在冲突重试中加入随机数。(或者参考tcp的重传机制)

另类的线程安全

不变性

不变的东西是不会存在线程安全问题的。良好的编程风格中有如下说法:

除非需要更高的可见性,否则应将所有的域都声明为私有欲,是一个良好的编程习惯。
除非需要某个域是可变的,否则应将其声明为final域,也是一个良好的编程习惯。

封闭性

当一个线程内的数据不会被其他线程共享时,也是线程安全的。例如无状态的Servlet,独立的Servlet线程完成独立的工作,不需要和其他线程协作,此时就是线程安全的。

发布与逸出

上面提到的封闭性,一个类中的成员变量如果不希望被其他线程或对象访问到,就应该设置成private,甚至不应该在public函数中返回该变量的引用,一旦返回了该变量的引用就相当于将该对象发布出去了,其他线程就可以修改该对象。

如果想要把一个对象发布出去,应当尽量按照如下方式:

  • 在静态初始化函数中初始化一个对象的引用
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象中
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中

参考文献

下面两本书我极力推荐,它们对并发编程解释得非常全面。

[1] Brian Goetz等著 童云兰译..机械工业出版社
[2]Maurice Herlihy, Nir Shavit著,<多处理器编程的艺术>,北京 : China Machine Press 2013

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 10岁走路内八字怎么办 8岁孩走路内八字怎么办 一岁宝宝足外翻怎么办 2岁宝宝小腿弯怎么办啊 一岁宝宝小腿弯怎么办 一岁小儿o型腿怎么办 两岁宝宝o型腿怎么办 狗狗前腿外八字怎么办 20岁走路内八字怎么办 9岁儿童脚内八字怎么办 5岁宝宝脚内八字怎么办 一岁宝宝内八字怎么办 两人八字合不合怎么办 考到不好的大学怎么办 考的大学不理想怎么办 只考上二本大学怎么办 w7电脑中病毒了怎么办 电脑中病毒了该怎么办 泰迪呼吸急促怎么办啊 狗狗呼吸急促是怎么办 狗狗着凉了呕吐怎么办 狗鼻子流黄鼻涕怎么办 刚出生婴儿睡觉不踏实怎么办 有人溺水后你该怎么办 借钱不还怎么办没欠条 私人欠货款不还怎么办 公司欠货款不还怎么办 两个人离婚一方不同意怎么办 比亚迪l3油耗高怎么办 u盘密码忘记了怎么办 主板没有m.2接口怎么办 点痣留下了疤怎么办 危险三角区长痘痘怎么办 挤了危险三角区怎么办 三角区长痘挤了怎么办 三角区发红长痘怎么办 激光祛斑碰水了怎么办 激光打痣留下坑怎么办 点痣之后留下坑怎么办 去痣留下的红印怎么办 激光点痦子留疤怎么办