一、线程安全问题
并发安全问题是指多个线程同时操作一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。
多个线程可以同时操作主内存中的共享变量,如果多个线程只是读取共享资源,那么就不会存在线程安全问题,但如果是写操作呢?以计数器为例,不考虑内存可见性的问题,实现起来大致是:
(1)线程A先从主存中获取计数变量count=0,然后使count=count+1;
(2)同一时间,线程B从主存中获取count=0;
(3)线程A把count=1写回主存;
(4)同一时间,线程B使count=count+1;
(5)线程B把count=1写回主存。
这里就会发现问题,在两个线程计数过后,count为1而不是2,这就是共享变量的线程安全问题,对此,就需要在线程访问共享变量时进行同步(如synchronized)。
二、内存可见性问题
对JMM内存模型了解的朋友应该清楚,所有的变量存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫工作内存,线程对共享变量的操作实际上是操作自己工作内存中的变量,操作完后,再将变量值更新到主内存中。
三、 synchronized关键字
前面提到的内存可见性的问题,java提供了synchronized关键字,可以解决内存可见性的问题。进入synchronized块实际上是把在synchronized块内使用到的变量从线程的工作内存中清除,在synchronized快内使用的变量会从主存中获取,退出synchronized时,会把synchronized块内的共享变量刷新到主存中。
除了解决共享变量的内存可见性问题,synchronized还用来实现原子操作,但是synchronized会引起上线文切换并带来调度开销
示例代码:
虽然synchronized能解决共享变量可见性的问题,但是由于synchronized的特性,只能允许一个线程获取共享变量资源,其他调用线程都会被阻塞,就会产生线程上下文切换和线程调度的开销。
synchronized也是实现原子性的重要手段,以Java常见的 ++ 操作举例,简单的 ++ 操作实际上会经历读—写—读三步操作:
原子性操作就是指一系列的操作要么全部执行,要么全部不执行,如果在上述读—写—读过程中,有其他线程干预,则会破坏这种原子性,而synchronized能保证共享资源同一时间只允许一个线程操作,即原子性得到保障。
四、volatile关键字
大名鼎鼎的volatile关键字,针对共享变量可见性的问题,也提供了自己的解决方式。volatile可以确保一个变量的更新对其他线程马上可见。当一个变量被生命为volatile时,线程在写入该变量时会直接把值刷回主存,而不是放在线程的工作空间。
示例代码:
虽然都能解决共享变量内存可见性问题,但与synchronized不同的是,volatile避免了线程阻塞、线程调度与线程上下文切换的开销。