线程安全问题产生的原理 解决线程安全问题_同步代码块 同步技术的原理
线程安全产生的原因
什么是线程安全
在操作系统中,因为线程的调度是随机的(抢占式执行),正是因为这中随机性,才会让代码中产生很多bug 如果认为是因为这样的线程调度才导致代码产生了bug,则认为线程是不安全的, 如果这样的调度,并没有让代码产生bug,我们则认为线程是安全的
这里的安全指代的是代码中有没有产生bug,与我们平常认为的安全是两种截然不同的概念,我们所熟知的安全是由黑客造成的,他们会不会侵入你的电脑,攻击你的计算机,这是我门不能够制止的,我们所要做的就是让代码不会产生bug.
使用两个线程,对同一个整型变量进行自增操作,每个线程自增五万次,看最后的结果,代码如下
package Demo01_Sleep; 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); } }
当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在票问题,Java中提供了同步机制(synchronized)来解决。
根据案例简述︰
窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。
为了保证每个线程都能正常执行原子操作Java引入了线程同步机制。那么怎么去使用呢?有三种方式完成同步操作∶
1.同步代码块。
2.同步方法。
3.锁机制。
众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢?
一:首先了解jvm内存的运行时数据区
1.堆区:存储对象实例(和实例变量),数组等
2.java虚拟机栈(方法·栈),存放方法声明,局部变量,对象的引用变量,基本数据类型变量等
3.本地方法栈:存储一些本地方法(native关键字修饰的方法,如hashCode()方法,clone方法,Thread类的star0()方法)
4.方法区:存储类元数据,常量,静态变量等
5.程序计数器:记录程序执行的位置,保证cpu切换上下文时,可以从上一次执行的位置开始执行
二:内存空间的共享情况
堆区与方法区都是线程共享的,而栈区如方法栈则是线程私有的
三:一个线程的大致组成结构
1.每一个线程都有自己的线程栈,因此线程与线程之间是相互独立的
2.一个线程栈里面有自己的程序计数器,方法栈等
3.方法栈里面又是通过栈帧的形式存储局部变量表,操作数栈,方法出口等
4.方法中的局部变量则是存储在局部变量表中,数据操作则是在操作数栈中进行
四:多线程引起线程安全原因(实质是造成了读写不一致)
1.当多个线程操作共享空间中的变量时,就有可能造成线程安全问题(如一个线程更新变量之前,另一个线程读到了旧值并已经更新了,导致该线程再去更新时,更新的值相对来说就不正确了)
2.结合内存空间的共享性,也就是说,当多个线程同时操作堆区中对象的成员变量,或者方法区中的静态变量时,就会造成线程安全问题
五:深入理解为什么线程之间会造成读写不一致
首先线程并发导致安全问题的根本原因主要有3个
1.原子性:线程切换会带来原子性问题,使用锁即可解决。java中只有简单的赋值操作,如i = 100是原子性操作,但是i = j则不是
2.可见性:由于cpu高速缓存的存在,可能会导致线程对一个变量修改没有及时被其他线程所看见,使用volatile关键字即可解决
3.有序性:jvm会对代码进行优化,从而会把代码进行重排序,使用volatile关键字可以禁止重排序
注意:volatile只能保证可见性与有序性,不能保证原子性
那么volatile是如何保证可见性与有序性的呢?
首先说明为什么线程并发会导致可见性问题,以及可见性带来的影响
java内存模型分为线程的工作内存(可以理解为线程栈)与主内存(可以理解为堆以及方法区)。主内存则会存放着一些共享变量;工作内存则是每一个线程独有的。当要操作主内存的变量时,线程会先从主内存中复制一份缓存到自己的工作内存,然后在自己的工作内存对值进行修改,之后再把值更新到主缓存中。因此当有一些线程事先缓存了变量或者线程修改的变量没有及时更新到主内存中,就会导致线程安全问题
volatile则是可以保证线程修改变量后,马上更新到主内存,而且其他线程中即使缓存了该变量,也强制必须从主内存中获取值,从而解决了共享变量的可见性问题
那为什么不能保证原子性呢?
举个例子,比如 volatile i = 100,i++;首先一个线程读取到i = 100,然后阻塞了,另外一个线程进来,读取到 i = 100,再自增并且赋值,刷新主内存i = 101,此时其他线程对i的修改肯定是可见的,但是上一个阻塞的线程已经读到了 i = 100了,然后再++,依然是101,那么也就是说,volatile并没有保证原子性(i++有三个操作,读取,自增,赋值,java中只有简单的赋值才是原子性操作)
五·:解决线程安全问题的思路(同时满足原子性,可见性与有序性)
1.避免线程修改共享空间中变量的值
2.使用无状态对象,即不共享状态(数据)给多个线程
3.使用不可变对象,不可修改,就不会存在读写不一致的问题
4.使用线程特有对象,如TheadLocal
5.装饰者模式,即使用原子类,原子操作
6.使用锁,保证线程同步,如Syconized,RetranceLock等
package Demo01_Sleep; public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket station = new BuyTicket(); new Thread(station,"小李").start(); new Thread(station,"王五").start(); new Thread(station,"李四").start(); } } class BuyTicket implements Runnable{ //票 private int ticketNums = 10; boolean floag = true; @Override public void run() { //买票 while (floag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } //synchronized 同步方法,锁的是this private synchronized void buy() throws InterruptedException { if(this.ticketNums <= 0) { floag = false; return; } Thread.sleep(100); //买票 System.out.println(Thread.currentThread().getName() +"拿到了第"+ this.ticketNums--); } }
Synchronized的基本使用
Synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。Synchronized的作用主要有三个:
1、确保线程互斥的访问同步代码
2、保证共享变量的修改能够及时可见
3、有效解决重排序问题。
从语法上讲,Synchronized总共有三种用法:
1、修饰普通方法
2、修饰静态方法
3、修饰代码块
接下来我就通过几个例子程序来说明一下这三种使用方式(为了便于比较,三段代码除了Synchronized的使用方式不同以外,其他基本保持一致)。