ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制

news2024/11/19 16:21:15

ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制

在这里插入图片描述

文章目录

    • ThreadLocal 的原理讲述 + 基于ThreadLocal实现MVC中的M层的事务控制
    • 每博一文案
    • 1. ThreadLocal 给概述
    • 2. 抛砖引玉——>ThreadLocal
    • 3. ThreadLocal 的模拟编写
    • 4. ThreadLocal 源码原理分析
      • 5. ThreadLocal 常用方法
        • 5.1 ThreadLocal的set()方法
        • 5.2 ThreadLocal的get( )方法
        • 5.3 ThreadLocal的remove( )方法
        • 5.4 ThreadLocal 的 initialValue( )方法
    • 6. ThreadLocal 注意移除数据
    • 7. ThreadLocal 内存泄漏
    • 8. 正确的使用ThreadLocal
    • 9. ThreadLocal 常见使用场景
    • 10. 案例:MVC三层架构 + 面向接口编程 + ThreadLocal 事务处理实:现用户转账功能的优化
      • 10.1 M(Model 模型层/业务逻辑处理层)
      • 10.2 C(Controller 控制层)
      • 10.3 V(View 显示层)
        • 10.4 测试
    • 11. ThreadLocal与Synchronized的区别
    • 12. ThreadLocal与Thread,ThreadLocalMap之间的关系
    • 13. 总结:
    • 14. 最后:

每博一文案

生活不是努力了就可以变好的,喜欢做的事情也不是轻易就可以做的。以前总听别人说,
坚持就好了,努力就好了,都会好的,可是真的做起来压根就不是这样。这种时候要怎么办?
这种时候还能轻易地相信时间吗?
我总是一时间不知道怎么回答:直到今天我决定记录这些日子的生活时,直到我写完以上的文字时,我
脑海里才出现了一个清晰的答案。四个字:尽力而为。
我想这样的。世事无常,分道扬镳,生老病死,我们常常没法得偿所愿。
然而我们都必须尽力而为。
我觉得挺好的:把眼前的事情做好就行了,路都是走着走着才知道能走到哪里的。
越是焦虑,就越是要回到生活里去。因为身处迷雾中本就很难找到方向,能看见的也就
眼前的五米,那就五米五米地一步步走下去。
至于路能走成什么样,又能走去哪里......
走着走着,就都知道了。
但或许其实终点到底是哪里也不是那么重要。
重要的是,我们走了很远的路,最终找到的人,是我们自己。
是哪个可以很好地应对挫折,应对痛苦,应对生活的变故的自己。
是那个依然前行,依然努力,依然能够为了小事而欣喜,为了善良而感动的自己。
是那个终于学会了珍惜的自己,是那个不再害怕平方的自己。
生活如河,自己就是自己的船。

                            ——————卢思浩《你也走了,很远的路吧》

1. ThreadLocal 给概述

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程 ,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

  • 这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量
  • 在线程的生命周期内起作用,可以减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

下图可以增强理解:

在这里插入图片描述

2. 抛砖引玉——>ThreadLocal

从上述一篇文章中:我们运用 **MVC的架构模式——> 实现了用户转账的功能:**🔜🔜🔜 MVC 三层架构案例详细讲解_ChinaRainbowSea的博客-CSDN博客 但是其中存在,一个事务处理的问题。

如下事务控制的处理是在:M(Model 模型层/业务逻辑处理层) 的源码(注意: 对应事务上的控制一定是在 M层当中的),我们可以看到其中并没有进行一个事务上从处理。

package com.RainbowSea.bank.mvc;


/**
 * service 翻译为:业务。
 * AccountService 专门处理Account业务的一个类
 * 在该类中应该编写纯业务代码。(只专注域业务处理,不写别的,不和其他代码混合在一块)
 * 只希望专注业务,能够将业务完美实现,少量bug.
 * <p>
 * 业务类一般起名:XXXService,XXXBiz...
 */
public class AccountService {

    // 这里的方法起名,一定要体现出,你要处理的是什么业务:
    // 我们要提供一个能够实现转账的业务的方法(一个业务对应一个方法)
    // 比如:UserService StudentService OrderService

    // 处理Account 转账业务的增删改查的Dao
    private AccountDao accountDao = new AccountDao();

    /**
     * 完成转账的业务逻辑
     *
     * @param fromActno 转出账号
     * @param toActno   转入账号
     * @param money     转账金额
     */
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
        // 查询余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        if (fromAct.getBalance() < money) {
            throw new MoneyNotEnoughException("对不起,余额不足");
        }

        // 程序到这里说明余额充足
        Account toAct = accountDao.selectByActno(toActno);

        // 修改金额,先从内存上修改,再从硬盘上修改
        fromAct.setBalance(fromAct.getBalance() - money);
        toAct.setBalance(toAct.getBalance() + money);


        // 从硬盘数据库上修改
        int count = accountDao.update(fromAct);
        
        count += accountDao.update(toAct);

        if(count != 2) {
            throw new AppException("账户转账异常,请联系管理员");
        }

    }
}

如下:如果我们没有进行事务处理控制存在一个什么样的问题:

假如:

用户 act001 ——> 转账给用户 act002 ,10000元

转账的过程中,突然用户 act001 网络出现了问题,转账失败了。

注意:这里转账失败了,用户act001的钱是不应该减少的,因为我们没有转账成功嘛

可是这里,并没有进行一个事务上的控制,导致的结果就是,我们转账失败的,但是用户 act001 的钱少 10000,用户act002 的钱却没有增加。其中 用户 act001 的 10000 元丢失在了,网络中,这是不可以的。用户会发飙的,钱转账失败了,钱还少了。这不是坑钱嘛。

如下测试:
在这里插入图片描述

我们执行转账操作:act002 转账给用户 act001 ,10000元,中途发生网络中断:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

下面我们进行“事务的控制”:

在Java中要进行事务的控制就需要使用到 Connectino对象了。

 // 开启事务,不会自动提交数据给数据库
connection.setAutoCommit(false);
connection.commit();  // 提交数据
connection.rollback();  // 事务的回滚
package com.RainbowSea.bank.mvc;


import com.RainbowSea.bank.utils.DBUtil;

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

/**
 * service 翻译为:业务。
 * AccountService 专门处理Account业务的一个类
 * 在该类中应该编写纯业务代码。(只专注域业务处理,不写别的,不和其他代码混合在一块)
 * 只希望专注业务,能够将业务完美实现,少量bug.
 * <p>
 * 业务类一般起名:XXXService,XXXBiz...
 */
public class AccountService {

    // 这里的方法起名,一定要体现出,你要处理的是什么业务:
    // 我们要提供一个能够实现转账的业务的方法(一个业务对应一个方法)
    // 比如:UserService StudentService OrderService

    // 处理Account 转账业务的增删改查的Dao
    private AccountDao accountDao = new AccountDao();

    /**
     * 完成转账的业务逻辑
     *
     * @param fromActno 转出账号
     * @param toActno   转入账号
     * @param money     转账金额
     */
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {
        // 查询余额是否充足
        Account fromAct = accountDao.selectByActno(fromActno);
        Connection connection = DBUtil.getConnection();

        try {
            if (fromAct.getBalance() < money) {
                throw new MoneyNotEnoughException("对不起,余额不足");
            }

            // 程序到这里说明余额充足
            Account toAct = accountDao.selectByActno(toActno);

            // 修改金额,先从内存上修改,再从硬盘上修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            // 开启事务,不会自动提交数据给数据库
            connection.setAutoCommit(false);

            // 从硬盘数据库上修改
            int count = accountDao.update(fromAct);

            // null 引用异常,模拟转账过程中发生网络异常,转账失败
            String s = null;
            s.toString();
            count += accountDao.update(toAct);


            if(count != 2) {
                throw new AppException("账户转账异常,请联系管理员");
            }

            // 程序走到这说明,没有问题,提交数据给数据库
            connection.commit();  // 提交数据
        } catch (SQLException e) {
            try {
                connection.rollback();  // 事务的回滚
            } catch (SQLException ex) {
                throw new RuntimeException(ex);
            }
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection,null,null);
        }

    }
}

在这里插入图片描述

我们再次进行一个,转账看看是否,真的做到的事务的处理:也就是,转账失败的,act001用户的钱不会减少。

在这里插入图片描述

如果结果:为什么我们使用了Connection 对象进行了一个事务的控制,但是,还是会少钱???事务上并没有控制成功。

在这里插入图片描述

为什么会出现如上:情况,明明我们使用了 Connection 进行了一个事务上的控制,但是,却还是会少钱,就没有把事务真正控制上了。

解释:

这里你却是是将事务开启了,但是,你开启的事务是对于当前:AccountService类当中的transfer( )方法当中的Connetion 局部变量进行了一个事务上的控制。我们对数据库的修改是存在两个位置的:

在这里插入图片描述

其中这两个位置上的 accountDao.update() 方法中同样是存在了一个 Connection 对象的

在这里插入图片描述

简单的说就是:我们对数据的更新,需要通过:两个位置上的更新

  1. AccountService类当中的transfer( )方法当中的Connetion 局部变量进行了一个事务上的控制。
  2. AccountDao 类当中的update() 方法到当中的Connection 局部变量进行一个事务上的控制。
存在一个问题就是:我们这里的service 层虽然进行了事务的控制,但是这里的使用的 Connection事务控制
的对象是不一致的,也就是说:我们Connection的事务控制对应不上,我们对数据库修改的操作上,就导致无法对
数据库进行事务控制。

操作同一个事务,但是存在两个Connection ,而且这两者之间的Connection 对象是不一致的,就会导致事务的控制失败。

因为你控制了一个Connection,但是还存在一个Connection ,没有对事务进行控制。

在这里插入图片描述

如何解决上述问题:

解决方法:

既然一个操作同一个事务,存在两个不同的Connection。

那我们就控制成:同一个事务,虽然存在两个Connection,但是它们的值是一样的,也就是同一个事务上的处理,一个Connection就够了。

怎么做到,共用一个Connection,我们可以通过传引用类型参数的方式:

如下修改:

在这里插入图片描述

测试:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

优化:

虽然我们上述:通过传引用类型的参数,对Connetion 对象进行了共用的操作。

但是存在一个问题就是:我们每次对数据库操作,进行一个事务的处理,在 M(Model)层都要先创建一个Connection对象,并将该对象作为参数传送给 XXxDao,这样的操作,大大提高代码的耦合度。背离了 “高内聚,低耦合” 的思想。

有没有别的方法,将Connetion 存储起来,做到同一个线程当中获取到的Connection 都是同一个,不同的线程获取到的Connection是不同的。

有的,我们的ThreadLocal 就实现了这种方式。

同一个线程,我们知道在同一个线程当的 Thread 线程对象是一样的如下测试:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

,既然同一个线程的Thread 是一样,那么我们可不可以,创建一个大Map ,将 Thread 作为 key ,其中Connection 作为value,所有需要同一个线程共用Connection对象的,都从这个 大Map当中获取。因为同一个线程的 Thread 都是一样的,而通过Thread 作为key ,获取到的Value(也就是 Connection )也就是一样的了。

3. ThreadLocal 的模拟编写

根据上述的讲述,我们这里来模拟编写一个 大Map,将 Thread 作为 key ,其中Connection 作为value。其中这种在Java中就叫做:ThreadLocal
在这里插入图片描述

首先,这里我们先演示没有使用:大Map的结果:

自定义的Connection 类

package com.rainbowSea.testThreadLocal;

public class MyConnection {
}

package com.rainbowSea.testThreadLocal;

public class UserDao {

    public void insert(){
        MyConnection myConnection = new MyConnection();
        System.out.println("UserDao Connection : " + myConnection);

        Thread thread = Thread.currentThread();
        System.out.println("UserDao Thread : " + thread);
    }
}

package com.rainbowSea.testThreadLocal;

public class UserService {

    public UserDao userDao = new UserDao();

    public void save() {
        MyConnection myConnection = new MyConnection();
        System.out.println("UserService Connection :" + myConnection);

        Thread thread = Thread.currentThread();
        System.out.println("UserService Thread : " + thread);
        userDao.insert();
        
        
    }
}

package com.rainbowSea.testThreadLocal;

public class Test {
    public static void main(String[] args) {
        MyConnection myConnection = new MyConnection();
        System.out.println("Test Connection : " + myConnection);

        Thread thread = Thread.currentThread();
        System.out.println("Test Thread : " + thread);

        UserService userService = new UserService();
        userService.save();
    }
}

在这里插入图片描述

使用 “大Map处理:”

package com.rainbowSea.testThreadLocal;

import java.util.HashMap;
import java.util.Map;

public class MyThreadLocal<T> {

    /**
     * 所有需要和当前线程绑定的数据要放到整个容器当中
     */
    private Map<Thread,T> map = new HashMap<Thread,T>();


    /**
     * 向ThreadLocal 中绑定数据
     * 注意是:Thread.currentThread() 当前线程作为 key 存在
     */
    public void set(T t) {
        map.put(Thread.currentThread(),t);

    }


    /**
     * 向ThreadLocal 当中获取数据
     * 注意:获取到的是当前线程的Connection绑定的数据,
     */
    public T get() {
        // 通过 Thread.currentThread()当中线程作为key ,获取到对应的 value值
        return map.get(Thread.currentThread());

    }

    /**
     * 移除ThreadLocal当中数据
     * 注意:移除的是Thread.currentThread()当前线程作为key 存储的数据信息。
     */
    public void remove() {
        map.remove(Thread.currentThread());
    }
}

package com.rainbowSea.testThreadLocal;



public class DBUtil {

    // 静态变量特点:类加载时执行,并且只执行一次
    // 全局的大Map集合
    public static MyThreadLocal<MyConnection> local = new MyThreadLocal<MyConnection>();


    /**
     * 每一次都调用这个方法来获取Connection 对象
     */
    public static MyConnection getConnection() {
        // 从这个大的Map当中获取 Connection 对象
        MyConnection connection = local.get();

        // 如果是第一次:获取到的话这个 大MyThreadLocal 是没有存储到 Connection 对象的
        // 所有我们需要向 MyThreadLocal 添加上
        if (connection == null) {
            connection = new MyConnection();
            // 添加到 这个大Map当中
            local.set(connection);
        }

        // 返回从这个大Map当中获取到的Connection对象
        return connection;
    }

}

package com.rainbowSea.testThreadLocal;

public class Test {
    public static void main(String[] args) {
        // 从大Map MyThreadLocal中获取Connection对象
        MyConnection myConnection = DBUtil.getConnection();
        System.out.println("Test Connection : " + myConnection);


        UserService userService = new UserService();
        userService.save();
    }
}

package com.rainbowSea.testThreadLocal;

public class UserService {

    public UserDao userDao = new UserDao();

    public void save() {
        // 从大Map MyThreadLocal中获取Connection对象
        MyConnection myConnection = DBUtil.getConnection();
        System.out.println("UserService Connection :" + myConnection);

        userDao.insert();

    }
}

package com.rainbowSea.testThreadLocal;

public class UserDao {

    public void insert(){
        // 从大Map MyThreadLocal中获取Connection对象
        MyConnection myConnection = DBUtil.getConnection();
        System.out.println("UserDao Connection : " + myConnection);
    }
}

测试:

在这里插入图片描述

4. ThreadLocal 源码原理分析

下面我们来看看,Java为我们提供的 ThreadLocal 类吧
在这里插入图片描述

在这里插入图片描述

ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal, 但是实际上使用的值value却是自己独有的一份。 用一图直接表示threadlocal 的使用方式

img

从图中我们可以当线程使用threadlocal 时,是将threadlocal当做当前线程thread的属性ThreadLocalMap 中的一个Entry的key值,实际上存放的变量是Entry的value值,我们实际要使用的值是value值。 value值为什么不存在并发问题呢,因为它只有一个线程能访问。threadlocal我们可以当做一个索引看待,可以有多个threadlocal 变量,不同的threadlocal对应于不同的value值,他们之间互不影响。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。简单的说就是:实现一个线程当中的信息对象共用,共享。代替传引用类型参数的方式。

这里我们使用ThreadLocal是基于一个用户转账的案例来讲解的,为了解决事务上控制问题,一个线程共用一个Connection 。其中的我们的ThreadLocal 就作为了一个容器,其中的key 存储的就是当前线程,而value值则是对应Connection

在这里插入图片描述

5. ThreadLocal 常用方法

在这里插入图片描述

方法名描述
ThreadLocal()创建ThreadLocal对象
public void set( T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public T remove()移除当前线程绑定的局部变量,该方法可以帮助JVM进行GC
protected T initialValue()返回当前线程局部变量的初始值

5.1 ThreadLocal的set()方法


 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
 }
/**
     * 设置当前线程对应的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,this这里指调用此方法的ThreadLocal对象
            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);
    }

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。大家最后自己再idea上跟下源码,会有更深的认识。

static class ThreadLocalMap {
 
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
  1. 获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
  3. 如果Map为空,则给该线程创建 Map,并设置初始值

在这里插入图片描述

5.2 ThreadLocal的get( )方法

/**
     * 返回当前线程中保存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;
    }

执行流程

  1. 获取当前线程, 根据当前线程获取一个Map
  2. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entrye,否则转到4
  3. 如果e不为null,则返回e.value,否则转到4
  4. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

在这里插入图片描述

5.3 ThreadLocal的remove( )方法

/**
     * 删除当前线程中保存的ThreadLocal对应的实体entry
     */
     public void remove() {
        // 获取当前线程对象中维护的ThreadLocalMap对象
         ThreadLocalMap m = getMap(Thread.currentThread());
        // 如果此map存在
         if (m != null)
            // 存在则调用map.remove
            // 以当前ThreadLocal为key删除对应的实体entry
             m.remove(this);
     }

执行流程:

  1. 首先获取当前线程,并根据当前线程获取一个Map
  2. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

remove()方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。

为什么要删除,这涉及到内存泄露的问题?。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

5.4 ThreadLocal 的 initialValue( )方法

  • 此方法的作用是返回该线程局部变量的初始值
  • 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次
  • 这个方法缺省实现直接返回一个null
  • 如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
/**
  * 返回当前线程对应的ThreadLocal的初始值
  * 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
  * 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
  * 通常情况下,每个线程最多调用一次这个方法。
  *
  * <p>这个方法仅仅简单的返回null {@code null};
  * 如果想ThreadLocal线程局部变量有一个除null以外的初始值,
  * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
  * 通常, 可以通过匿名内部类的方式实现
  *
  * @return 当前ThreadLocal的初始值
  */
protected T initialValue() {
    return null;
}

6. ThreadLocal 注意移除数据

当我们对于 ThreadLocal 中的value值资源对象,使用完毕的时候,一定要执行ThreadLocal.remove()对象方法的移除绑定在ThreadLocal中的资源信息。因为一般ThreadLocal的使用场景都是在 多线程的,而多线程一般都是使用线程池管理线程的。就会存在一个问题?

我们通过上述账户转账案例来讲解这个问题:

我们的运行环境是在Tomcat10,Tomcat10本身就是一个多线程的。

如下当我们对应一个数据库操作完以后,我们需要将对应的资源释放。最后使用的最先关闭,分开 try,防止关闭资源的时候出现异常导致其他资源没有关闭。

如下:我们的ThreadLocal对应的 value 是 Connection 对象

// 创建 ThreadLocal 容器存储绑定线程相关的 信息
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    /**
     * 这里没有使用数据库连接池,直接创建连接对象
     */
    public static Connection getConnection() {
        Connection connection = threadLocal.get();  // 从ThreadLocal容器中获取
        try {

            // 第一次ThreadLocal 是为空的
            if (connection == null) {
                connection = DriverManager.getConnection(url, user, password);
                threadLocal.set(connection);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }


if (connection != null) {
            try {
                connection.close();
                threadLocal.remove();  // 注意关闭资源的时候需要将绑定在threadLocal移除
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }

当我们将Connection 资源关闭了,但是:其中该Connection 存储在ThreadLocal当中value值,却是还存储着是为 当前Connection 关闭了的对象的。由于Tomcat是多线程,其中Tomcat服务器是内置了一个线程池的。线程池中的多线程对象是有限的,
这样线程对象 t1,t2,t3 都是提前创建好的,也就是说,t1,t2,t3,是在重复使用的,如果你没有将其 ThreadLocal.remove( 移除掉),
当新的用户,一个新的线程,出现的时候,可能会获取到上一个Connection 已经关闭了的对象,t1线程对象。从而导致的结果就是 这个新用户使用的是 t1 这个对应上的Connection 对象已经关闭了,出现错误。

所以对于: ThreadLocal 中的value值资源对象,使用完毕的时候,一定要执行ThreadLocal.remove()对象方法的移除绑定在ThreadLocal中的资源信息

在这里插入图片描述

7. ThreadLocal 内存泄漏

1 . 没有手动删除这个 Entry
2 . CurrentThread 当前线程依然运行

​ 第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法删除对应的 Entry ,就能避免内存泄漏。
​ 第二点稍微复杂一点,由于ThreadLocalMap 是 Thread 的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期跟 Thread 一样长。如果threadlocal变量被回收,那么当前线程的threadlocal 变量副本指向的就是key=null, 也即entry(null,value),那这个entry对应的value永远无法访问到。实际私用ThreadLocal场景都是采用线程池,而线程池中的线程都是复用的,这样就可能导致非常多的entry(null,value)出现,从而导致内存泄露。
综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,对于重复利用的线程来说,如果没有手动删除(remove()方法)对应 key 就会导致entry(null,value)的对象越来越多,从而导致内存泄漏.

8. 正确的使用ThreadLocal

  1. 将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露
  2. 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

9. ThreadLocal 常见使用场景

如上文所述,ThreadLocal 适用于如下两种场景

  1. 每个线程需要有自己单独的实例
  2. 实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

10. 案例:MVC三层架构 + 面向接口编程 + ThreadLocal 事务处理实:现用户转账功能的优化

如下是我们对于上篇MVC三层架构🔜🔜🔜 MVC 三层架构案例详细讲解_ChinaRainbowSea的博客-CSDN博客 存在事务安全问题的优化:

对应的包架构:

  • resources: 表示一些资源:比如这里是一些连接数据库的一些配置信息:
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/mvc
user=root
password=MySQL
  • lib : 该项目所需要的依赖:注意:该目录名一定要为:lib,不然Tomcat 无法识别到,该目录一定要在web/WEB-INF/下,不然Tomcat 无法识别到的。

在这里插入图片描述

  • M(Model 模型层/业务逻辑处理层)
    • utils : 工具包,这里使用一个数据库连接的工具包。
    • dao : 表示对应XXx数据表的业务逻辑上的处理(增删改查)。面向接口编程:AccountDao,接口定义规范,这里的AccoutDao 定义了对于这张account数据表的业务逻辑上的操作规范。
      • Impl: 表示实现的接口的类:AccountDaoImplImpl命名为后缀,表示接口的实现类。这是大家共识的一种规范。
    • exceptions : 表示对应业务上自定义的异常。
    • javaBean: 表示对应的封装数据的实体类。
    • service: 表示对应的业务逻辑的处理。面向接口编程:AccountService,接口定义规范,这里的AccountService定义了对于这张account数据表的业务逻辑处理上的操作规范,可能需要多个 Dao同时配合。获取多个Service 之间相互配合。
      • Impl: 表示实现的接口的类:AccountServiceImplImpl命名为后缀,表示接口的实现类。这是大家共识的一种规范。
  • C(Controller 控制层):对应 M层,V的之间的桥梁,进行一个调度处理,本身仅仅只做一个调度,不进行业务的处理。比如一个事情:需要调度M层进行处理,同时需要将该M层处理的结果,通过调度V层显示给用户。

在这里插入图片描述

在这里插入图片描述

10.1 M(Model 模型层/业务逻辑处理层)

package com.RainbowSea.bank.utils;


import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ResourceBundle;

public class DBUtil {

    // resourceBundle 只能读取到 properties 后缀的文件,注意不要加文件后缀名
    private static ResourceBundle resourceBundle = ResourceBundle.getBundle("resources/jdbc");
    private static String driver = resourceBundle.getString("driver");
    private static String url = resourceBundle.getString("url");
    private static String user = resourceBundle.getString("user");
    private static String password = resourceBundle.getString("password");


    // DBUtil 类加载注册驱动
    static {
        try {
            Class.forName(driver);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }


    // 将构造器私有化,不让创建对象,因为工具类中的方法都是静态的,不需要创建对象
    // 为了防止创建对象,故将构造方法私有化
    private DBUtil() {

    }


    // 创建 ThreadLocal 容器存储绑定线程相关的 信息
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>();

    /**
     * 这里没有使用数据库连接池,直接创建连接对象
     */
    public static Connection getConnection() {
        Connection connection = threadLocal.get();  // 从ThreadLocal容器中获取
        try {

            // 第一次ThreadLocal 是为空的
            if (connection == null) {
                connection = DriverManager.getConnection(url, user, password);
                threadLocal.set(connection);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        return connection;
    }


    /**
     * 资源的关闭
     * 最后使用的最先关闭,逐个关闭,防止存在没有关闭的
     */
    public static void close(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet) {

        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }


        if (preparedStatement != null) {
            try {
                preparedStatement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }


        if (connection != null) {
            try {
                connection.close();
                threadLocal.remove();  // 注意关闭资源的时候需要将绑定在threadLocal移除
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

package com.RainbowSea.bank.javabeen;

import java.io.Serializable;
import java.util.Objects;


/**
 * 账户实体类,封装账户信息的
 * 一般是一张表一个。
 * pojo 对象
 * 有的人也会把这种专门封装数据的对象,称为:"bean对象" (javabean对象,咖啡豆)
 * 有的人也会把这种专门封装数据的对象,称为领域模型对象,domain对象
 * 不同的程序员不同的习惯。
 */
public class Account implements Serializable {  // 这种普通的简单的对象被成为pojo对象
    // 注意我们这里定义的数据类型,使用引用数据类型
    // 因为我们数据库中可能存在 null 值,而基本数据类型是不可以存储 null值的

    private Long id = null;  // id
    private String actno;  // 账号
    private Double balance; // 余额

    // 反序列化
    private static final long serialVersionUID = 1L;

    public Account() {
    }


    public Account(Long id, String actno, Double balance) {
        this.id = id;
        this.actno = actno;
        this.balance = balance;
    }


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public Double getBalance() {
        return balance;
    }

    public void setBalance(Double balance) {
        this.balance = balance;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Account)) return false;
        Account account = (Account) o;
        return Objects.equals(getId(), account.getId()) && Objects.equals(getActno(), account.getActno()) && Objects.equals(getBalance(), account.getBalance());
    }

    @Override
    public int hashCode() {
        return Objects.hash(getId(), getActno(), getBalance());
    }

    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", actno='" + actno + '\'' +
                ", balance=" + balance +
                '}';
    }
}

package com.RainbowSea.bank.dao;

import com.RainbowSea.bank.javabeen.Account;

import java.util.List;

public interface AccountDao {


    /**
     * 插入数据
     *
     * @param account
     * @return
     */
    public int insert(Account account);


    /**
     * 通过Id删除数据
     *
     * @param id
     * @return
     */
    public int deleteById(String id);

    /**
     * 更新数据
     *
     * @param account
     * @return
     */
    public int update(Account account);



    /**
     * 通过 actno 查找账户信息
     *
     * @param actno
     * @return
     */
    public Account selectByActno(String actno);

    /**
     * 查询所有的账户信息
     *
     * @return
     */
    public List<Account> selectAll();

}

package com.RainbowSea.bank.dao.impl;


import com.RainbowSea.bank.dao.AccountDao;
import com.RainbowSea.bank.javabeen.Account;
import com.RainbowSea.bank.utils.DBUtil;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * AccountDao 是负责Account 数据的增上改查
 * <p>
 * 1. 什么是DAO ?
 * Data Access Object (数据访问对象)
 * 2. DAO实际上是一种设计模式,属于 JavaEE的设计模式之一,不是 23种设计模式
 * 3.DAO只负责数据库表的CRUD ,没有任何业务逻辑在里面
 * 4.没有任何业务逻辑,只负责表中数据增上改查的对象,有一个特俗的称谓:DAO对象
 * 5. 为什么叫做 AccountDao 呢?
 * 这是因为DAO是专门处理t_act 这张表的
 * 如果处理t_act 表的话,可以叫做:UserDao
 * 如果处理t-student表的话,可以叫做 StudentDao
 * <p>
 * int insert() ;
 * int deleteByActno();
 * int update() ;
 * Account selectByActno();
 * List<Account> selectAll();
 */
public class AccountDaoImpl implements AccountDao {


    /**
     * 插入数据
     *
     * @param account
     * @return
     */
    public int insert(Account account) {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        int count = 0;
        try {
            String sql = "insert into t_act(actno,balance) values(?,?)";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, account.getActno());
            preparedStatement.setDouble(2, account.getBalance());
            count = preparedStatement.executeUpdate();


        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }


        return count;

    }


    /**
     * 通过Id删除数据
     *
     * @param id
     * @return
     */
    public int deleteById(String id) {
        Connection connection = DBUtil.getConnection();
        int count = 0;
        PreparedStatement preparedStatement = null;
        try {
            String sql = "delete from t_act where id = ?";
            preparedStatement = connection.prepareStatement(sql);
            preparedStatement.setString(1, id);
            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, null);
        }

        return count;

    }


    /**
     * 更新数据
     *
     * @param account
     * @return
     */
    public int update(Account account) {
        PreparedStatement preparedStatement = null;
        Connection connection = DBUtil.getConnection(); // 从 ThreadLocal中获取到的
        int count = 0;

        System.out.println("update: "+connection);

        try {
            String sql = "update t_act set balance = ?, actno = ? where id = ?";
            preparedStatement = connection.prepareStatement(sql);

            //注意设置的 set类型要保持一致。
            preparedStatement.setDouble(1, account.getBalance());
            preparedStatement.setString(2, account.getActno());
            preparedStatement.setLong(3, account.getId());

            count = preparedStatement.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(null, preparedStatement, null);
        }

        return count;
    }


    /**
     * 通过 actno 查找账户信息
     *
     * @param actno
     * @return
     */
    public Account selectByActno(String actno) {
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        Account account = new Account();
        Connection connection = DBUtil.getConnection(); // 从 ThreadLocal中获取到的
        System.out.println("selectByActno :" + connection);


        try {
            String sql = "select id,actno,balance from t_act where actno = ?";
            preparedStatement = connection.prepareStatement(sql);

            //注意设置的 set类型要保持一致。
            preparedStatement.setString(1, actno);

           resultSet = preparedStatement.executeQuery();

            if (resultSet.next()) {
                Long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                // 将结果集封装到java 对象中
                account.setActno(actno);
                account.setId(id);
                account.setBalance(balance);

            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(null, preparedStatement, resultSet);
        }

        return account;
    }


    /**
     * 查询所有的账户信息
     *
     * @return
     */
    public List<Account> selectAll() {
        Connection connection = DBUtil.getConnection();
        PreparedStatement preparedStatement = null;
        ResultSet resultSet = null;
        List<Account> list = null;

        try {
            String sql = "select id,actno,balance from t_act";
            preparedStatement = connection.prepareStatement(sql);

            resultSet = preparedStatement.executeQuery();

            while (resultSet.next()) {
                String actno = resultSet.getString("actno");
                Long id = resultSet.getLong("id");
                Double balance = resultSet.getDouble("balance");
                // 将结果集封装到java 对象中
                Account account = new Account(id,actno,balance);

                // 添加到List集合当中
                list.add(account);

            }

        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DBUtil.close(connection, preparedStatement, resultSet);
        }

        return list;
    }


}

package com.RainbowSea.bank.exceptions;


/**
 * 余额不足异常
 */
public class AppException extends Exception{

        public AppException() {

        }

        public AppException(String msg) {
            super(msg);
        }

}

package com.RainbowSea.bank.exceptions;


/**
 * 余额不足异常
 */
public class MoneyNotEnoughException extends Exception{
    public MoneyNotEnoughException() {

    }

    public MoneyNotEnoughException(String msg) {
        super(msg);
    }
}

package com.RainbowSea.bank.service;

import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;

public interface AccountService {
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException;
}

package com.RainbowSea.bank.service.impl;


import com.RainbowSea.bank.dao.AccountDao;
import com.RainbowSea.bank.dao.impl.AccountDaoImpl;
import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;
import com.RainbowSea.bank.javabeen.Account;
import com.RainbowSea.bank.service.AccountService;
import com.RainbowSea.bank.utils.DBUtil;

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

/**
 * service 翻译为:业务。
 * AccountService 专门处理Account业务的一个类
 * 在该类中应该编写纯业务代码。(只专注域业务处理,不写别的,不和其他代码混合在一块)
 * 只希望专注业务,能够将业务完美实现,少量bug.
 * <p>
 * 业务类一般起名:XXXService,XXXBiz...
 */
public class AccountServiceImpl implements AccountService {

    // 这里的方法起名,一定要体现出,你要处理的是什么业务:
    // 我们要提供一个能够实现转账的业务的方法(一个业务对应一个方法)
    // 比如:UserService StudentService OrderService

    // 处理Account 转账业务的增删改查的Dao
    private AccountDao accountDao = new AccountDaoImpl();  // 多态:父类的引用指向子类

    /**
     * 完成转账的业务逻辑
     *
     * @param fromActno 转出账号
     * @param toActno   转入账号
     * @param money     转账金额
     */
    public void transfer(String fromActno, String toActno, double money) throws MoneyNotEnoughException, AppException {

        Thread thread = Thread.currentThread();  // 获取当前线程
        System.out.println("seervice tranfer: " + thread);

        Connection connection = DBUtil.getConnection(); // 从ThreadLocal获取到的
        // service 层控制事务:
        // 事务的控制需要 Connection 对象
        try {  // 自动管理,会自动关闭资源
            // 开启事务
            connection.setAutoCommit(false);

            System.out.println("service transfer: " + connection);


            // 查询余额是否充足
            Account fromAct = accountDao.selectByActno(fromActno);
            if (fromAct.getBalance() < money) {
                throw new MoneyNotEnoughException("对不起,余额不足");
            }

            // 程序到这里说明余额充足
            Account toAct = accountDao.selectByActno(toActno);

            // 修改金额,先从内存上修改,再从硬盘上修改
            fromAct.setBalance(fromAct.getBalance() - money);
            toAct.setBalance(toAct.getBalance() + money);

            // 从硬盘数据库上修改
            int count = accountDao.update(fromAct);

            // 模拟异常
        /*    String s = null;
            s.toString();*/


            count += accountDao.update(toAct);

            if (count != 2) {
                throw new AppException("账户转账异常,请联系管理员");
            }


            // 提交事务
            connection.commit();

        } catch (SQLException e) {
            // 事务的回滚
            // 因为我们这里是失败了,是不会提交数据的,数据库也就不会发生改变了。
            throw new AppException("账户异常,请联系管理员");
        } finally {
            // 关闭资源,移除ThreadLocal当中绑定的 Connection 对象
            DBUtil.close(connection, null, null);
        }


    }
}

10.2 C(Controller 控制层)

package com.RainbowSea.bank.web;

import com.RainbowSea.bank.exceptions.AppException;
import com.RainbowSea.bank.exceptions.MoneyNotEnoughException;
import com.RainbowSea.bank.service.AccountService;
import com.RainbowSea.bank.service.impl.AccountServiceImpl;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;


/**
 * 账户小程序
 * AccountServlet 是一个司令官,他负责调度其他组件来完成任务。
 *
 */
@WebServlet("/transfer")
public class AccountServlet extends HttpServlet { // AccountServlet 作为一个 Controller 司令官

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,
            IOException {

        // 获取数据
        String fromActno = request.getParameter("fromActno");
        String toActno = request.getParameter("toActno");
        double money = Double.parseDouble(request.getParameter("money"));

        // 调用业务方法处理业务(调度Model处理业务,其中是对应数据表的 CRUD操作)
        AccountService accountService = new AccountServiceImpl();  // 多态 父类的引用指向子类
        try {
            accountService.transfer(fromActno,toActno,money);
            // 执行到这里说明,成功了,
            // 展示处理结束(调度 View 做页面展示)

            response.sendRedirect(request.getContextPath()+"/success.jsp");
        } catch (MoneyNotEnoughException e) {
            // 执行到种类,说明失败了,(余额不足
            // 展示处理结束(调度 View 做页面展示)
            response.sendRedirect(request.getContextPath()+"/error.jsp");

        } catch (AppException e) {
            // 执行到种类,说明失败了,转账异常
            // 展示处理结束(调度 View 做页面展示)
            response.sendRedirect(request.getContextPath()+"/error.jsp");

        }

        // 页面的展示 (调度View做页面展示)


    }
}

10.3 V(View 显示层)


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
  <title>银行账号转账</title>
</head>
<body>
<form action="<%=request.getContextPath()%>/transfer" method="post">
  转出账户: <input type="text" name="fromActno" /> <br>
  转入账户: <input type="text" name="toActno" /> <br>
  转账金额: <input type="text" name="money" /><br>
  <input type="submit" value="转账" />
</form>
</body>
</html>


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>转账成功</title>
</head>
<body>

<h3>转账成功</h3>
</body>
</html>


<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>转账失败</title>
</head>
<body>
<h3>转账失败</h3>
</body>
</html>

10.4 测试

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

11. ThreadLocal与Synchronized的区别

ThreadLocal其实是与线程绑定的一个变量。 ThreadLocal和Synchonized都用于解决多线程并发访问。

但是ThreadLocal与synchronized有本质的区别:

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  2. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

  3. 而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量地址是一样的。

12. ThreadLocal与Thread,ThreadLocalMap之间的关系

img

img

Thread、THreadLocal、ThreadLocalMap之间啊的数据关系图

从这个图中我们可以非常直观的看出,ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维护ThreadLocalMap这个属性指的一个工具类。Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

13. 总结:

  1. ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程 ,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

  2. ThreadLocal的主要用途是实现线程间变量的隔离,表面上他们使用的是同一个ThreadLocal, 但是实际上使用的值value却是自己独有的一份。 简单的说就是:实现一个线程当中的信息对象共用,共享。代替传引用类型参数的方式。

  3. ThreadLocal 常用的方法。

  4. 当我们对于 ThreadLocal 中的value值资源对象,使用完毕的时候,一定要执行ThreadLocal.remove()对象方法的移除绑定在ThreadLocal中的资源信息

14. 最后:

⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐ 感谢如下博主的分享: ⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

【1】https://blog.csdn.net/u010445301/article/details/111322569?spm=1001.2014.3001.5502

【2】https://blog.csdn.net/u010445301/article/details/124935802?csdn_share_tail

【3】https://blog.csdn.net/silence_yb/article/details/124265702?ops_request_misc

限于自身水平,其中存在的错误,希望大家,给予指教,韩信点兵——多多益善,谢谢大家,江湖再见,后会有期!!!

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

云厂商降价潮背后:来中小企业战场「拼刺刀」

如果说过往云厂商的降价打响的是从C端进军B端的营销战&#xff0c;那么在这一轮降价潮背后&#xff0c;对应的则是云厂商从大型KA客户向中小企业进军的信号&#xff0c;强被集成&#xff0c;强获客。 云厂商又一轮降价潮袭来。 5月16日&#xff0c;移动云宣布部分产品线最高降…

技术探秘:揭秘Bean Factory与FactoryBean的区别!

大家好&#xff0c;我是小米&#xff0c;一个热衷于技术分享的29岁小编。今天&#xff0c;我们来聊一聊在Spring框架中常用的两个概念&#xff1a;beanFactory和FactoryBean。它们虽然看似相似&#xff0c;但实际上有着不同的用途和作用。让我们一起来揭开它们的神秘面纱吧&…

SaleSmartly聊天机器人如何帮助您的电商业务(二)

上文为大家介绍了两类机遇SaleSmartly&#xff08;ss客服)强大的自动化编辑器构建的两类聊天机器人&#xff0c;接下来继续为大家介绍两种类型的聊天机器人。SaleSmartly&#xff08;ss客服&#xff09;可构建的聊天机器人还有以下两类&#xff1a; 3. NLP聊天机器人 SaleSma…

Datax+Datax-web2.1实现MySQL数据库数据同步(二)

目录 流程1、创建项目2、创建数据源3、创建任务模板4、构建任务5、执行同步任务 安装文档参考&#xff1a;https://blog.csdn.net/zhanremo3062/article/details/130728287 流程 这里以2.1.2版本为例&#xff0c;datax-web使用起来还是很顺滑的&#xff0c;操作流程跟其他任务…

结合自由能计算

结合自由能计算 打分函数 背景 打分函数广泛应用于基于结构的计算辅助药物设计&#xff0c;其通过定量化评估药-靶的相互作用为药物研发中的药效评估提供理论依据&#xff0c;提高活性化合物甄别的效率。定量评估药物与靶标蛋白的相互作用通常分为两步&#xff0c;一步是对接…

Android之 fragment页面碎片详解

一 简介 1.1 Fragment是Android3.0新增的概念&#xff0c;中文意思是“碎片”&#xff0c;它与Activity非常相似&#xff0c;是用来描述一些行为或者一部分用户界面 1.2 可以在一个单独的Activity中建立多个Fragment面板&#xff0c;也可以在多个Activity中复用Fragment 1.3…

【OpenCV】C++红绿灯轮廓识别+ROS话题实现

目录 前言 一、背景知识 Opencv轮廓检测 ROS相关知识 二、环境依赖 三、具体实现 Step1&#xff1a;初始化ROS&#xff0c;订阅话题 Step2&#xff1a;接收话题&#xff0c;进入回调 1. 帧处理 2. 膨胀腐蚀处理 Step3&#xff1a;红绿特征处理 1. 提取绘制轮廓 2…

20230517提升cv1826的打印等级

20230517提升cv1826的打印等级 2023/5/17 17:43 https://www.xitongjiaocheng.com/linux/2017/53494.html Linux内核log等级与printk打印消息控制 时间&#xff1a;2017-03-13 出处&#xff1a;系统之家复制分享人气(206次) 【大中小】 printk打印消息控制 // linux/includ…

认养农业小程序开发 赋予农业发展新模式

传统农业发展到今天&#xff0c;无论是规模还是方式都发生了很大的改变&#xff0c;尤其是在信息化时代影响下&#xff0c;智慧农业一步步发展起来&#xff0c;通过认养这种新模式与都市中想要体验农场乐趣的人群联系起来&#xff0c;既满足了都市人群体验农场生活乐趣的目的也…

720vr全景线上看车帮助企业快速对接到意向客户

360VR全景看车可以高精度还原线下展厅和各类汽车车型&#xff0c;进入VR汽车3d展厅&#xff0c;实现360度无死角VR看车&#xff0c;可以任意的查看和缩放&#xff0c;消费者根据自己的喜好更换车身的颜色&#xff0c;一键对比不同车型的外观、性能、耗能等&#xff0c;不用出门…

小程序架构足够安全吗?数据安全如何保障?

小程序大家已经再熟悉不过了&#xff0c;就是一种在移动操作系统中运行的轻量级应用程序&#xff0c;小程序发展这么多年来&#xff0c;是中国 IT 行业里为数不多的能够真正影响到普通程序员的创新成果。 当然随着小程序的流行&#xff0c;小程序的各个方面都是开发者讨论的热…

Navicat 数据备份与恢复功能 | 有效预防误删误改、删库跑路、软硬件故障造成的数据丢失

当今社会已进入了数据爆炸的时代&#xff0c;数据成为企业最重要的资产之一。如果没有采取数据备份和数据恢复手段与措施&#xff0c;就会导致数据的丢失&#xff0c;有时造成的损失是无法弥补估量的。市场上有很多数据备份的方法&#xff0c;今天我们给大家分享广受业界好评的…

首届百度商业AI技术创新大赛重磅启动,以前沿科技革新生产力

随着生成式AI在全球范围的热议&#xff0c;你可以想象在不远的将来AI将与你的工作、生活、出行等各种场景紧密相连。正如百度创始人、董事长兼CEO李彦宏所说&#xff1a;“以深度学习、算法为代表的人工智能革命是第四次科技革命”。 作为拥有强大互联网基础的领先AI公司&…

神经网络:Zero2Hero 3 - Tanh、Gradient、BatchNormal

Zero2Hero : 3 - Tanh、Gradient、BatchNormal 接上篇&#xff0c;对MLP模型有进一步进行了修改&#xff0c;增加BatchNormal、和激活函数。深入研究深层网络的内部&#xff0c;激活、反向传递梯度以及随机初始化的陷阱。BatchNormal的作用。 import torch import torch.nn.f…

python:图形用户界面GUI(模拟登录、计算器...)

文章目录 一、Tkinter简介1、第一个tkinter窗口2、在窗口内加入组件2.1 思考题&#xff08;问题与答案&#xff09; 3、坐标管理器 二、Tkinter组件及其属性1、Label组件和Entry组件2、计算器代码 引言&#xff1a;我们以QQ为例&#xff0c;当我们点击QQ图标时候&#xff0c;它…

SpringBoot整合MyBatis-Plus实现增删改查

简介 MyBatis-Plus (opens new window)的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入&#xff1a;只做增强不做改变&#xff0c;引入它不会对现有工程产生影响&#xff0c;如丝般顺滑损耗小&#xff1a;启…

一个有趣的avs编码器(注意,是avs,而不是avs2噢)

本章附件是一个清华大学写的关于avs编解码器: https://download.csdn.net/download/weixin_43360707/87793302 该编码器遵循了stuffing bit: 打开文件夹后&#xff0c;如下&#xff1a; 可以看出这个是个跨平台的工程&#xff0c;提供了windows vs2015的工程文件sln&#x…

【数据结构】栈的详解

☃️个人主页&#xff1a;fighting小泽 &#x1f338;作者简介&#xff1a;目前正在学习C语言和数据结构 &#x1f33c;博客专栏&#xff1a;数据结构 &#x1f3f5;️欢迎关注&#xff1a;评论&#x1f44a;&#x1f3fb;点赞&#x1f44d;&#x1f3fb;留言&#x1f4aa;&…

pom里加依赖和把jar包放到lib文件夹下的区别

首先,什么是jar包,jar包其实就是一个a项目打成了a.jar包,然后b项目引入了a.jar包,然后b项目就能用到a项目里面的工具类了. b项目怎么引入a.jar包呢. 第一种:直接把a.jar包放到lib文件夹下(不推荐) 第二种:在pom里添加maven依赖,把a.jar包引过来(推荐) 在pom里加的依赖跟直接…

小学妹刚毕业没地方住想来借宿,于是我连夜用Python给她找了个好房子,我真是太机智了

事情是这样的&#xff0c;小学妹刚毕业参加工作&#xff0c;人生地不熟的&#xff0c;因为就在我附近上班&#xff0c;所以想找我借宿。。。 想什么呢&#xff0c;都不给住宿费&#xff0c;想免费住&#xff1f;于是我用Python连夜给她找了个单间&#xff0c;自己去住吧&#…