《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

相关文章
|
2月前
|
Java 开发者 Kotlin
华为仓颉语言初识:并发编程之线程的基本使用
本文详细介绍了仓颉语言中线程的基本使用,包括线程创建(通过`spawn`关键字)、线程名称设置、线程执行控制(使用`get`方法阻塞主线程以获取子线程结果)以及线程取消(通过`cancel()`方法)。文章还指出仓颉线程与Java等语言的差异,例如默认不提供线程名称。掌握这些内容有助于开发者高效处理并发任务,提升程序性能。
111 2
|
6月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
478 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
6月前
|
安全 Java 程序员
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
182 11
|
7月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
165 7
|
7月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
205 3
|
8月前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
733 6
|
8月前
|
设计模式 安全 Java
Java 多线程并发编程
Java多线程并发编程是指在Java程序中使用多个线程同时执行,以提高程序的运行效率和响应速度。通过合理管理和调度线程,可以充分利用多核处理器资源,实现高效的任务处理。本内容将介绍Java多线程的基础概念、实现方式及常见问题解决方法。
313 1
|
7月前
|
Java 关系型数据库 MySQL
【JavaEE“多线程进阶”】——各种“锁”大总结
乐/悲观锁,轻/重量级锁,自旋锁,挂起等待锁,普通互斥锁,读写锁,公不公平锁,可不可重入锁,synchronized加锁三阶段过程,锁消除,锁粗化
|
2月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
110 0
|
5月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
104 26