1. ThreadLocal記憶體洩漏
ThreadLocal 記憶體洩漏是指由於沒有及時清理 ThreadLocal 例項所儲存的資料,導致這些資料線上程池或長時間執行的應用中累積過多,最終導致記憶體佔用過高的情況。
記憶體洩漏通常發生在以下情況下:
- 執行緒池場景下的 ThreadLocal 使用不當: 在使用執行緒池時,如果執行緒被重用而沒有正確清理 ThreadLocal 中的資料,那麼下次使用這個執行緒時,它可能會攜帶上一次執行任務所遺留的資料,從而導致資料累積並消耗記憶體。
- 長時間執行的應用中未清理 ThreadLocal 資料: 在一些長時間執行的應用中,比如 Web 應用,可能會建立很多 ThreadLocal 例項並儲存大量資料。如果這些資料在使用完後沒有及時清理,就會導致記憶體洩漏問題。
- 沒有使用
remove()
方法清理 ThreadLocal 資料: 在使用完 ThreadLocal 儲存的資料後,如果沒有呼叫remove()
方法清理資料,就會導致資料長時間存在於 ThreadLocal 中,從而可能引發記憶體洩漏。
實線代表強引用,虛線代表弱引用
每一個Thread維護一個ThreadLocalMap, key為使用弱引用的ThreadLocal例項,value為執行緒變數的副本。
強引用,使用最普遍的引用,一個物件具有強引用,不會被垃圾回收器回收。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不回收這種物件。
如果想取消強引用和某個物件之間的關聯,可以顯式地將引用賦值為null,這樣可以使JVM在合適的時間就會回收該物件。
弱引用,JVM進行垃圾回收時,無論記憶體是否充足,都會回收被弱引用關聯的物件。在java中,用java.lang.ref.WeakReference類來表示。
2. 文章詳情
2.1 介面說明
介面url:/articles/view/{id}
請求方式:POST
請求引數:
引數名稱 | 引數型別 | 說明 |
---|---|---|
id | long | 文章id(路徑引數) |
返回資料:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
2.2 涉及到的表
CREATE TABLE `blog`.`ms_article_body` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
`article_id` bigint(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `article_id`(`article_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.cherriesovo.blog.dao.pojo;
import lombok.Data;
@Data
public class ArticleBody { //文章詳情表
private Long id;
private String content;
private String contentHtml;
private Long articleId;
}
#文章分類
CREATE TABLE `blog`.`ms_category` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.cherriesovo.blog.dao.pojo;
import lombok.Data;
@Data
public class Category {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
2.3 Controller
//json資料進行互動
@RestController
@RequestMapping("articles")
public class ArticleController {
/*
* 透過id獲取文章
* */
@PostMapping("view/{id}")
//@PathVariable("id") 註解用於將 URL 中的 {id} 賦值給 articleId 引數。
public Result findArticleById(@PathVariable("id") Long articleId) {
return articleService.findArticleById(articleId);
}
}
2.4 Service
ArticleVo findArticleById(Long id);
public interface ArticleService {
//檢視文章詳情
Result findArticleById(Long articleId);
}
package com.cherriesovo.blog.vo;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo {
private Long id;
private String title;
private String summary;
private int commentCounts;
private int viewCounts;
private int weight;
/**
* 建立時間
*/
private String createDate;
private String author;
private ArticleBodyVo body;
private List<TagVo> tags;
private CategoryVo category;
}
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private ArticleMapper articleMapper;
@Autowired
private TagService tagService;
@Autowired
private SysUserService sysUserService;
@Autowired
private CategoryService categoryService;
public ArticleVo copy(Article article,boolean isAuthor,boolean isBody,boolean isTags,boolean isCategory){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article, articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//並不是所有的介面都需要標籤,作者資訊
if(isTags){
Long articleId = article.getId();
articleVo.setTags(tagService.findTagsByArticleId(articleId));
}
if(isAuthor){
Long authorId = article.getAuthorId();
//getNickname()用於獲取某個物件或實體的暱稱或別名
articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
}
if (isBody){
Long bodyId = article.getBodyId();
articleVo.setBody(findArticleBodyById(bodyId));
}
if (isCategory){
Long categoryId = article.getCategoryId();
articleVo.setCategory(categoryService.findCategoryById(categoryId));
}
return articleVo;
}
private List<ArticleVo> copyList(List<Article> records,boolean isAuthor,boolean isBody,boolean isTags) {
List<ArticleVo> articleVoList = new ArrayList<>();
for (Article article : records) {
ArticleVo articleVo = copy(article,isAuthor,false,isTags,false);
articleVoList.add(articleVo);
}
return articleVoList;
}
private List<ArticleVo> copyList(List<Article> records,boolean isAuthor,boolean isBody,boolean isTags,boolean isCategory) {
List<ArticleVo> articleVoList = new ArrayList<>();
for (Article article : records) {
ArticleVo articleVo = copy(article,isAuthor,isBody,isTags,isCategory);
articleVoList.add(articleVo);
}
return articleVoList;
}
@Autowired
private ArticleBodyMapper articleBodyMapper;
private ArticleBodyVo findArticleBodyById(Long bodyId) {
ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
ArticleBodyVo articleBodyVo = new ArticleBodyVo();
articleBodyVo.setContent(articleBody.getContent());//setContent()是articleBodyVo的set方法
return articleBodyVo;
}
@Override
public List<ArticleVo> listArticlesPage(PageParams pageParams) {
// 分頁查詢article資料庫表
QueryWrapper<Article> queryWrapper = new QueryWrapper<>();
Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
List<ArticleVo> articleVoList = copyList(articlePage.getRecords(),true,false,true);
return articleVoList;
}
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getViewCounts); //根據瀏覽量倒序
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
//select id,title from article order by view_counts desc limit 5
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false,false));
}
@Override
public Result newArticles(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit "+limit);
//select id,title from article order by create_date desc limit 5
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false,false));
}
@Override
public Result listArchives() {
List<Archives> archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
@Override
public Result findArticleById(Long articleId) {
/*
* 1、根據id查詢文章資訊
* 2、根據bodyId和categoryId去做關聯查詢
* */
Article article = this.articleMapper.selectById(articleId);
ArticleVo articleVo = copy(article, true, true, true,true);
return Result.success(articleVo);
}
}
package com.cherriesovo.blog.vo;
import lombok.Data;
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
}
package com.cherriesovo.blog.vo;
import lombok.Data;
@Data
public class ArticleBodyVo {
private String content;
}
package com.cherriesovo.blog.service;
import com.cherriesovo.blog.vo.CategoryVo;
import java.util.List;
public interface CategoryService {
CategoryVo findCategoryById(Long categoryId);
}
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public CategoryVo findCategoryById(Long categoryId) {
Category category = categoryMapper.selectById(categoryId);
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
}
package com.cherriesovo.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.ArticleBody;
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}
package com.cherriesovo.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.cherriesovo.blog.dao.pojo.Category;
public interface CategoryMapper extends BaseMapper<Category> {
}
2.5 測試
3. 使用執行緒池 更新閱讀次數
3.1 執行緒池配置
taskExecutor
是一個執行緒池物件,在這段程式碼中透過@Bean("taskExecutor")
註解定義並配置了一個執行緒池,並將其命名為 "taskExecutor"。asyncServiceExecutor()
方法是一個Bean
方法,用於建立並配置一個執行緒池,並以taskExecutor
作為 Bean 的名稱。ThreadPoolTaskExecutor
是 Spring 框架提供的一個實現了Executor
介面的執行緒池- 在方法中建立了一個
ThreadPoolTaskExecutor
例項executor
,並對其進行了一系列配置:
setCorePoolSize(5)
: 設定核心執行緒數為 5,即執行緒池在空閒時會保持 5 個核心執行緒。setMaxPoolSize(20)
: 設定最大執行緒數為 20,即執行緒池中允許的最大執行緒數量。setQueueCapacity(Integer.MAX_VALUE)
: 配置佇列大小為整數的最大值,即任務佇列的最大容量。setKeepAliveSeconds(60)
: 設定執行緒活躍時間為 60 秒,即執行緒在空閒超過該時間後會被銷燬。setThreadNamePrefix("CherriesOvO部落格專案")
: 設定執行緒名稱的字首為 "CherriesOvO部落格專案"。setWaitForTasksToCompleteOnShutdown(true)
: 設定在關閉執行緒池時等待所有任務結束。initialize()
: 執行執行緒池的初始化。- 最後,將配置好的執行緒池返回為一個
Executor
Bean,供其他元件使用。
package com.cherriesovo.blog.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync //開啟多執行緒
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 設定核心執行緒數
executor.setCorePoolSize(5);
// 設定最大執行緒數
executor.setMaxPoolSize(20);
//配置佇列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 設定執行緒活躍時間(秒)
executor.setKeepAliveSeconds(60);
// 設定預設執行緒名稱
executor.setThreadNamePrefix("CherriesOvO部落格專案");
// 設定等待所有任務結束後再關閉執行緒池
executor.setWaitForTasksToCompleteOnShutdown(true);
//執行初始化
executor.initialize();
return executor;
}
}
3.1 使用
透過
@Async("taskExecutor")
註解,該方法標記為非同步執行,並指定了使用名為 "taskExecutor" 的執行緒池。
articleMapper.update(articleUpdate, updateWrapper)
是一個 MyBatis-Plus 中的更新操作,用於更新資料庫中的文章記錄。
update
方法接受兩個引數:
articleUpdate
:表示需要更新的文章物件,其中包含了新的閱讀量。updateWrapper
:表示更新條件,即確定哪些文章需要被更新的條件。這段程式碼透過
Thread.sleep(5000)
方法在當前執行緒中休眠了5秒鐘。這樣做的目的是為了模擬一個耗時操作,以展示在非同步執行緒中執行的任務不會影響到主執行緒的執行。
package com.cherriesovo.blog.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.cherriesovo.blog.dao.mapper.ArticleMapper;
import com.cherriesovo.blog.dao.pojo.Article;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class ThreadService {
//期望此操作線上程池中執行,不會影響原有的主執行緒
@Async("taskExecutor")
public void updateArticleViewCount(ArticleMapper articleMapper, Article article){
int viewCounts = article.getViewCounts();
Article articleUpdate = new Article();
articleUpdate.setViewCounts(viewCounts + 1);
LambdaQueryWrapper<Article> updateWrapper = new LambdaQueryWrapper<>();
updateWrapper.eq(Article::getId,article.getId());
//設定一個 為了在多執行緒環境下 執行緒安全
updateWrapper.eq(Article::getViewCounts,article.getViewCounts());
//update article set view_count=? where view_count=? and id=?
articleMapper.update(articleUpdate,updateWrapper);
try {
//睡眠5秒 證明不會影響主執行緒的使用,5秒後資料才會出現
Thread.sleep(5000);
// System.out.println("更新完成了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Service
public class ArticleServiceImpl implements ArticleService {
@Autowired
private ThreadService threadService;
@Override
public Result findArticleById(Long articleId) {
/*
* 1、根據id查詢文章資訊
* 2、根據bodyId和categoryId去做關聯查詢
* */
Article article = this.articleMapper.selectById(articleId);
ArticleVo articleVo = copy(article, true, true, true,true);
//檢視完文章了,新增閱讀數,有沒有問題?
//檢視完文章之後,本應該直接返回資料了,這時候做了一個更新操作,更新時加寫鎖,阻塞其他讀操作,效能比較低
//更新增加此時介面的耗時,如果一旦更新出問題,不能影響檢視文章的操作
//執行緒池 可以把更新操作扔到執行緒池中去執行,和主執行緒就不相關了
threadService.updateArticleViewCount(articleMapper,article);
return Result.success(articleVo);
}
}
3.3 測試
睡眠 ThredService中的方法 5秒,不會影響主執行緒的使用,即文章詳情會很快的顯示出來,不受影響