专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

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

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

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

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

    常见场景:

    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
>     over
>     
  • 修改不可见。读线程无法感知到其它线程写入的值

    常见场景:

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

如何构造线程安全的类?

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

> 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
            } 

        }

    > [官方文档][Link 1]
 *  私有锁
        public class PrivateLock{
            private Object mylock = new Object(); //私有锁
            void myMethod(){
                synchronized(mylock){
                    //do something
                }
            }
        }

    它也可以用来保护对象,相对内置锁,优势在于私有锁可以有多个,同时可以让客户端代码显示的获取私有锁
 *  类锁  
    在staic方法上修饰的,一个类的所有对象共用一把锁

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

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

视情况而定。

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://tech.souyunku.com/47207

未经允许不得转载:搜云库技术团队 » 什么时候线程不安全?怎样做到线程安全?怎么扩展线程安全的类?

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们