当前位置:   article > 正文

线程共享内存及可能存在的问题

线程共享内存及可能存在的问题

线程共享内存及可能存在的问题

每个线程表示一条单独的执行流,有自己的程序计数器,有自己的栈,但线程之间可以共享内存,他们可以访问和操作相同的对象。

package com.claa.javabasic.Thread;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: claa
 * @Date: 2020/03/29 21:21
 * @Description:
 */
public class ShareMemoryDemo {
    private static int shared = 0;
    private  static void incrShared() {
      shared ++;
    }

    static class ChildThread extends Thread {
        List<String> list;

        public ChildThread(List<String> list) {
            this.list=list;
        }

        @Override
        public void run() {
            incrShared();
            list.add(Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<String>();

        Thread t1 = new ChildThread(list);
        Thread t2 = new ChildThread(list);
        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(shared);

        System.out.println(list);
    }

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

输出结果

2
[Thread-0, Thread-1]
  • 1
  • 2

通过这个例子,想强调说明执行流、内存、程序代码之间的关系。

(1)有两条执行流,一条执行main方法,另外两条执行ChildThread的run方法

(2)不同执行流可以访问和操作相同的变量,如本例中的shared和list变量。

(3)不同的执行流可以执行相同的程序代码,如本例中incrShared方法,ChildThread的run方法,被两条ChildThread执行流执行,incrShared方法是在外部定义的,但被ChildThread 的执行流执行。在分析代码执行过程时,理解代码在哪个线程执行时很重要的。

(4)当多条执行流执行相同的程序代码时,每条执行流都有单独的栈,方法中的参数和局部变量都有自己的一份。

当多条执行流可以操作相同的变量时,可能会出现一些意料之外的结果,包括竞态条件和内存可见性问题。

1.竞态条件

所谓竞态条件是指多个线程访问和操作同一个对象时,最终执行结果与执行时序有关,可能正确也可能不正确。

package com.claa.javabasic.Thread;

/**
 * @Author: claa
 * @Date: 2020/03/29 22:10
 * @Description:
 */
public class CountThread extends Thread{
       private static int counter = 0;

    @Override
    public void run() {
        for(int i=0; i< 1000;i++) {
            counter++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int num =1000;
        Thread[] threads = new Thread[num];
        for(int i = 0; i < num; i++) {
           threads[i] = new CountThread();

           threads[i].start();
        }

        for(int i =0; i < num; i++) {
           threads[i].join();
        }

        System.out.println(counter);
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

期望的结是100万,但实际执行,发现每次输出的结果都不一样,一般都不是100万,经常99万,为什么呢?
因为counter++ 这个操作不是原子操作,它分为三步:
(1)取counter的当前值;
(2)在当前值基础上加1;
(3)将新值重新赋值给counter。

两个线程可能同时执行第一步,取到了相同的counter值,比如都取到了100,第一个线程执行完后counter变为101,而第二个线程执行完后还101,最终结果就是与期望不符。

怎么解决呢?
使用synchronized关键字;
使用显示锁;
使用原子变量。

2.内存可见性

多个线程可以共享访问和操作相同的变量,但一个线程对一个共享变量的修改,另一个线程不一定马上就可以看到,甚至永远也看不到。

package com.claa.javabasic.Thread;

/**
 * @Author: claa
 * @Date: 2020/03/29 22:35
 * @Description:内存可见性
 */
public class VisibilityDemo {
    private static boolean  shutdown = false;

    static class HelloThread extends Thread{
        @Override
        public void run() {
            while(! shutdown) {
              // do nothing
            }
            System.out.println("exit hello");
        }
    }

    public static void main(String[] args)  throws InterruptedException{
       new HelloThread().start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        shutdown = true;

        System.out.println("exit main");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

输出结果

exit main
  • 1

期望的结果是两个线程都退出,但实际执行时,很可能发现HelloThread 永远都不退出,也就是说在HelloThread执行流看来shutdown永远为false,即使main线程已经更改为true。

这是因为内存可见性问题。在计算机系统中,除了内存,数据还会被缓冲存在cpu的寄存器以及各级缓冲中,当访问一个变量时,可能直接从寄存器或cpu中获取,不一定到内存中去取,当修改为一个变量时,也可能是先写到缓冲中,稍后才同步更新到内存中。在单线程的程序中,这一般不是问题,但在多线程的程序中,尤其是在多cpu 的情况下,这就是严重的问题。一个线程对内存的修改,另一个线程看不到,一是修改没有及时同步到内存中,二是另外一个线程根本没有从内存读。

怎么解决呢?
使用volatile 关键字
使用synchronzied 关键字或显示同步锁。

参考文章

java编程的逻辑基础(马俊昌)

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/566388
推荐阅读
相关标签
  

闽ICP备14008679号