当前位置: 首页 > article >正文

Java多线程——对象的共享

可见性

一个线程修改了对象状态后,其他线程能够看到发生的状态变化

public class NoVisibility {

    private static boolean ready;
    private static int number;

    private static class ReaderThread extends Thread {
        @Override
        public void run() {
            while (!ready)
                Thread.yield();
            System.out.println(number);
        }
    }

    public static void main(String[] args) {
        new ReaderThread().start();
        number = 42;
        ready = true;
    }
}

如上,子线程判断ready和输出number,主线程修改后,可能出现

  • 子线程一直循环(未看到ready的值)
  • number输出0(子线程可能先看到写入ready后写入number)

数据在多个线程共享时,就应该使用同步确保可见性

失效数据

对于如下类,可能出现一个线程在set过程中,另外一个线程调用get获取失效数据

class MutableInteger {
    private int value;
    public  int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

对于可共享变量,应需要同时对get和set同步

class MutableInteger {
    @GuardedBy("this")
    private int value;
    public synchronized int getValue() {
        return value;
    }
    public synchronized void setValue(int value) {
        this.value = value;
    }
}

加锁不仅仅局限于互斥,还包含内存可见性,为了所有线程都能获得共享数据的最新值,其get和set都必须在同一个锁上同步

非原子的64位操作

非volatile类型的64位数值变量(double和long)读写操作分为两个32位操作,多线程下可能出现失效数据,应该用volatile或锁保护

volatile变量

  • 用于确保变量的更新操作通知到其他线程
  • 对该变量的操作不会重排序,不会被缓存在寄存器,
  • 可以理解对其get和set都声明为synchronized,但实际实现不会加锁也不会导致线程阻塞,更为轻量级

经典用法:检测某个状态标志位以判断是否退出循环,此时比用锁更简便

class A {
    volatile boolean isSleep;
    public void check() {
        while (!isSleep) {
            //......
        }
    }
}

加锁和原子变量能确保可见性和原子性,volatile只能确保可见性

使用volatile变量的条件:

  • 对变量的写入操作不依赖变量的当前值,或者能确保只有单个
    线程更新变量的值
  • 该变量不会与其他状态变量一起纳入不变性条件中
  • 在访问变量时不需要加锁

发布与逸出

发布指的是对象在当前作用域之外的代码中使用,当某个不应该发布的对象被发布时称为逸出,常见的逸出有

  • 对象引用保持到公有静态变量,而不是不可变对象
  • 非私有方法返回一个对象引用,而不是其不可变/克隆对象
  • 外部内部类导致外部类逸出

第三种常见形式是this在构造函数中逸出,如下构造函数还未结束,线程就持有了对象实例,这会导致未知状态

class A {
    public A() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                A.this.test();
            }
        }).start();
        //...
    }


    private void test() {
        System.out.println(getClass());
    }
}

只有构造函数返回时,对象才处于可预测和一致的状态,构造函数创建线程不应立即启动,如下等构造完后再启动线程

class A {
    private void test() {
        System.out.println(getClass());
    }

    private final Runnable mRunnable;

    private A() {
        mRunnable = new Runnable() {
            @Override
            public void run() {
                A.this.test();
            }
        };
    }

    public static A getInstance() {
        A a = new A();
        new Thread(a.mRunnable).start();
        return a;
    }
}

线程封闭

仅在单线程内访问数据,就不需要同步,称为线程封闭

栈封闭

局部变量封闭在执行线程的栈中,其他线程无法访问这个栈

class A {
    public int test() {
        int num = 0;
        //...
        Set<String> set = new HashSet<>();
        return num;
    }
}

如上,基本数据类型num无引用,可封闭在线程内,若是返回set则封闭性被破坏导致对象逸出

ThreadLocal

使线程中的某个值与保存值的对象关联起来,每个使用该变量的线程都存有一个独立的副本,ThreadLocal<T>相当于Map<Thread, T>

当将单线程应用移植到多线程环境中,可以将共享的全局变量转换为ThreadLocal对象,以维持线程安全性

不变性

不可变对象一定是线程安全的,不可变对象需满足

  • 对象创建后其状态不能修改
  • 对象所有域都是final
  • 对象创建期间,this引用没有逸出

可通过volatile和不可变对象确保线程安全性和可见性

public class A {

    @GuardedBy("this")
    private long count;
    @GuardedBy("this")
    private boolean isOdd;

    public long getCount() {
        return count;
    }

    public void test() {
        //....
        synchronized (this) {
            count++;
            isOdd = getCount() % 2 == 0;
        }
    }
}

对于如上加锁代码,提取不可变类,用volatile确保数据更新后的可见性,每次更新创建新的不可变对象

public class A {

    private volatile Counter mCounter = new Counter(0);

    private long currentCount = 0;

    public void test() {
        //....
        currentCount++;
        if (currentCount != mCounter.getCount()) {
            mCounter = new Counter(currentCount);
        }
    }
}

class Counter {
    private final long count;
    private final boolean isOdd;

    Counter(long count) {
        this.count = count;
        this.isOdd = count % 2 == 0;
    }

    public long getCount() {
        return count;
    }
}

安全发布

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见

常用模式

一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用,如pubic static A a = new A()
  • 将对象的引用保存到volatile类型的域或者AtomicReferance对象
  • 将对象的引用保存到某个正确构造对象的final类型域中
  • 将对象的引用保存到一个由锁保护的域中,如系统自带的线程安全库

线程安全库中的容器类提供了以下的安全发布保证:

  • 通过将一个键或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问)
  • 通过将某个元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程
  • 通过将某个元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程

事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,如下将可变的Date放入Map就不会改变,访问时不需要额外的同步

public Map<String, Date> lastLogin = Collections.synchronizedMap(new HashMap<>());

http://www.kler.cn/a/527593.html

相关文章:

  • electron 应用开发实践
  • AI学习指南HuggingFace篇-Hugging Face 的环境搭建
  • Pandas进行MongoDB数据库CRUD
  • Qt中json的使用
  • 创建 priority_queue - 进阶(内置类型)c++
  • 中国股市“慢牛”行情的实现路径与展望
  • DeepSeek本地部署(windows)
  • 软件测试(认识测试)
  • 无人机图传模块 wfb-ng openipc-fpv,4G
  • 【易理解】04_什么是try-catch-throw语句?
  • socket编程短平快
  • 计算机网络一点事(24)
  • 漏洞扫描工具之xray
  • 【视频+图文讲解】HTML基础2-html骨架与基本语法
  • OpenCV:Harris、Shi-Tomasi角点检测
  • 【小白学AI系列】NLP 核心知识点(六)Softmax函数介绍
  • 如何优化轮式移动机器人的运动稳定性?
  • 仿真设计|基于51单片机的低频信号控制系统仿真
  • PostgreSQL图插件AGE
  • DeepSeek-R1 论文解读 —— 强化学习大语言模型新时代来临?
  • Java 泛型<? extends Object>
  • 小程序-基础加强
  • 最新Java开发进阶!Java进阶面试资料无偿分享_java面试最新资料
  • SpringBoot入门:快速构建第一个Web应用
  • 需求分析应该从哪些方面来着手做?
  • 高低频混合组网系统中基于地理位置信息的信道测量算法matlab仿真