Java为什么会有并发问题?

因为Java是一种多线程的访问处理模型。
所以当一个请求过来的时候,Java会将产生一个线程来处理这个请求。
如果多个线程访问同一个共享变量的时候,就会出现并发问题。

所以,并发问题产生的条件之一是“共享变量”。那么什么样的变量是共享变量呢?
这就涉及到Java中的运行时数据区结构了

Java运行时数据区

Java运行时数据区

如上图所示,Java运行时数据区分为五个部分,分别为程序计数器虚拟机栈本地方法栈方法区,这五个部分的功能主要如下。

  • 程序计数器:指向当前线程正在执行的字节码指令的地址、行号
  • 虚拟机栈:存储当前线程运行方法时所需要的数据、指令、返回地址
  • 本地方法栈:与虚拟机栈功能类似,区别为处理虚拟机使用到的Native方法服务
  • 方法区:类信息、静态变量等
  • 堆:所有线程共享的一块内存区域,在虚拟机启动时创建,几乎所有的对象实例都在这里分配内存

上述五个部分,由于功能不同,线程运行时的分布也不同。程序计数器虚拟机栈本地方法栈线程私有的,不会被共享,自然也就不会存在竞争问题;而方法区线程共享的,当多个线程访问到共享变量(实例字段、静态字段和构成数组元素的对象),就可能会发生并发问题。

但是问题说到这里其实只是一个表象,为什多线程访问共享变量就会发生并发问题呢?
这就涉及到Java内存模型了

Java内存模型

Java内存模型(JMM)定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
Java内存模型

从上图的Java内存模型中可以看出,一个Java线程,要想获取到一个变量,需要先将变量从主内存放入工作内存,然后再通过工作内存获取,经历一个lock->read->load->use的过程。
每一个线程都有这样一个过程才能获取到变量,这样自然就有可能出现A线程获取到变量,还未赋值回主内存,就被B线程读取或更改的场景,这样自然就会出现不一致问题。

并发问题及一般解决方案

  • 可见性问题
    参照上图Java内存模型,如果线程A对变量obj的更改还未完成,线程B就获取到obj的值了,这样导致的数据不一致问题就属于可见性问题。
    要想符合可见性,则当一个线程修改了obj的值,新值对于其他线程来说是必须是可以立即可见的。

    可见性问题可以使用volatile关键字来解决。
    当一个变量被volatile修饰时,就不会从本地工作内存中获取了该变量的值了。volatile实际上是通过强制使用主内存中的值来解决可见性问题的。

  • 原子性问题
    但是volatile并没有完全解决并发问题,因为上述我们所假设的操作,都默认当成了原子操作。实际上,Java里面大量的运算并非原子操作。这就是原子性问题。
    解决原子性问题,可以使用Java并发包中提供的Atomic类,它的原理是 CAS 乐观锁。

当然,对于可见性原子性问题,最重量级的解决方案,同时也是一般程序员们最喜欢使用的方式,就是使用synchronized进行加锁了。synchronized的使用和其他相关的并发问题,这里不再赘述了。