1.什么是线程安全
线程安全的本身并不好定义,我们可以认为当某一段代码在多线程环境下运行的结果和在单线程环境下运行的结果是相同的,就说这个线程是安全的。
2.线程不安全的原因
1.操作系统中,线程的调度顺序是随机的;
2.两个线程针对同一个变量进行修改;
以下的操作都是没问题的:
-
一个线程针对一个变量修改
-
两个线程针对不同的变量修改
-
两个线程针对同一个变量读取
3.修改操作不是原子的
我们来看如下代码:
class Counter { public int count = 0; public void increase() { count++; } } class Main{ public static void main(String[] args) throws InterruptedException { final Counter count = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i <10000; i++) { count.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 10000; i++) { count.increase(); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(count.count); } }//结果:14324、15634、15315……
我们可以看到同样的一段代码,第一次执行的结果和第二次执行的结果并不相同。
我们见这个图:
count加一的过程要分三个步骤进行:获取一个count的副本(即取值),副本值加一,将这个副本值给count;
对于一个线程,这时完全没有问题的,但是在多线程环境下(这里一两个线程举例子),上图中的两个线程的三个步骤可能会交叉执行。就上图而言,我们想要的是count等于2,但事实上,count的值是1.这个过程的原因就是该过程不是原子性(原子性可以理解为不可分割的意思)的。那么我们该如何解决这个问题呢?答案是就是给这个代码加锁。
class Counter { public int count = 0; synchronized public void increase() { count++; } } class Main{ public static void main(String[] args) throws InterruptedException { final Counter count = new Counter(); Thread t1 = new Thread(()->{ for (int i = 0; i <10000; i++) { count.increase(); } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 10000; i++) { count.increase(); } }); t1.start(); t2.start(); Thread.sleep(1000); System.out.println(count.count); } }
我们给上面的Counter类中的increase方法进行加锁,这时候取值,count自增,赋值这三个就变得不可分割。从而保证了原子性
4.内存可见性问题;
什么是内存可见性:CPU读取内存的操作,其实很慢。为了提高效率,编译器可能会对代码进行优化,把一些原本要读内存的操作优化成读取寄存器,从而减少了读内存的次数,提高了整体的运行效率。对于单线程来说,这个优化一般不会有什么问题,但是涉及多线程时,可能会出现逻辑错误。这个时候只要用volatile来修饰变量,这个变量就不会有上述优化。
5.指令重排序问题;
指令重排序也是编译器优化的一种,就是把一些执行的执行顺序给他改变了,这在单线程的情况下肯定任何问题,但是在多线程的情况下,就不一定了,多线程的情况下可能会导致wait()这种关键字得到错误的信息,那我们怎么办呢?
还是加volatile关键字,给我们可能发生指令重排序的实例或者变量加上这个关键字的时候,就不会发生指令重排序的问题了。