Java中的并发

什么是并发


并发,实际上是多个程序之间或者同一个程序内不同部分之间能够并行执行的一种能力。
它极大的提升了CPU的吞吐量,特别是对于多核CPU尤为明显。(随着并发量的增加会达到性能上限,并发量过大,致使运行上下文切换过于频繁,甚至有可能会降低性能)
同时,也提升了程序的交互性,比如在App开发过程中,同一个页面需要若干线程同时执行,绘制UI线程,后台线程等等,分工提供高质量的用户交互需求。

进程并发和线程并发

多个程序之间的并行,表现为进程的并行,这一点很普遍,也不会存在什么问题。并行的进程之间,一般不会相互影响,它们是相互独立的,相互不可见,进程的资源比如内存和CPU时间被操作系统管理。在应用层的表现,在电脑上,你可以在听音乐的同时打字。
线程也被称为是轻量级的进程。同一个进程可以拥有多个线程,每个线程有自己的本地内存(调用栈和缓冲内存等)。它的控制,是由进程管理的,并且,重要的是多个线程之间可以共享进程中的变量以达到通信的目的。

线程并发中的问题

根据JVM的内存模型,一个线程访问主内存中的共享变量的步骤是:

  1. 总是先将该内存变量的内容副本拷贝进自己的本地内存中;
  2. 对于基础类型的变量,在更改了副本中之后,会将更改后的值同步刷新到主内存;引用类型则不存在这种同步操作。
    同时,我们知道:
  3. 对一个变量的基本操作,哪怕是简单的变量自增操作,虽然代码上只有一行,但其实CPU是分几步完成的:得到变量副本,加一,生成临时变量接收结果,然后将结果写会到主内存刷新变量值。##Java中原子操作包括:除了long和double之外的赋值操作(long和double是64位的,在32位机器上将分两次进行操作);对于引用的赋值操作##)##
  4. JVM在不影响最终执行结果的情况下,会进行优化,完成指令重排,这对同一个线程不会产生什么影响,但是在不同的线程之间就不同了。
    因此,最终会产生的问题是:
  5. 如何在高层级上保证操作的原子性,对于加一操作而言,最终目的就是保证这句代码已经完成之后,再允许其他线程访问该变量,否则可能导致的结果是加一操作并没有完成,其他线程读取到了未进行该操作之前的值;
  6. 当一个线程更改了变量(基础类型变量)的值后,如何保证立刻对其他线程是可见性,即当其他线程再访问该变量的时候,获取到的是最新的该变量的副本;
  7. 如何消除由于指令重排引发的多线程问题?
    总结起来,其实就是三个方面:变量操作的原子性、可见性(对其它线程)、顺序执行。

Java中的同步


每个Java程序默认运行在它自己进程的线程中,Java同时支持多个线程并发执行,这一点是通过Thread对象实现的。Java应用可以通过Thread类实现多线程开发。在使用过程中,为了保证程序的正常运行,它同时提供了一些安全机制,来实现线程操作的原子性、可见性和顺序性。

锁和线程同步

Java提供了锁来控制某段代码在不同线程之间的互斥执行。使用锁的一个最简单的方式,是通过 synchronized 关键字修饰方法或者代码块。
synchronized 关键字保证了:

  1. 同时只能由一个线程执行代码块,实现多个线程对同一代码块或者方法的互斥访问;(对于自增操作而言,实际上仍然并非是原子的,但是通过互斥访问达到了这样的目的)
  2. 每个线程在进入同步代码块之后,会看到先前所有对代码块中变量的修改。(保证可见性和顺序)
//同步方法
public synchronized void add() {
}
public void delete() {
  //同步代码块
  syschronized(this){
  }
}

上述在使用 synchronized 时,使用的锁实际上是通过该类实例化出来的对象。当然,也可以使用其他对象作为锁。

private Object lock = new Object();
public void add(){
  synchronized(lock){
  }
}

对于静态方法,需要使用类对象

class Test{
            
}
public static void add(){
    synchronized(Test.class){
    }
}
总结

使用锁进行操作无疑是最安全最可靠的做法。但是它最大的弊端在于对CPU资源的消耗。

Volatile使用

Volatile的特点就是在,任何一个线程读取主内存的变量时,将读取的是该变量最新的值。即满足线程对变量修改后,在其他线程中的可见性。即如果A和B同时读取了变量param的值为1,此时如果在A中设置param为2.
下面一段代码可以证明(代码来源)

public class Test extends Thread {
     boolean keepRunning = true;
        public static void main(String[] args) throws InterruptedException {
            Test t = new Test();
            t.start();
            Thread.sleep(1000);
            t.keepRunning = false;
            System.out.println("keepRunning is false");
        }
        public void run() {
            int x = 10;
            while (keepRunning) 
            {
                x++;
            }
            System.out.println("x:"+x);
        }
}
-------------------------------------------------------------------------------------------------------
实测结果:
执行 x++ 的while循环一直没有停下来过。

我对于一直处于while死循环也感到有点不解。可以肯定是,这一定和共享变量的可变性有关。查了一些资料,一个可以接受的说法是:
由于对keepRunning变量没有使用任何同步措施,比如volatile和synchronized,编译器认为这个变量不会被多个线程共享,从而进行了循环不变式的优化,优化结果为:

if(keepRunning){
  while(true){
    x++ ;
  }
}

而这种优化实际上跟具体的运行环境有关。也就是说,在有些设备上,不一定会出现这种死循环。
使用volatile是可以保证共享变量的可见性,然而这并不能完全解决线程之间的同步问题。

static volatile int counter = 0;
for (int i = 0; i < 10; i++) {
            new Thread() {
                public void run() {
                    for (int j = 0; j < 1000; j++)
                        counter++;
                };
            }.start();
}

预计结果应该是 10000,然而实际上,运行结果每次都不一样,最终的结果很少有到10000的。问题就在于,多个线程同时对 counter 进行了修改,只是保证了 counter 在每个线程中的可见性,但是并不能保证它的同步。
以上,我们开了十个线程同时修改 counter

总结

volatile只能作为一个轻量级的synchronized,而不能完全替代synchronized。volatile适用于那些只会在一个线程中进行更改数据的情况,因为大多数情况下对数据变量的操作 都不是原子的。更加安全的做法是使用synchronized,但是synchronized由于需要使用锁,这将更加消耗CPU资源。

AtomicInteger

这是java.util.concurrent.atomic种的类,其中还包括: AtomicBoolean; AtomicLong;AtomicReference等。具体我们来看一看AtomicInteger,其他的大同小异。
AtomicInteger内部其实有一个volatile的value。

private volatile int value;

而它内部的操作,大都使用了 sun.misc.Unsafe 这个类。这个类被称为魔术类,会绕过Java的安全检查机制,直接操作内存以获取高性能,而对于它的set、get、compareAndSet操作都是原子性的。
对于这个类有多强大,可以看这篇文章,这是个强大到即将在JDK9中不被支持。太强大的东西往往都是一把双刃剑,用不好就是灾难。

总结

AtomicX中借用volatile和Unsafe实现了对共享变量操作的互斥性。是一种很安全的使用方式。注意,单纯的AtomicInteger的set和get方法仅仅是返回上面的value,并没有使用Unsafe操作。

感想

在项目过程中由于使用了多线程操作,涉及到多线程同步的问题,所以对此进行了一次梳理,记录在此。
其实在使用过程中,更多的是使用synchronized,这也是非常安全的一个做法,但是问题就在于性能消耗大,但是对于多线程读写共享变量时,又不得不这么做。
后来查看RxJava的源码实现,发现其使用AtomicX(AtomicInteger、AtomicReference等)实现了一个多线程共享的队列,它借此完全做到了对变量原子性、可见性、顺序性的功能,同时性能消耗比synchronized要低。我觉得对于不需要复杂同步操作时,使用AtomicX是一种很好的选择。
当然,volatile的性能相对于synchronized和AtomicX应该是最高的,它不涉及到锁的操作。但是却只能保证共享变量的可见性,而无法保证原子性。所以,它的应用场景在于那些只有一个线程需要对共享变量做复合赋值操作的情况。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容