线程安全问题的原因和解决方案

一、首先产生线程安全的原因:

(一)站在开发者的角度看:①多个线程之间操作同一个数据②至少有一个线程修改这个数据(不是读操作而是写操作)

(二)站在系统的角度看:一条语句对应多个指令,线程调度可以发生任何时刻(线程调度不会切割指令),所以线程调度不确定性导致指令执行顺序不确定,导致问题。

(三)线程不安全表现为三个部分:①原子性被破坏②内存可见性问题③代码重排序问题

一、原子性:

列如:int r=0;r++; r--; r++和r--应该是原子性的,但实际执行起来不能确保原子性,所以预期结果r=0,但可能最终结果为1。

如图:

二、内存可见性:一个线程对数据的处理,有可能其他线程无法感知,甚至某种情况下被优化的完全看不见。

把一个线程想象成一个cpu

内存分为:主内存(实际内存),工作内存(cpu缓存中的模拟)

      线程对数据所有的读写操作:①从主内存中加载到工作内存中②工作内存进行处理,可以允许工作内存操作很长时间③完成工作后,把数据同步到主内存中

如班费花销,班上有个记录本(用来记录班费的总额),团支书拿着一部分班费去买班级用的东西,团支书脑子(工作内存)知道已经花销了,但还没有在记录本记录(主内存),班长去按照记录表上的班费去购买物品,会出现花费超值的现象。

三、代码重排序:我们写的程序,中间经过很多优化,导致不能确保最终执行的语句和我们写的语句顺序不一样。jvm中要求单线程没有这种变化导致的出错,但多线程可能会。

如图所示:A,B为两个线程,最后导致出错。

一点小小的知识点:Vector Stack Dictionary StringBuffer 是线程安全的,linkedList, ArrayList,hashMap StringBuilder没有线程安全。

二、解决线程安全的方法:锁(理论是数据,多个线程共用的数据)

加锁和解锁就是确保不是多个线程同时使用操作同一个数据,导致线程不安全。

锁有两种:synchronized 和lock

一、synchronized(同步锁)

synchronized{临界区代码},synchronized是对临界区代码加锁,不在大括号内的不加锁。

所谓加锁就是以下理论:一个线程持有锁(加锁成功),其余加锁失败的线程,放弃cpu进入堵塞队列(运行->堵塞)。打个比方一个房间只能一次进一个人,一个人进去了上锁(加锁成功),直到那个人出来之前其他人在外面等着(堵塞)。

 怎么确保线程安全,就是在synchronized锁基础上,产生互斥。

 互斥的条件:①都有加锁操作②都申请同一把锁

有以下代码:确定是不是互斥

首先我们要知道以下知识:

①synchronized void method(){  }   等价于 void method(){ synchronized (this){} }

②static synchronized void method(){}   等价于 static void method(){synchronized (类.class){} }

③synchronized 修饰普通方法 ,视为对“当前对象”(哪个对象调用的这个同步方法)加锁 即synchronized(this){}

④synchronized修饰静态方法视为所在类加锁  即 synchronized(类.class){}

 

有s1=new SomeClass(); s2=new SomeClass(); s3=s1;三个对象

如图所示以下线程是否互斥情况:

 看一些特别的列子:s1.m1() 线程 是对普通方法加锁   加锁对象(对普通方法加锁是为谁调用它的对象即是s1) 即与s3.m1()是互斥的。 s1.m2() 对静态方法加锁 加锁对象(对静态方法加锁是所在类加锁即为someclass.class)即与s1.m5()是互斥的。

如果synchronized(ref){} ref为null 一定有 NullPointerException异常。

二、Lock是java.util.concurrent.locks.Lock的接口

Lock有以下方法:①lock()加锁 ②lockInterruptibly()加锁,但可以被中断

③tryLock(), tryLock(long time,TimeUnit unit)尝试加锁,返回没有加锁上false

④unlock()解锁

有个实现类为 ReentrantLock 即有以下模块:

Lock lock =new ReentrantLock();

lock.lock();//加锁

try{//临界区代码

}finall{

lock.unlock();

}放在finall中,确保在任何情况下都可以解锁

三、解决线程安全还有:volatile ,堵塞队列BlockingQueque<>接口,wait和notify

一volatile作用:

①volatile 主要作用是保护变量的可见性

②long a=100; double a=100;  高32位+低32位分别写入,不是原子的,即volatile保护了原子性

volatile long a=100;volatile double a=100;

③SomeObject s=new SomeObject();保证对象实例化正确

1.堆里申请内存空间,初始化为0x0

2.对象初始化工作:构造代码块,属性的定义时初始化,构造方法(这才算是一个正确对象)

3.赋值给s

volatile禁止重排序,只能1->2->3,如果1->3->2在3到2之间有线程(线程调度随机)使用对象,其对象是错的即出现问题。

能准确的表明其作用是单列模式:

单列模式分为饿汉模式(在类加载期间就进行对象实例化),懒汉模式(第一次用到时进行对象的实例化)

其懒汉模式实现如下:假如多个线程走先判断对象没有实例化,对类加锁(一个线程持有锁,但这是不知道是否实例化),所以要再判断是否实例化,没有实例化进行实例化,实例化了就返回对象,这里volatile就是要确保实例化正确。

 二、bockingqueque<>方法有:①put(e)加入  take()取出 ②超时的情况offer(e,time,until)poll(time,until)

三、wait()(等待)和notify()(唤醒)是Object类,意味着java中所有对象都有着两种方法。wait和notify必须对等待对象进行synchronized加锁。

wait()唤醒的情况有:①notify()/notifyAll()方法唤醒 ②线程被中止 ③超时() ④假唤醒

 
















 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值