什么时候线程不安全?怎样做到线程安全?一文安利的明明白白
点击上方 "编程技术圈"关注, 星标或置顶一起成长
后台回复“大礼包”有惊喜礼包!
每日英文
Trust yourself. Think for yourself. Act for yourself. Speak for yourself. Be yourself.
相信自己,为自己考虑,为自己行动,为自己说话,做你自己。
每日掏心话
心态决定一切。要想活得开心,先要让自己安心。真正的安心,不是达成自己的愿望,因为欲海无边,而是淡然。壁立千仞,无欲则刚。责编:乐乐 | 来自:爬蜥链接:juejin.im/post/5b7d68f66fb9a019d80a9002
编程技术圈(ID:study_tech)第 1268 次推文
往日回顾:GitHub 近两万 Star,无需编码,可一键生成前后端代码,这个开源项目有点强!
正文
当多个线程去访问某个类时,如果类会表现出我们预期出现的行为,那么可以称这个类是线程安全的。
什么时候会出现线程不安全?操作并非原子。多个线程执行某段代码,如果这段代码产生的结果受不同线程之间的执行时序影响,而产生非预期的结果,即发生了竞态条件,就会出现线程不安全;
常见场景:
count++。它本身包含三个操作,读取、修改、写入,多线程时,由于线程执行的时序不同,有可能导致两个线程执行后 count 只加了 1,而原有的目标确实希望每次执行都加 1;
单例。多个线程可能同时执行到instance == null成立,然后新建了两个对象,而原有目标是希望这个对象永远只有一个;
public MyObj getInstance(){ if (instance == null){ instance = new MyObj(); } return instance }解决方式是:当前线程在操作这段代码时,其它线程不能对进行操作常见方案:单个状态使用 java.util.concurrent.atomic 包中的一些原子变量类,注意如果是多个状态就算每个操作是原子的,复合使用的时候并不是原子的;
加锁。比如使用 synchronized 包围对应代码块,保证多线程之间是互斥的,注意应尽可能的只包含在需要作为原子处理的代码块上;
synchronized 的可重入性当线程要去获取它自己已经持有的锁是会成功的,这样的锁是可重入的,synchronized 是可重入的
class Paxi {public synchronized void sayHello(){System.out.println("hello");}
}class MyClass extends Paxi{public synchronized void dosomething(){System.out.println("do thing ..");super.sayHello();System.out.println("over");}
}
它的输出为
do thing ..
hello
over
复制代码修改不可见。读线程无法感知到其它线程写入的值
常见场景:
重排序。在没有同步的情况下,编译器、处理器以及运行时等都有可能对操作的执行顺序进行调整,即写的代码顺序和真正的执行顺序不一样, 导致读到的是一个失效的值
读取 long、double 等类型的变量。JVM 允许将一个 64 位的操作分解成两个 32 位的操作,读写在不同的线程中时,可能读到错误的高低位组合
常见方案:
加锁。所有线程都能看到共享变量的最新值;
使用 Volatile 关键字声明变量。只要对这个变量产生了写操作,那么所有的读操作都会看到这个修改;
注意:Volatile 并不能保证操作的原子性,比如count++操作同样有风险,它仅保证读取时返回最新的值。使用的好处在于访问 Volatile 变量并不会执行加锁操作,也就不会阻塞线程。
不同步的情况下如何做到线程安全?线程封闭。即仅在单线程内访问数据,线程封闭技术有以下几种:
Ad-hoc 线程封闭。即靠自己写程序来实现,比如保证程序只在单线程上对 volatile 进行 读取-修改-写入
栈封闭。所有的操作都反生执行线程的栈中,比如在方法中的一个局部变量
ThreadLocal 类。内部维护了每个线程和变量的一个独立副本
只读共享。即使用不可变的对象。
使用 final 去修饰字段,这样这个字段的 “值” 是不可改变的
推荐:面试经典:HashMap 容量为什么总是为 2 的次幂?注意 final 如果修饰的是一个对象引用,比如 set, 它本身包含的值是可变的
创建一个不可变的类,来包含多个可变的数据。class OneValue{ //创建不可变对象,创建之后无法修改,事实上这里也没有提供修改的方法 private final BigInteger last; private final BigInteger[] lastfactor; public OneValue(BigInteger i,BigInteger[] lastfactor){ this.last=i; this.lastfactor=Arrays.copy(lastfactor,lastfactor.length); } public BigInteger[] getF(BigInteger i){ if(last==null || !last.equals(i)){ return null; }else{ return Arrays.copy(lastfactor,lastfactor.length) } } } class MyService { //volatile使得cache一经更改,就能被所有线程感知到 private volatile OneValue cache=new OneValue(null,null); public void handle(BigInteger i){ BigInteger[] lastfactor=cache.getF(i); if(lastfactor==null){ lastfactor=factor(i); //每次都封装最新的值 cache=new OneValue(i,lastfactor) } nextHandle(lastfactor) } }如何构造线程安全的类?
实例封闭。将一个对象封装到另一个对象中,这样能够访问被封装对象的所有代码路径都是已知的,通过合适的加锁策略可以确保被封装对象的访问是线程安全的。
推荐:面试官为什么喜欢问 HashMap 和 ConcurrentHashMap ?java 中的 Collections.synchronizedList 使用的原理就是这样。部分代码为
public static List synchronizedList(List list) {return (list instanceof RandomAccess ?new SynchronizedRandomAccessList<>(list) :new SynchronizedList<>(list));}
复制代码SynchronizedList 的实现, 注意此处用到的 mutex 是内置锁
搜索公众号后端架构师后台回复“面试”,获取一份惊喜礼包。static class SynchronizedListextends SynchronizedCollectionimplements List {private static final long serialVersionUID = -7754090372962971524L;final List list;public E get(int index) {synchronized (mutex) {return list.get(index);}}public E set(int index, E element) {synchronized (mutex) {return list.set(index, element);}}public void add(int index, E element) {synchronized (mutex) {list.add(index, element);}}public E remove(int index) {synchronized (mutex) {return list.remove(index);}}}
复制代码mutex 的实现
static class SynchronizedCollection implements Collection, >Serializable { private static final long serialVersionUID = 3053995032091335093L; final Collection c; // Backing Collection final Object mutex; // Object on which to synchronize SynchronizedCollection(Collection c) { if (c==null) throw new NullPointerException(); this.c = c; mutex = this; // mutex实际上就是对象本身 }什么是监视器模式
java 的监视器模式,将对象所有可变状态都封装起来,并由对象自己的内置锁来保护, 即是一种实例封闭。比如 HashTable 就是运用的监视器模式。它的 get 操作就是用的 synchronized,内置锁,来实现的线程安全
public synchronized V get(Object key) { Entry tab[] = table; int hash = hash(key); int index = (hash & 0x7FFFFFFF) % tab.length; for (Entry e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return e.value; } } return null;}内置锁
每个对象都有内置锁。内置锁也称为监视器锁。或者可以简称为监视器
线程执行一个对象的用 synchronized 修饰的方法时,会自动的获取这个对象的内置锁,方法返回时自动释放内置锁,执行过程中就算抛出异常也会自动释放。
以下两种写法等效:
synchronized void myMethdo(){ //do something}void myMethdo(){ synchronized(this){ //do somthding } }官方文档
私有锁
public class PrivateLock{private Object mylock = new Object(); //私有锁void myMethod(){synchronized(mylock){//do something}}
}
它也可以用来保护对象,相对内置锁,优势在于私有锁可以有多个,同时可以让客户端代码显示的获取私有锁
类锁
在 staic 方法上修饰的,一个类的所有对象共用一把锁
把线程安全性委托给线程安全的类
如果一个类中的各个组件都是线程安全的,该类是否要处理线程安全问题?视情况而定。
只有单个组件,且它是线程安全的。
public class DVT{ private final ConcurrentMap locations; private final Map unmodifiableMap; public DVT(Map points){ locations=new ConcurrentHashMap(points); unmodifiableMap=Collections.unmodifiableMap(locations); } public Map getLocations(){ return unmodifiableMap; } public Point getLocation(String id){ return locations.get(id); } public void setLocation(String id,int x,int y){ if(locations.replace(id,new Point(x,y))==null){ throw new IllegalArgumentException("invalid "+id); } } } public class Point{ public final int x,y; public Point(int x,int y){ this.x=x; this.y=y; } }线程安全性分析Point 类本身是无法更改的,所以它是线程安全的,DVT 返回的 Point 方法也是线程安全的
DVT 的方法 getLocations 返回的对象是不可修改的,是线程安全的
setLocation 实际操作的是 ConcurrentHashMap 它也是线程安全的
综上,DVT 的安全交给了‘locations’,它本身是线程安全的,DVT 本身虽没有任何显示的同步,也是线程安全。这种情况下,就是 DVT 的线程安全实际是委托给了‘locations’, 整个 DVT 表现出了线程安全。
线程安全性委托给了多个状态变量
只要多个状态变量之间彼此独立,组合的类并不会在其包含的多个状态变量上增加不变性。依赖的增加则无法保证线程安全
public class NumberRange{private final AtomicInteger lower = new AtomicInteger(0);private final AtomicInteger upper = new AtomicInteger(0); public void setLower(int i){ //先检查后执行,存在隐患 if (i>upper.get(i)){ throw new IllegalArgumentException('can not ..'); } lower.set(i); } public void setUpper(int i){ //先检查后执行,存在隐患 if(i{ public List list=Collections.synchronizedList(new ArrayList()); ... public synchronized boolean putIfAbsent(E x){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; } }这里的 putIfAbsent 并不能带来线程安全,原因是 list 的内置锁并不是 ListHelper, 也就是 putIfAbsent 相对 list 的其它方法并不是原子的。Collections.synchronizedList 是锁在 list 本身的,正确方式为
public boolean putIfAbsent(E x){ synchronized(list){ boolean absent = !list.contains(x); if(absent){ list.add(x); } return absent; }}另外可以不管要操作的类是否是线程安全,对类统一添加一层额外的锁。实现参考 Collections.synchronizedList 方法PS:欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,欢迎转发分享给更多人。版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!欢迎加入后端架构师交流群,在后台回复“学习”即可。最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。
别找了,想获取史上最简单的Java大厂面试题学习资料
扫下方二维码回复「面试」就好了猜你还想看
阿里、腾讯、百度、华为、京东最新面试题汇集
超实用!18 个开箱即用的 Shell 脚本,拿好了~受不了996压力,某程序员搭建涉黄网站,获利420万被抓985研究生组团诈骗,一个中招就关App,涉案金额超1亿,受害人遍布全国
嘿,你在看吗?
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
