线程安全问题,是我们在面试中遇见的最常见的有关线程的问题,所以熟练掌握线程安全问题是非常有必要的.
在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的
这里的安全指代的是代码中有没有产生bug,与我们平常认为的安全是两种截然不同的概念,我们所熟知的安全是由黑客造成的,他们会不会侵入你的电脑,攻击你的计算机,这是我门不能够制止的,我们所要做的就是让代码不会产生bug.
栗子 :使用两个线程,对同一个整型变量进行自增操作,每个线程自增五万次,看最后的结果,代码如下
class Counter{
int count = 0;
public void increase(){
count++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
运行结果如下
在此,我们需要了解一下count++,是如何在CPU上运行的?
count++,实际上是三个CPU指令
- 上面的指令分别会在两个线程中执行,在执行中,各个线程的指令调度是随机的,若按照下面的顺序执行指令调度,则count的值会正确运算
- 下面的执行顺序也不会产生bug
- 但是,下列两种顺序则会产生bug
从以上的解析我门会发现,count在自增的过程中,两个进程在并发的执行过程中,count可能会发生重复自增,也就是说count自增了两次,但事实上只是自增了一次,而且因为线程调度中伴随着随机性,所以上述哪一种状况都可能发生,无法预测;这就产生了线程安全问题,也就是bug
编译器优化:这里我们简单的了解一下什么叫做编译器优化
看上面的截图,有线程t1,t2,t1执行的操作是频繁的读内存的数据t2则是基础的三连(读写存),t1在内存中频繁的读取是非常低效的,而t2又迟迟的不进行修改,t1读到的值又始终是一个值,因此 t1就会产生一个大胆的想法,直接从寄存器中读取数据,不在执行load了,这时t2,对数据进行修改了,但是t1无法读取,这就会让代码产生bug了
编译器优化产生的主要原因就是编译器不相信程序猿,认为程序猿瞎b写代码,代码都是粑粑,然后编译器在原逻辑不变的情况下,主观的对代码进行调整,从而提高代码的效率,这就会改变代码的原始目的,所以产生了bug.使线程变得不安全
对于这种情况我们可以使用synchronized和volatile关键字解决,我们在此先不做详解介绍,一会在分析
内存可见性是编译器优化范围中的一个典型案例
指令重排序也是编译器优化的一种
系统不按照我们所给的命令执行程序,而是为了提高效率,改变指令的执行顺序,这样就会产生bug,这也是编译器认为程序猿瞎b写代码的案列(编译器优化)
根据上文我们可以了解到,线程不安全的原因主要分为三种
- 线程间的调度是随机的,这是我们无法改变的,这也是线程不安全的万恶之源,对于这种随机性,我们也我可奈何
2.多个线程对同一个变量进行操作,并且这些调度指令不是捆绑在一起的,这也会产生线程不安全
3.内存可见性(编译器优化)
我们主要对第2,3条原因进行干涉,这就需要加锁的方法
加锁
加锁就是把线程上锁,这个线程所有的资源,方法,指令都被上了锁,在这个过程中其他的进程无法在对这里的资源进行修改操作,只能在上了锁的资源解了锁之后,才可以访问,也就是将多线程并发变为了单线程串行,降低了运行效率,但也确保了线程的安全性
那么,讲到这里就会有人问,那么,上了锁之后,并发编程不就是很鸡肋了吗?
这是不对的!!!
到这里,我们已经知道了加锁就是为了确保线程的安全性,但是与其这样,还不如单线程串行编程呢
我们要知道,在一个进程中,由若干个线程,并不是每一个线程都需要加锁的,一般来说,只有最后的那几个线程,才需要加锁,来确保线程安全,其他的线程不需要加锁
这样既保证了,代码执行的效率,也确保了线程的安全性
synchronized翻译过来是同步的意思!!!
在计算机中,同步这个词,在不同的环境中所代表的含义不同
在多线程中同步的含义代表互斥,也就是加锁的概念,但是在其他的环境中,如在IO或网络编程中,同步和线程就没有关系了,在这里表示的是消息的发送方,如何获取到消息
1. 使用关键字synchronized****(一定要记住怎么读和拼写!!!)
使用
class Counter{
int count = 0;
synchronized public void increase(){
count++;
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
运行结果
我们发现加上了synchronized后,数据正确,也就是说线程安全了,synchronized既能保证指令的原子性,也能保证内存可见性
被synchronized包装起来的代码编译器不会轻易的进行优化
1.直接修饰普通方法,也就相当于把锁对象指定为this了,具体操作如下
synchronized public void increase(){
count++;
}
2.把synchronized加到代码块上,如果是针对某个代码块加锁,就需要手动制定
锁对象:针对哪个对象加锁
class Counter{
int count = 0;
public void increase(){
synchronized(this){
count++;
}
}
}
3.把synchronized加到静态方法中
所谓的静态方法更准确的叫法是:类方法,普通方法更严谨的叫法是:实例方法
synchronized public static void func(){}
相当于
public static void func(){
synchronized (Counter.class){
}
}
以上就是synchronized的使用方法!!!(单词一定要会读,会拼)
volatile的作用和synchronized差不多,它也禁止了编译器的优化,但是与synchronized不同的是,volatile不能保证指令的原子性
class Counter{
volatile int count = 0;
public void increase(){
count++;
}
public static void func(){
synchronized (Counter.class){
}
}
}
public class Demo1 {
private static Counter counter = new Counter();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread thread2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(counter.count);
}
}
从上面的代码中,我们得知,volatile不能保证线程的安全性.
他俩本质没什么区别,都是Java的关键字,功能也相同,都是对线程进行加锁,保证线程的安全性,只不过synchronized的能保证指令的原子性
那么,有人会问,既然synchronized的功能更全,直接无脑用就好了?
❌ 这是不对的!!!
synchronized的使用时需要付出代价的,一旦使用了synchronized,就很容易使线程阻塞,也就是说代码的执行效率会大大降低,虽说保证了线程的安全性,但是效率也变得更低了
volatile则不会让线程阻塞,效率也相对的提高
所以说具体问题具体分析,需要知道你需要干什么,在选择相应的关键字来加锁