并发编程学习笔记之线程安全(一)
最近在复习、整理之前学习的多线程的知识,本着燃烧自己,照亮他人的想法,把自己整理的一些关于多线程的学习笔记、心得分享给大家.
博主准备把自己关于多线程的学习笔记写成三个部分分享给大家: 基础、实战、测试&优化
这三个部分是一环扣一环的.
1.基础: 多线程操作的对象必须是线程安全的,所以构建线程安全的对象是一切的基础.这一部分讲的就是如何构建线程安全的类,和一些多线程的基础知识.
2. 实战: 构建好了线程安全的类,我们就可以用线程/线程池,去构建我们的并发程序了,如何执行任务?如何关闭线程池?如何扩展线程池?这里都会给你答案
3. 测试&优化: 构建好的程序会不会发生死锁? 如何优化程序? 如何知道运行的结果是否正确? 这一部分会 一 一为你解答.
好了废话不多说,本篇博客是系列的第一篇,我们来讲述一下线程安全.
线程安全
在多线程环境下,保证线程访问的数据的安全格外重要.编写线程安全的代码,本质上就管理状态的访问,而且通常是共享的、可变的状态.
状态:可以理解为对象的成员变量.
共享: 是指变量可以被多个线程访问
可变: 是指变量的值在生命周期内可以改变.
保证线程安全就是要在不可控制的并发访问中保护数据.
如果对象在多线程环境下无法保证线程安全,就会导致脏数据和其他不可预期的后果
有很多在单线程环境下运行良好的代码,在多线程环境下却有问题.例如自增操作:
public class Increment {
private int num = 0;
public void doSomething(){
//do something
num++;
}
}
每次调用doSomething()方法的时候,num都会执行自增操作.但是在多线程环境下,这段代码是有问题的.
原因在于num++并不是原子操作,而是由三个离散操作组合而来的:"读-改-写",读取当前的值,加1,写入变量.
可能会出现某一时刻,两个线程同时读到num的数值,然后分别+1,分别写入.这样其中一次计数就不存在了.
有一个专门形容这类情况的名词,叫竞争条件
当计算的正确性依赖于运行时相关的时序或者多线程的交替时,会产生竞争条件.
我对竞争条件的理解就是,多个线程同时访问一段代码,因为顺序的问题,可能导致结果不正确,这就是竞争条件.
"检查-再运行",也是一种竞争条件.
public class Singleton {
private Singleton singleton;
private Singleton() {
}
public Singleton getSingleton(){
if(singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
看这个例子,我们把构造方法声明为private的这样就只能通过getSingleton()来获得这个对象的实例了,先检查这个对象是否被实例化了,如果没有,那就实例化并返回,但是可能同一时刻两个线程同时通过了条件判断,这样就产生了两个对象的实例.
问题已经很清楚了,那么如何解决问题呢?
Java提供了synchronized(同步)关键字.只要是使用了synchronized关键字修饰的方法,就被加锁了,synchronized锁是互斥锁,同一时间只能有一个线程占有锁,其他对象想要获得锁只能等到占有锁的线程释放锁.
我们来修改一下上面的代码,使它们成为线程安全的:
private int num = 0;
public synchronized void doSomething(){
//do something
num++;
}
public synchronized void test(){
if (state){
//做一些事
}else{
// 做另外一些事
}
}
好了,现在它们又是线程安全的了.
这种方式虽然很简单,但是由于synchronized块包住的代码都会顺序的执行,有时会导致令人无法忍受的响应速度
决定synchronized块的大小需要权衡各种设计要求,包括安全性、简单性和性能,其中安全性是绝对不能妥协的,而简单性和性能又是互相影响的(将整个方法声明为synchronized很简单,但是响应速度不太好,将同步块的代码缩小,可能很麻烦,但是性能变好了).
那么在简单性和性能之间我们要如何取舍呢? 这里有个原则: 通常简单性与性能之间是相互牵制的,实现一个同步策略时,不要过早地为了性能而牺牲简单性(这是对安全性潜在的妥协).
如有耗时长的操作(I/O啊,长时间的计算啊),切记不能放在锁里,否则可能引发活跃度(死锁)与性能(响应慢)的风险.
下面我们再看一段代码:
1 public class Employees { 2 //程序员的等级 3 private int level; 4 //技能库 5 public Map<String,String> skills; 6 7 //工资 8 private int sal; 9 10 public void updateSal(String multithreading){ 11 // 如果有会多线程这个技术 12 if (multithreading.equals(skills.get(multithreading))){ 13 //根据你的等级升职加薪操作.. 14 sal = level * sal; 15 }else{ 16 //如果不会多线程,学习多线程,更改等级为中级 17 skills.put(multithreading,multithreading); 18 level = 2; 19 //根据等级加薪,.. 20 updateSal(multithreading); 21 } 22 } 23 }
员工类有个方法,根据你会不会多线程技术来提高你的薪水,如果你的技能库里有多线程技术,执行加薪操作,如果没有会让你学习,给你的技能库加上这个技能,并且提高你的等级,但是在一些极端的情况下会出现问题,线程A走到17行添加完技能又没修改等级的时候,可能有另一个线程重新调用方法,通过了12行的验证,但是等级没有改变,执行加薪操作的时候是按照等级的过期值执行的.
这里我们就要注意了,当不变约束涉及到多个变量的时候,要原子的更新它们.在这个方法上加锁就又可以保证这个方法是线程安全的了.
最后给大家介绍一下原子变量atomic,使用原子变量也可以把自增操作变为原子的.
private AtomicLong num = new AtomicLong(0);
public void doSomething(){
//do something
num.incrementAndGet();
}
好了关于线程安全和锁就为大家简单的介绍到这里,博主下一篇会更新关于安全发布对象的知识,这两篇结合起来就可以帮助我们构建线程安全的类了.
如果大家有任何疑问或者对博主有什么建议,欢迎大家留下评论.楼主一定会尽快回复.本期分享就到这里,我们下期再见吧!
喜欢我的博客就请点赞+【关注】一波