单核CPU仍然存在线程安全问题

先从Java内存模型说起:

Java内存模型是什么?

          引用大师的一句话:“The Java Memory Model describes what behaviors are legal in multithreaded code, and how threads may interact through memory.”

          翻译过来就是:Java内存模型描述了在多线程代码中哪些行为是合法的,以及线程如何通过内存进行交互。

所以其他涉及到多线程的编程语言都会涉及到内存模型,如C/C++等,并不是Java所特有的。

那为什么会出现内存模型呢?

         归结到底是因为CPU的频率无法再提高了,计算机开始面向多核化发展。为了充分利用多核带来的性能提升,便引入的并发编程,并发编程的难度是比较大的。

       为了弥补cpu与缓存直接的速度落差,提供了多级高速缓存,使缓存更加接近cpu,处理速度更快。与此同时,就会出现缓存不一致问题,在多线程的状况下,每个cpu的计算结果会暂存在高速缓存中,而cpu1的缓存对于cpu2来说是不可见的,对于多个线程处理同一个共享数据时就会产生问题。如何解决?这里的问题根源就是可见性!

      另外,cpu是一种很宝贵的资源,其内部运行采用了“流水线”的思想,比如一条指令的执行包含“取指、译码、执行、访问存储器、写回”等步骤,当一条指令进行译码的同时,cpu又可以继续取指操作。为了能更好地保证流水线的“完美”(尽量让cpu的每个步骤不闲着),在不影响单线程执行结果前提下,cpu会适当移动代码执行顺序(指令重排序),这样做cpu它觉得很完美,能够提高自身的执行效率,它不会管其他cpu怎么这么样。可是在多cpu协作的情况下,有时这是不允许的。如:

Class Reordering {
  int x = 0, y = 0;
  public void writer() {
    x = 1;
    y = 2;
  }

  public void reader() {
    int r1 = y;
    int r2 = x;
  }
}

       假入有两个线程,一个执行writer,一个执行reader,当reader读取到r1==2时,常理x读到的应该是1(因为x=1在y=2前面)。但实际是不一定的,cpu可能对xy的赋值语句调换了顺序,导致x可能读到0。这个的问题根源就是有序性!

针对这两个问题,让我这种码农来处理显然是不现实的,所以大师们在计算机指令的层面已经为我们实现好了,并提供相应的语法,并发类供我们使用。解决这些问题的过程中,大师们设定了一些规则,避免了上述错误情况的发生,这就是JAVA内存模型。

 参考:http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#finalWrong

Java内存模型中有哪些规则?

       其中最重要的就是happens-before原则,我认为这个原则表达的是“哪些情况下后面的代码对前面的代码是可见的”,接下来让我们看看有哪些情况:

引入大师的说法:

  • Each action in a thread happens before every action in that thread that comes later in the program's order.
  • An unlock on a monitor happens before every subsequent lock on that same monitor.
  • A write to a volatile field happens before every subsequent read of that same volatile.
  • A call to start() on a thread happens before any actions in the started thread.
  • All actions in a thread happen before any other thread successfully returns from a join() on that thread.

翻译一下:

1、单线程中,前面的代码应该happens before于后面的代码。(这个很好理解,先来后到)

2、对同一个监视器(锁)的解锁应该happens before于后面的加锁。(一个监视器只能同时被一个线程持有,前一个线程解锁,后面的线程才能加锁,这也是synchronized遵守的规则之一)

3、volatile字段的写入应该happens before于后面对同一个volatile字段的读取。

4、主线程中启动子线程,子线程能看到启动前主线程中的所有操作。

5、主线程中启动子线程,然后子线程调用join方法,主线程等待子线程执行结束,执行结束返回后,主线程对看到子线程的所有操作。

另外还提供了synchronized、volatile、final三个关键字来解决可见性、有序性问题。

 

例子1:

单核cpu仍然存在线程安全问题,因为如果操作不是原子操作,你无法控制cpu在什么时机切换线程,我采用了阿里云上的一台单核服务器做实验,以下是实验代码:

public class OneCpuCoreTest implements Runnable {

	private static int count;

	@Override
	public void run() {
		int idx = 0;
		while (idx++ < 100000) {
			count += 1;
		}

	}

	public static void main(String[] args) throws InterruptedException {
		Thread thread1 = new Thread(new OneCpuCoreTest());
		Thread thread2 = new Thread(new OneCpuCoreTest());
		thread1.start();
		thread2.start();
		thread1.join();
		thread2.join();
		System.out.println(count);
	}
}

我们知道,自增操作是不具备原子性的,它包含取数据、+1、数据写回操作,所以这里的count应该是小于200000,count是线程不安全的。

PC机上(多核):

而在阿里云(单核cpu)上:

看似在单核cpu上是没有线程安全问题。但错了,这里是因为在一个cpu时间片内执行完了,所以不明显,当把循环次数调大。

结果就不一样了。

所以单核cpu上多线程仍然会存在线程安全问题,因为单核cpu仍然存在线程切换,在执行非原子操作的时候,仍然存在线程问题。

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值