Java线程安全示例
基础知识
根据前面学到的Java内存模型理论知识,我们来解释一下常见的线程不安全场景的原因。为了更加详细的解释这些问题,补充一些Java内存模型之外的基础知识。
1. 对象的状态
对象的状态是指类变量,即类中实例或静态成员变量,方法内的变量都是线程安全的。 根据Java内存运行时数据分配,静态变量存于方法区中,实例对象存于堆中,此二区域为线程共享,而方法中的变量存于虚拟机栈,为线程私有。对象的状态可能包括其他依赖对象的域,也就是说如果类变量是个引用类型,还要涉及到此引用类型中的状态。
2. 内置锁
内置锁即synchronized关键字来加锁实现同步,synchronized可加在方法,代码块上,由锁保护的代码将以原子方式执行,同时保证了可见性。对于实例方法,锁就是方法调用所在的实例对象,对于静态方法,锁就是类的Class对象。volatile变量仅仅保证内存可见性,加锁机制确保可见性与原子性。
3. 非原子操作导致竞态条件
原子性是指不可分割的,要么全部执行要不不执行。由于不恰当的执行时序会出现不正确的执行结果,基于一个可能已经失效的观测结果来决定下一步的动作也会导致程序执行不正确。常见的例子:
- 延迟初始化:常见的就是“先检查后执行”,if(instance==null){instance=new Instance()}。假设A线程看到instance为空要创建对象,B线程又来判断是否为空,要取决于不可预测的执行顺序,包括线程调度方式,A需要花费多长时间来初始化并设置instance的值。
- 复合操作:非原子性的典型如i++,或其他复合操作,多个原子操作组成的复合操作。
示例
示例一,竞态条件
public class UnsafeCounting { public int count;
public void add() {
for (int i = 0; i < 10000; i++) {
count++;
}
}
}
public void test() throws InterruptedException { UnsafeCounting unsafeCounting = new UnsafeCounting();
Thread t1 = new Thread(() -> unsafeCounting.add());
Thread t2 = new Thread(() -> unsafeCounting.add());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(unsafeCounting.count);
}
执行10次结果:17784
14845
15600
18361
19201
20000
19417
20000
19861
15236
首先count++并非原子性操作,而是包含“读取-修改-写入”的三个独立操作,这一点破坏了上面提到的原子性,假设A,B线程在某一时刻同时读取count=100,同时执行+1操作,就会导致偏差1。这种由于不恰当的执行时序而出现不正确的结果的情况,有一个正式的名字叫做竞态条件,通过使用synchronized
关键字使方法同步,保证内存可见性与原子性可解决这个问题。
示例二,内存可见性
public class VisibilityThread extends Thread { private boolean stop;
@Override
public void run() {
int i = 0;
System.out.println("start loop.");
while (!getStop()) {
i++;
}
System.out.println("finish loop,i=" + i);
}
public void stopIt() {
stop = true;
}
public boolean getStop() {
return stop;
}
}
public void test2() throws InterruptedException { VisibilityThread v = new VisibilityThread();
v.start();
//停顿1秒等待新启线程执行
Thread.sleep(1000);
System.out.println("即将置stop值为true");
v.stopIt();
Thread.sleep(1000);
System.out.println("finish main");
System.out.println("main中通过getStop获取的stop值:" + v.getStop());
}
start loop.即将置stop值为true
finish main
main中通过getStop获取的stop值:true
这个示例所要表达的是,在main线程中修改了 stop = true;
,但是v.start();
并没有停止,并没有输出finish loop,i=
语句。即内存可见性问题。通过声明private volatile boolean stop;
stop为volatile 可解决这个问题。
示例三,重排序
public class NoVisibility { private static boolean ready;
private static int number;
private static class ReaderThread extends Thread{
public void run(){
while (!ready) {
Thread.yield();
}
System.out.println(number);
}
}
public static void main(String args[]) throws Exception{
new ReaderThread().start();
number=42;
ready=true;
}
}
线程可能会一直循环下去,因为可能看不到ready的值。另一种可能是会输出number为0,因为线程可能读到了ready=true,却没有看到number的值,出现这种现象是因为重排序,破坏了有序性。将ready声明为volatile ,先排除可见性影响,然后这段代码实际运行可能并不会出现输出0的情况,但这并不能说明代码是正确的,线程安全问题本就是不确定,难以复现的问题。
示例四,单例双重检查锁机制
这个是单例模式的双重检查锁机制,为什么要同时声明volatile对象和使用synchronized呢?
public class Singleton { private volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { // 1
synchronized (Singleton.class) {
if (singleton == null) { // 2
singleton = new Singleton();
}
}
}
return singleton;
}
}
在首次singleton为null时,多线程调用getInstance()情况下,假设A,B线程都执行到代码1处,由于synchronized锁定当前类的Class对象,A,B线程只有一个可以进入2处,另一个等待。假设A线程进入2处,执行实例化,解锁。B线程然后获取锁,执行2处代码,此时再次检查singleton是否为null,如果singleton对象不是声明为volatile类型,则可能看不到singleton已经被A线程实例化,进而继续执行实例化,导致两个实例出现。
以上是 Java线程安全示例 的全部内容, 来源链接: utcz.com/z/518034.html