我们先看以下代码:
public class ThreadDemo9 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
count++;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
两个线程同时对count变量加10000次,不出意外结果应该是20000,但实际上结果却是:
为什么?这就要谈到线程在cpu上的执行过程了。也就是线程安全问题。
首先,count++这个操作,站在cpu的角度上看,其实是3个指令。
load:把内存中的数据,加载到寄存器中。
add:把寄存器中的值+1.
save:把寄存器中的值写回到内存。
上述代码中,两个线程并发的进行count++,多线程的执行,是随机调度,抢占式的执行模式。也就是说某个线程执行指令的过程中,当它执行到任何一个指令的时候,都有可能被其他线程把它的cpu给抢占走。
结合上述两点,实际并发执行的时候,两个线程执行指令的相对顺序就可能会存在多种可能。
上面这两种可能是不影响结果的。
但是如果出现这些情况,就可能使3次甚至4次++,只起到1次的效果。
总结:一个线程的save在另一个线程的load之前,就是ok的。
一个线程的save在另一个线程的load之后,就都是有问题的。
出现线程不安全的原因:
1)线程在系统中是随机调度,抢占式执行的。
2)当前代码中,多个线程同时修改同一个变量。
3)线程针对变量的修改操作,不是原子的。(不可拆分的最小单位,就叫"原子",如果某个代码操作,对应到一个cpu指令,就是原子的,对应到多个,就不是原子的)
count++就不是原子操作 ->3个指令。
4)内存可见性问题,引起的线程不安全。
5)指令重排序,引起的线程不安全。
上述例子就告诉大家,多个线程并发执行的时候,具体指令执行的先后顺序,可能存在无数种情况。我们要保证每一种情况下计算结果都得是对的才行。
解决方案
那么如何解决上述问题?要从原因入手,原因1是系统规定好的,我们无法干预。
原因2,我们的目的就是要多个线程修改同一个变量啊,也无从下手。
原因3是解决线程安全问题最普适的方案。
我们可以通过一些操作,把上述一系列”非原子“的操作,打包成一个”原子“操作。
这个操作就是加锁。
锁本质上也是操作系统提供的功能,内核提供的功能=>通过api给应用程序了。java(JVM)对于这样的系统api又进行了封装。
关于锁,主要的操作是两个方面
1)加锁 t1加上锁之后,t2也尝试进行加锁,就会阻塞等待(都是系统内核控制)(BLOCKER状态)
2)解锁 直到t1解锁了之后,t2才有可能拿到锁(加锁成功)
在一个程序中,锁不一定只有一把,如果你有两个线程,针对不同的代码加锁,不会产生互斥的(也称为锁竞争/锁冲突),只有针对同一段代码加锁,才有互斥。
总结:1)锁涉及到两个核心操作 : 加锁,解锁
2)锁的主要特性:互斥:一个线程获取到锁之后,另一个线程也尝试加这个锁,就会阻塞等待(也叫做锁竞争/锁冲突)
3)代码中,可以创建出多个锁,只有多个线程竞争同一把锁,才会产生互斥,针对不同的锁,则不会。
我们利用锁改进上述代码:
public class ThreadDemo9 {
private static int count;
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
synchronized (locker){
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 10000; i++) {
synchronized (locker){
count++;
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count = " + count);
}
}
运行结果:
注:synchronized 后面带上(),()里面就是写的“锁对象”。锁对象的用途有且只有一个,就是用来区分两个线程是否针对同一个对象加锁。如果是,就会出现锁竞争/锁冲突/互斥,就会引起阻塞等待。如果不是,就不会出现锁竞争,也就不会阻塞等待。和对象具体是啥类型和它里面有啥属性,有啥方法,接下来是否要操作这个对象没有任何关系。
synchronized下面跟者{}用于包裹代码块,当进入到代码块,就是给上述()锁对象进行了加锁操作,当出了代码块,就是给上述()锁对象进行了解锁操作。
当加锁的生命周期和方法的生命周期是一样的时候,synchronized还可以直接写到方法上。
第二种写法相当于一进入方法就针对this加锁。锁对象就是this。
synchronized修饰普通方法,是相当于针对this加锁了。
synchronized还可以修饰static方法,
synchronized修饰static方法,相当于针对该类的类对象加锁。也就是利用java机制反射,类名.class获得类对象。
以上,关于线程安全问题,希望对你有所帮助。