《JUC并发编程 - 高级篇》03 - 共享对象之管程 上篇(共享带来的问题 | synchronized | 线程八锁 | 线程安全类)(三)

简介: 《JUC并发编程 - 高级篇》03 - 共享对象之管程 上篇(共享带来的问题 | synchronized | 线程八锁 | 线程安全类)

例6

//本类是线程安全的,因为userService中没有可变的属性.
public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
      userService.update(...);
    }
}
//本类是线程安全的,因为userDao被定义成了局部变量.
public class UserServiceImpl implements UserService {
    // 是否安全 Yes
    public void update() {
        UserDao userDao = new UserDaoImpl();
      userDao.update();
    }
}
//本类的分析过程同上 例5
public class UserDaoImpl implements UserDao {
    //是否安全 No
    private Connection conn = null;//不建议这样写,即使外部service中使用的局部变量
    public void update() throws SQLException{
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","")
         //...
        conn.close();
    }
}

例7

public abstract class Test {
  public void bar() {
        // 是否安全 No
        //只含有局部变量的类不一定就是线程安全的,要看这个变量引用是否在子类/其他方法 以一个新的线程被用到
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        foo(sdf);
    }
    public abstract foo(SimpleDateFormat sdf);
    public static void main(String[] args) {
       new Test().bar();
    }
}

其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

//当子类这样重写foo的方法时,就不是线程安全的
public void foo(SimpleDateFormat sdf) {
  String dateStr = "1999-10-11 00:00:00";
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            try {
            sdf.parse(dateStr);
            } catch (ParseException e) {
            e.printStackTrace();
            }
        }).start();
    }
}

请比较 JDK 中 String 类的实现


b0480a2597a194a56b521a8cf014b1fa.png


String中的char数组被final private 修饰,并且没有提供修改字符数组的方法, 从而导致字符数组的不可变.


另外String类被final修饰,避免了子类继承,从而破坏其不可变的性质。


**扩展:**思考下面程序执行的结果


a67daeb1a5362fbd4e71c8ecdb857572.png


3.6 常见线程安全类

String

Integer

StringBuffer

Random

Vector

Hashtable

java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为

public void test(){
    Hashtable table = new Hashtable();
    //以下关于table的操作是线程安全的
    new Thread(()->{
        table.put("key", "value1");
    }).start();
    new Thread(()->{
        table.put("key", "value2");
    }).start();
}
  • 它们的每个方法是原子的
  • 但注意它们多个方法的组合不是原子的,见后面分析

3.6.1 线程安全类方法的组合

分析下面代码是否线程安全?

Hashtable table = new Hashtable();
// 创建 线程1,线程2,分别执行一下代码
if( table.get("key") == null) {
  table.put("key", value);
}

ba105ef942e3161b6bece1cbd756d7de.png


线程1执行完get之后,锁释放。此时有可能线程2抢占到了锁,所以线程2也判断get==null成立,这时线程2可以进入下面的put逻辑。线程2执行完后,线程1继续执行做put操作,从而会出现值覆盖。此时两个线程table的操作就不是线程安全的。


3.6.2 不可变类线程安全性


String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的

有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?


本质是当执行这些操作时,会创建新对象来保存值。

9fa40558ab12f55c40be8d9d29f3a61c.png

3.7 习题

3.7.1 卖票练习

测试下面代码是否存在线程安全问题,并尝试改正

/**
 * @author lxy
 * @version 1.0
 * @Description 卖票案例 
 * @date 2022/6/8 22:21
 */
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    public static void main(String[] args) throws InterruptedException {
        //售票窗口,默认有10000张票
        TicketWindow window = new TicketWindow(10000);
        //卖出的票数统计
        List <Integer> amountList = new Vector <>();
        //所有线程的集合
        List<Thread> threadList = new ArrayList <>();
        //创建2000个线程,模拟用户来买票
        for (int i = 0; i < 2000; i++) {
            Thread thread = new Thread(() -> {
                //买票
                int amount = window.sell(randomAmount());
                amountList.add(amount);
            });
            thread.start();
            threadList.add(thread);
        }
        //等所有乘客买完票再进行统计
        for (Thread thread : threadList) {
            thread.join();
        }
        //统计卖出的票数和剩余票数
        log.debug("余票:{}",window.getCount());
        log.debug("卖出的票数:{}",amountList.stream().mapToInt(i->i).sum());
    }
    //Random为线程安全
    static Random random = new Random();
    //产生 1 - 5的随机数
    public static int randomAmount(){
        return random.nextInt(5)+1;
    }
}
//售票窗口
class TicketWindow{
    private int count;
    public TicketWindow(int count) {
        this.count = count;
    }
    // 获取余票数量
    public int getCount() {
        return count;
    }
    // 售票
    public void setCount(int count) {
        this.count = count;
    }
    //售票
    public int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }else{
            return 0;
        }
    }
}

另外,用下面的代码行不行,为什么?

List<Integer> sellCount = new ArrayList<>();//这里不可以这样写,因为sellCount需要被多个线程所操作,会有线程安全的问题,所以需要使用线程安全的集合.

测试脚本运行:

for /L %n in (1,1,10) do java -cp ".;E:\Soft\apache-maven-3.8.1\mvn_repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;E:\Soft\apache-maven-3.8.1\mvn_repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;E:\Soft\apache-maven-3.8.1\mvn_repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar" com.rg.thread.ExerciseSell
//解释:
//window下执行多次  java文件的脚本(可用于测试观察文件多次执行的结果)
//从1开始,每次递增1,循环到10   cp:类路径   "当前路径,所需jar包路径" 
概括来说就是:下边的原子操作不依赖上边原子操作的结果的话,就不用考虑两个原子操作合在一起的安全性

b7eb89d061fe0120ddb3b04f412304fd.png


分析并修改代码可能存在线程安全的部分

  • 寻找临界区(多个线程对共享变量的读写操作的区域)

f992d7f3257ab0402a87345487708503.png

再次运行:

6622b2885b6785e68368429bffd77256.png

3.7.2 转账练习

测试下面代码是否存在线程安全问题,并尝试改正

/**
 * @author lxy
 * @version 1.0
 * @Description 转账案例
 * @date 2022/6/10 15:53
 */
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
    public static void main(String[] args) throws InterruptedException {
        Account a = new Account(1000);
        Account b = new Account(1000);
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.transfer(b, randomAmount());//a向b转一个随机金额
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());//b向a转一个随机金额
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        //查看转账2000次后的总金额
        log.debug("total:{}",(a.getMoney()+b.getMoney()));
    }
    // Random 为线程安全
    static Random random = new Random();
    //随机 1-100
    public static int randomAmount(){
        return random.nextInt(100);
    }
}
// 账户
class Account{
    private int money;
    public Account(int money) {
        this.money = money;
    }
    public int getMoney() {
        return money;
    }
    public void setMoney(int money) {
        this.money = money;
    }
    //转账
    public void transfer(Account target,int amount){
        if(this.money >= amount){
            this.setMoney(this.getMoney()- amount);//转账者金额减少
            target.setMoney(target.getMoney()+amount);//收款者金额增加
        }
    }
}

运行结果:

1b20db2994a8a7f607c6852f53c72eec.png

分析:

f44459a8787d13510461e5f382d8e37d.png

修改:5e8f52dc294bede40774928428de9b5d.png

相关文章
|
3天前
|
Java 程序员 开发者
深入理解Java并发编程:线程同步与锁机制
【4月更文挑战第30天】 在多线程的世界中,确保数据的一致性和线程间的有效通信是至关重要的。本文将深入探讨Java并发编程中的核心概念——线程同步与锁机制。我们将从基本的synchronized关键字开始,逐步过渡到更复杂的ReentrantLock类,并探讨它们如何帮助我们在多线程环境中保持数据完整性和避免常见的并发问题。文章还将通过示例代码,展示这些同步工具在实际开发中的应用,帮助读者构建对Java并发编程深层次的理解。
|
3天前
|
Linux API C++
c++多线程——互斥锁
c++多线程——互斥锁
|
1天前
|
存储 缓存 前端开发
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
Java串口通信技术探究3:RXTX库线程 优化系统性能的SerialPortEventListener类
10 3
|
3天前
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
|
3天前
|
安全 调度 Swift
【Swift开发专栏】Swift中的多线程与并发编程
【4月更文挑战第30天】本文探讨Swift中的多线程与并发编程,分为三个部分:基本概念、并发编程模型和最佳实践。介绍了线程、进程、并发与并行、同步与异步的区别。Swift的并发模型包括GCD、OperationQueue及新引入的结构体Task和Actor。编写高效并发代码需注意任务粒度、避免死锁、使用线程安全集合等。Swift 5.5的并发模型简化了异步编程。理解并掌握这些知识能帮助开发者编写高效、安全的并发代码。
|
4天前
|
消息中间件 安全 算法
通透!从头到脚讲明白线程锁
线程锁在分布式应用中是重中之重,当谈论线程锁时,通常指的是在多线程编程中使用的同步机制,它可以确保在同一时刻只有一个线程能够访问共享资源,从而避免竞争条件和数据不一致性问题。
|
4天前
|
安全 算法 关系型数据库
线程安全--深入探究线程等待机制和死锁问题
线程安全--深入探究线程等待机制和死锁问题
|
4天前
|
缓存 安全 Java
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
多线程--深入探究多线程的重点,难点以及常考点线程安全问题
|
4天前
|
安全 Java 开发者
构建高效微服务架构:后端开发的新范式Java中的多线程并发编程实践
【4月更文挑战第29天】在数字化转型的浪潮中,微服务架构已成为软件开发的一大趋势。它通过解耦复杂系统、提升可伸缩性和促进敏捷开发来满足现代企业不断变化的业务需求。本文将深入探讨微服务的核心概念、设计原则以及如何利用最新的后端技术栈构建和部署高效的微服务架构。我们将分析微服务带来的挑战,包括服务治理、数据一致性和网络延迟问题,并讨论相应的解决方案。通过实际案例分析和最佳实践的分享,旨在为后端开发者提供一套实施微服务的全面指导。 【4月更文挑战第29天】在现代软件开发中,多线程技术是提高程序性能和响应能力的重要手段。本文通过介绍Java语言的多线程机制,探讨了如何有效地实现线程同步和通信,以及如
|
4天前
|
数据采集 安全 Java
Python的多线程,守护线程,线程安全
Python的多线程,守护线程,线程安全