赞
踩
什么是线程安全?
线程安全就是保证多个线程同时对某一对象或资源进行操作时不会出错。 比如当我们购物时,两个用户同时下单将商品加入购物车,此时两个用户可以看作两个线程,在线程安全的情况下,两个用户同时下单购买时,我们商品总额会减少两个。线程不安全就是指多个线程执行结果不符合预期的情况。
此时当我们正在进行num++时,中途其他线程插入,此时由于线程的抢占式调度,插入的线程就可能中途也执行并对num进行修改,此时num的值就不符合预期。
代码演示:
我们通过两个线程对num++,各自增5w次,看最后结果是否是10w。
package threading; /** * @author zq * 线程不安全实例 */ class Counter{ private int count =0; public void add(){ count++; } public int getCount(){ return count; } } public class ThreadDemo8 { public static void main(String[] args) throws InterruptedException { //创建两个线程对cout进行自增五万次 Counter counter =new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.add(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { counter.add(); } }); t1.start(); t2.start(); //等待两个进程结束后main进程打印结果 t1.join(); t2.join(); System.out.println(counter.getCount()); } }
运行结果: 不符合预期,并且每次结果不一致。
举个例子:当我们有两个线程,t1线程频繁的读取一个共享变量的值,t2对共享变量修改,并写入主内存。由于编译器优化,不去读取主内存中的变量的值,直接读取他工作内存的值,导致修改不能被识别到,就会出现线程不安全。
代码演示: 我们创建一个线程循环读入flag的值,当为0是循环结束,另一个线程修改flag的值:
package threading; import java.util.Scanner; /** * @author zq * 演示由于内存可见性引起的线程不安全 */ public class ThreadDemo9 { public static int flag =0; public static void main(String[] args) { Thread t1 = new Thread(()->{ while (flag==0){ //空着 } System.out.println("循环结束,t1结束"); }); Thread t2 = new Thread(()->{ Scanner scanner = new Scanner(System.in); System.out.println("请输入一个整数"); flag = scanner.nextInt(); }); t1.start(); t2.start(); } }
运行结果: 由于循环读入效率低,编译器优化导致我们输入不为0的数仍然不结束线程
举个例子: 对于Student类,在t1线程我们new一个Student类,在t2线程对Student类对象进行判空,并调用其中的方法。
t1:
s = new Student();
t2:
if(s!=null)
s.learn();
我们new一个对象时,主要分为三部分操作:
1.申请内存空间
2.调用构造方法(初始化数据)
3.把对象的引用赋值给s
由于编译器会自己优化指令,所以2,3,的顺序是可以改变的,而此时如果t2
线程来进行对s的读取,如果此时按1、3、2顺序执行,我们s中就不会有learn()方法。此时就出现了线程不安全的情况。
注意:
对于CPU的随机调度,抢占式执行我们是改变不了的。
synchronized加锁之后,可以保证一个变量只被一个线程修改,同时也能保证操作的原子性。
synchronized的特性:
互斥。synchronized 会起到互斥效果,当两个线程对同一个对象进行加锁时就会出现锁竞争,然后阻塞等待,只有当获得锁的线程释放锁才能再进行加锁。
进入 synchronized 修饰的代码块, 相当于加锁。
退出 synchronized 修饰的代码块, 相当于解锁。
刷新内存,通过synchronized的工作过程就可以保证内存可见性,此处由于网上资料没有确定的证据,在笔者这存疑。
synchronized 的⼯作过程:
1.获得互斥锁
2.从主内存拷贝变量的最新副本到⼯作的内存
3.执行代码
4.将更改后的共享变量的值刷新到主内存
5.释放互斥锁
可重入。在同一个线程中如果synchronized对同一对象进行两次加锁,此时不会发生锁竞争,从而阻塞等待。
synchronized的使用
public class SynchronizedDemo {
public synchronized void methond() {
}
}
public class SynchronizedDemo {
public synchronized static void method() {
}
}
解释说明: 我们的.java源代码文件,经过编译之后,就会生成二进制字节码文件,此时jvm执行这个文件得先将文件内容读取到内存中(类加载),而类对象就是来表示.class文件内容的,描述了类方方面面的信息。例如类名、类的属性、方法、权限等等。
public class SynchronizedDemo {
public void method() {
synchronized (this) {
}
}
}
synchronized的锁机制
这个可以参考我的另一篇博客Java中的锁策略以及死锁的成因
volatile关键字可以帮我们解决内存可见性和指令重排序问题,但是解决不了原子性问题。
== 当volatile修饰变量时==
(1)写入过程:
(2)读取过程:
此时我们就可以发现,volatile会强制我们每次读取主内存中的变量的值,这时候就能保证内存可见性,以免直接读入工作内存中变量的值出现线程不安全问题。
同样的我们volatile也可以禁止编译器进行优化,解决指令重排序问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。