专栏首页Java冰冻三尺并发业务中,线程安全与否很重要,来看看你懂多少?

并发业务中,线程安全与否很重要,来看看你懂多少?

作者:爬蜥

链接: https://juejin.im/post/5b7d68f66fb9a019d80a9002

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

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

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

常见场景:

1.count++。它本身包含三个操作,读取、修改、写入,多线程时,由于线程执行的时序不同,有可能导致两个线程执行后 count 只加了 1,而原有的目标确实希望每次执行都加 1;

2.单例。多个线程可能同时执行到instance == null成立,然后新建了两个对象,而原有目标是希望这个对象永远只有一个;

public MyObj getInstance(){

   if (instance == null){

        instance = new MyObj();

   }

    return instance

 }

解决方式是:当前线程在操作这段代码时,其它线程不能对进行操作

常见方案:

1.单个状态使用 java.util.concurrent.atomic 包中的一些原子变量类,注意如果是多个状态就算每个操作是原子的,复合使用的时候并不是原子的;

2.加锁。比如使用 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

ove

修改不可见。读线程无法感知到其它线程写入的值

常见场景:

1.重排序。在没有同步的情况下,编译器、处理器以及运行时等都有可能对操作的执行顺序进行调整,即写的代码顺序和真正的执行顺序不一样, 导致读到的是一个失效的值

2.读取 long、double 等类型的变量。JVM 允许将一个 64 位的操作分解成两个 32 位的操作,读写在不同的线程中时,可能读到错误的高低位组合

常见方案:

1.加锁。所有线程都能看到共享变量的最新值;

2.使用 Volatile 关键字声明变量。只要对这个变量产生了写操作,那么所有的读操作都会看到这个修改;

注意:Volatile 并不能保证操作的原子性,比如count++操作同样有风险,它仅保证读取时返回最新的值。使用的好处在于访问 Volatile 变量并不会执行加锁操作,也就不会阻塞线程。

不同步的情况下如何做到线程安全?

1.线程封闭。即仅在单线程内访问数据,线程封闭技术有以下几种:

Ad-hoc 线程封闭。即靠自己写程序来实现,比如保证程序只在单线程上对 volatile 进行 读取-修改-写入

栈封闭。所有的操作都反生执行线程的栈中,比如在方法中的一个局部变量

ThreadLocal 类。内部维护了每个线程和变量的一个独立副本

2.只读共享。即使用不可变的对象。

使用 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 <T> List<T> synchronizedList(List<T> list) {

        return (list instanceof RandomAccess ?

                new SynchronizedRandomAccessList<>(list) :

                new SynchronizedList<>(list));

    }

SynchronizedList 的实现, 注意此处用到的 mutex 是内置锁

    static class SynchronizedList<E>

        extends SynchronizedCollection<E>

        implements List<E> {

        private static final long serialVersionUID = -7754090372962971524L;



        final List<E> 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<E> implements Collection<E>, >Serializable {

    private static final long serialVersionUID = 3053995032091335093L;

    final Collection<E> c;  // Backing Collection

    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> 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<K,V> 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 方法上修饰的,一个类的所有对象共用一把锁

把线程安全性委托给线程安全的类

如果一个类中的各个组件都是线程安全的,该类是否要处理线程安全问题?

视情况而定。

1.只有单个组件,且它是线程安全的。

public class DVT{

   private final ConcurrentMap<String,Point> locations;

   private final Map<String,Point> unmodifiableMap;

       

   public DVT(Map<String,Point> points){

       locations=new ConcurrentHashMap<String,Point>(points);

       unmodifiableMap=Collections.unmodifiableMap(locations);

       }

       

   public Map<String,Point> 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 表现出了线程安全。

2.线程安全性委托给了多个状态变量

只要多个状态变量之间彼此独立,组合的类并不会在其包含的多个状态变量上增加不变性。依赖的增加则无法保证线程安全

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<lower.get(i)){

       throw new IllegalArgumentException('can not ..');

       }

       upper.set(i);

       }

           

   }

setLower 和 setUpper 都是‘先检查后执行’的操作,但是没有足够的加锁机制保证操作的原子性。假设原始范围是 (0,10), 一个线程调用 setLower(5), 一个设置 setUpper(4) 错误的执行时序将可能导致结果为(5,4)

如何对现有的线程安全类进行扩展?

假设需要扩展的功能为 ‘没有就添加’。

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

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

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

   public class ListHelper<E>{

       public List<E> list=Collections.synchronizedList(new ArrayList<E>());

       ...

       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://juejin.im/post/5b7d68f66fb9a019d80a9002

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一次非常有意思的 SQL 优化经历: 从 30248.271s 到 0.001s

    链接:https://www.cnblogs.com/tangyanbo/p/4462734.html

    Java小咖秀
  • 玩转Java8中的 Stream之从零认识 Stream

    相信Java8的Stream 大家都已听说过了,但是可能大家不会用或者用的不熟,文章将带大家从零开始使用,循序渐进,带你走向Stream的巅峰。

    Java小咖秀
  • SpringBoot的埋点监控你做了吗

    spring-actuator做度量统计收集,使用Prometheus(普罗米修斯)进行数据收集,Grafana(增强ui)进行数据展示,用于监控生成环境机器的...

    Java小咖秀
  • 探寻ASP.NET MVC鲜为人知的奥秘(2):与Entity Framework配合,让异步贯穿始终

    Why 在应用程序,尤其是互联网应用程序中,性能一直是很多大型网站的困扰,由于Web2.0时代的到来,人们更多的把应用程序从C/S结构迁移到B/S结构,这样会带...

    小白哥哥
  • JAVA中volatile、synchronized和lock解析

    在研究并发程序时,我们需要了解java中关键字volatile和synchronized关键字的使用以及lock类的用法。

    哲洛不闹
  • 并发编程的基础

      对于并发编程这块知识点的掌控一直不是很好,基本都是停留在使用synchronized阶段,于是决定开一博客专题记录知识点。

    会说话的丶猫
  • 线程间通讯:WaitHandler使用实例及分析

    实例效果: ? 1.点击“启动线程”会启动一个线程t每隔2秒在listbox上插入一条新记录。 2.点击“关闭线程”会停止线程t,但不是马上停止而是等待线程t当...

    ^_^肥仔John
  • 微服务开源框架TARS的RPC源码解析 之 初识TARS C++服务端

    导语:微服务开源框架TARS的RPC调用包含客户端与服务端,《微服务开源框架TARS的RPC源码解析》系列文章将从初识客户端、客户端的同步及异步调用、初识服务端...

    TARS基金会
  • 线程生命周期,五大状态转换分析

    本章学习完成,你将会对线程的生命周期有清楚的认识,并且明白不同状态之间是如何转换的,以及对java线程状态枚举类解读。

    公众号 IT老哥
  • Java进阶(二)当我们说线程安全时,到底在说什么

    Jason Guo

扫码关注云+社区

领取腾讯云代金券