并发编程 synchronized (四) 深入分析线程安全

本文深入探讨了Java中的线程安全问题,分析了Vector虽然方法内部同步但组合使用时仍可能出现线程不安全的情况。还讨论了不可变类如Integer和String的线程安全性,指出其不可变性确保了线程安全。此外,通过卖票和转账的示例,阐述了解决线程安全问题的方法,包括使用对象锁和类锁。最后,提出了自定义不可变类的设计原则。

目录

常见线程安全类

线程安全类也会线程不安全 

如:Vector

 Vector 线程不安全情况举例

 产生原因

不可变类线程安全性

Integer、String 等为什么线程安全?

String 中  replace 方法如何解释?

String 等类什么时候会出现线程安全问题?如何解决? 

自定义不可变类

解决线程安全问题

卖票

转账


 

常见线程安全类

String

Integer

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的类

线程安全类也会线程不安全 

如:Vector

public synchronized int size() { // 返回大小
        return elementCount;
}

public synchronized E remove(int index) { // 移除操作
        ......
}

① 如上展示了 Vector 两个基本操作,获取大小移除指定位置元素

② 方法都加了锁,保证了内部一系列操作的原子性;但方法组合在一起为非原子性

 Vector 线程不安全情况举例

@Slf4j(topic = "c.UnsafeVector")
public class UnsafeVector {
    private static List<Integer> vector = new Vector();
    static { // 加入 1 个元素
        vector.add(0);
    }

    public static void main(String[] args) {
        log.debug("开始测试前,vector 容量为 {}", vector.size());
        for(int i = 0; i < 2; i++){ // 启动线程数量大于 vector 容量
            new Thread(() -> {
                if(vector.size() != 0){
                    try {
                        Thread.sleep(1000); // 睡一秒,以防当前线程刚释放锁又马上获得锁
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.debug("移除 {}", vector.remove(0));
                    return;
                }
                log.debug("容量为 0 无法执行移除操作");
            }, "t" + i).start();
        }
    }
}

① 唯一元素,在线程 t1 被移除 

② 线程 t0 本应输出 "容量为 0 无法执行移除操作" 但是抛出数组越界异常

当两个原子性方法组合到一起,就不是原子性的了

 产生原因

省略了上下文切换说明

不可变类线程安全性

Integer、String 等为什么线程安全?

① 类用 final 修饰,代表不可继承,也就无法重写它们原本的方法

② 大部分属性用 final 修饰,代表不可变;即使没有 final 修饰,没有任何可以修改属性的方法;都足以说明:它们的属性是不可变的

总结:想要设计出一个线程安全的类,只要它的属性都不可变即可(或者根本没有属性)

String 中  replace 方法如何解释?

假如与原来相比没有变化,则直接返回原字符串;假如发生了变化,则新建一个字符串返回 

并没有改变原本字符串的属性

String 等类什么时候会出现线程安全问题?如何解决? 

不可变类的引用是可以改变,这种时候就会出现线程安全问题

解决:用 final 使其引用不可变;读写操作时加锁

自定义不可变类

public class MyInteger {
    private final int value; // 不可变属性

    public MyInteger(){ // 无参构造方法,自动赋初值
        this.value = 0;
    }

    public MyInteger(int value){ // 有参构造方法,通过传进来的参数赋初值
        this.value = value;
    }

    public MyInteger(MyInteger myInteger){ // 有参构造方法,通过传进来的参数赋初值
        this.value = myInteger.value;
    }

    public int getValue(){ // 获取属性值的方法
        return this.value;
    }

    public MyInteger replace(int newValue){ // 一个替换方法
        if(newValue != this.value)
        return new MyInteger(newValue);
        return this;
    }
}

@Slf4j(topic = "c.Test9")
class Test9{
    public static void main(String[] args) {
        MyInteger myInteger1 = new MyInteger();
        log.debug("myInteger1 属性值为 {}", myInteger1);
        MyInteger myInteger2 = myInteger1.replace(1);
        log.debug("myInteger1 == myInteger2 ? {}", myInteger1 == myInteger2);
    }
}

解决线程安全问题

卖票

@Slf4j(topic = "c.SellTicket")
public class SellTicket {
    private final static int BUYER_NUMS = 1000; // 用户数量

    public static void main(String[] args) throws InterruptedException {
        Seller seller = new Seller(2000);
        List<Thread> buyers = new ArrayList(BUYER_NUMS);
        for(int i = 0; i < BUYER_NUMS; i++){
            Thread thread = new Thread(() -> {
                new Buyer().buyTickets(seller);
            }, "t" + i);
            buyers.add(thread);
        }

        for(Thread thread : buyers){
            thread.start();
        }

        for(Thread thread : buyers){
            thread.join();
        }

        List<Integer> sellNums = seller.soldTickets();
        int sum = 0;
        for(Integer sellNum : sellNums){
            sum += sellNum;
        }
        log.debug("卖出票:{},剩余:{}", sum, seller.restTicketNums());
    }
}

@Slf4j(topic = "c.Seller")
class Seller{
    private int ticketNums; // 票数
    private List<Integer> sellNums; // 售票记录

    public Seller(int ticketNums){
        this.ticketNums = ticketNums;
        this.sellNums = new Vector();
    }

    public void sell(int count){ // 卖票
        if(this.ticketNums >= count){
            ticketNums -= count;
            this.sellNums.add(count);
            return;
        }
        log.debug("票数不足~");
    }

    public int restTicketNums(){ // 剩余票数
        return this.ticketNums;
    }

    public List<Integer> soldTickets(){ // 卖票记录
        return sellNums;
    }
}

class Buyer{
    private int needTicketNums = new Random().nextInt(10); // 用户需要的票数

    public void buyTickets(Seller seller){ // 买票
        seller.sell(needTicketNums);
    }
}

buyers 数组实际只被 main 线程使用,并没有共享

对象 seller 被多个 buyer 对象共享,临界区如下

public void sell(int count){ // 卖票
      if(this.ticketNums >= count){
          ticketNums -= count; // 减掉卖出票数
          this.sellNums.add(count); // 添加卖票记录
          return;
      }
      log.debug("票数不足~");
}

① 对票数 ticketNums 与 售票记录 sellNums 都存在读写操作 

② 但是,售票记录为 Vector 对象 ,已经有对象锁保证它单个操作的原子性(线程安全)

③ 票数 ticketNums 需要被保护;它只存在 seller 对象中,所以只需要加一把对象锁

public synchronized void sell(int count)

public synchronized int restTicketNums() // 剩余票数
public synchronized List<Integer> soldTickets() // 卖票记录   

 由于此实例中,在其他线程都结束时才调用这两个方法,所以可以不加锁;但其实加锁更好

转账

@Slf4j(topic = "c.TransferMoney")
public class TransferMoney {
    private static final int TRANSFER_NUMS = 1000;

    public static void main(String[] args) throws InterruptedException {
        Account account1 = new Account(100); // 金额初始值都为 100
        Account account2 = new Account(100); // 金额初始值都为 100
        Random random = new Random();

        Thread t1 = new Thread(() -> {
            for(int i = 0; i < TRANSFER_NUMS; i++){ // 都循环转账 1000 次
                account2.transfer(account1, random.nextInt(5));
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < TRANSFER_NUMS; i++){ // 都循环转账 1000 次
                account1.transfer(account2, random.nextInt(5));
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        log.debug("两个账号金额总和:{}", account1.getMoney() + account2.getMoney());
    }
}

@Slf4j(topic = "c.Account")
class Account{
    private int money;

    public Account(int money){
        this.money = money;
    }

    public int getMoney() { // 获取金额
        return this.money;
    }

    public void setMoney(int money) { // 设置新金额
        this.money = money;
    }

    public void transfer(Account other, int num){ // 转账
        if(this.money >= num){
            this.setMoney(this.money - num); // 自己账户减钱
            other.setMoney(other.getMoney() + num); // 对方账户加钱
            return;
        }
        log.debug("余额不足~");
    }
}

 很明显 account1 account2 被线程 t1、t2 共享;临界区如下

public void transfer(Account other, int num){ // 转账
   if(this.money >= num){
       this.setMoney(this.money - num); // 自己账户减钱
       other.setMoney(other.getMoney() + num); // 对方账户加钱
       return;
   }
   log.debug("余额不足~");
}

① 假如直接加对象锁,那么 account1account2 相当于两把锁

② 当 t1 获取 account1 锁时,不妨碍 t2 获取 account2 锁;

当对象不同时,但是锁需要一样时;那就加类锁

synchronized (Account.class){ // 转账
   if(this.money >= num){
        this.setMoney(this.money - num); // 自己账户减钱
        other.setMoney(other.getMoney() + num); // 对方账户加钱
        return;
   }
   log.debug("余额不足~");
}

这是一个基于AI视觉识别与3D引擎技术打造的沉浸式交互圣诞装置。 简单来说,它是一棵通过网页浏览器运行的数字智慧圣诞树,你可以用真实的肢体动作来操控它的形态,并将自己的回忆照片融入其中。 1. 核心技术组成 这个作品是由三个尖端技术模块组成的: Three.js 3D引擎:负责渲染整棵圣诞树、动态落雪、五彩挂灯和树顶星。它创建了一个具备光影和深度感的虚拟3D空间。 MediaPipe AI手势识别:调用电脑摄像头,实时识别手部的21个关键点。它能读懂你的手势,如握拳、张开或捏合。 GSAP动画系统:负责处理粒子散开与聚合时的平滑过渡,让成百上千个物体在运动时保持顺滑。 2. 它的主要作用与功能 交互式情感表达: 回忆挂载:你可以上传本地照片,这些照片会像装饰品一样挂在树上,或者像星云一样环绕在树周围。 魔法操控:握拳时粒子迅速聚拢,构成一棵挺拔的圣诞树;张开手掌时,树会瞬间炸裂成星光和雪花,照片随之起舞;捏合手指时视线会拉近,让你特写观察某一张选中的照片。 节日氛围装饰: 在白色背景下,这棵树呈现出一种现代艺术感。600片雪花在3D空间里缓缓飘落,提供视觉深度。树上的彩色粒子和白色星灯会周期性地呼吸闪烁,模拟真实灯串的效果。 3. 如何使用 启动:运行代码后,允许浏览器开启摄像头。 装扮:点击上传照片按钮,选择温馨合照。 互动:对着摄像头挥动手掌可以旋转圣诞树;五指张开让照片和树化作满天星辰;攥紧拳头让它们重新变回挺拔的树。 4. 适用场景 个人纪念:作为一个独特的数字相册,在节日陪伴自己。 浪漫惊喜:录制一段操作手势让照片绽放的视频发给朋友。 技术展示:作为WebGL与AI结合的案例,展示前端开发的潜力。
【顶级EI复现】计及连锁故障传播路径的电力系统 N-k 多阶段双层优化及故障场景筛选模型(Matlab代码实现)内容概要:本文提出了一种计及连锁故障传播路径的电力系统N-k多阶段双层优化及故障场景筛选模型,并提供了基于Matlab的代码实现。该模型旨在应对复杂电力系统中可能发生的N-k故障(即多个元件相继失效),通过构建双层优化框架,上层优化系统运行策略,下层模拟故障传播过程,从而实现对关键故障场景的有效识别与筛选。研究结合多阶段动态特性,充分考虑故障的时序演化与连锁反应机制,提升了电力系统安全性评估的准确性与实用性。此外,模型具备良好的通用性与可扩展性,适用于大规模电网的风险评估与预防控制。; 适合人群:电力系统、能源互联网及相关领域的高校研究生、科研人员以及从事电网安全分析、风险评估的工程技术人员。; 使用场景及目标:①用于电力系统连锁故障建模与风险评估;②支撑N-k故障场景的自动化筛选与关键脆弱环节识别;③为电网规划、调度运行及应急预案制定提供理论依据和技术工具;④服务于高水平学术论文复现与科研项目开发。; 阅读建议:建议读者结合Matlab代码深入理解模型构建细节,重点关注双层优化结构的设计逻辑、故障传播路径的建模方法以及场景削减技术的应用,建议在实际电网数据上进行测试与验证,以提升对模型性能与适用边界的认知。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值