相关:
精湛细腻版-Java多线程与并发编程
硬核学习Synchronized原理(底层结构、锁优化过程)
主要是共享变量带来的问题:
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
package c2;
public class TestJoin {
static int count = 0 ; //共享变量
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count ++ ;
}
},"t1") ;
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count -- ;
}
},"t2") ;
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(count);
}
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?
因为 Java 中对静态变量的自增,自减并不是原子操作
要彻底理解,必须从字节码来进行分析
例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值
iconst_1 // 准备常量1
iadd // 自增
putstatic i // 将修改后的值存入静态变量i
而 Java 的内存模型如下,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换:
如果是单线程以上 8 行(算上i–)代码是顺序执行(不会交错)没有问题
但多线程下这 8 行代码可能交错运行,如下
本质上,这种问题是指令交错运行导致的(假设单核多线程)
在之后,我会讲之抽象到从JMM内存模型来看待这个问题
那么,多线程如何解决共享变量的正确性的呢?
一个程序运行多个线程本身是没有问题的
问题出在多个线程访问共享资源
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++; }
static void decrement()
// 临界区
{
counter--; }
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
解决方案:为了避免临界区的竞态条件发生,有多种手段可以达到目的
基于乐观锁思想的阻塞式的解决方案:synchronized,Lock
基于悲观锁思想的非阻塞式的解决方案:原子变量
Java - synchronized 解决方案
硬核学习Synchronized原理(底层结构、锁优化过程)
类成员变量:
局部变量:
局部变量是线程安全的,因为JVM会为每个线程建立栈帧,局部变量存在栈帧中,是线程独享的,不会被共享
但局部变量引用的对象则未必
如果该对象没有逃离方法的作用范围,它是线程安全的(逃逸分析),或引用的对象在方法内,为局部变量
如果该对象逃离方法的作用范围,或引用的对象为类成员变量,需要考虑线程安全
重点说下局部变量的引用问题
观察下面代码,分析
package c2;
import java.util.ArrayList;
class ThreadUnsafe {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200; //循环次数
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) { //创建两个线程
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + i).start();
}
}
/*类成员变量*/
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2();
method3();
// }
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
分析:
无论哪个线程中的 method2、method3 引用的都是同一个对象, list 成员变量
将list改为局部变量,则线程安全
public void method1(int loopNumber) {
/*类成员变量*/
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
// { 临界区, 会产生竞态条件
method2(list);
method3(list);
// }
}
}
private void method2(List list) {
list.add("1");
}
private void method3(List list) {
list.remove(0);
}
list 是局部变量,每个线程调用时会创建其不同实例,没有共享
而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象,在同一个线程内的方法的同步调用的
method3 的参数分析与 method2 相同
这样就安全了吗?未必,如果method2或method3的方法修饰符是public,同时又有一个子类继承了这两个方法的一个,并且在子类的重写方法中又创建了一个线程,那么又造成了多个线程访问一个变量的情况,即使是局部变量,它也变成了共享的了,这是逃逸分析中的对象参数逃逸,简单理解为list逃逸了原来方法的作用范围,跑到了子类中的方法
可以看出 private 或 fifinal 提供【安全】的意义所在,请体会设计模式中开闭原则中的【闭】
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。如
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
线程安全类的的每个方法是原子的
但注意它们多个方法的组合不是原子的
Hashtable table = new Hashtable();
// 线程1,线程2
// --- 整段代码非线程安全
if( table.get("key") == null) {
table.put("key", value);
} //
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
你或许有疑问,String 有 replace,substring 等方法可以改变值啊,那么这些方法又是如何保证线程安全的呢?事实上,这些方法并不是真的改变值,而是通过建立新的串或值来达到改变的效果