线程安全分析

2023-11-18

 1、成员变量和静态变量是否线程安全?

  • 如果它们没有被共享,则线程安全
  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

2、局部变量是否线程安全?

  • 局部变量是线程安全的 
  • 但局部变量引用的对象则未必
    • 如果该对象没有逃离方法的作用访问,它是线程安全的 
    • 如果该对象逃离方法的作用范围,需要考虑线程安全

3、局部变量线程安全分析

案例1

public static void test1() { 
    int i = 10;
    i++; 
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。

案例2

public class Test1 {
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                test.method1();
            }, "Thread" + i).start();
        }
    }
}

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1() {
        for (int i = 0; i < 20000; i++) {
        // { 临界区, 会产生竞态条件
            method2();
            method3();
            // } 临界区
        }
    }
    public void method2() {
        list.add("1");
    }
    public void method3() {
        list.remove(0);
    }
}

出现 IndexOutOfBoundsException ,索引越界异常。

具体分析 

ArrayList 的 add 方法源码如下,remove 方法类似

  public boolean add(E e) {
    /**
     * 添加一个元素时,做了如下两步操作
     * 1.判断列表的capacity容量是否足够,是否需要扩容
     * 2.真正将元素放在列表的元素数组里面
     */
        ensureCapacityInternal(size + 1);
        elementData[size++] = e;
        return true;
  }

add元素时,实际做了两个大的步骤:

  • 判断 elementData 数组容量是否满足需求
  • 在 elementData 对应位置上设置值

ensureCapacityInternal(size + 1) 多线程情况下会出现索引越界异常,这里主要分析elementData[size++],它不是一个原子操作,是分两步执行的。 

elementData[size] = e;
size++;

size++的字节码 :

getfield #284 <java/util/ArrayList.size>
dup_x1
iconst_1
iadd
putfield #284 <java/util/ArrayList.size>

思考这样一种情况:

  1. 列表为空 size = 0。
  2. 线程 A 执行完 elementData[size] = e 之后挂起。A 把 "a" 放在了下标为 0 的位置,此时 size = 0。
  3. 线程 B 执行 elementData[size] = e。因为此时 size = 0,所以 B 把 "b" 放在了下标为 0 的位置,于是刚好把 A 的数据给覆盖掉了。
  4. 线程 B 将 size 的值增加为 1, 线程 A将 size 的值增加为 1。

结果:size 为 1,elementData 下标为 0 的位置变成了 B,下标 1 的位置上什么都没有。此时再执行两次删除操作,就会出现异常。 

解决办法:将 list 修改为局部变量

如果为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,是否有线程安全问题?

class ThreadSafe {
    public void method1() {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < 20000; i++) {
            method2(list);
            method3(list);
        }
    }
    public void method2(ArrayList<String> list) {
        list.add("1");
    }
    public void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

class ThreadSafeSubClass extends ThreadSafe{
    /**
     *多个线程同时操作list会存在线程安全问题,
     * 可以打印size查看
     */
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

多个线程同时操作list,依然存在线程安全问题,如上源码,ThreadSafeSubClass 类重写了 method3 方法,导致 method1 执行时依然是多个线程操作 list。

解决办法:private 或 final 修饰方法,让子类不能重写。

从这个例子可以看出 private 或 final 提供【安全】的意义所在,请体会开闭原则中的【闭】

4、常见线程安全类

  • String 
  • Integer 
  • StringBuffer 
  • Random 
  • Vector 
  • Hashtable
  • java.util.concurrent 包下的类

这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的,也可以理解为:它们的每个方法是原子的,但注意它们多个方法的组合不是原子的,见后面分析

Hashtable table = new Hashtable(); 

new Thread(()->{
    table.put("key", "value1"); 
}).start();

new Thread(()->{
    table.put("key", "value2"); 
}).start();

 1)线程安全类方法的组合 

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

Hashtable table = new Hashtable(); 
// 线程1,线程2
if( table.get("key") == null) { 
    table.put("key", value);
}

2)不可变类线程安全性

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

5、实例分析

例1

public class MyServlet extends HttpServlet {
    // 是否安全? 否
    Map<String, Object> map = new HashMap<>();
    // 是否安全? 是
    String S1 = "...";
    // 是否安全? 是
    final String S2 = "...";
    // 是否安全? 否
    Date D1 = new Date();
    // 是否安全? 是
    final Date D2 = new Date();

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        // 使用上述变量
    }
}

例2

否,userService的成员变量的修改操作是线程不安全的

public class MyServlet extends HttpServlet {
    // 是否安全? 
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 记录调用次数
    private int count = 0;
    public void update() {
// ... 
        count++;
    }
}

例3

否,Spring中的对象没有加额外说明的话都是单例模式,需要被共享,就会有线程安全问题。

解决办法:环绕通知,环绕通知可以把start,end做成环绕通知的局部变量。

注意:不能改成多例模式,如果进入前置通知和后置通知的对象不一样,也会产生问题。

@Aspect
@Component
public class MyAspect {
    // 是否安全?
    private long start = 0L;
    @Before("execution(* *(..))")
    public void before() {
        start = System.nanoTime();
    }
    @After("execution(* *(..))")
    public void after() {
        long end = System.nanoTime();
        System.out.println("cost time:" + (end-start));
    }
}
    

例4 

相比于例2,没有可更改的成员变量,所以三个都是线程安全的

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    public void update() {
        String sql = "update user set password = ? where username = ?";
        // 是否安全
        try (Connection conn = DriverManager.getConnection("","","")){
            // ...
        } catch (Exception e) {
            // ...
        }
    }
}

例5

否,conn对象被多个线程共享,例如:一个线程拿到conn对象,此时另一个线程将conn对象关闭,就会出现线程安全问题。

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    // 是否安全
    private UserDao userDao = new UserDaoImpl();
    public void update() {
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection conn = null;
    public void update() throws SQLException {
        String sql = "update user set password = ? where username = ?";
        conn = DriverManager.getConnection("","","");
        // ...
        conn.close();
    }
}

 例6

相比于例5,UserServiceImpl将userDao变为局部变量,这样虽然不存在线程安全问题,但不推荐这样写。

public class MyServlet extends HttpServlet {
    // 是否安全
    private UserService userService = new UserServiceImpl();
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        userService.update(...);
    }
}
public class UserServiceImpl implements UserService {
    public void update() {
        UserDao userDao = new UserDaoImpl();
        userDao.update();
    }
}
public class UserDaoImpl implements UserDao {
    // 是否安全
    private Connection conn = null;
    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() {
        // 是否安全
        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 的行为是不确定的,可能导致不安全的发生,被称之为外星方法

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

对比String类,由于被设计成final,子类不能继承,不会有线程安全问题。 

例8

不是线程安全的

    private static Integer i = 0;
    public static void main(String[] args) throws InterruptedException {
        List<Thread> list = new ArrayList<>();
        for (int j = 0; j < 2; j++) {
            Thread thread = new Thread(() -> {
                for (int k = 0; k < 5000; k++) {
                    synchronized (i) {
                        i++;
                    }
                }
            }, "" + j);
            list.add(thread);
        }
        list.stream().forEach(t -> t.start());
        list.stream().forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        log.debug("{}", i);
    }

Integer属于不可变对象,一旦被创建,就不可能被修改。  

 i++的字节码

  8 getstatic #4 <p4_4/Test4.i>
 11 astore_2
 12 getstatic #4 <p4_4/Test4.i>
 15 invokevirtual #5 <java/lang/Integer.intValue>
 18 iconst_1
 19 iadd
 20 invokestatic #6 <java/lang/Integer.valueOf>
 23 dup
 24 putstatic #4 <p4_4/Test4.i>

 相当于执行

i = Integer.valueOf(i.intValue()+1)

进一步查看valueOf方法

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

i++的本质是创建一个新的对象,由于在多线程间,并不一定能看到同一个对象(因为i对象一直在变),因此,两个线程加锁可能加在了不同的对象实例上。

6、习题

1、卖票练习

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

@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
    // Random为 线程安全
    static Random random = new Random();
    // 随机 1~5
    public static int randomCount(){
        return random.nextInt(5) + 1;
    }
    public static void main(String[] args) {
        TicketWindow window = new TicketWindow(1000);
        List<Thread> threadList = new ArrayList();
        //用来存储卖出去多少钱,线程安全
        List<Integer> sellCount = new Vector();
        for (int i = 0; i < 500; i++){
            Thread thread = new Thread(() -> {
                int count = window.sell(randomCount());
                sellCount.add(count);
            });
            threadList.add(thread);
            thread.start();
        }
        //主线程等待执行完毕
        threadList.forEach((t) ->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        log.debug("剩余票:{}",window.getCount());
        log.debug("卖出去的票:{}",sellCount.stream().mapToInt(i -> i).sum());
  }
}
class TicketWindow{
    private int count;

    public TicketWindow(int count) {
        this.count = count;
    }

    public int getCount() {
        return count;
    }

    public int sell(int amount){
        if(this.count >= amount){
            this.count -= amount;
            return amount;
        }
        return 0;
    }
}

只需要在sell方法加synchronized即可

注意这两段代码

    int count = window.sell(randomCount());
    sellCount.add(count);

前面说过每个方法是原子的,但是组合到一起会有线程安全问题。

    if( table.get("key") == null) {
        table.put("key", value);
    }

注意两者区别:table是对一个共享变量读和写,现在的例子中window是一个共享变量, sellCount是另一个共享变量,我们只要求每个共享变量的临界区被保护就可以了。

2、转账练习

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

public class ExerciseTransfer {
    // Random 为线程安全
    static Random random = new Random();
    // 随机 1~100
    public static int randomAmount() {
        return random.nextInt(100) +1;
    }
    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());
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                b.transfer(a, randomAmount());
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        // 查看转账2000次后的总金额
        log.debug("total:{}",(a.getMoney() + b.getMoney()));
    }
}
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);
        }
    }
}

这样改正行不行,为什么?

    public synchronized void transfer(Account target, int amount) {
        if (this.money > amount) {
            this.setMoney(this.getMoney() - amount);
            target.setMoney(target.getMoney() + amount);
        }
    }

不行,只会保护this对象的money,不能保护target 的money。

可以这样改,但性能低下

    public void transfer(Account target, int amount) {
        synchronized (Account.class){
            if (this.money > amount) {
                this.setMoney(this.getMoney() - amount);
                target.setMoney(target.getMoney() + amount);
            }
        }
    }
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

线程安全分析 的相关文章

  • 11-JUC中的Condition对象

    文章目录 ConditionCondition常用方法总结参考 Condition 任何一个java对象都天然继承于Object类 xff0c 在线程间实现通信的往往会应用到Object的几个方法 xff0c 比如wait wait lon
  • JUC中对线程的协同合作控制

    线程的协同合作控制 CountDownLatch使用代码演示小结 Semaphore使用小结 Condition使用代码 CyclicBarrier使用 xff1a 代码演示 小结 在使用多线程的时候 xff0c 我们可以使用一些工具来达到
  • 【Java面试题汇总】多线程、JUC、锁篇(2023版)

    导航 黑马Java笔记 踩坑汇总 Java基础 JavaWeb SSM SpringBoot 瑞吉外卖 SpringCloud 黑马旅游 谷粒商城 学成在线 设计模式 牛客面试题 目录 0 请你说说线程和进程的区别 1 请你说说多线程 2
  • 线程中断标志位 interrupt()、interrupted()、isInterrupted() 的认识

    常见问题 首先你是怎么去关闭一个开启的线程 调用中断方法之后 线程就立即停止运行吗 带着这两个问题探讨一下 主要围绕着这三个方法讲述 interrupt interrupted isInterrupted 归类为中断 什么是中断标识位 首先
  • 黑马并发编程JUC总结

    并发编程总结1 并发编程 2 进程和线程 2 1定义 2 2并发和并行 2 3应用 异步调用 并发应用 3 java线程 3 1线程创建 创建线程方法1 创建方法2 Thread和Runable的区别 创建方法3 3 2线程运行 3 3线程
  • 创建线程四种方法详解;及说明ThreadPoolExecutor方式创建线程池

    一 继承Thread类的方式 创建一个线程 class MyThred extends Thread public MyThred String name super name Override public void run 线程内的操作
  • 【JUC】浅析ConcurrentLinkedQueue

    JUC 浅析ConcurrentLinkedQueue 文章目录 JUC 浅析ConcurrentLinkedQueue 一 前言 二 ConcurrentLinkedQueue的结构 三 入队列 3 1 入队列的过程 3 2 定位尾节点
  • JUC源码分析2-原子变量-AtomicIntegerArray/AtomicLongArray/AtomicReferenceArray

    JUC针对数组元素的原子封装 先看AtomicIntegerArray private static final Unsafe unsafe Unsafe getUnsafe arrayBaseOffset获取数组首个元素地址偏移 priv
  • 黑马并发编程JUC(信号量、线程安全类)总结

    黑马并发编程JUC总结 9 JUC Semaphore 定义 原理 acquire release CountDownLatch 为什么需要用到CountDownLatch 定义 为什么加载的时候需要使用到countDownLock 商品问
  • Java线程的同步机制(synchronized关键字)

    线程的同步机制 synchronized 1 背景 例子 创建个窗口卖票 总票数为100张 使用实现Runnable接口的方式 1 问题 卖票过程中 出现了重票 错票 gt 出现了线程的安全问题 2 问题出现的原因 当某个线程操作车票的过程
  • 详解ThreadLocal

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 1 ThreadLocal介绍 1 1 官方介绍 1 2 基本用法 1 2 1 常用方法 1 2 2 使用案例 1 3 ThreadLocal与synchroniz
  • ThreadLocal从变量副本的角度解决多线程并发安全问题

    ThreadLocal从变量副本的角度解决多线程并发安全问题 之前我们讲的高并发场景下的线程安全问题 可以使用Synchronized同步关键字 Lock手动加锁的方式去解决 什么轻量级锁 偏向锁 重量级锁 可重入锁等等 实际上本质都是控制
  • JUC并发编程之AQS原理

    1 AQS 原理 1 1 概述 全称是 AbstractQueuedSynchronizer 是阻塞式锁和相关的同步器工具的框架 特点 用 state 属性来表示资源的状态 分独占模式和共享模式 子类需要定义如何维护这个生态 控制如何获取锁
  • 安全线程的集合

    1 CopyOnWriteArrayList package com kuang unsafe import java util import java util concurrent CopyOnWriteArrayList java u
  • JUC(2): 阻塞队列+线程池(重点)+新时代程序员必会

    一 阻塞队列 ArrayBlockingQueue 一个由数组结构组成的有界阻塞队列 LinkedBlockingQueue 一个由链表结构组成的有界阻塞队列 PriorityBlockingQueue 一个支持优先级排序的无界阻塞队列 D
  • JUC并发编程学习

    JUC并发编程学习 目录 JUC并发编程学习 1 什么是JUC 1 1 JUC简介 1 2 进程与线程 1 3 线程的状态 1 3 1 线程状态Thread State 枚举类 1 3 2 wait sleep 区别 1 4 并发与并行 1
  • 线程安全分析

    1 成员变量和静态变量是否线程安全 如果它们没有被共享 则线程安全 如果它们被共享了 根据它们的状态是否能够改变 又分两种情况 如果只有读操作 则线程安全 如果有读写操作 则这段代码是临界区 需要考虑线程安全 2 局部变量是否线程安全 局部
  • 【JUC并发编程】CopyOnWrite容器详解

    JUC并发编程 CopyOnWrite容器详解 文章目录 JUC并发编程 CopyOnWrite容器详解 一 什么是CopyOnWrite容器 二 CopyOnWriteArrayList 三 CopyOnWrite的业务中实现 一 什么是
  • JUC并发编程共享模型之管程(三)(中)

    4 5Monitor概念 Java 对象头 以 32 位虚拟机为例 在32位虚拟机中 1个机器码等于4字节 也就是32bit 在64位虚拟机中 1个机器码是8个字节 也就是64bit 普通对象 数组对象 其中Mark Word 结构为 最后
  • 万文详解JUC(超详细)

    生命无罪 健康万岁 我是laity 我曾七次鄙视自己的灵魂 第一次 当它本可进取时 却故作谦卑 第二次 当它在空虚时 用爱欲来填充 第三次 在困难和容易之间 它选择了容易 第四次 它犯了错 却借由别人也会犯错来宽慰自己 第五次 它自由软弱

随机推荐

  • MongoDB的安装与基本使用

    首先 得从MongoDB官网下载到对应操作系统的数据库安装包 MongoDB官网 https www mongodb com MongoDB下载地址 https www mongodb com download center communi
  • python中idx函数_python 常用函数、内置函数清单

    文章内容摘自 http www cnblogs com vamei 1 type 查询变量的类型 例 gt gt gt a 10 gt gt gt print a 10 gt gt gt print type a 2 dir 查询一个类或者
  • maven本地仓库配置

    来配置一下maven本地仓库 第一步 下载到官网下载maven包 下载地址 http maven apache org download cgi 第二步 找个盘符创建个文件夹将将下载的maven报放进去 然后将下载的包解压了 第三步 配置M
  • 树莓派3B+使用镜像烧录安装系统与配置教程(入门向)

    设备 Raspberry 3B 开发板一块 显示屏 台式计算机或笔记本电脑 显示屏 HDMI转VGA线或HDMI线或USB视频采集卡 千万不要用HDMI线从树莓派直接连接到显卡的HDMI口或者笔记本的HDMI口 轻则平安无事 重则电脑CPU
  • React移动端项目-02

    一些不实装功能的静态页面 底部导航栏 问答 页面 目标 实现问答页面的静态结构和样式 操作步骤 将资源包的样式拷贝到 pages Question 目录下 然后在该目录下的 index js中编写组件代码 import NavBar fro
  • UVA 1601 The Morning after Halloween - Japan 2007

    include
  • 创建任意程序为系统服务

    网上流传的创建系统服务的方法我看着真麻烦 把我自己弄的很简单的代码共享下 create SERVICENAME bat ECHO OFF sc delete SERVICENAME sc create SERVICENAME start a
  • 51单片机按键识别与LED显示(显示0-9的数字)

    实验内容 单片机外接10个按键 编号为0 9 编程实现任意按键则LED显示对应数字 一 硬件电路原理图 1 共阴共阳两种不同的方式 2 数码管显示表 3 矩阵键盘介绍 矩阵键盘是 单片机 外部设备中所使用的排布类似于矩阵的键盘组 矩阵式结构
  • WIN10+VS2013+CUDA10安装方法

    1 先安装VS 先安装VS 先安装VS 安装CUDA会配置VS文件 反向的话VS中找不到文件 VS安装参考 https blog csdn net m0 37477061 article details 83447773 2 安装CUDA
  • E-R模型应用示例

    E R模型应用示例 例1 1 设有某计算机系统集成制造公司需要建立一个零配件物资管理系统 该公司组装不同型号计算机所用的零配件由不同供货商供给 存放在多个仓库中 由多名仓库管理员管理 试用E R模型对该公司的零配件管理工作进行分析 根据该公
  • STM32移植lwip之官方源码解析

    本篇目标 分析stm32的ETH MAC控制器 初始化及lwip是如何与stm32底层连接的 材料准备 官方资料 包含代码和移植手册 stm32官方移植lwip资料 修改代码 包含移植后的代码 STM32官方移植lwip修改代码 修改参考
  • C++智能指针:shared_ptr用法详解

    C 智能指针 shared ptr用法详解 shared ptr是C 11里的新特性 其包装了new操作符在堆上分配的动态对象 如 shared ptr
  • MATLAB自动生成标记点

    在测试算法准确性的过程中 需要在图像上生成一些大小已知的标记点来识别 同时又需要生成的标记点位置随机 以测试算法的适用性 本人搜索自动生成标记点没有找到相关内容 因此将完成思路整理如下 随机生成点位置 MATLAB生成随机点很容易实现 使用
  • 解决django.core.exceptions.ImproperlyConfigured: mysqlclient 1.4.3 or newer is required; you have 1.0.

    最近在配置环境时遇到这样的问题 在网上查找了很多资料 比如注释base py中的代码 又或是添加如下代码 这些方法对我来说都不管用 后面又查到说可以降低django的版本 随后我重新安装了django2的版本 然后报以下错误 TypeErr
  • layui php+PHPExcel 拉取excel表格数据一键导入

    需求 大量数据需要导入数据库 直接拉取excel表格进行读取数据并存入数据库 过程中不对文件进行存储 使用上传过程中的缓存文件 前端代码 layui php
  • vs2019配置Qt5开发环境

    使用visual studi 2019配置qt5开发环境 以及创建qt项目s 一 下载安装visual Studio2019 1 进入visual studio 官网下载community 2019版本 安装器 2 下载完成后启动visua
  • Vue2里的computed的传参方法

    在使用element ui的时候 后台返回的字段需要你来判断显示什么 这时候可能就需要计算属性了 但是使用computed无法传值 可以通过computed定义的函数里面返回一个函数来接受传值 进行判断 如果直接使用computed传参是报
  • 使用nrm管理npm仓库

    引言 目前遇到了这样的问题 因为个人和公司的npm仓库环境不一样 导致使用时需要频繁的切换npm指向 所以 为了提高工作效率 就采用了nrm的方式来管理多个npm仓库 老规矩 先举一个 For example 当前有两个仓库指向 分别是 公
  • PyCharm安装教程最新版(社区版)

    1 官网下载地址 PyCharm the Python IDE for Professional Developers by JetBrains 2 安装 直接Install进行安装 最后点击finish即可 3 新建项目并测试 新建一个项
  • 线程安全分析

    1 成员变量和静态变量是否线程安全 如果它们没有被共享 则线程安全 如果它们被共享了 根据它们的状态是否能够改变 又分两种情况 如果只有读操作 则线程安全 如果有读写操作 则这段代码是临界区 需要考虑线程安全 2 局部变量是否线程安全 局部