Java并发编程-核心理论

多线程编程经常会遇到很多问题,那么这些问题可能是由什么导致的呢?

  • 数据存在共享

如果线程访问的都是只在线程里有效的数据,那么多线程不会造成什么问题;但是如果线程操作到线程外的数据,并且这些数据别的线程也访问和操作到,那么就可能存在问题了.比如

    /**
     * 共享变量
     */
    private static int share = 0;

    public static void main(String[] args) throws Exception{

        //用100个线程,每个线程 + 1000次,理论上最终结果 应该是 100000
        for(int i = 0; i < 100; i ++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j = 0; j < 1000; j ++){
                        share ++;
                    }
                }
            }).start();
        }
        //暂停5秒,保证线程都能执行完毕
        Thread.sleep(5000);
        //最后输出的结果往往都没有达到100000的数
        System.out.println("i = " + share);
    }

在工作开发中,数据共享导致的多线程问题更是随处可见,比如现在没有做什么并发措施,A,B两个人几乎同时向一个手机账户(手机余额0元)里面充了50块钱话费,在代码中,是启动了a,b两个线程,然后线程进行以下操作 :

  1. 先从数据库里取手机账户信息
  2. 然后手机账户 + 50块
  3. 最后再写回数据库.

假设现在a线程还没把充值好的手机余额(50元)写回数据库,b线程已经从数据库里面取手机余额(0元),a写回数据库是50元,b操作完再写回数据库也是50元,而不是我们想要的100元.

  • 资源不互斥

共享资源(可能是代码,也可能是变量)在同一时间不允许多个线程对其操作,这便是多线程的互斥性.上面充话费失败的例子便是资源不互斥导致的.假如我们线程a 在执行1,2,3任何一个操作的时候,其他线程都不能执行这段代码,a执行完这段代码,其他线程方可执行这段代码,那么这样子就不会出现充值不正确的情况,而要达到这样的效果,我们可以给1,2,3操作加一个锁,这样子代码1,2,3便具有了互斥性.

  • 数据不可见

JVM内存图

在Java中,假设我们要操作这样一段代码,都发生了些什么呢?

private static int i = 0;
public static void main(String[] args) {
  i = 1;
}

首先,这个 成员变量i 是存储在主内存中(比如堆里面)的,当我们执行了 i = 1以后,主线程会先把这个i从主内存中copy到主线程的本地内存中,(假如现在有多个线程执行了 i = 2或者 i = 3,那么这些线程都是把变量copy到线程各自的本地内存中,然后线程对本地内存中的变量进行修改,)然后再不定时的刷回到主内存中,线程为什么要这么大费周章呢?原来如果线程直接去修改主内存的数据会很慢,这个本地内存相当于一个高速缓存的作用.这个本地内存只对本线程可见, 其他线程是无法修改改本地内存的.这就是不可见性.那有没有办法证明这一现象呢?请看下面代码:

    //一个控制变量
    static  boolean flag = false;
    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                  //当 flag = false 的时候,一直死循环; 当 false = true的时候跳出循环
                    while (!flag) {}
                    System.out.println(Thread.currentThread().getName());
                }
            }.start();
        }
       //休眠1秒钟,保证上面的线程都已经执行了
        Thread.sleep(1000);
        new Thread() {
            @Override
            public void run() {
                //修改控制变量为true,想让上面的线程都跳出循环,输出线程号
                flag = true;
            }
        }.start();
        Thread.sleep(1000);
        new Thread() {
            @Override
            public void run() {
              //一般输出 flag = true
                System.out.println("flag = " + flag);
            }
        }.start();
    }

上面的代码多执行几次后,我们会发现有死循环的现象,出现死循环是因为其他线程的flag还是等于false,说明读取的还是本地内存.哪有什么办法来破除这一现象吗?那就是volatile.
volatile运用了缓存一致性的原理,被修饰了volatile的共享变量当被修改后,系统会通过MESI协议通知其他线程里的本地内存里的该共享变量,令其失效,当线程重新读取这一变量的时候,就不是从本地内存里取了,而是去主存里取.在上面的代码中,如果我们把上面的代码改为
static volatile boolean flag = false;
便不会出现死循环的情况.

  • 操作不是原子操作

原子性是指一个操作或多个操作要么全部执行,且执行的过程不会被线程调度器打断,要么就都不执行。像我们的i++就不是原子操作,因为 i++ 包括 读取 i , i + 1 等于啥, 写回 i 等几个步骤,cpu执行到i + 1的时候可能先去执行其他线程,然后再回来执行这个,在java中,对long,double的操作可能不是原子操作,因为long,double是64位数据类型,当我们的虚拟机为32位的时候,对long的操作其实是分两步操作的,高位处理和低位处理.

Java中的原子操作
lock:将一个变量标识为被一个线程独占状态
unclock:将一个变量从独占状态释放出来,释放后的变量才可以被其他线程锁定
read:将一个变量的值从主内存传输到工作内存中,以便随后的load操作
load:把read操作从主内存中得到的变量值放入工作内存的变量的副本中
use:把工作内存中的一个变量的值传给执行引擎,每当虚拟机遇到一个使用到变量的指令时都会使用该指令
assign:把一个从执行引擎接收到的值赋给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时,都要使用该操作
store:把工作内存中的一个变量的值传递给主内存,以便随后的write操作
write:把store操作从工作内存中得到的变量的值写到主内存中的变量

CAS
CAS(compare and swap)是操作系统的一条指令,作用为当我们要修改内存中value的值,我们可以通过比较value与expect的关系,再决定是否要把newValue写进去

  1. 当value == expect 时, 说明内存中的value还没被修改过,可以把newValue写进去,并返回true
  2. 当value != expect时, 说明内存中的value已经被修改过了,并返回false

假如现在内存上有一个变量 n = 10, 有A,B两个CPU同时想通过CAS修改n的值, Acpu的expect = 10, newValue= 11;Bcpu的except = 10, newValue = 12,假设Acpu先取的n的修改权,这时候会锁定内存n,B就无法同时访问n了,必须等A执行完毕,A的except = value = 10,所以可以执行value= newValue,被修改过后的n的值为11,A执行完以后就轮到B了,B比较except = 10,而此刻n = 11,两个不相同,所以执行失败.

那么在java中是否有对应的方法直接使用了CAS这条指令呢?那就是Unsafe这个类的
public final native boolean compareAndSwapObject(Object object, long offset, Object expect, Object newValue);
其中object是指某个对象,offset是指对象的某个字段的偏移量,cas就是通过这个偏移量去找到内存中对象内对应的字段的位置,expect表示期待值,newValue表示新值,如果返回true,表示object这个对象偏移了offset的字段filed已经被修改成expect了,反之则失败.

怎么用CAS安全的自增

 //一个普通的对象,这个对象里面有个value的字段,注意这个value字段是volatile
            final CAS cas = new CAS();
            //unsafe的初始化代码可以在网上找
            final Unsafe unsafe = getUnsafe();
            //获取value字段在对象里面的偏移量
            final long offset = getOffset(unsafe);

            for(int i = 0; i < 10000; i ++){
                new Thread(){
                    @Override
                    public void run(){
                        for(int j = 0; j < 10000; j ++){
                            int v ;
                            do{
                                //获取value的值
                                v = cas.getValue();
                                //做cas操作,如果失败的话重新获取value的值然后再做cas操作,直至成功
                            }while (!unsafe.compareAndSwapInt(cas, offset, v, v + 1));

                        }
                    }
                }.start();
            }
           //保证所有的线程都能执行完
            Thread.sleep(20000);
           //最后正确的输出value:100000000
            System.out.println("value : " +cas.getValue());
  • CPU运行时指令重排序

程序执行的顺序并不一定按照代码的先后顺序执行,比如现在有一段代码:

int i = 0;
int j = 1;
i = 10; //语句1
j = 100;//语句2

在真正代码执行过程中,语句1并不一定比语句2先执行,cpu会根据代码的实际情况优化代码的执行顺序..所以你看到的可能是语句2比较先执行,但是调整过顺序的执行顺序最后一定会跟没有调整过执行顺序的结果是一致的..
在单线程中,调整顺序可能不会造成什么影响,但是多线程的话就不一定了..比如:

boolean a = false;
Context c = null;

//线程 1
c = initContext();
a = true;

//线程2
while(!a){
sleep();
}
dosomething(context);

我们本来的打算是 同时开启两个线程,线程1初始化容器,初始化完容器以后把a的状态置为 true..线程2先判断 a的值,在本来的打算中,因为线程1的容器初始完以后才会改a的状态,也就是说当a的状态是false的时候,说明c还没初始完,c没初始完就不能执行dosomething,会报空指针异常.所以我们sleep一下给线程1充足的的时间去初始化容器..但是现在如果线程1 被cpu调整了执行顺序,即先把a=true先执行了...那么线程2会以为容器c已经初始化完了..从而执行something 从而报错..除了用锁可以解除这种重排序的困扰,还可以用volatile.

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

推荐阅读更多精彩内容