什么是线程安全问题,简单来说就是某个代码在多线程的环境下执行结果不符合我们的预期。
多次运行后代码结果:
上述代码通过两个线程分别对count++了50000次,那这样最后count的值就为100000。但是代码运行后的结果并不是100000,运行了4次有4次不同的结果,这就是线程安全问题,程序运行结果与我们的预期不符。
那是什么造成线程的安全问题?
1.最主要的原因就是线程是随机调度,抢占式执行的,这就导致了线程执行的顺序不可预期,所以必须保证多线程的任何执行顺序下的程序结果都是正确的。
2.多个线程共同修改同一份数据也是原因之一
3.执行的操作不是'原子'的
什么是原子,简单来说就是不可分割的最小单位,站在CPU的角度上一条条指令就是不可分割的最小单位,那上述代码中的count++就不是原子的,count++在CPU上可以简单分为有3条指令(1.load(从内存上加载数据) 2.add(执行相加操作) 3.save(把结果保存到内存中) ),由于线程调度的随机性,就存在t1线程刚执行完相加指令还没把结果保存到内存中,t2线程就把加好的数据存在内存中,当t1线程把数据保存到内存中之后,就把t2线程已经加完的数据给覆盖掉了,就导致t2线程执行了count++,但是count++的结果没有保存下来。相反也存在t1线程的结果没有保存下来的情况,所以才会导致上述代码中的count的结果不为100000。
线程安全问题的解决方案
想解决线程安全问题就要从造成线程安全问题的原因来寻找解决方案,从上述的3个原因来看,第一个原因是操作系统执行的工作,我们无法干预,第二个原因是有些场景必须要这么做的,也不能解决问题,所以就需要从第三个原因入手,让线程执行的非原子操作变成是原子的。
怎样才能让线程执行的操作是原子的——加锁
加锁是什么意思呢?简单来说就是一个线程在执行非原子操作的时候给这个线程加上锁,加上锁之后其他线程要是想获得这把锁必须等待上一个线程执行完代码后释放锁,这就使得这个线程的操作变成原子的。用上述代码来解释就是让t1和t2线程都加上同一把锁,(锁竞争是随机的,就是t1与t2线程谁先获得锁是不确定的),假如t1先获得锁,t1就会先执行count++操作,而t2就会阻塞等待t1执行完count++操作并释放锁后,t2才能获得锁并执行count++操作,这时t2拿到锁,t1也要等待t2释放锁才能执行操作,以此类推。
在Java中提供了synchronized关键字来完成加锁操作,synchronized会起到互斥效果,某个线程执行到某个对象的synchronized中时,其他线程如果也执行到同⼀个对象synchronized就会阻塞等待。进入synchronized修饰的代码块,相当于加锁,退出synchronized修饰的代码块,相当于解锁。
上述代码使用锁之后:
运行结果:
在上述代码中使用了synchronized关键字给线程t1与t2都加上了锁,进入synchronized修饰的代码块,相当于加锁,退出synchronized修饰的代码块,相当于解锁,synchronized的()里要写Object类或者Object的子类的对象,是什么对象不重要,重要的是两个要加锁的线程必须使用同一个对象来加锁。