首先我们先看一个线程不安全的例子:
class Counter {
public int count = 0;
public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadDemo10 {
public static void main(String[] args) throws InterruptedException {
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();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
//结果本来应该是10000,结果却为63996
//这种多线程的实际结果与预期结果不一致就是bug。
上述代码出现的BUG的原因是:
1.线程之间的调度是不确定的(抢占式执行)
2.多个线程修改同一个变量
3.修改操作,不是原子的
例如count++ 操作就不是原子的,它被分为了三个操作~load,add,save
load,将count的值从内存上读到寄存器上
add,给寄存器里的count值加一
save,把寄存器里的值重新写入内存当中
如果是使用 = 直接赋值,那么就是一个原子的操作
那么什么叫原子操作?
某个操作,对应的cpu指令是多个,那么就不是原子的,反之,如果是一个cpu指令,那么就是原子的
还有两个导致线程不安全的原因:
内存可见性引起的线程不安全(和count++的例子无关)
指令重排序引起的线程不安全(和count++的例子无关)
解决不安全的问题
加锁,即保证”原子性“
一旦某个线程加锁了之后,其他线程也想加锁,就不能直接加上,就需要阻塞等待,一直等到拿到锁的线程释放锁了为止
Java中如何进行加锁
synchronized(同步):java中的一个关键字,可使用这个关键字来实现加锁效果
以下代码是对上述例子的修改:
public void add() {
//这里的this表示的是锁对象
这里的this就是counter对象
synchronized (this) {
count++;
}
}
//相当于:
synchronized public void add() {
count++;
}
此处使用代码块的方式来表示。
进入synchronized修饰的代码块就会触发加锁
出了synchronized代码块就会触发解锁
加锁本质上是把并发的变成了串行的
join是将两个线程完整的进行串行,加锁是两个线程的某个小部分串行,大部分是并发的
使用this,就是谁调用的this就是给谁加锁
如果直接给普通方法使用synchronized修饰,此时就相当于以this为锁对象。
如果synchronized修饰静态方法,此时就不是给this加锁了,而是给类对象加锁
如果多个线程尝试对同一个锁对象加锁,那么就会出现锁竞争
针对不同对象加锁,就不会产生锁竞争
1>static修饰的方法是啥意思?
标志类方法。static在c语言中代表特殊的内存空间。而c++引入了面向对象,为了表示普通实例方法和类方法,必须引入一个关键字,但是为了兼容代码,所以就用static表示。实际上类属性/类方法 和 ‘静态’ 没有关系
2>类对象是啥?
javac会把Java文件编译成.class文件,.class文件要先把文件内容读取到内存中(类加载),类对象就可以来表示这个.class文件的内容(所有的详细信息)。
由于内存可见性引起的线程不安全
先看一个BUG:
public class ThreadTest3 {
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 in = new Scanner(System.in);
System.out.println("请输入一个整数:");
int flag = in.nextInt();
});
t1.start();
t2.start();
}
}
//预期结果是输入一个数之后t1线程结束,输出"t1线程结束循环"
//但是实际的结果是输入一个数之后,t1仍然在死循环状态
以上BUG就是因为内存可见性引起的
上述代码在执行的过程中,由于flag == 0,这个代码实际上是两个操作
一是load加载,将flag的值从内存加载到寄存器上
二是cmp比较,在寄存器里比较0和flag的值
但是由于load的开销很大,编译器就会把load优化掉
所以只有第一次编译器执行了load,后续代码都只执行cmp,不执行load
编译器优化:就是能够智能的调整我的代码执行逻辑,在保证程序结果不变的前提下,通过加减语句,语句变换,一系列操作让程序效率提高
但是编译器对于‘程序结果不变’在单线程下的判断十分准确,但是多线程就不一定了
所谓的内存可见性就是在多线程环境下,编译器优化产生了误判,从而引起了bug
处理方式:让编译器在这个场景下暂停优化。使用volatile关键字
语法:volatile public .....
volatile public static int flag = 0;
volatile不保证原子性!保证内存可见性
适用场景:必须是一个线程读一个线程写的情况
synchronized则是多个线程写
volatile还有一个效果:禁止指令重排序
指令重排序也是编译器优化的一个策略,调整代码执行顺序,让程序更高效。
例如 Student s = new Student(); 就会导致指令重排序
new 对象的步骤可以分为三步:
1.申请内存空间
2.调用构造方法(初始化内存的数据)
3.把对象的引用赋值给s(内存地址的赋值)
需要注意的是发生指令重排序的可能性很小