前言
首先我们引入多线程是为了解决多次创建进程和销毁进程带来的巨大开销,线程可以共享内存和硬盘资源等等,这里我们就会想,他们共享这些东西会不会涉及到一些安全问题呢?他们没有独立分配自己的资源是一定会有安全问题的,但是就目前在这个快节奏的社会来说,效率的提升是必然需要的,我们只能去发现和解决这些安全问题,效率第一!!
举例
下面我们给出一段代码,我们从代码的效果和原因来解析这段代码产生的线程安全问题以及解决方案.
package Thread; public class ThreadDemo17 { private static int count = 0; private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 50000; i++) { count++; } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 50000; i++) { count++; } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); } }
我们这里创建两个线程,t1和t2让他们合作完成100000次自增操作,并且阻塞main线程,最后让main线程来收集结果.
下面我给出几次运行产生的结果
运行相同的代码,怎么就一直产生错误的结果,而且错误的还是不一样的呢???
.
下面我给出解析举例
我们观察一下count++这一个自增操作需要几步吧
这里为我们其实只有三步
第一步是取元素,可以看到是getstatic取到count这个变量
第二步是自增1,就是iadd这个操作
第三步是putstatic,气死就是一个保存的操作
iconst_1其实是将1放到栈区的操作数栈顶
由于这个count++的操作是不原子的,所以这里会产生线程安全的问题
下面我们模拟一下不安全的一种用例(以下操作将上述多个操作抽象成取值,自增,保存三个操作)
假设这里只有一次自增,t1中的count先加载为0,此时t2读到的count也是0,t1最后自增保存了一个1,t2进行更新的时候其实更新的也是1,这就会造成了线程不安全问题,最后导致的结果是1而不是2,与我们的理想情况相悖.
导致这个结果的原因有很多,最重要的就是cpu的这种抢占式调度系统,你无法控制cpu先调度哪个线程,执行到哪个命令之后执行其他的命令.
解决方案
这里我们采用锁来保证线程的安全性,你可以理解为此时张三正在上厕所,咔嚓以下把门锁起来了,这个时候,你再急也进不去,完成不了你的任务,即使此时线程调度到你了,你也只是一个阻塞的状态,无法完成任务
我们使用synchronized关键字来修饰,我们会发现需要一个参数,这个参数其实无论你造一个什么对象都是可以的,只要保证这两个线程的参数是同一个对象即可
举例如下
package Test; public class ThreadDemo1125 { private static int count = 0; private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(()->{ for (int i = 0; i < 10000; i++) { synchronized (lock){ count++; } } }); Thread t2 = new Thread(()->{ for (int i = 0; i < 10000; i++) { synchronized (lock){ count++; } } }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("count = "+ count); } }
此时线程就是安全的了,但也意味着线程由完全并发式的执行变成了半并发半串行的执行
我们发现代码中,synchronized修饰的代码块是串行执行的,但是其他代码还是并发执行的,所以相较于完全串行使用一个线程来执行还是提升了不少的效率的,下面我们来看代码运行结果