多线程,内存存放数据检验

Posted hyiam

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程,内存存放数据检验相关的知识,希望对你有一定的参考价值。

背景:为提高帐户校验效率,将原来的数据库检验方式变更为将数据存于内存,动态变更内存数据,检验取内存数据的方式,解决检验问题

方案:

  1. 创建对象Map用于存放帐户当前可用金额信息

  2. 检验时,判断金额信息是否存在,存在则使用内存金额进行校验,不存在则查询数据库获取最新的可用金额信息

  3. 检验通过,则更新可用金额

  4.每隔一段时间,对内存消息进行核对,将数据库查询到的信息更新到内存中

关键点实现:

  1. 存放帐户可用金额的对用,必须是线程安全的,使用ConcurrentHashMap,可以在保证效率的同时做到线程安全。

  eg:

  // 账户可用额度相关信息Map
  public static ConcurrentHashMap<Long,AccountGroupInfo> currentAccountGroupAmountMap = new ConcurrentHashMap<Long,AccountGroupInfo>();

  2. 在对帐户可用额度Map进行操作时,需要进行加锁,保证多线程调用时的数据正确性,同时为了满足加锁未成功的情况下,可设置等待时间,不让校验操作无限等待。

在Map中设计isLock字段,标志帐户是被锁,进行校验,更新操作时,需要对帐户进行加锁。

  3. 在进行加锁操作时,需要进行同步处理,保证锁的获取是同步的。使用synchronized字段实现锁,但是只对单个的帐户对象进行加锁,保证在同一个帐户的加锁操作上是同步的。

  4. 对加锁操作进行尝试次数和等待时间控制,加锁操作可以尝试5次,每次尝试失败等待100毫秒,如果在5次100毫秒内未加锁成功,则抛出异常,将此次的校验置为失败,进行下次操作

  eg:

synchronized (accountGroupInfo) {
            boolean lockSuccess = false;
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"尝试加锁次数"+(i+1)+"==========laccountid============"+laccountid);
                if(!accountGroupInfo.isLock()){
                    accountGroupInfo.setLock(true);
                    lockSuccess = true;
                    System.out.println("---------------加锁成功"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                    break;
                }else{
                    Thread.sleep(100);
                }
            }
            if(!lockSuccess){
                System.out.println("加锁失败"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                throw new Exception("加锁失败");
            }
        }

附:

  1.对象加锁

       String key = getKey();
       Object object = getLock(key);
       synchronized (object ) {
             // 逻辑处理代码
       }
        /**
     * 得到对象锁
     */
    private static Object getLock(Long key){
        Object value = new Object();
        Object v = locks.putIfAbsent(key, value);
        if(v == null){
            return value;
        }
        return v;
    } 

  

 

  2. 锁逻辑原则

   3. 整体代码实现

package com.iss.itreasury.accountgroup;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.concurrent.ConcurrentHashMap;

import com.iss.itreasury.settlement.base.SettlementException;
import com.iss.itreasury.settlement.setting.dao.SettAccountGroupSettingDao;
import com.iss.itreasury.settlement.util.UtilOperation;
import com.iss.itreasury.util.DataFormat;
import com.iss.itreasury.util.Database;
import com.iss.itreasury.util.Env;

public class AccountGroupOperation {
    
    // 账户与账户组的关联关系
    public static ConcurrentHashMap<Long,Long> accountGroup = new ConcurrentHashMap<Long,Long>();
    
    // 账户组可用额度相关信息Map
    public static ConcurrentHashMap<Long,AccountGroupInfo> currentAccountGroupAmountMap = new ConcurrentHashMap<Long,AccountGroupInfo>();
    
    // 帐户组锁
    public static ConcurrentHashMap<Long, Object> locks = new ConcurrentHashMap<Long, Object>();
    

    /**
     *  判断该账户所在账户组本区间是否透支
     * @param 账户ID,支付金额
     * @return 透支true   不透支false
     * @throws Exception 
     * */
    public static boolean isOverDraftAccgroup(long laccountid,double payamount,long lOfficeID,long lCurrencyID) throws Exception{
        
        return isOverDraftAccgroup(laccountid, payamount, lOfficeID, lCurrencyID,false);
    }
    
    
    /**
     *  判断该账户所在账户组本区间是否透支
     * @param 账户ID,支付金额
     * @return 透支true   不透支false
     * @throws Exception 
     * */
    public static boolean isOverDraftAccgroup(long laccountid,double payamount,long lOfficeID,long lCurrencyID,boolean isEbank) throws Exception{
        boolean flag = false;
        // 开机日期
        Timestamp systemDate = Env.getSystemDate(lOfficeID, lCurrencyID);
        SettAccountGroupSettingDao dao = new SettAccountGroupSettingDao();
        // 1. 帐户是否加入了帐户组
        long accountGroupId;
        // 如果账户与帐户组关联关系没有,进行初始化
        if(isEbank){
            accountGroup = new ConcurrentHashMap<Long,Long>();
        }
        if(accountGroup.size()==0){
            AccountGroupOperation.init();
        }
        try {
            // 1.1 获取帐户组ID
            accountGroupId = findAccountGroup(laccountid);
        } catch (Exception e1) {
            e1.printStackTrace();
            throw new Exception("获取帐户组关系失败");
        }
        // 1.2 帐户不在帐户组中,无需校验
        if(accountGroupId == -1){
            System.out.println("帐户:"+laccountid+"不在帐户组中,无需校验");
            return false;
        }
        Object lock = getLock(accountGroupId);
        // 2. 内存currentAccountGroupAmountMap中是否存在该账户组可用额度相关信息
        synchronized (lock) {
            if(currentAccountGroupAmountMap.get(accountGroupId)==null||isEbank){
                System.out.println("==================账户组可用额度Map初始化==================");
                // 2.1 账户组额度信息初始化
                AccountGroupInfo accountGroupInfo = new AccountGroupInfo();
                accountGroupInfo.setLock(false);
                accountGroupInfo.setLimitAmount(-1);
                accountGroupInfo.setLimitDate(systemDate);
                boolean isAccgroupBudget = false;
                try {
                    // 账户是否加入账户组且该年月设置了账户组额度
                    isAccgroupBudget = dao.CheckAccgroupExistBudget(laccountid, accountGroupInfo.getLimitDate()) > 0;
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new Exception("账户是否加入账户组且该年月设置了账户组额度校验失败");
                }
                accountGroupInfo.setAccgroupBudget(isAccgroupBudget);
                currentAccountGroupAmountMap.put(accountGroupId, accountGroupInfo);
            }
        }
        AccountGroupInfo accountGroupInfo = currentAccountGroupAmountMap.get(accountGroupId);
        // 3    额度校验 是否设置了预算
        // 如果月份发生改变,对是否设置预算进行重新赋值
        if(!(DataFormat.getRealMonthString(accountGroupInfo.getLimitDate()).equals(DataFormat.getRealMonthString(systemDate))&&DataFormat.getYearString(accountGroupInfo.getLimitDate()).equals(DataFormat.getYearString(systemDate)))){
            boolean isAccgroupBudget = dao.CheckAccgroupExistBudget(laccountid, accountGroupInfo.getLimitDate()) > 0;
            accountGroupInfo.setAccgroupBudget(isAccgroupBudget);
            // 有变成无,移除缓存内容
            if(accountGroupInfo.isAccgroupBudget()&&!isAccgroupBudget){
                currentAccountGroupAmountMap.remove(accountGroupId);
                System.out.println("帐户:"+laccountid+"不需要进行账户组额度校验");
                return false;
            }
        }
        if(!accountGroupInfo.isAccgroupBudget()){
            System.out.println("帐户:"+laccountid+"不需要进行账户组额度校验");
            return false;
        }
        // 4.帐户组加锁
        synchronized (accountGroupInfo) {
            boolean lockSuccess = false;
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"尝试加锁次数"+(i+1)+"==========laccountid============"+laccountid);
                if(!accountGroupInfo.isLock()){
                    accountGroupInfo.setLock(true);
                    lockSuccess = true;
                    System.out.println("---------------加锁成功"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                    break;
                }else{
                    Thread.sleep(1000);
                }
            }
            if(!lockSuccess){
                System.out.println("加锁失败"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                throw new Exception("加锁失败");
            }
        }
        // 4.2 额度校验 额度核对(如果“当前可用额度”为-1或“额度日期”不等于开机日或“更新时间”与当前应用服务器时间差值超过1个小时,则执行核对逻辑)
        try{
            checkAccountGroup(accountGroupId,lOfficeID,lCurrencyID);
        }catch (Exception e) {
            e.printStackTrace();
            if(accountGroupInfo.isLock()){
                accountGroupInfo.setLock(false);
            }
            throw new Exception("帐户组额度核对失败");
        }
        // 4.3 额度校验
        System.out.println("======占用前======");
        System.out.println("账户ID======"+laccountid);
        System.out.println("账户组ID======"+accountGroupId);
        System.out.println("账户组可用金额======"+DataFormat.formatNumber(accountGroupInfo.getLimitAmount(),2));
        
        if(UtilOperation.Arith.sub(payamount, accountGroupInfo.getLimitAmount()) > 0 ){
            if(accountGroupInfo.isLock()){
                accountGroupInfo.setLock(false);
            }
            System.out.println("帐户组透支---------------解锁成功"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
            return true;
        }
        
        // 5  额度占用
        accountGroupInfo.setLimitAmount(UtilOperation.Arith.sub(accountGroupInfo.getLimitAmount(), payamount));
        accountGroupInfo.setUpdateTime(new Timestamp(System.currentTimeMillis()));
        
        System.out.println("======占用后======");
        System.out.println("账户ID======"+laccountid);
        System.out.println("账户组ID======"+accountGroupId);
        System.out.println("账户组可用金额======"+DataFormat.formatNumber(accountGroupInfo.getLimitAmount(),2));
        
        // 6  帐户组解锁
        if(accountGroupInfo.isLock()){
            accountGroupInfo.setLock(false);
        }
        System.out.println("---------------解锁成功"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
        return flag;
    }
    
    
    
    /**
     *  释放账户组额度
     * @param 账户ID,释放金额
     * @throws Exception 
     * */
    public static void releaseDraftAccgroup(long laccountid,double payamount) throws Exception{
        
        
        // 1. 帐户是否加入了帐户组
        long accountGroupId;
        try {
            // 1.1 获取帐户组ID
            accountGroupId = findAccountGroup(laccountid);
        } catch (Exception e1) {
            e1.printStackTrace();
            throw new Exception("获取帐户组关系失败");
        }
        // 1.2 帐户不在帐户组中,无需校验
        if(accountGroupId == -1){
            System.out.println("帐户:"+laccountid+"不在帐户组中");
            return;
        }
        AccountGroupInfo accountGroupInfo = currentAccountGroupAmountMap.get(accountGroupId);
        // 没有缓存无需释放
        if(accountGroupInfo==null){
            System.out.println("账户组:"+accountGroupId+"没有缓存无需释放");
            return;
        }
        // 2  额度校验 是否设置了预算
        if(!accountGroupInfo.isAccgroupBudget()){
            System.out.println("账户组:"+accountGroupId+"没有设置预算");
            return;
        }
        // 3   帐户组加锁
        Object lock = getLock(accountGroupId);
        boolean lockSuccess = false;
        synchronized (lock) {
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"尝试加锁次数"+(i+1)+"==========laccountid============"+laccountid);
                if(!accountGroupInfo.isLock()){
                    accountGroupInfo.setLock(true);
                    lockSuccess = true;
                    System.out.println("---------------加锁成功"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                    break;
                }else{
                    Thread.sleep(1000);
                }
            }
            if(!lockSuccess){
                System.out.println("加锁失败"+Thread.currentThread().getName()+"==========laccountid============"+laccountid);
                throw new Exception("加锁失败");
            }
        }
        System.out.println("======释放前======");
        System.out.println("账户ID======"+laccountid);
        System.out.println("账户组ID======"+accountGroupId);
        System.out.println("账户组可用金额======"+DataFormat.formatNumber(accountGroupInfo.getLimitAmount(),2));
        
        // 4  可用额度释放
        accountGroupInfo.setLimitAmount(UtilOperation.Arith.add(accountGroupInfo.getLimitAmount(), payamount));
        
        System.out.println("======释放后======");
        System.out.println("账户ID======"+laccountid);
        System.out.println("账户组ID======"+accountGroupId);
        System.out.println("账户组可用金额======"+DataFormat.formatNumber(accountGroupInfo.getLimitAmount(),2));
        // 5  帐户组解锁
        if(accountGroupInfo.isLock()){
            accountGroupInfo.setLock(false);
        }
    }
    
    /**
     *  帐户组更新方法
     * @param 账户ID,支付金额
     * @return 透支true   不透支false
     * @throws Exception 
     * */
    public static void DraftAccgroupUpdate(long accountGroupId,String year,String month,long lOfficeID,long lCurrencyID) throws Exception{
        // 开机日期
        Timestamp systemDate = Env.getSystemDate(lOfficeID, lCurrencyID);
        SettAccountGroupSettingDao dao = new SettAccountGroupSettingDao();
        
        // 如果预算不为当前月份
        if(!(DataFormat.getRealMonthString(systemDate).equals(month)&&DataFormat.getYearString(systemDate).equals(year))){
            // 无需通知
            System.out.println("==================预算不为当前月份无需通知==================");
            return;
        }
        
        Object lock = getLock(accountGroupId);
        // 2. 内存currentAccountGroupAmountMap中是否存在该账户组可用额度相关信息
        synchronized (lock) {
            if(currentAccountGroupAmountMap.get(accountGroupId)==null){
                System.out.println("==================账户组可用额度Map初始化==================");
                // 2.1 账户组额度信息初始化
                AccountGroupInfo accountGroupInfo = new AccountGroupInfo();
                accountGroupInfo.setLock(false);
                accountGroupInfo.setLimitAmount(-1);
                accountGroupInfo.setLimitDate(systemDate);
                boolean isAccgroupBudget = false;
                try {
                    // 账户是否加入账户组且该年月设置了账户组额度
                    isAccgroupBudget = dao.CheckGroupExistBudget(accountGroupId, accountGroupInfo.getLimitDate()) > 0;
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new Exception("账户是否加入账户组且该年月设置了账户组额度校验失败");
                }
                accountGroupInfo.setAccgroupBudget(isAccgroupBudget);
                currentAccountGroupAmountMap.put(accountGroupId, accountGroupInfo);
                return;
            }
        }
        AccountGroupInfo accountGroupInfo = currentAccountGroupAmountMap.get(accountGroupId);
        // 4.帐户组加锁
        synchronized (accountGroupInfo) {
            boolean lockSuccess = false;
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"尝试加锁次数"+(i+1)+"==========laccountid============"+accountGroupId);
                if(!accountGroupInfo.isLock()){
                    accountGroupInfo.setLock(true);
                    lockSuccess = true;
                    System.out.println("---------------加锁成功"+Thread.currentThread().getName()+"==========laccountid============"+accountGroupId);
                    break;
                }else{
                    Thread.sleep(1000);
                }
            }
            if(!lockSuccess){
                System.out.println("加锁失败"+Thread.currentThread().getName()+"==========laccountid============"+accountGroupId);
                throw new Exception("加锁失败");
            }
        }
        // 额度更新为-1
        accountGroupInfo.setLimitAmount(-1);
        // 6  帐户组解锁
        if(accountGroupInfo.isLock()){
            accountGroupInfo.setLock(false);
        }
        System.out.println("---------------解锁成功"+Thread.currentThread().getName()+"==========laccountid============"+accountGroupId);
    }
    /**
     * 得到帐户组锁
     */
    private static Object getLock(Long key){
        Object value = new Object();
        Object v = locks.putIfAbsent(key, value);
        if(v == null){
            return value;
        }
        return v;
    }
    /**
     * add by jiecheni 2017-11-02 
     * 账户与账户组关联关系初始化
     */
    public static void init() {
        System.out.println("==========================账户与账户组关联关系初始化开始==========================");
        Connection con = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            //先清理缓存
            accountGroup.clear();
            
            con = Database.getConnection();
            StringBuffer sb = new StringBuffer();
            sb.append(" select a.naccgroupid,a.naccountid from sett_budgetaccassign a where a.nstatusid > 0 ");
            ps = con.prepareStatement(sb.toString());
            rs = ps.executeQuery();
            while (rs.next()) {
                try {
                    accountGroup.put(rs.getLong("naccountid"), rs.getLong("naccgroupid"));
                } catch (NullPointerException e) {
                    continue;
                }
            }
            rs.close();
            rs = null;
            ps.close();
            ps = null;
            con.close();
            con = null;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null)
                {
                    rs.close();
                    rs = null;
                }
                if (ps != null)
                {
                    ps.close();
                    ps = null;
                }
                if (con != null)
                {
                    con.close();
                    con = null;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }    
        }
        System.out.println("==========================账户与账户组关联关系初始化结束,初始化关系数量=========================="+accountGroup.size());
    }
    
    /**
     * add by jiecheni 2017-11-02 
     * 查询账户是否在账户组
     * @param id
     * @return
     * @throws Exception
     */
    public static long findAccountGroup(long id) throws Exception {
        Long result = null;
        try {
            result = accountGroup.get(Long.valueOf(id));
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception("查询出现异常,出错原因:"+e.getMessage(), e);
        }
        return result == null ? -1 : result;
    }
    
    /**
     * add by jiecheni 2017-11-02 
     * 将账户与账户组关联信息存入缓存
     * @param accountId
     * @param accountGroupId
     */
    public static void putAccountGroup(long accountId,long accountGroupId) {
        accountGroup.put(accountId, accountGroupId);
    }
    
    /**
     * add by jiecheni 2017-11-02 
     * 将账户与账户组关联信息从缓存中清除
     * @param accountId
     */
    public static void removeAccountGroup(long accountId) {
        accountGroup.remove(accountId);
    }
    
    /**
     * 账户组额度核对
     * @param accountGroupId    账户组Id
     * @param lOfficeID            办事处Id
     * @param lCurrencyID        币种id
     * @throws Exception
     */
    public static void checkAccountGroup(long accountGroupId,long lOfficeID,long lCurrencyID) throws Exception{
        int isCheckResult = isCheckAccountGroup(accountGroupId,lOfficeID,lCurrencyID);
        AccountGroupInfo accountGroupInfo = currentAccountGroupAmountMap.get(accountGroupId);
        Timestamp limitDate = accountGroupInfo.getLimitDate();
        if(isCheckResult == 3){
            Timestamp systemDate = Env.getSystemDate(lOfficeID,lCurrencyID);
            double accountGroupAmount = UtilOperation.Arith.sub(queryAccountGroupBudget(accountGroupId,systemDate),queryAccountGroupUsedAmount(accountGroupId,systemDate));
            accountGroupInfo.setLimitDate(systemDate);
            accountGroupInfo.setLimitAmount(accountGroupAmount);
            accountGroupInfo.setUpdateTime(new Timestamp(System.currentTimeMillis()));
            currentAccountGroupAmountMap.put(accountGroupId, accountGroupInfo);
        }else if(isCheckResult > 0){
            double accountGroupAmount = UtilOperation.Arith.sub(queryAccountGroupBudget(accountGroupId,limitDate),queryAccountGroupUsedAmount(accountGroupId,limitDate));
            accountGroupInfo.setLimitAmount(accountGroupAmount);
            accountGroupInfo.setUpdateTime(new Timestamp(System.currentTimeMillis()));
            currentAccountGroupAmountMap.put(accountGroupId, accountGroupInfo);
        }
    }
    
    /**
     * 是否核对账户组额度
     * @param accountGroupId    账户组Id    
     * @param lOfficeID            办事处Id
     * @param lCurrencyID        币种id
     * @return 
     * 1: 当前可用额度为-1.
     * 2: 账户组更新时间与当前应用服务器时间差是否超过一个小时.
     * 3: 开机日是否与额度日期不同.
     * 
     */
    public static int isCheckAccountGroup(long accountGroupId,long lOfficeID,long lCurrencyID) throws Exception{
        int isCheckAccountGroup = 0;
        AccountGroupInfo accountGroupInfo = currentAccountGroupAmountMap.get(accountGroupId);
        if(accountGroupInfo != null){
            //如果"当前可用额度"为-1或"额度日期"不等于开机日或"更新时间"与当前应用服务器时间差值超过1个小时,则执行核对逻辑
            //a、"当前可用额度"为-1
            if(accountGroupInfo.getLimitAmount() <= 0){
                return 1;
            }
            //b、账户组更新时间与当前应用服务器时间差是否超过一个小时
            if(accountGroupInfo.getUpdateTime() != null){ 
                if((System.currentTimeMillis()-accountGroupInfo.getUpdateTime().getTime()) > 1*60*60*1000){
                    return 2;
                }
            }
            //c、开机日是否与额度日期不同
            Timestamp systemDate = Env.getSystemDate(lOfficeID,lCurrencyID);
            Timestamp limitDate = accountGroupInfo.getLimitDate();
            boolean isSameDate = com.iss.itreasury.util.UtilOperation.isSameDate(systemDate,limitDate);
            if(!isSameDate){
                return 3;
            }
        }else{
            //初始化账户组额度信息
            initAccountGroup(accountGroupId);
        }
        return isCheckAccountGroup;
    }
    
    /**
     * 初始化账户组额度信息
     * @param accountGroupId    账户组Id
     * @throws Exception
     */
    private static void initAccountGroup(long accountGroupId) throws Exception{
        AccountGroupInfo accountGroupInfo = new AccountGroupInfo();
        Timestamp systemDate = Env.getSystemDate(1,1);
        double accountGroupAmount = UtilOperation.Arith.sub(queryAccountGroupBudget(accountGroupId,systemDate),queryAccountGroupUsedAmount(accountGroupId,systemDate));
        accountGroupInfo.setAccgroupBudget(true);
        accountGroupInfo.setLock(false);
        accountGroupInfo.setLimitDate(systemDate);
        accountGroupInfo.setLimitAmount(accountGroupAmount);
        accountGroupInfo.setUpdateTime(new Timestamp(System.currentTimeMillis()));
        currentAccountGroupAmountMap.put(accountGroupId, accountGroupInfo);
    }
    
    /**
     * 查询账户组设置预算额度
     * @param accountGroupId    账户组Id
     * @param executedate        执行日期
     * @return
     * @throws Exception
     */
    public static double queryAccountGroupBudget(long accountGroupId,Timestamp executedate) throws Exception{
        //账户组设置预算额度
        double accountGroupBudgetAmount = 0.0;
        int year = 0;//年份
        int month = 0;//月份
        
        AccountGroupDao agDao = new AccountGroupDao();
        if(executedate != null){
            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(executedate.getTime());
            year = calendar.get(GregorianCalendar.YEAR);
            month = calendar.get(GregorianCalendar.MONTH) + 1;
        }else{
            throw new SettlementException("执行日期为空",new Exception());
        }
        //查询账户组设置预算额度
        accountGroupBudgetAmount = agDao.queryAccountGroupBudgetAmount(accountGroupId,executedate,year,month);

        return accountGroupBudgetAmount;
    }
    
    /**
     * 统计查询账户组已使用额度 
     * 账户组已使用额度 = 网银、CBE使用额度 + 结算、银行流水入账使用额度
     * @param accountGroupId    账户组Id
     * python多线程

[Python3] 043 多线程 简介

Java 多线程安全机制

八.多进程与多线程

多线程编程

多线程介绍