Java 并发 - ThreadLocal详解

ThreadLocal是通过线程隔离的方式防止任务在共享资源上产生冲突, 线程本地存储是一种自动化机制,可以为使用相同变量的每个不同线程都创建不同的存储。 @立刀旁

目录

# 带着BAT大厂的面试问题去理解

# ThreadLocal简介

# ThreadLocal理解

# ThreadLocal原理

# 如何实现线程隔离

# ThreadLocalMap对象是什么

# ThreadLocal造成内存泄露的问题

# 再看ThreadLocal应用场景

# 每个线程维护了一个“序列号”

# Session的管理

# 在线程内部创建ThreadLocal

# java 开发手册中推荐的 ThreadLocal

# 参考文章


# 带着BAT大厂的面试问题去理解

提示

请带着这些问题继续后文,会很大程度上帮助你更好的理解相关知识点。@立刀旁

  • 什么是ThreadLocal? 用来解决什么问题的?
  • 说说你对ThreadLocal的理解
  • ThreadLocal是如何实现线程隔离的?
  • 为什么ThreadLocal会造成内存泄露? 如何解决
  • 还有哪些使用ThreadLocal的应用场景?

# ThreadLocal简介

我们在Java 并发 - 并发理论基础总结过线程安全(是指广义上的共享资源访问安全性,因为线程隔离是通过副本保证本线程访问资源安全性,它不保证线程之间还存在共享关系的狭义上的安全性)的解决思路:

  • 互斥同步: synchronized 和 ReentrantLock
  • 非阻塞同步: CAS, AtomicXXXX
  • 无同步方案: 栈封闭,本地存储(Thread Local),可重入代码

这个章节将详细的讲讲 本地存储(Thread Local)。官网的解释是这样的:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

总结而言:ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类; 当使用ThreadLocal来维护变量时, ThreadLocal会为每个线程创建单独的变量副本, 避免因多线程操作共享变量而导致的数据不一致的情况。

# ThreadLocal理解

提到ThreadLocal被提到应用最多的是session管理和数据库链接管理,这里以数据访问为例帮助你理解ThreadLocal:

  • 如下数据库管理类在单线程使用是没有任何问题的
class ConnectionManager {
    private static Connection connect = null;

    public static Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public static void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。

  • 为了解决上述线程安全的问题,第一考虑:互斥同步

你可能会说,将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理,比如用Synchronized或者ReentrantLock互斥锁。

  • 这里再抛出一个问题:这地方到底需不需要将connect变量进行共享?

事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。即改后的代码可以这样:

class ConnectionManager {
    private Connection connect = null;

    public Connection openConnection() {
        if (connect == null) {
            connect = DriverManager.getConnection();
        }
        return connect;
    }

    public void closeConnection() {
        if (connect != null)
            connect.close();
    }
}

class Dao {
    public void insert() {
        ConnectionManager connectionManager = new ConnectionManager();
        Connection connection = connectionManager.openConnection();

        // 使用connection进行操作

        connectionManager.closeConnection();
    }
}

这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不仅严重影响程序执行效率,还可能导致服务器压力巨大。

  • 这时候ThreadLocal登场了

那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}
  • 再注意下ThreadLocal的修饰符

ThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。

# ThreadLocal原理

# 如何实现线程隔离

主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于Connection的对象, dbConnectionLocal(以上述例子中为例) 这个变量为Key, 以新建的Connection对象为Value; 这样的话, 线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个Connection对象并且返回;

具体关于为线程分配变量副本的代码如下:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap threadLocals = getMap(t);
    if (threadLocals != null) {
        ThreadLocalMap.Entry e = threadLocals.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  • 首先获取当前线程对象t, 然后从线程t中获取到ThreadLocalMap的成员属性threadLocals
  • 如果当前线程的threadLocals已经初始化(即不为null) 并且存在以当前ThreadLocal对象为Key的值, 则直接返回当前线程要获取的对象(本例中为Connection);
  • 如果当前线程的threadLocals已经初始化(即不为null)但是不存在以当前ThreadLocal对象为Key的的对象, 那么重新创建一个Connection对象, 并且添加到当前线程的threadLocals Map中,并返回
  • 如果当前线程的threadLocals属性还没有被初始化, 则重新创建一个ThreadLocalMap对象, 并且创建一个Connection对象并添加到ThreadLocalMap对象中并返回。

如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
  • 首先调用我们上面写的重载过后的initialValue方法, 产生一个Connection对象

  • 继续查看当前线程的threadLocals是不是空的, 如果ThreadLocalMap已被初始化, 那么直接将产生的对象添加到ThreadLocalMap中, 如果没有初始化, 则创建并添加对象到其中;

同时, ThreadLocal还提供了直接操作Thread对象中的threadLocals的方法

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这样我们也可以不实现initialValue, 将初始化工作放到DBConnectionFactory的getConnection方法中:

public Connection getConnection() {
    Connection connection = dbConnectionLocal.get();
    if (connection == null) {
        try {
            connection = DriverManager.getConnection("", "", "");
            dbConnectionLocal.set(connection);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
    return connection;
}

那么我们看过代码之后就很清晰的知道了为什么ThreadLocal能够实现变量的多线程隔离了; 其实就是用了Map的数据结构给当前线程缓存了, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程;

当然了在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;

现在我们知道了ThreadLocal到底是什么了, 又知道了如何使用ThreadLocal以及其基本实现原理了是不是就可以结束了呢? 其实还有一个问题就是ThreadLocalMap是个什么对象, 为什么要用这个对象呢?

# ThreadLocalMap对象是什么

本质上来讲, 它就是一个Map, 但是这个ThreadLocalMap与我们平时见到的Map有点不一样

  • 它没有实现Map接口;
  • 它没有public的方法, 最多有一个default的构造方法, 因为这个ThreadLocalMap的方法仅仅在ThreadLocal类中调用, 属于静态内部类
  • ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>
  • 该方法仅仅用了一个Entry数组来存储Key, Value; Entry并不是链表形式, 而是每个bucket里面仅仅放一个Entry;

要了解ThreadLocalMap的实现, 我们先从入口开始, 就是往该Map中添加一个值:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

先进行简单的分析, 对该代码表层意思进行解读:

  • 看下当前threadLocal的在数组中的索引位置 比如: i = 2, 看 i = 2 位置上面的元素(Entry)的Key是否等于threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的Entry的Value替换成最新的就可以了;
  • 如果当前位置上面的 Entry 的 Key为空, 说明ThreadLocal对象已经被回收了, 那么就调用replaceStaleEntry
  • 如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的Table进行重新哈希 所以, 该HashMap是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;

了解完Set方法, 后面就是Get方法了:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

先找到ThreadLocal的索引位置, 如果索引位置处的entry不为空并且键与threadLocal是同一个对象, 则直接返回; 否则去后面的索引位置继续查找。

# ThreadLocal造成内存泄露的问题

网上有这样一个例子:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(5000 * 4);
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

如果用线程池来操作ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的LocalVariable对象也不会释放, 就造成了内存泄露; 如果LocalVariable对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable对象的大小;

所以, 为了避免出现内存泄露的情况, ThreadLocal提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的remove方法:

private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

找到Key对应的Entry, 并且清除Entry的Key(ThreadLocal)置空, 随后清除过期的Entry即可避免内存泄露。

# 再看ThreadLocal应用场景

除了上述的数据库管理类的例子,我们再看看其它一些应用:

# 每个线程维护了一个“序列号”

再回想上文说的,如果我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例。

每个线程维护了一个“序列号”

public class SerialNum {
    // The next serial number to be assigned
    private static int nextSerialNum = 0;

    private static ThreadLocal serialNum = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new Integer(nextSerialNum++);
        }
    };

    public static int get() {
        return ((Integer) (serialNum.get())).intValue();
    }
}

# Session的管理

经典的另外一个例子:

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

# 在线程内部创建ThreadLocal

还有一种用法是在线程类内部创建ThreadLocal,基本步骤如下:

  • 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
  • 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
  • 在ThreadDemo类的run()方法中,通过调用getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。
public class ThreadLocalTest implements Runnable{
    
    ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();

    @Override
    public void run() {
        String currentThreadName = Thread.currentThread().getName();
        System.out.println(currentThreadName + " is running...");
        Random random = new Random();
        int age = random.nextInt(100);
        System.out.println(currentThreadName + " is set age: "  + age);
        Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值
        Student.setAge(age);
        System.out.println(currentThreadName + " is first get age: " + Student.getAge());
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println( currentThreadName + " is second get age: " + Student.getAge());
        
    }
    
    private Student getStudentt() {
        Student Student = StudentThreadLocal.get();
        if (null == Student) {
            Student = new Student();
            StudentThreadLocal.set(Student);
        }
        return Student;
    }

    public static void main(String[] args) {
        ThreadLocalTest t = new ThreadLocalTest();
        Thread t1 = new Thread(t,"Thread A");
        Thread t2 = new Thread(t,"Thread B");
        t1.start();
        t2.start();
    }
    
}

class Student{
    int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    
}

# java 开发手册中推荐的 ThreadLocal

看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:

import java.text.DateFormat;
import java.text.SimpleDateFormat;
 
public class DateUtils {
    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };
}

然后我们再要用到 DateFormat 对象的地方,这样调用:

DateUtils.df.get().format(new Date());

# 参考文章

  • https://blog.csdn.net/vking_wang/article/details/14225379
  • https://mp.weixin.qq.com/s/mo3-y-45_ao54b5T7ez7iA
  • https://www.xttblog.com/?p=3087
  • https://blog.csdn.net/whut2010hj/article/details/81413887
  • https://segmentfault.com/a/1190000018399795

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/764016.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

爬虫逆向实战(42)-某巢登陆(AES、MD5、RSA、滑块验证码)

一、数据接口分析 主页地址&#xff1a;某巢 1、抓包 通过抓包可以发现在登录时&#xff0c;网站首先请求captcha/querySlideImage/来获取滑块验证码的图片&#xff0c;然后请求captcha/checkCode/接口来验证滑块验证码。滑块验证码校验成功后&#xff0c;请求noshiro/getPu…

nlp--最大匹配分词(计算召回率)

最大匹配算法是一种常见的中文分词算法&#xff0c;其核心思想是从左向右取词&#xff0c;以词典中最长的词为优先匹配。这里我将为你展示一个简单的最大匹配分词算法的实现&#xff0c;并结合输入任意句子、显示分词结果以及计算分词召回率。 代码 : # happy coding…

Ubuntu24.04之安装KVM(二百五十五)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

2-18 基于matlab的关于联合对角化盲源分离算法的二阶盲识别(SOBI)算法

基于matlab的关于联合对角化盲源分离算法的二阶盲识别&#xff08;SOBI&#xff09;算法。通过联合对角化逼近解混矩阵。构建的四组信号&#xff0c;并通过认为设置添加噪声比例&#xff0c;掩盖信号信息。通过SOBI算法实现了解混。程序已调通&#xff0c;可直接运行。 2-18联合…

JavaScript中location对象的主要属性和方法

属性 href&#xff1a;获取或设置整个URL。protocol&#xff1a;获取URL的协议部分&#xff0c;如"http:"或"https:"。host&#xff1a;获取URL的主机名&#xff08;包括端口号&#xff0c;如果有的话&#xff09;。hostname&#xff1a;获取URL的主机名&…

netlink通信——读取路由表获取通信网卡IP

读取路由表获取通信网卡IP是什么意思呢&#xff1f;且听我一一道来… 下面是我虚拟机两个网卡的IP&#xff0c;很明显两个网卡是不同网段的&#xff0c;我的物理机网卡网段是192.168.1.0/24&#xff0c;与我物理机和外网通信的网卡是ens160&#xff0c;即192.168.31.0/24网段&a…

2018年全国大学生数学建模竞赛A题高压油管的压力控制(含word论文和源代码资源)

文章目录 一、部分题目二、部分论文三、部分源代码问题1&#xff08;1&#xff09;绘制弹性模量与压力函数图&#xff08;2&#xff09;求最优单次开阀时间 问题二&#xff08;1&#xff09;极径与极角关系&#xff08;2&#xff09;求最优凸轮角速度 四、完整word版论文和源代…

多语言模型(Multilingual Models)用于推理(Inference)

在深入探讨多语言模型&#xff08;Multilingual Models&#xff09;用于推理&#xff08;Inference&#xff09;的详细内容时&#xff0c;我们需要首先理解多语言模型的基本概念、它们如何工作、为什么它们在现代自然语言处理&#xff08;NLP&#xff09;中变得如此重要&#x…

物理建模的一个重要概念:因果/非因果建模

物理系统的建模仿真&#xff0c;根据建模思想可划分为&#xff1a; 因果建模&#xff08;Causal Modeling&#xff09;非因果建模&#xff08;Acausal Modeling&#xff09; 二者的核心思想是通过信号流还是方程来定义模型的行为。 像我们熟知的Simulink就是基于因果建模的思…

【C++知识点总结全系列 (05)】:IO 类的详细总结和分析

1、基类 istream 和 ostream (1)istream A.What 输入流的抽象类&#xff0c;是所有输入流类的基类 B.Why&#xff08;输入流的作用&#xff09; 用于从数据源&#xff08;如文件、标准输入设备等&#xff09;读取数据 (2)ostream A.What 输出流的抽象类&#xff0c;是所有输…

Vue组件间通信方式超详细(父传子、父传后代、子传父、后代传父、兄弟组件传值、没有关系的组件传值)

Vue组件间通信方式超详细(父传子、父传后代、子传父、后代传父、兄弟组件传值)_vue 父传子-CSDN博客 vue 组件间传值&#xff1a;父传子 / 子传父 / 子传子 / 祖传孙 - 简书

RFID无线测温技术在数据中心管理中的革新与应用。

在现代信息技术飞速发展的背景下&#xff0c;数据中心作为承载企业、集团、机构核心业务的关键设施&#xff0c;其可靠性要求极高。随着大数据、云计算等技术的应用日益普及&#xff0c;数据中心面临着前所未有的挑战和机遇。其中&#xff0c;RFID无线测温技术作为一种新兴的智…

喜报 | 极限科技获得北京市“创新型”中小企业资格认证

2024年6月20日&#xff0c;北京市经济和信息化局正式发布《关于对2024年度4月份北京市创新型中小企业名单进行公告的通知》&#xff0c;极限数据&#xff08;北京&#xff09;科技有限公司凭借其出色的创新能力和卓越的企业实力&#xff0c;成功获得“北京市创新型中小企业”的…

Paimon 在汽车之家的业务实践

汽车之家基于Paimon的实践 摘要&#xff1a;本文分享自汽车之家的王刚、范文、李乾⽼师。介绍了汽车之家基于 Paimon 的一些实践&#xff0c;和一些背景。内容主要为以下四部分&#xff1a; 一、背景 二、业务实践 三、paimon 优化实践 四、未来规划 一、背景 在使用Paimon之前…

ACM美国计算机协会简介及个人下载ACM文献途径

ACM美国计算机协会简介&#xff1a; ACM&#xff08;Association for Computing Machinery&#xff09; 创立于1947年&#xff0c; 是全球历史最悠久和最大的计算机教育、科研机构。ACM目前提供的服务遍及全球100多个国家&#xff0c;会员数超过9万名&#xff0c;涵盖工商业&a…

从入门到深入,Docker新手学习教程

编译整理&#xff5c;TesterHome社区 作者&#xff5c;Ishaan Gupta 以下为作者观点&#xff1a; Docker 彻底改变了我们开发、交付和运行应用程序的方式。它使开发人员能够将应用程序打包到容器中 - 标准化的可执行组件&#xff0c;将应用程序源代码与在任何环境中运行该代码…

用 AI 生成绘本,含大量 prompt

画图过程&#xff0c;为了保证绘本输出的风格统一&#xff0c;角色连贯&#xff0c;画面内容与故事保持一致 1、画风统一的解决办法&#xff1a;固定一个插画师的风格&#xff0c;可以输入插画师的名字&#xff0c;或者垫图&#xff0c;即上传你需要借鉴风格的图片 2、角色连贯…

Linux库概念及相关编程(动态库-静态库)

Linux库概念及相关编程 分文件编程案例 分文件编程是指将程序按功能模块划分成不同的文件进行编写&#xff0c;这种方法有以下好处&#xff1a; 功能责任划分&#xff1a;每个文件对应一个功能模块&#xff0c;职责明确&#xff0c;易于理解和维护。方便调试&#xff1a;可以…

走进开源企业 | 湖南大学OpenHarmony技术实训活动在开鸿智谷顺利举办!

6月24日-6月26日&#xff0c;2024开放原子校源行之湖南大学信息科学与工程学院师生走进开源企业实训交流活动顺利落下帷幕。湖南大学信息科学与工程学院的师生代表团一行90人参与了湖南开鸿智谷数字产业有限公司&#xff08;以下简称“开鸿智谷”&#xff09;与母公司拓维信息系…

从BeanFactory源码看Bean的生命周期

下图是我搜索“Spring Bean生命周期”找到的图片&#xff0c;来自文章——Spring Bean的生命周期 [](https://img2022.cnblogs.com/blog/1942408/202207/1942408-20220713150530777-1198523052.png) 下面&#xff0c;我们从AbstractAutowireCapableBeanFactory的源码中来分析…