JUC并发编程(4)—ThreadLocal介绍(超详细 集大成版本!!!)
ThreadLocal简介
ThreadLocal本地线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全的问题
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
- ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景
ThreadLocal与Synchronized的区别
ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
synchronized或者lock,有个管理员,好比,现在大家签到,多个同学(线程),但是只有一只笔,只能同一个时间,只有一个线程(同学)签到,加锁(同步机制是以时间换空间,执行时间不一样,类似于排队)
ThreadLocal,人人有份,每个同学手上都有一支笔,自己用自己的,不用再加锁来维持秩序(同步机制是以空间换时间,为每一个线程都提供了一份变量的副本,从而实现同时访问,互不干扰同时访问,肯定效率高啊)
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
- Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。
ThreadLocal API
- protected T initialValue():initialValue():返回此线程局部变量的当前线程的"初始值"
(对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法) - static <> ThreadLocal <> withInitial(Supplier<? extends S> supplier):创建线程局部变量
- T get():返回当前线程的此线程局部变量的副本中的值
- void set(T value):将当前线程的此线程局部变量的副本设置为指定的值
- void remove():删除此线程局部变量的当前线程的值
ThreadLocal的使用案例
卖房示例代码:
/***
* 看每个销售员可以出售多少套房子
*/
class House{
/**
initialValue():返回此线程局部变量的当前线程的"初始值"
对于initialValue()较为老旧,jdk1.8又加入了withInitial()方法
ThreadLocal<Integer>threadLocal=new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};*/
//public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
//withInitial(Supplier<? extends S> supplier):创建线程局部变量
//ThreadLocal本地线程变量,线程自带的变量副本
ThreadLocal<Integer>threadLocal=
ThreadLocal.withInitial(()->0);
public void saleHouse(){
//T get():返回当前线程的此线程局部变量的副本中的值。
Integer value = threadLocal.get();
value++;
//void set(T value):将当前线程的此线程局部变量的副本设置为指定的值。
threadLocal.set(value);
}
}
public class ThreadLocalDemo {
public static void main(String[] args) {
House house = new House();
new Thread(()->{
try{
for (int i = 1; i <=3; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
//void remove():删除此线程局部变量的当前线程的值
//在阿里巴巴手册中有说明,尽量在代理中使用try-finally块进行回收
house.threadLocal.remove();
//下面获取到的值是线程的初始值0
System.out.println("**********"+house.threadLocal.get());
}
},"t1").start();
new Thread(()->{
try{
for (int i = 1; i <=5; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
house.threadLocal.remove();
}
},"t2").start();
new Thread(()->{
try{
for (int i = 1; i <=8; i++) {
house.saleHouse();
}
System.out.println(Thread.currentThread().getName()+"\t"+"卖出:"+house.threadLocal.get());
}catch (Exception e){
e.getStackTrace();
}finally {
house.threadLocal.remove();
}
},"t3").start();
System.out.println(Thread.currentThread().getName()+"\t"+"卖出了:"+house.threadLocal.get());
}
}
/**
* t1 卖出:3
* t2 卖出:5
* **********0
* main 卖出了:0
* t3 卖出:8
* */
代码总结
- 因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用
- 既然其他Thread不可访问,那就不存在多线程共享的问题
- 统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的
- 加入synchronized或者lock控制线程的访问顺序,而ThreadLocal人手一份,大家各自安好,没必要抢夺
阿里ThreadLocal规范
用途说明:在对一些业务日志写入数据库的时候,日期调用了sdf的静态,导致了会报错或者日期乱了(生产故障)
此时可以使用ThreadLocal解决该问题
代码示例如下:
/**
* 在对一些业务日志写入数据库的时候,日期调用了sdf的静态,导致了会报错或者日期乱了
* */
public class ThreadLocalDataUtils {
public static SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
解决方案一:加入synchronized,用时间换空间,效率低
*/
/**
如果不加会导致线程安全问题,SimpleDateFormat类内部有一个Calendar对象引用,
SimpleDateFormat相关的日期信息,例如sdf.parse(dateStr),sdf.format(date)
诸如此类的方法参数传入的日期相关String,Date等等, 都是交由Calendar引用来储存的.
这样就会导致一个问题如果你的SimpleDateFormat是个static的,那么多个thread之间
就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用(相当于买票案列)
*/
//public static synchronized Date parse(String stringDate) throws ParseException {
public static Date parse(String stringDate) throws ParseException {
System.out.println(sdf.parse(stringDate));
return sdf.parse(stringDate);
}
/***
* 解决方案二:使用ThreadLocal,用空间换时间,效率高
* ThreadLocal中变量副本会人手一份,每次使用完了threadLocal后都要将资源进行释放的处理
*/
public static final ThreadLocal<SimpleDateFormat>sdfThreadLocal=
ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parseByThreadLocal(String stringDate) throws ParseException {
return sdfThreadLocal.get().parse(stringDate);
}
//3 DateTimeFormatter 代替 SimpleDateFormat
public static final DateTimeFormatter DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatForDateTime(LocalDateTime localDateTime) {
return DATE_TIME_FORMAT.format(localDateTime);
}
public static LocalDateTime parseForDateTime(String dateString) {
return LocalDateTime.parse(dateString,DATE_TIME_FORMAT);
}
public static void main(String[] args) throws Exception{
for (int i = 1; i <=3; i++) {
new Thread(()->{
try {
//ThreadLocalDataUtils.parse("2021-03-30 11:20:30");
//System.out.println(ThreadLocalDataUtils.parseByThreadLocal("2021-03-30 11:20:30"));
System.out.println(ThreadLocalDataUtils.parseForDateTime("2021-03-30 11:20:30"));
// System.out.println(ThreadLocalDataUtils.formatForDateTime(LocalDateTime.now()));
} catch (Exception e) {
e.printStackTrace();
}finally {
ThreadLocalDataUtils.sdfThreadLocal.remove();
}
},String.valueOf(i)).start();
}
}
}
ThreadLocal的原理
源码分析:
1. Thread|ThreadLocal|ThreadLocalMap三者直接的关系
- Thread类中有一个ThreadLocal.ThreadLocalMap threadLocals = null的变量,这个ThreadLocal相当于是Thread类和ThreadLocalMap的桥梁,在ThreadLocal中有静态内部类ThreadLocalMap,ThreadLocalMap中有Entry数组
- 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放
- t.threadLocals = new ThreadLocalMap(this, firstValue) 如下这行代码,可以知道每个线程都会创建一个ThreadLocalMap对象,每个线程都有自己的变量副本
2.set方法详解
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key
- 如果Map为空,则给该线程创建 Map,并设置初始值
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/*
* firstKey : 本ThreadLocal实例(this)
* firstValue : 要保存的线程本地变量
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table
table = new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];
//计算索引(重点代码)
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//设置值
table[i] = new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);
size = 1;
//设置阈值
setThreshold(INITIAL_CAPACITY);
}
3.get方法详解
- 先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
4.remove方法详解
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
ThreadLocal内存泄漏问题
为什么源代码用弱引用?
- 当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象
- 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
- 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null
key为null的entry,原理解析
- ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链
- 虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value
- 因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug
- 如果当前thread运行结束,threadLocal,threadLocalMap, Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收
- 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心
- 出现内存泄漏的真实原因 (1). 没有手动删除这个Entry (2). CurrentThread依然运行
set、get方法会去检查所有键为null的Entry对象
结论
(在finally后面调用remove方法!!!)
ThreadLocal 常见使用场景
如上文所述,ThreadLocal 适用于如下两种场景
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。
场景一、存储用户Session
一个简单的用ThreadLocal来存储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;
}
场景二、数据库连接,处理数据库事务
场景三、数据跨层传递(controller,service, dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。
public class ThreadLocalDemo {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1 {
public void service1(User user){
//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用户:"+user.name);
new Service3().service3();
}
}
class Service3 {
public void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用户:"+user.name);
//在整个流程执行完毕后,一定要执行remove
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
//创建ThreadLocal保存User对象
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name){
this.name = name;
}
}
执行的结果:
service2拿到的用户:jack
service3拿到的用户:jack
场景四、Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。
这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
参考文献:
B站周扬老师的JUC并发编程
https://www.bilibili.com/video/BV1ar4y1x727
JUC并发编程(4)—ThreadLocal介绍(超详细 集大成版本!!!)的更多相关文章
- 并发编程 01—— ThreadLocal
Java并发编程实践 目录 并发编程 01—— ThreadLocal 并发编程 02—— ConcurrentHashMap 并发编程 03—— 阻塞队列和生产者-消费者模式 并发编程 04—— 闭 ...
- JUC并发编程基石AQS之主流程源码解析
前言 由于AQS的源码太过凝练,而且有很多分支比如取消排队.等待条件等,如果把所有的分支在一篇文章的写完可能会看懵,所以这篇文章主要是从正常流程先走一遍,重点不在取消排队等分支,之后会专门写一篇取消排 ...
- JUC并发编程学习笔记
JUC并发编程学习笔记 狂神JUC并发编程 总的来说还可以,学到一些新知识,但很多是学过的了,深入的部分不多. 线程与进程 进程:一个程序,程序的集合,比如一个音乐播发器,QQ程序等.一个进程往往包含 ...
- 【Java并发编程】面试常考的ThreadLocal,超详细源码学习
目录 ThreadLocal是啥?用来干啥? ThreadLocal的简单使用 ThreadLocal的实现思路? ThreadLocal常见方法源码分析 ThreadLocal.set(T valu ...
- Java并发编程:ThreadLocal
Java并发编程:深入剖析ThreadLocal Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用 ...
- 【并发编程】ThreadLocal的兄弟InheritableThreadLocal
本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 引子 public class InheritableT ...
- JUC并发编程与高性能内存队列disruptor实战-上
JUC并发实战 Synchonized与Lock 区别 Synchronized是Java的关键字,由JVM层面实现的,Lock是一个接口,有实现类,由JDK实现. Synchronized无法获取锁 ...
- Java基础篇——线程、并发编程知识点全面介绍(面试、学习的必备索引)
原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10739579.html,希望大家多多支持!!! 一.线程基础 1.线程与进程 线程是指进程 ...
- c++ 网络编程课设入门超详细教程 ---目录
原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/9663167.html c++ 网络编程(一)TCP/UDP windows/linux 下入门 ...
- 【并发编程】ThreadLocal其实很简单
什么是ThreadLocal ThreadLocal有点类似于Map类型的数据变量.ThreadLocal类型的变量每个线程都有自己的一个副本,某个线程对这个变量的修改不会影响其他线程副本的值.需要注 ...
随机推荐
- 10个 Istio 流量管理 最常用的例子,你知道几个?
10 个 Istio 流量管理 最常用的例子,强烈建议收藏起来,以备不时之需. 为了方便理解,以Istio官方提供的Bookinfo应用示例为例,引出 Istio 流量管理的常用例子. Bookinf ...
- 使用hashicorp Raft开发分布式服务
使用hashicorp Raft开发高可用服务 开发raft时用到的比较主流的两个库是Etcd Raft 和hashicorp Raft,网上也有一些关于这两个库的讨论.之前分析过etcd Raft, ...
- C# Collections
1. Generic 1.1 List<T> No need to say this is the most commonly used data structure in C# coll ...
- Unity的IPreprocessBuild:深入解析与实用案例
Unity IPreprocessBuild Unity IPreprocessBuild是Unity引擎中的一个非常有用的功能,它可以让开发者在构建项目时自动执行一些操作.这个功能可以帮助开发者提高 ...
- go web学习(四)
跟着b站https://space.bilibili.com/361469957 杨旭老师学习做的笔记 中间件 什么是中间件 请求----> 中间件 ----> Handler 响应 &l ...
- Day07_Java_作业
A:简答题 1. 构造方法的作用是什么?构造方法的特点是什么?构造方法的注意事项? 构造方法中可不可以写return语句呢? 2. 一个类的缺省构造方法可以有参数吗? 3. 静态代码块和构造代码块的特 ...
- PhotoShop AI 爱国版保姆级安装和使用
上篇Photoshop AI 令人惊叹的生成式填充介绍了 PhotoShop AI 的新特性功能,有人以为我收了 Adobe 公司的钱帮它们做推广~~~.别不信,事实上确有其事,某平台审核直接把它删掉 ...
- 代码随想录贪心专题-day1
35. 分发糖果 n 个孩子站成一排.给你一个整数数组 ratings 表示每个孩子的评分. 你需要按照以下要求,给这些孩子分发糖果: 每个孩子至少分配到 1 个糖果. 相邻两个孩子评分更高的孩子会获 ...
- [db2]缓冲池管理
简介 缓冲池指的是从硬盘读取表和索引数据时,数据库管理器分配的用于高速缓存这些表和索引数据的内存区域.每个数据库都必须具有至少一个缓冲池,创建数据库时会自动创建一个名为IBMDEFAULTBP的缓冲池 ...
- PE文件结构1
引言 PE文件格式是Windows操作系统下的可执行文件的格式,包括.exe文件和.dll文件,通过PE文件格式的学习,可以帮助我们更加熟悉有关Windows系统下的逆向分析和PC端病毒的学习,同时P ...