资讯专栏INFORMATION COLUMN

什么时候线程不安全?怎样做到线程安全?怎么扩展线程安全的类?

wh469012917 / 3301人阅读

摘要:什么时候会出现线程不安全操作并非原子。只有单个组件,且它是线程安全的。这种情况下,就是的线程安全实际是委托给了整个表现出了线程安全。

当多个线程去访问某个类时,如果类会表现出我们预期出现的行为,那么可以称这个类是线程安全的。

什么时候会出现线程不安全?

操作并非原子。多个线程执行某段代码,如果这段代码产生的结果受不同线程之间的执行时序影响,而产生非预期的结果,即发生了竞态条件,就会出现线程不安全;

常见场景:

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去修饰字段,这样这个字段的“值”是不可改变的

注意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)
   }
}

如何构造线程安全的类?

实例封闭。将一个对象封装到另一个对象中,这样能够访问被封装对象的所有代码路径都是已知的,通过合适的加锁策略可以确保被封装对象的访问是线程安全的。

java中的Collections.synchronizedList使用的原理就是这样。部分代码为

  public static  List synchronizedList(List list) {
           return (list instanceof RandomAccess ?
                   new SynchronizedRandomAccessList<>(list) :
                   new SynchronizedList<>(list));
       }
SynchronizedList的实现,注意此处用到的mutex是内置锁
       static class SynchronizedList
           extends SynchronizedCollection
           implements 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
    } 
    
}
> [官方文档](https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html)

私有锁

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
    
setLower和setUpper都是‘先检查后执行’的操作,但是没有足够的加锁机制保证操作的原子性。假设原始范围是(0,10),一个线程调用 setLower(5),一个设置setUpper(4)错误的执行时序将可能导致结果为(5,4)  
如何对现有的线程安全类进行扩展?
假设需要扩展的功能为 ‘没有就添加’。

直接修改原有的代码。但通常没有办法修改源代码

继承。继承原有的代码,添加新的功能。但是同步策略保存在两份文件中,如果底层同步策略变更,很容易出问题

组合。将类放入一个辅助类中,通过辅助类的操作代码。
比如扩展 Collections.synchronizedList。期间需要注意锁的机制,错误方式为

    public class ListHelper{
        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方法

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/72400.html

相关文章

  • 假如我是面试官,我会这样虐你

    摘要:又是金三银四的时候,我希望这份面试题能够祝你一臂之力自我和项目相关自我介绍你觉得自己的优点是你觉得自己有啥缺点你有哪些你为什么要离开上家公司你上家公司在,我们公司在,离这么远为什么要选择我们这里上家公司的同事和领导是怎么评价你的介绍下你的上 又是金三银四的时候,我希望这份面试题能够祝你一臂之力! 自我和项目相关 1、自我介绍 2、你觉得自己的优点是?你觉得自己有啥缺点? 3、你有哪些 ...

    Benedict Evans 评论0 收藏0
  • 史上最全阿里 Java 面试题总结

    摘要:以下为大家整理了阿里巴巴史上最全的面试题,涉及大量面试知识点和相关试题。的内存结构,和比例。多线程多线程的几种实现方式,什么是线程安全。点击这里有一套答案版的多线程试题。线上系统突然变得异常缓慢,你如何查找问题。 以下为大家整理了阿里巴巴史上最全的 Java 面试题,涉及大量 Java 面试知识点和相关试题。 JAVA基础 JAVA中的几种基本数据类型是什么,各自占用多少字节。 S...

    winterdawn 评论0 收藏0
  • Java并发编程之旅总览

    摘要:线程安全的概念什么时候线程不安全怎样做到线程安全怎么扩展线程安全的类对线程安全的支持对线程安全支持有哪些中的线程池的使用与中线程池的生命周期与线程中断中的锁中常见死锁与活锁的实例线程同步机制显示锁使用与原理原理剖析原理中的与原理偏向锁状态 showImg(https://segmentfault.com/img/bVblUE9?w=1354&h=1660); 线程安全的概念 showI...

    Harpsichord1207 评论0 收藏0
  • 40道阿里巴巴JAVA研发岗多线程面试题详解,你能答出多少

    摘要:但是单核我们还是要应用多线程,就是为了防止阻塞。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。 1、多线程有什么用?一个可能在很多人看来很扯淡的一个问题:我会用多线程就好了,还管它有什么用?在我看来,这个回答更扯淡。所谓知其然知其所以然,会用只是知其然,为什么用才是知其所以然,只有达到知其然知其所以然的程度才可以说是把一个知识点...

    lpjustdoit 评论0 收藏0
  • Java问题汇总,持续更新到GitHub

    摘要:目录介绍问题汇总具体问题好消息博客笔记大汇总年月到至今,包括基础及深入知识点,技术博客,学习笔记等等,还包括平时开发中遇到的汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善开源的文件是格式的同时也开源了生活博客,从年 目录介绍 00.Java问题汇总 01.具体问题 好消息 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技...

    beita 评论0 收藏0

发表评论

0条评论

wh469012917

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<