102. 二叉树的层次遍历

https://leetcode-cn.com/problems/binary-tree-level-order-traversal/description/

题目

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。

例如:

给定二叉树:  [3,9,20,null,null,15,7],


    3
   / \
  9  20
    /  \
   15   7
   

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

解题思路:

层次遍历,没啥好说的,就是本题需要将层次区分开来。因此传统的方法,需要做一些修订。用一个队列保存当前层次的所有节点。

每一轮遍历的时候,依次出队所有的节点,存入结果,并遍历出队的所有节点,如果有子节点则存入队列,等待下一轮遍历。

也就是有两层的遍历,用一个临时的队列,保存中间结果

代码:

public List<List<Integer>> levelOrder(TreeNode root) {
    Queue<TreeNode> queue = new ArrayDeque<>();
    if (root != null) {
        queue.add(root);
    }
    List<List<Integer>> result = new ArrayList<>();
    while(!queue.isEmpty()){
        List<Integer> ans = new ArrayList<>();
        Queue<TreeNode> tempQueue = new ArrayDeque<>(queue);
        while (!tempQueue.isEmpty()){
            TreeNode node = tempQueue.poll();
            ans.add(node.val);
        }
        tempQueue = new ArrayDeque<>(queue);
        queue.clear();
        while(!tempQueue.isEmpty()){
            TreeNode node = tempQueue.poll();
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
        result.add(ans);
    }
    return result;
}
2019/02/01 15:24 下午 posted in  LeetCode

122. 买卖股票的最佳时机 II

https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/description/

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:

输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:

输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。

解题思路:

dp题。尝试写出状态转移方程。

设s[i],第i天股票的价格

1) dp[i], 到第i天,最优的股票收益。

dp[i] = max {max{ s[i] - s[j] + dp[j - 1]} j = 1...i - 1,

dp[i - 1]代表不卖,以及s[i] < s[j]的情况,算法复杂度O(n2)

代码:

public int maxProfit(int[] prices) {
    if (prices.length == 0){
        return 0;
    }
    int[] dp = new int[prices.length];
    for (int i = 1 ; i < prices.length; i++){
        int max = dp[i - 1];
        for (int j = 0; j < i; j++){
            int temp = prices[i] - prices[j];
            temp = temp < 0 ? 0 : temp;
            if (j > 0) {
                if (temp > 0) {
                    temp += dp[j - 1];
                } else {
                    temp = dp[j - 1];
                }
            }
            if (temp > max){
                max = temp;
            }
        }
        dp[i] = max;
    }
    return dp[prices.length - 1];
}
2019/02/01 16:53 下午 posted in  LeetCode

1349. 参加考试的最大学生数

给你一个 m * n 的矩阵 seats 表示教室中的座位分布。如果座位是坏的(不可用),就用 '#' 表示;否则,用 '.' 表示。

学生可以看到左侧、右侧、左上、右上这四个方向上紧邻他的学生的答卷,但是看不到直接坐在他前面或者后面的学生的答卷。请你计算并返回该考场可以容纳的一起参加考试且无法作弊的最大学生人数。

学生必须坐在状况良好的座位上。
 

示例 1:

输入:seats = [["#",".","#","#",".","#"],
              [".","#","#","#","#","."],
              ["#",".","#","#",".","#"]]
输出:4
解释:教师可以让 4 个学生坐在可用的座位上,这样他们就无法在考试中作弊。 

示例 2:

输入:seats = [[".","#"],
              ["#","#"],
              ["#","."],
              ["#","#"],
              [".","#"]]
输出:3
解释:让所有学生坐在可用的座位上。

示例 3:

输入:seats = [["#",".",".",".","#"],
              [".","#",".","#","."],
              [".",".","#",".","."],
              [".","#",".","#","."],
              ["#",".",".",".","#"]]
输出:10
解释:让学生坐在第 1、3 和 5 列的可用座位上。

提示:

  • seats 只包含字符 '.' 和'#'
  • m == seats.length
  • n == seats[i].length
  • 1 <= m <= 8
  • 1 <= n <= 8

2. 结题思路

2.1. 思路过程

  1. 当前行的状态只和前一行的状态有关。
  2. 为了表示前一行的状态,加上题目矩阵规模小于8,可以使用状态压缩。
  3. 一个简便方法,采用位运算快速进行移位,以及有效状态的判断。

2.2. 细节考虑

  1. 位运算,通过左移,右移一位,可以快速判断,当前的状态是否合法。
  2. 将合法的座位,映射为0。坏掉的座位,映射为1,与当前状态与(&),可以快速判断当前状态是否合法

3. 代码

public int maxStudents(char[][] seats) {
    int m = seats.length;
    int n = seats[0].length;
    int[][] dp = new int[m + 1][1 << n];
    int[] tmp = new int[m];
    int sum;
    for (int i = 0; i < m; i++) {
        sum = 0;
        for (char c : seats[i]) {
            sum = sum << 1;
            if (c == '#') {
                sum += 1;
            }
        }
        tmp[i] = sum;
    }
    for (int i = m - 1; i >= 0; i--) {
        for (int j = 0; j < 1 << n; j++) {
            if ((j & (j << 1)) == 0 && (j & (j >> 1)) == 0 && (j & tmp[i]) == 0) {
                for (int k = 0; k < 1 << n; k++) {
                    if ((k & (j << 1)) == 0 && (k & (j >> 1)) == 0) {
                        dp[i][j] = Math.max(dp[i][j], Integer.bitCount(j) + dp[i + 1][k]);
                    }
                }
            }
        }
    }
    int result = 0;
    for (int i = 0; i < 1 << n; i++) {
        result = Math.max(0, dp[0][i]);
    }
    return result;
}

其他解法

mark一下,学习:
https://leetcode-cn.com/problems/maximum-students-taking-exam/solution/er-fen-tu-zui-da-du-li-ji-by-lightcml/

2020/02/11 17:01 下午 posted in  LeetCode

169. 求众数

https://leetcode-cn.com/problems/majority-element/description/


给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于  ⌊ n/2 ⌋ 的元素。

你可以假设数组是非空的,并且给定的数组总是存在众数。

示例 1:

输入: [3,2,3]
输出: 3
示例 2:

输入: [2,2,1,1,1,2,2]
输出: 2

解题思路:

  我的思路是,排序,然后逐一比对。直到找到众数为止。但是直觉告诉我应该有更优雅的方式。

  这是一道求众数的问题,有很多种解法,其中我感觉比较好的有两种,一种是用哈希表,这种方法需要O(n)的时间和空间,另一种是用一种叫摩尔投票法 Moore Voting,需要O(n)的时间和O(1)的空间,比前一种方法更好。这种投票法先将第一个数字假设为众数,然后把计数器设为1,比较下一个数和此数是否相等,若相等则计数器加一,反之减一。然后看此时计数器的值,若为零,则将当前值设为候选众数。以此类推直到遍历完整个数组,当前候选众数即为该数组的众数。不仔细弄懂摩尔投票法的精髓的话,过一阵子还是会忘记的,首先要明确的是这个叼炸天的方法是有前提的,就是数组中一定要有众数的存在才能使用,下面我们来看本算法的思路,这是一种先假设候选者,然后再进行验证的算法。我们现将数组中的第一个数假设为众数,然后进行统计其出现的次数,如果遇到同样的数,则计数器自增1,否则计数器自减1,如果计数器减到了0,则更换当前数字为候选者。这是一个很巧妙的设定,也是本算法的精髓所在,为啥遇到不同的要计数器减1呢,为啥减到0了又要更换候选者呢?首先是有那个强大的前提存在,一定会有一个出现超过半数的数字存在,那么如果计数器减到0了话,说明目前不是候选者数字的个数已经跟候选者的出现个数相同了,那么这个候选者已经很weak,不一定能出现超过半数,我们选择更换当前的候选者。那有可能你会有疑问,那万一后面又大量的出现了之前的候选者怎么办,不需要担心,如果之前的候选者在后面大量出现的话,其又会重新变为候选者,直到最终验证成为正确的众数,佩服算法的提出者啊。

2019/02/13 15:46 下午 posted in  LeetCode

189. 旋转数组

给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:

输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释: 
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:

尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的原地算法。

解题思路:

  关键在于O(1)的算法。如果是O(n)的话,方法就太多了。这里就不说了。主要说一下O(1)的做法。

  采用翻转字符的方法,思路是先把前n-k个数字翻转一下,再把后k个数字翻转一下,最后再把整个数组翻转一下:

1 2 3 4 5 6 7
4 3 2 1 5 6 7
4 3 2 1 7 6 5
5 6 7 1 2 3 4

2019/02/13 15:48 下午 posted in  LeetCode

258. 各位相加


给定一个非负整数  num,反复将各个位上的数字相加,直到结果为一位数。

示例:

输入: 38
输出: 2 
解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。 由于 2 是一位数,所以返回 2。
进阶:
你可以不使用循环或者递归,且在 O(1) 时间复杂度内解决这个问题吗?

解题思路:

普通的方法都会解,这里就不重复了。说下O(1)的解法。

这里用到 一个很巧妙的算法。

根据以上,可以得出,快速求解,就是将数字不断减去9,直到不能减为止即为正解。

那么不断减去9的过程,可以化为--> mod 9

代码如下:

class Solution {
    public int addDigits(int num) {
        if (num == 0){
            return num;
        }
        num %= 9;
        return num == 0 ? 9 : num;
    }
}
2019/02/13 16:45 下午 posted in  LeetCode

32. 最长有效括号

https://leetcode-cn.com/problems/longest-valid-parentheses/description/

给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:

输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
示例 2:

输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

代码:

public int longestValidParentheses(String s) {
    int[] dp = new int[s.length()];
    int max = 0;
    for (int i = 1; i < s.length(); i++){
        if (s.charAt(i) == '('){
            dp[i] = 0;
        }else{
            if (dp[i - 1] != 0) {
                int index = i - dp[i - 1] - 1;
                if (index >= 0 && s.charAt(index) == '(') {
                    dp[i] = dp[i - 1] + 2;
                    if (index - 1 >= 0 ){
                        dp[i] += dp[index - 1];
                    }
                }
            }
            if (s.charAt(i - 1) == '('){
                if (i >= 2) {
                    dp[i] = dp[i] > dp[i - 2] + 2 ? dp[i] : dp[i - 2] + 2;
                }else{
                    dp[i] = 2;
                }
            }
            if (max < dp[i]){
                max = dp[i];
            }
        }
    }
    return max;
}
2019/01/21 00:04 上午 posted in  LeetCode

39. 组合总和

https://leetcode-cn.com/problems/combination-sum/description/

给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明:

所有数字(包括 target)都是正整数。
解集不能包含重复的组合。

示例 1:

输入: candidates = [2,3,6,7], target = 7,
所求解集为:
[
  [7],
  [2,2,3]
]
示例 2:

输入: candidates = [2,3,5], target = 8,
所求解集为:
[
  [2,2,2,2],
  [2,3,3],
  [3,5]
]

题目分析:

  1. 一看到这道题,就感觉要用递归来做,应该是回溯算法。百度证实无误。
  2. 然后自己还没有生写过回溯。想了一会儿,结果不对,和正确解法做了一下比较,修改了一下自己的算法。标答是C++的,在java上需要修改一下,一些特别的地方要处理一下。

直接上代码:

/**
 * 核心算法
 * @param candidates 数组
 * @param target 当前递归子问题需要计算的target
 * @param start 开始查找的index
 * @param result 当前递归的result数组
 * @param ans 最后的答案
 */
private void findOne(int[] candidates, int target, int start, List<Integer> result, List<List<Integer>> ans){
    if (target == 0){
        List<Integer> list = new ArrayList<>(result); //这里需要新开一个数组,否则会一直复用这个对象,导致结果不对
        ans.add(list);
        return;
    }else if(target < candidates[start]){
        return; //不符合的结果,不处理。
    }else{
        for (int i = start; i < candidates.length; i++){
            result.add(candidates[i]);
            findOne(candidates, target - candidates[i], i, result, ans);
            result.remove(result.size() - 1);
            //这里有点像树的遍历,这里就是要一个节点和不要一个节点的分支。然后可以重复使用元素,所以递归子问题,仍然从i开始。
        }
    }
}

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> ans = new ArrayList<>();
    Arrays.sort(candidates); //题目没有说排好序的数组,所以这里要先拍个序
    findOne(candidates, target, 0, new ArrayList<>(), ans);
    return ans;
}
2019/01/21 00:06 上午 posted in  LeetCode

448. 找到所有数组中消失的数字

题目描述

给定一个范围在 1 ≤ a[i] ≤ n ( n = 数组大小 ) 的 整型数组,数组中的元素一些出现了两次,另一些只出现一次。

找到所有在 [1, n] 范围之间没有出现在数组中的数字。

您能在不使用额外空间且时间复杂度为O(n)的情况下完成这个任务吗? 你可以假定返回的数组不算在额外空间内。

示例:

输入:
[4,3,2,7,8,2,3,1]

输出:
[5,6]

思路分析

关键点在于不用额外空间,时间复杂度为O(n)。这两点限制了使用排序算法,或者用额外空间来记录。

那么,不能用额外空间,就只能用原有的数组空间了。这里有一个技巧就是用数组的下标也可以存储信息。因为数据的范围是在n之内,因此数组内出现的数字都是合法的下标。基于这个前提,本题有一个很巧妙的做法。详见代码。

代码

public List<Integer> findDisappearedNumbers(int[] nums) {
        List<Integer> result = new ArrayList<>();
        for (int i = 0; i < nums.length; i++){
            //将数组中存放的值作为坐标,进行二次访问。目标是将值设置为相反数(保证是负数)。那么没有变化的位置就是缺失的值
            nums[Math.abs(nums[i])-1] = - Math.abs(nums[Math.abs(nums[i])-1]);
        }
        for (int i = 0; i < nums.length; i++){
            if (nums[i] > 0){
                result.add(i);
            }
        }
        return result;
    }
2019/03/12 20:18 下午 posted in  LeetCode

55. 跳跃游戏

给定一个非负整数数组,你最初位于数组的第一个位置。

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个位置。

示例 1:

输入: [2,3,1,1,4]
输出: true
解释: 从位置 0 到 1 跳 1 步, 然后跳 3 步到达最后一个位置。
示例 2:

输入: [3,2,1,0,4]
输出: false
解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。

解题思路:

本道题用贪心。贪心的策略很巧妙。第一次独立没有想出来。
贪心的策略:

  1. 每一步都选择最大的可能去接近结果。
  2. 那么遍历一遍,如果最大的结果超过了最后一个位置。则说明最后一个位置是可达的,也就是返回true。

贪心的巧妙在于,我们不在意到底是如何选择的,只在乎是否可达。

代码:

 public boolean canJump(int[] nums) {
    int result = 0;
    if (nums.length == 1){ //注意边界,最后一个台阶的值是没有意义的。因此只有一个台阶的时候,永远是true
        return true;
    }
    for (int i = 0; i < nums.length; i++){
        if (i > result || (nums[i] == 0 && i == result)){
            break;
        }
        result = Math.max(result, nums[i] + i);
    }
    return result >= nums.length - 1;
}
2019/01/28 16:04 下午 posted in  LeetCode

72. 编辑距离

题目

给定两个单词 word1 和 word2,计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符
示例 1:

输入: word1 = "horse", word2 = "ros"
输出: 3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
示例 2:

输入: word1 = "intention", word2 = "execution"
输出: 5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

解题思路

  1. 先作弊了,看到要用dp。思考许久还是没有想法。
    解析一下自己的错误思路。
    看到有三种变换方式,有一点不知道该怎么处理。

  2. 看了网上的解题报告,写下自己理解后的东西。

    首先,确认dp的含义
    word1记为s1, word2记为s2
    dp[i][j] = s1 从0...i, s2从0...j 两个字符串的编辑距离。所以dp的大小应该是dp[s1.length + 1][s2.length + 1].

  • 转换公式
    计算dp[i][j]有三种变化方式

    1. dp[i - 1][j], 由于对比dp[i][j],s1少了一个,所以要insert一个,编辑距离 + 1
    2. dp[i][j - 1] 由于对比dp[i][j],s1少了一个,所以要delete一个,编辑距离 + 1
    3. dp[i - 1][j - 1],对比s1[i - 1] 和 s2[j - 1]的情况,如果相等,编辑距离不变,否则需要+1(替换)

    从上文可以看出,三种变换方式都有了。

    接下来处理边界条件

    dp[0][i] = i和dp[i][0] = i,分别代表增加i个,和删除i个。

代码

 public int minDistance(String word1, String word2) {
    int[][] dp = new int[word1.length() + 1][word2.length() + 1];
    for (int i = 0; i <= word1.length(); i++) {
        dp[i][0] = i;
    }
    for (int j = 0; j <= word2.length(); j++) {
        dp[0][j] = j;
    }
    for (int i = 1; i <= word1.length(); i++) {
        for (int j = 1; j <= word2.length(); j++) {
            int a = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
            int cost = word1.charAt(i - 1) == word2.charAt(j - 1) ? 0 : 1;
            dp[i][j] = Math.min(a, dp[i - 1][j - 1] + cost);
        }
    }
    return dp[word1.length()][word2.length()];
}
2019/02/01 11:46 上午 posted in  LeetCode

JSON反序列化乱序的问题

2019/09/16 14:48 下午 posted in  JSON

Jackson fasterxml和codehaus的区别

作为最出名的Json解析库之一,jackson有着两个完全不一样的包名版本。com.fasterxml.jackson&&org.codehaus.jackson

这两个版本有什么区别呢?

他们是Jackson的两大分支、也是两个版本的不同包名。Jackson从2.0开始改用新的包名fasterxml;1.x版本的包名是codehaus。除了包名不同,他们的Maven artifact id也不同。1.x版本现在只提供bug-fix,而2.x版本还在不断开发和发布中。如果是新项目,建议直接用2x,即fasterxml jackson。

2019/09/24 15:33 下午 posted in  JSON

Java中的代理模式

1. 静态代理和动态代理

    本章节参考了https://www.ibm.com/developerworks/cn/java/j-lo-proxy-pattern/

1.1. 定义

    静态代理和动态代理指的是实现代理模式的方式。静态模式意思是所有的代码是静态写好的。而动态代理则相对,部分代码是动态生成的。在动态代理中还分为JDK动态代理和CGLib动态代理。

1.2. 关键实现

1.2.1. 静态代理

静态代理是基于接口实现的,他要求真实类和代理类实现同样的接口。

public interface IDBQuery {
    String request();
}
public class DBQuery implements IDBQuery{
    public DBQuery(){
        try{
            Thread.sleep(1000);//假设数据库连接等耗时操作
        }catch(InterruptedException ex){
            ex.printStackTrace();
        }
    }

    @Override
    public String request() {
// TODO Auto-generated method stub
        return "request string";
    }


}
public class DBQueryProxy implements IDBQuery{
    private DBQuery real = null;

    @Override
    public String request() {
// TODO Auto-generated method stub
//在真正需要的时候才能创建真实对象,创建过程可能很慢
        if(real==null){
            real = new DBQuery();
        }//在多线程环境下,这里返回一个虚假类,类似于 Future 模式
        return real.request();
    }

}
public class Main {
    public static void main(String[] args){
        IDBQuery q = new DBQueryProxy(); //使用代里
        q.request(); //在真正使用时才创建真实对象
    }
}

1.2.2. JDK代理

当使用JDK代理时,一个最直观的变化就是代理类不需要和真实类实现同一个接口了。取而代之的是代理类实现了InvocationHandler,并Override了invoke方法。在方法里可以统一对实现方法做处理(方法调用前,方法调用后)。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;


public class DBQueryHandler implements InvocationHandler{
    IDBQuery realQuery = null;//定义主题接口

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
// TODO Auto-generated method stub
        //如果第一次调用,生成真实主题
        if(realQuery == null){
            realQuery = new DBQuery();
        }
        //method.invoke(target, args); 执行调用的方法。
        //返回真实主题完成实际的操作
        return realQuery.request();
    }
    public static IDBQuery createProxy(){
        IDBQuery proxy = (IDBQuery)Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{IDBQuery.class}, new DBQueryHandler()); // 注意,生成的代理类实例被强转为IDBQuery
        return proxy;
    }
}

1.2.3. CGLib代理

CGLib一个直观的最大的特点就是真实类无需实现接口(当然实现了也没关系)。

接口类

public interface BookProxy {
    public void addBook();
}

实现类

//该类并没有申明 BookProxy 接口
public class BookProxyImpl {
    public void addBook() { 
        System.out.println("增加图书的普通方法..."); 
    } 
}

代理类

import java.lang.reflect.Method; 
import net.sf.cglib.proxy.Enhancer; 
import net.sf.cglib.proxy.MethodInterceptor; 
import net.sf.cglib.proxy.MethodProxy;

public class BookProxyLib implements MethodInterceptor {
    private Object target;
    /**
     * 创建代理对象 
     *
     * @param target
     * @return
     */
    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        // 回调方法 
        enhancer.setCallback(this);
        // 创建代理对象 
        return enhancer.create();
    }

    @Override
// 回调方法 
    public Object intercept(Object obj, Method method, Object[] args,
                            MethodProxy proxy) throws Throwable {
        System.out.println("事物开始");
        proxy.invokeSuper(obj, args);
        System.out.println("事物结束");
        return null;
    }
}

调用方法

public class TestCglib { 
    public static void main(String[] args) { 
        BookProxyLib cglib=new BookProxyLib(); 
        BookProxyImpl bookCglib=(BookProxyImpl)cglib.getInstance(new BookProxyImpl()); 
        bookCglib.addBook();  //可以看到BookProxyLib并没有声明BookProxy接口,但是仍然可以调用addBook方法
    } 
}

1.3. 区别和共同点

静态代理是通过在代码中显式定义一个业务实现类一个代理,在代理类中对同名的业务方法进行包装,用户通过代理类调用被包装过的业务方法;

JDK动态代理是通过接口中的方法名,在动态生成的代理类中调用业务实现类的同名方法;

CGlib动态代理是通过继承业务类,生成的动态代理类是业务类的子类,通过重写业务方法进行代理;

https://blog.csdn.net/neosmith/article/details/51072840

2. 实际应用场景举例

todo:没有理解cglib不用真实类实现接口的意义。因为真实类没有实现接口,但是暴露了public的方法。这和直接调用有啥区别?

另外cglib的试用场景,真实类没有实现的接口意义何在?如果没有接口来规范统一的调用逻辑,例如一堆的实现类实现了A接口,因此必须实现A接口中定义的B方法。这样才有意义吧?

查看spring源码,了解spring中,cglib的使用方法,来解答上述疑问。

代理模式的意义

  1. 有代理,便于解耦
  2. 静态代理太麻烦,每个都要
  3. JDK代理受限于要实现接口
  4. CGLib不需要实现接口,看上去无法统一接口的方法,但是可能是用在一些common的方法,例如Object的方法。用在类创建时刻。
  5. 另外让方法运行只是最基本的,代理模式的最大用途是管理原方法的运行前,后,时(切面,AOP)。

3. Spring AOP和动态代理

4. OC中的动态代理模式浅谈

5. CGLib和JDK代理的性能对比

2019/05/26 20:59 下午 posted in  Java

Java并发编程

  本文对Java中的并发变成进行了简单的描述。是本人阅读《Java并发编程的艺术》一书的读书笔记。本文对重要的概念进行了记录。

  本文首先介绍了各种各样的和锁相关的概念。然后介绍了Java多线程的技术要点,最后介绍了一些经典使用案例。

Read more   2019/01/20 22:56 下午 posted in  Java

Java序列化的那些事

1. 什么是序列化和反序列化

(1)Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程;

(2)序列化:对象序列化的最主要的用处就是在传递和保存对象的时候,保证对象的完整性和可传递性。序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。序列化后的字节流保存了Java对象的状态以及相关的描述信息。序列化机制的核心作用就是对象状态的保存与重建。

(3)反序列化:客户端从文件中或网络上获得序列化后的对象字节流后,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

(4)本质上讲,序列化就是把实体对象状态按照一定的格式写入到有序字节流,反序列化就是从有序字节流重建对象,恢复对象状态。

2、为什么需要序列化与反序列化

我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。

那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的!如何做到呢?这就需要Java序列化与反序列化了!

换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。

当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。

总的来说可以归结为以下几点:

(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;

3、实现Java对象序列化与反序列化的方法

假定一个User类,它的对象需要序列化,可以有如下三种方法:

(1)若User类仅仅实现了Serializable接口,则可以按照以下方式进行序列化和反序列化

ObjectOutputStream采用默认的序列化方式,对User对象的非transient的实例变量进行序列化。
ObjcetInputStream采用默认的反序列化方式,对对User对象的非transient的实例变量进行反序列化。

(2)若User类仅仅实现了Serializable接口,并且还定义了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),则采用以下方式进行序列化与反序列化。

ObjectOutputStream调用User对象的writeObject(ObjectOutputStream out)的方法进行序列化。
ObjectInputStream会调用User对象的readObject(ObjectInputStream in)的方法进行反序列化。

(3)若User类实现了Externalnalizable接口,且User类必须实现readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,则按照以下方式进行序列化与反序列化。

ObjectOutputStream调用User对象的writeExternal(ObjectOutput out))的方法进行序列化。
ObjectInputStream会调用User对象的readExternal(ObjectInput in)的方法进行反序列化。

4. serialVersionUID

序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途:

在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;

在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

参考资料

原文链接:https://blog.csdn.net/xlgen157387/article/details/79840134
https://blog.csdn.net/u014750606/article/details/80040130

2020/03/16 00:05 上午 posted in  Java

MongoDB的启动和停止

1. 启动

  1. 用命令行,加各种的特殊参数
  2. 用命令行,但是所有的参数都放在一个配置文件中。如

    mongod -f /etc/mongod.conf
    
  3. 用传统的service mongod start

1.1. service mongod start

  • 该命令本身没有任何问题。只需要注意配置启动的时候执行的语句就可以达到同样的效果。在实际使用的过程中,并不会频繁的变更需求和配置,因此用service来启动足以。
  • 另外如果用命令行启动,还存在权限问题。
    用mongod启动了以后,权限全部变为root。这时候再用service mongod start来启动就会有各种各样的权限问题。

  • service start启动的脚本

    /usr/lib/systemd/system/mongod.service
    
[Unit]
Description=MongoDB Database Server
After=network.target
Documentation=https://docs.mongodb.org/manual

[Service]
User=mongod
Group=mongod
Environment="OPTIONS=-f /etc/mongod.conf"
EnvironmentFile=-/etc/sysconfig/mongod
ExecStart=/usr/bin/mongod $OPTIONS
ExecStartPre=/usr/bin/mkdir -p /var/run/mongodb
ExecStartPre=/usr/bin/chown mongod:mongod /var/run/mongodb
ExecStartPre=/usr/bin/chmod 0755 /var/run/mongodb
PermissionsStartOnly=true
PIDFile=/var/run/mongodb/mongod.pid
Type=forking
# file size
LimitFSIZE=infinity
# cpu time
LimitCPU=infinity
# virtual memory size
LimitAS=infinity
# open files
LimitNOFILE=64000
# processes/threads
LimitNPROC=64000
# locked memory
LimitMEMLOCK=infinity
# total threads (user+kernel)
TasksMax=infinity
TasksAccounting=false
# Recommended limits for for mongod as specified in
# http://docs.mongodb.org/manual/reference/ulimit/#recommended-settings

[Install]
WantedBy=multi-user.target

2. 停止

强制关闭MongoDB(不建议使用);
service mongod stop
或者,从MongoDB的admin中关闭(推荐用这种方法):

>use admin
switched to db admin
>db.shutdownServer()
server should be down...

或者

mongod --shutdown

使用shutdownServer关闭MongoDB,如有MongoDB主从服务器,则在服务关闭前同步主从服务器;强制关闭则不会;

2019/01/20 23:56 下午 posted in  MongoDB

RAC中的宏定义魔法

https://onevcat.com/2014/01/black-magic-in-macro/

1. #的作用

2. ##的作用

3. VA_ARGS的作用

4. RAC()

4.1 函数功能

RAC(),有两种调用方式,一个是两个参数,一个是三个参数的。函数的第一个参数代表着要绑定的对象,第二个参数要绑定的对象的属性,第三个参数代表当追踪的结果为nil时,应该赋予的值。

这里就有一个很有趣的问题,ReactiveCocoa是如何做到用一个函数宏,自动识别参数个数,并调用正确的函数的呢?

Read more   2019/01/21 00:13 上午 posted in  iOS

Realm学习笔记

  • Useful文章

https://www.jianshu.com/p/6704afc62d6c

Realm 核心数据库引擎探秘

1. 模型定义

参照官网的demo,建立如下两个类:

@interface Dog : RLMObject
@property NSString *name;
@property NSData   *picture;
@property NSInteger age;
@end
@implementation Dog
@end
RLM_ARRAY_TYPE(Dog)
@interface Person : RLMObject
@property NSString             *name;
@property RLMArray<Dog *><Dog> *dogs;
@end
@implementation Person
@end
tips

1. 需要继承RLMObject
2. 所有的属性都不需要写任何的描述符,原子性,strong, assign等
3. Array类型需要用RLMArray
4. 使用Array 需要定义 RLM_ARRAY_TYPE , 这个定义放在类前类后都可以。

2.CRUD

Realm采用了MVCC设计架构,因此读写操作是不互斥的。但是写操作最好在一个Seperate thread中执行,否则会降低效率。

2.1 创建(Create)

// (1) Create a Dog object and then set its properties
    Dog *myDog = [[Dog alloc] init];myDog.name = @"Rex";myDog.age = 10;
// (2) Create a Dog object from a dictionary
    Dog *myOtherDog = [[Dog alloc] initWithValue:@{@"name" : @"Pluto", @"age" : @3}];
// (3) Create a Dog object from an array
    Dog *myThirdDog = [[Dog alloc] initWithValue:@[@"Pluto", @3]];

写入数据库

    // Get the default Realm
    RLMRealm *realm = [RLMRealm defaultRealm];
    // You only need to do this once (per thread)
    
    // Add to Realm with transaction
    [realm beginWriteTransaction];
    [realm addObject:province];
    [realm commitWriteTransaction];

2.2 查询(Retrieve)

// 使用断言字符串查询
    RLMResults<ProvinceEntity *> *provinceArray = [ProvinceEntity objectsWhere:@"shortName = '江苏'"];
    // 使用 NSPredicate 查询
    NSPredicate *pred = [NSPredicate predicateWithFormat:@"shortName = '江苏'"];
    provinceArray = [ProvinceEntity objectsWithPredicate:pred];

如果有多条件的话,可以用and,也可以分布查询,从查询结果中再做查询,支持链式查询:

RLMResults<Dog *> *tanDogs = [Dog objectsWhere:@"color = '棕黄色'"];
RLMResults<Dog *> *tanDogsWithBNames = [tanDogs objectsWhere:@"name BEGINSWITH '大'"];

RLMResults允许您指定一个排序标准,从而可以根据一个或多个属性进行排序。比如说,下列代码将上面例子中返回的狗狗根据名字升序进行排序:

// 排序名字以“大”开头的棕黄色狗狗
RLMResults<Dog *> *sortedDogs = [[Dog objectsWhere:@"color = '棕黄色' AND name BEGINSWITH '大'"] sortedResultsUsingProperty:@"name" ascending:YES];

2.3 更新(Update)

  • 你可以找到具体的一条数据然后去更新:
    RLMResults<ProvinceEntity *>* provinceArray=[ProvinceEntity allObjects];
    [[RLMRealm defaultRealm] transactionWithBlock:^{
        ProvinceEntity *province=[provinceArray firstObject];
        province.shortName=@"浙江";
    }];
  • 你也可以设置一个主键,根据主键去更新,更新需要拥有一个主键---Primary Keys:
// Creating a book with the same primary key as a previously saved 
bookBook *cheeseBook = [[Book alloc] init];
cheeseBook.title = @"Cheese recipes";
cheeseBook.price = @9000;
cheeseBook.id = @1;
// Updating book with id = 1
[realm beginWriteTransaction];
[realm addOrUpdateObject:cheeseBook];
[realm commitWriteTransaction];

2.4 删除(Delete)

  • 单条记录删除
// Delete an object with a transaction
[realm beginWriteTransaction];
[realm deleteObject:cheeseBook];
[realm commitWriteTransaction];
  • 多条记录删除
    [[RLMRealm defaultRealm] transactionWithBlock:^{
        [[RLMRealm defaultRealm] deleteObjects:result];
    }];
  • 全部删除:
// Delete an object with a transaction
    [[RLMRealm defaultRealm] transactionWithBlock:^{
        [[RLMRealm defaultRealm] deleteAllObjects];
    }];

3.进阶使用

  • 非空字段(Required properties)

By default, NSString *, NSData *, and NSDate * properties allow you to set them to nil. If you want to require that a value be present, override the +requiredProperties method on your RLMObject subclass.

For example, with the following model definition, trying to set the person’s name to nil will throw an exception, but setting their birthday to nil is allowed:

@interface Person : RLMObject
@property NSString *name;
@property NSDate *birthday;
@end

@implementation Person
+ (NSArray *)requiredProperties {
    return @[@"name"];
}
@end

例外:RLMObject subclass properties always can be nil, and thus cannot be included in requiredProperties. and RLMArray does not support storing nil.

  • 主键

Override +primaryKey to set the model’s primary key. Declaring a primary key allows objects to be looked up and updated efficiently and enforces uniqueness for each value. Once an object with a primary key is added to a Realm, the primary key cannot be changed.

@interface Person : RLMObject
@property NSInteger id;
@property NSString *name;
@end

@implementation Person
+ (NSString *)primaryKey {
    return @"id";
}
@end
  • 索引字段

To index a property, override +indexedProperties. Like primary keys, indexes make writes slightly slower, but makes queries using comparison operators faster. (It also makes your Realm file slightly larger, to store the index.) It’s best to only add indexes when you’re optimizing the read performance for specific situations.

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSArray *)indexedProperties {
    return @[@"title"];
}
@end
  • 忽略字段

If you don’t want to save a field in your model to its Realm, override +ignoredProperties. Realm won’t interfere with the regular operation of these properties; they’ll be backed by ivars, and you can freely override their setters and getters.

@interface Person : RLMObject
@property NSInteger tmpID;
@property (readonly) NSString *name; // read-only properties are automatically ignored
@property NSString *firstName;
@property NSString *lastName;
@end

@implementation Person
+ (NSArray *)ignoredProperties {
    return @[@"tmpID"];
}
- (NSString *)name {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
@end
  • 默认值

Override +defaultPropertyValues to provide default values every time an object is created.

@interface Book : RLMObject
@property float price;
@property NSString *title;
@end

@implementation Book
+ (NSDictionary *)defaultPropertyValues {
    return @{@"price" : @0, @"title": @""};
}
@end

4. 特性

5. 疑问

为什么要这样直接继承不可以吗?是不支持吗?

// Base Model
@interface Animal : RLMObject
@property NSInteger age;
@end
@implementation Animal
@end

// Models composed with Animal
@interface Duck : RLMObject
@property Animal *animal;
@property NSString *name;
@end
@implementation Duck
@end

@interface Frog : RLMObject
@property Animal *animal;
@property NSDate *dateProp;
@end
@implementation Frog
@end

// Usage
Duck *duck =  [[Duck alloc] initWithValue:@{@"animal" : @{@"age" : @(3)}, @"name" : @"Gustav" }];
2018/03/21 19:57 下午 posted in  iOS

一道LeetCode线程题引出Java线程协作的经典案例

1. 题目

1115. 交替打印FooBar

我们提供一个类:

class FooBar {
  public void foo() {
    for (int i = 0; i < n; i++) {
      print("foo");
    }
  }

  public void bar() {
    for (int i = 0; i < n; i++) {
      print("bar");
    }
  }
}

两个不同的线程将会共用一个 FooBar 实例。其中一个线程将会调用 foo() 方法,另一个线程将会调用 bar() 方法。

请设计修改程序,以确保 "foobar" 被输出 n 次。

 
示例 1:

输入: n = 1
输出: "foobar"
解释: 这里有两个线程被异步启动。其中一个调用 foo() 方法, 另一个调用 bar() 方法,"foobar" 将被输出一次。

示例 2:

输入: n = 2
输出: "foobarfoobar"
解释: "foobar" 将被输出两次。

来源:力扣(LeetCode)
链接:
https://leetcode-cn.com/problems/print-foobar-alternately
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

2. 分析

本题有两个要求

  1. 顺序性,即foo要在bar之前打印,需要考虑先执行print bar的情况。
  2. 交替性,foo和bar需要轮流打印。

2.1. 方案一(基于volatile)

用一个变量来标记当前打印的是foo还是bar。这样就知道下一个操作需要打印foo还是bar。这个变量需要在线程间进行共享。共享没有问题,FooBar内的变量对同一个对象是可以访问的。但是需要能够及时同步。因此我们需要一个volatile变量。

1.0版本:

class FooBar {
    private int n;

    volatile boolean flag = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (!flag){
            }
                // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            flag = false;
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {

        for (int i = 0; i < n; i++) {
                // printBar.run() outputs "bar". Do not change or remove this line.
            while (flag){
            }
            printBar.run();
            flag = true;
        }
    }
}

提交,超时了。为啥呢?

考虑CPU单核的情况,while (flag){}如果是bar线程先运行,将会不停执行while。foo线程无法抢占时间片,自然无法开始第一步print foo了。在多核环境下,虽然不会造成另一线程无法抢占时间片的问题,但是while循环是很耗时的,占用大量CPU资源,这也会使得运行时间变长而超时。

基于这样的分析,修改一下,增加Thread.sleep(),每次循环的时候,休眠一会儿。

2.0版本:

class FooBar {
    private int n;

    volatile boolean flag = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (!flag){
                Thread.sleep(20);
            }
                // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            flag = false;
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {

        for (int i = 0; i < n; i++) {
                // printBar.run() outputs "bar". Do not change or remove this line.
            while (flag){
                Thread.sleep(20);
            }
            printBar.run();
            flag = true;
        }
    }
}

提交,进步了一点。还是超时

我们已经把休眠时间调整的很小了(20ms),希望程序可以快点切换到下一个打印。我们或许可以通过继续把休眠时间调整的更小来通过这道题,但是我们有一个更好的方法。Thread.yield()

3.0版本来了:

class FooBar {
    private int n;

    volatile boolean flag = true;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while (!flag){
                Thread.yield();
            }
                // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            flag = false;
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {

        for (int i = 0; i < n; i++) {
                // printBar.run() outputs "bar". Do not change or remove this line.
            while (flag){
                Thread.yield();
            }
            printBar.run();
            flag = true;
        }
    }
}


通过了!!!

摘抄自LeetCode评论:https://leetcode-cn.com/problems/print-foobar-alternately/solution/xian-cheng-ping-zhang-de-wen-ti-yi-ban-you-san-cho/
while循环是比较耗费性能的,可能会导致执行结果超时。可以通过Thread.yield进一步控制线程的执行,而非比较粗力度的循环。当某个线程调用yield()方法时,就会从运行状态转换到就绪状态后,CPU从就绪状态线程队列中只会选择与该线程优先级相同或者更高优先级的线程去执行。总之加上Thread.yield性能会更高一点,因此用时会更少

什么是Thread.yield()?

摘抄自:https://www.cnblogs.com/java-spring/p/8309931.html

Java线程中的Thread.yield( )方法,译为线程让步。顾名思义,就是说当一个线程使用了这个方法之后,它就会把自己CPU执行的时间让掉,
让自己或者其它的线程运行,注意是让自己或者其他线程运行,并不是单纯的让给其他线程。
yield()的作用是让步。它能让当前线程由“运行状态”进入到“就绪状态”,从而让其它具有相同优先级的等待线程获取执行权;但是,并不能保证在当前线程调用yield()之后,其它具有相同优先级的线程就一定能获得执行权;也有可能是当前线程又进入到“运行状态”继续运行!
举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,
有可能是其他人先上车了,也有可能是Yield先上车了。
但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,
最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。

2.2 方案二 Semaphore

Semaphore
https://blog.csdn.net/hanchao5272/article/details/79780045

基于Semaphore的代码如下:

class FooBar {

    private int n;

    private Semaphore semaphore = new Semaphore(1);

    private volatile boolean foo = false;

    public FooBar(int n) {
        this.n = n;
    }

    public void foo(Runnable printFoo) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            semaphore.acquire();
            // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            foo = true;
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            while (!foo) {
            }
            // printBar.run() outputs "bar". Do not change or remove this line.
            printBar.run();
            foo = false;
            semaphore.release();
        }
    }
}

作者:san-mu-32
链接:https://leetcode-cn.com/problems/print-foobar-alternately/solution/tong-guo-yi-ge-xin-hao-liang-kong-zhi-foohe-barde-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

涉及多线程,运行时间并不稳定。和方案一类似,在while循环中加入Thread.yield(),速度有一定提升。

2.3. 方案三 notify && wait

https://www.jianshu.com/p/1dafbf42cc54

class FooBar {

    private int              n;
    private volatile boolean isFoo;

    public FooBar(int n) {
        this.n = n;
    }

    public synchronized void foo(Runnable printFoo) throws InterruptedException {

        for (int i = 0; i < n; i++) {
            //            synchronized (lock) {
            // printFoo.run() outputs "foo". Do not change or remove this line.
            printFoo.run();
            isFoo = true;
            this.notify();
            if (i < n - 1) {
                this.wait();
            }
            //            }
        }
    }

    public synchronized void bar(Runnable printBar) throws InterruptedException {
        if (!isFoo) {
            this.wait();
        }
        for (int i = 0; i < n; i++) {
            //            synchronized (lock) {
            // printBar.run() outputs "bar". Do not change or remove this line.
            printBar.run();
            this.notify();
            if (i < n - 1) {
                this.wait();
            }
            //            }
        }
    }
}


运行时间不稳定,应该是LeetCode的问题。

2.4. 方案四 CyclicBarrier

https://www.jianshu.com/p/333fd8faa56e

class FooBar {
    private int n;

    public FooBar(int n) {
        this.n = n;
    }

    CyclicBarrier cb = new CyclicBarrier(2);
    volatile boolean fin = true;

    public void foo(Runnable printFoo) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            while(!fin);
            printFoo.run();
            fin = false;
            try {
        cb.await();
        } catch (BrokenBarrierException e) {
        }
        }
    }

    public void bar(Runnable printBar) throws InterruptedException {
        for (int i = 0; i < n; i++) {
            try {
        cb.await();
        } catch (BrokenBarrierException e) {
        }
            printBar.run();
            fin = true;
        }
    }
}

作者:KevinBauer
链接:https://leetcode-cn.com/problems/print-foobar-alternately/solution/java-bing-fa-gong-ju-lei-da-lian-bing-by-kevinbaue/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2.5. 方案五 CyclicBarrier + CountdownLatch

CyclicBarrier用于保证每一轮的foobar的打印。CountdownLatch用于保证单轮内,先打印foo,再打印bar。

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
class FooBar {
    private int n;
    private CountDownLatch a;
    private CyclicBarrier barrier;// 使用CyclicBarrier保证任务按组执行
    public FooBar(int n) {
        this.n = n;
        a = new CountDownLatch(1);
        barrier = new CyclicBarrier(2);// 保证每组内有两个任务
    }

    public void foo(Runnable printFoo) throws InterruptedException {

        try {
            for (int i = 0; i < n; i++) {
                printFoo.run();
                a.countDown();// printFoo方法完成调用countDown
                barrier.await();// 等待printBar方法执行完成
            }
        } catch(Exception e) {}
    }

    public void bar(Runnable printBar) throws InterruptedException {

        try {
            for (int i = 0; i < n; i++) {
                a.await();// 等待printFoo方法先执行
                printBar.run();
                a = new CountDownLatch(1); // 保证下一次依旧等待printFoo方法先执行
                barrier.await();// 等待printFoo方法执行完成
            }
        } catch(Exception e) {}
    }
}

作者:bonaluo
链接:https://leetcode-cn.com/problems/print-foobar-alternately/solution/javashi-yong-yi-ge-countdownlatchhe-yi-ge-cyclicba/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3. 总结

本题需要解决两个问题。

  1. 两个线程必须先后执行,
  2. foo线程必须保证先执行。

为了解决这两个问题,5个方案选择了不同的方案组合。主要分为无锁和有锁两种方案。

为了解决foo线程先执行的问题。有使用volatile变量和CountdownLatch两种方法。volatile变量使用的是无锁的方案。通过一个死循环,组织bar线程先运行。优点是可以快速感知状态变换,无需线程切换。缺点是资源消耗大,需要使用Thread.yield()。否则会超时。CountdownLatch采用的是有锁的方案,因此会有线程的切换,单不会大量占用系统资源。在线程占用时间长的场景体验更佳。

为了让两个线程先后执行,需要在foo线程执行后挂起线程,让bar线程运行。在bar线程运行后,再让foo线程执行。无锁方案,继续用volatile变量即可。有锁方案则可以有几种选择。Semaphore,CyclicBarrier,notify&wait,Lock。

4. 引申

  在 单核 / 单CPU 的系统上使用 自旋锁 是没有意义的,因为它就一个运行线程/核心,你占着不放,那么其他线程将得不到运行,其他线程得不到运行,这个锁就不能被解锁。换句话说,在 单核 / 单CPU 系统使用 自旋锁,除了浪费点时间外没有一点好处。这时如果让这个线程(记为线程A)休眠,其他线程就得以运行,然后就可能会解锁这个 自旋锁,线程A就可能在重新被唤醒后,如愿以偿的持有锁。

  在 多核 / 多CPU 的系统上,特别是大量的线程只会短时间的持有锁的时候,这时如果使用 互斥锁,在使线程睡眠和唤醒上浪费大量的时间,也许会显著降低程序的运行性能。使用 自旋锁,线程可以充分利用系统调度程序分配的时间片(经常阻塞很短的时间,不用休眠,然后马上继续它们后面的工作了),以达到更高的处理能力和吞吐量。

https://www.cnblogs.com/shines77/p/4198046.html

2020/02/17 11:57 上午 posted in  Java

从@Transactional,@Async 失效说起

1、问题描述

当我们在同一个类中,调用一个被@Transactional,或@Async标注的方法时,这些注解是否会生效?

结论是不会。

2、原因

Spring采用动态代理(AOP)实现对bean的管理和切片,它为我们的每个class生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。

spring 在扫描bean的时候会扫描方法上是否包含@Async注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用时增加异步作用。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean,所以就没有增加异步作用,我们看到的现象就是@Async注解无效。

当我们调用A的bean的a()方法的时候,也是被proxyA拦截,执行proxyA拦截,执行proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以异步执行的效果也没有运行。

特别注意: 当Spring的事务在同一个类时,它的自我调用时事务将失效.
@Async

解决方案

方案一

在当前类中通过上下文获取自己的代理对象调用异步方法

直接@Autowired

方案二

基于 AopContext 暴露代理对象

EnableAspectJAutoProxy(exposeProxy=true)
AopContext.currentProxy()

https://mp.weixin.qq.com/s/m7p7AP_zT1JEZrxwmVISVQ

方案三

从外部类调用

2020/03/12 17:44 下午 posted in  数据库

从logger.isDebugEnabled()谈起

0. 问题

在很多框架中,我们看到在logger.debug处经常会这样写

if (logger.isDebugEnabled()) {
    logger.debug(message);
}

我们知道logger.debug(),在日志级别不够的时候是不会输出日志的。那么这么写的目的何在?

1. 分析

我们来看一个例子

String error = "debug日志";
logger.debug("这是一个" + error);

按照正常的逻辑,在执行logger.debug()之前需要先行计算括号里的内容。然后才会判断当前日志级别不符合不输出。这里就存在一个无用计算的过程。相比对直接执行语句对logger.isDebugEnabled()进行先判断,显然是一个更优的方案,避免了无用的计算过程。

然而这个结论在占位符的使用下,有了一点不同。

再来看一个例子

1.1. logger.debug("这是一个{}", model);

public static class Model{
    int age;

    @Override
    public String toString() {
        System.out.println("toString called");
        return "model{" +
                "age=" + age +
                '}';
    }
}
public static void main(String[] args) {
    Model model = new Model();
    model.age = 3;
    
    logger.debug("这是一个{}", model);
}

同样的作用,但是当日志级别高于debug时,不会执行括号内部的字符串拼接。也就是如果使用占位符{}来组合输出日志,可以不用判断logger.isDebugEnabled()

1.2. logger.debug("test:{}", test());

Model的toString方法在用占位符{}的方式时,没有执行,那么如果是一个普通的方法呢?

public static String test(){
    System.out.println("test");
    return "test";
}
public static void main(String[] args) {
    logger.debug("test:{}", test()); //日志级别info
}

结果,logger.debug("test:{}", test());无输出,但是test()方法在控制台打印出了test。

结论:有占位符的存在,但是如果字符串拼接调用了函数,仍然会先执行函数,这和日志级别无关。

1.3. logger.debug("test2:{}, {}", model, test());

public static String test(){
    System.out.println("print test");
    return "fdsf";
}
public static void main(String[] args) {
    Model model = new Model();
    model.age = 3;
    logger.debug("test1:" + model);
    model.age = 5;
    logger.debug("test2:{}, {}", model, test());
}

结果,最后一句debug,model的toString方法没有执行,而test()方法打印出了"print test"。也就是在占位符的作用下,对象的toString()方法不会执行,而不同方法仍然会执行。

2. 结论

通过logger.isDebugEnabled()进行先行判断,肯定是没有错的。虽然在占位符的帮助下,当日志级别高于debug时,对象的toString()方法不会执行,但是普通的方法仍然会执行。如果能够保证logger.debug()中的内容只涉及最简单的字符串拼接和toString(),那么可以简略logger.isDebugEnabled(),否则还是加上避免无用计算。

2019/04/02 20:43 下午 posted in  Java

从优化数据库查询速度谈起---MySQL优化相关知识梳理

0. 如何优化数据库查询速度?

优化数据库查询是一个老生常谈的问题。为了回答好这个问题,我们需要从以下几个方面来分析系统中的数据库查询问题。

  1. 数据库的设计是否合理。基于系统读大于写的场景,可以使用反范式化设计,减少join表操作。
  2. 数据库语句优化。使用的数据库语句是否合理。
  3. 索引优化。

1. 数据库的设计是否合理?

例如我们有一个设备信息表(t_device):

一个设备区域信息表(t_zone):

一个使用场景是,系统需要频繁的查询设备的基础信息,并且希望同时查出设备所属区域的名字。
为此,我们需要使用一个join操作来获得设备的区域名。

普遍的情况下,数据库设计应遵循三范式(3NF)设计。但是我们的系统如果存在查询请求多于插入和更新请求的情况。则可以考虑采用反范式化设计。

反范式化设计指的是违背第三范式设计。以上的设计符合第三范式设计的要求,即在device表中仅保存了zone_id,没有保存区域的名字,这使得我们需要通过join操作来获得区域信息。如果我们在t_device表中增加一个区域字段。

显然,我们不需要额外join,仅需要查询设备的基本信息表(t_device)就可以获取到设备的区域名。

1.1. 额外的问题

反范式化设计让我们减少了数据库的查询次数。达到提升的查询速度的目的。但是需要注意的是,经过改造后,设备的区域信息存放在了t_device, t_zone 两张表中。也就是如果需要更新设备的区域信息,我们需要同时修改这两张表中的数据。这也是反范式化设计核心理念,以空间换时间的体现。

2. SQL语句优化

网上可以搜到很多SQL语句的实战文章,其中大部分和索引有关系。一个高频的关键字就是避免全表扫描。本章主要介绍和索引无关(即不管是否建立索引都会有问题)的避免全表扫描的常见Tip。

1. 字段值中包含null

避免字段值中有null,可以使用一个默认值来代替null的含义。

2. 避免使用函数

如果需要使用函数应该在代码中提前计算好,避免使用SQL的函数。特别是在where中使用。

3. 避免隐式转义

例如查询一个varchar的字段,查询子句写成了 where id=1。

4. in 和 not in 也要慎用

会导致全表扫描。

3. 索引优化

索引是加速SQL查询的最主要手段。索引的相关概念有:

  1. 主键索引
  2. 唯一索引
  3. 普通索引
  4. 联合索引
  5. 聚簇索引
  6. 二级索引,辅助索引

接下来让我们一个一个了解。

3.1. 主键索引

主键顾名思义是主键所在的索引。在MySQL中,如果采用InnoDB引擎,则必须有一个主键索引。如果用户没有定义系统会自动选择第一个不包含NULL的唯一索引作为主键索引,如果还是没有,系统会帮忙生成一个。InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增)。

3.2.为什么InnoDB一定要有主键索引?

这是由InnoDB的底层数据结构B+树决定的。具体的资料网上有很多,这里就不复制黏贴了。

传送门:

MySQL中InnoDB表为什么要建议用自增列做主键

为什么InnoDB表必须有主键,并且推荐使用整型的自增主键?

InnoDB如果没有主键或者随机主键真的很可怕吗?

引申知识点(MySQL中的B+树)

3.2. 唯一索引&普通索引

唯一索引指的是建立索引的字段是唯一的。普通索引则没有这个限制。

3.3. 联合索引

联合索引指的是由多个字段建立的索引。例如

我们可以同时对device_ip, device_type 建立索引。建立联合索引后的效果等同于建立了两条索引,一条是device_ip索引,一条是device_ip, device_type索引。

3.4. 聚簇索引(聚集索引)

聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。具体的细节依赖于其实现方式,但InnoDB的聚簇索引实际上在同一个结构中保存了B-Tree索引和数据行。当表有聚簇索引时,他的数据行实际上存放在索引的叶子页(leaf page)中。术语 “聚簇”表示数据行和相邻的键值紧凑地存储在一起(这并非总成立)。

3.5.

3.6. 索引优化

1. 开启慢查询记录

了解了索引的相关知识,接下来就该开始定位系统中有问题的查询了。通过开启慢查询记录,我们可以获取到一段时间内的慢查询的执行记录。

mysql 开启慢查询及其用mysqldumpslow做日志分析

2. explain执行计划

索引失效的几种情况

  1. like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效。 PS:
    当真的需要两边都使用%来模糊查询时,只有当这个作为模糊查询的条件字段(例子中的name)以及所想要查询出来的数据字段(例子中的 id & name & age)都在索引列上时,才能真正使用索引,否则,索引失效全表扫描(比如多了一个 salary 字段)。我想,这应该就是 ‘覆盖索引(索引覆盖)’ 的本质吧。同时,这也能很好的证实 “尽量避免SELECT * 而是一一罗列出所需要查询的字段” 的道理吧,因为,搞不好 SELECT * 就多了一个字段,就导致了全表扫描。 参考资料:https://www.bbsmax.com/A/mo5kk1rK5w/
  2. or语句前后没有同时使用索引。当or左右查询字段只有一个是索引,该索引失效,只有当or左右查询字段均为索引时,才会生效

  3. 组合索引,不是使用第一列索引,索引失效,即不满足最左匹配原则。

  4. 数据类型出现隐式转化。如varchar不加单引号的话可能会自动转换为int型,使索引无效,产生全表扫描。

  5. 在索引列上使用 IS NULL 或 IS NOT NULL操作。索引是不索引空值的,所以这样的操作不能使用索引,可以用其他的办法处理,例如:数字类型,判断大于0,字符串类型设置一个默认值,判断是否等于默认值即可。

  6. 在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。

  7. 对索引字段进行计算操作、字段上使用函数。(索引为 emp(ename,empno,sal))

  8. 当全表扫描速度比索引速度快时,mysql会使用全表扫描,此时索引失效。

3. 优化目标

根据explian中ref的字段,情况从好到坏顺序是
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL

比较常见的是const eq_ref ref range index all
通常需要将语句优化到index以上。

4. 引申知识

4.1. 回表查询

通常发生在select * 的查询下。在这种情况下,即使我们定义了很好的索引,where子句已经找到了他需要的数据。由于我们要查询*,也就是所有的数据。而这些数据没有存在索引表中,所以我们需要返回到聚集索引根据主键在查找一次才可以获取所需的信息。

应该要注意到,不只是select * 会触发回表操作,只要select的字段没有建立索引就会触发回表操作。
与回表查询相对应的是覆盖索引。

4.2. 覆盖索引

索引覆盖是一种避免回表查询的优化策略。具体的做法就是将要查询的数据作为索引列建立普通索引(可以是单列索引,也可以一个索引语句定义所有要查询的列,即联合索引),这样的话就可以直接返回索引中的的数据,不需要再通过聚集索引去定位行记录,避免了回表的情况发生。

如果一个索引覆盖(包含)了所有需要查询的字段的值,这个索引就是覆盖索引。因为索引中已经包含了要查询的字段的值,因此查询的时候直接返回索引中的字段值就可以了,不需要再到表中查询,避免了对主键索引的二次查询,也就提高了查询的效率。

要注意的是,不是所有类型的索引都可以成为覆盖索引的。因为覆盖索引必须要存储索引的列值,而哈希索引、空间索引和全文索引等都不存储索引列值,索引MySQL只能使用B-Tree索引做覆盖索引。

4.3. 索引下推

索引下推是一种数据库内的自我优化手段。有了索引下推优化,可以在有like条件查询的情况下,减少回表次数。

例如有这样一个语句:

select * from user_table where username like '张%' and age > 10

语句有两种执行可能:

根据(username,age)联合索引查询所有满足名称以“张”开头的索引,然后回表查询出相应的全行数据,然后再筛选出满足年龄小于等于10的用户数据
根据(username,age)联合索引查询所有满足名称以“张”开头的索引,然后直接再筛选出年龄小于等于10的索引,之后再回表查询全行数据。

明显的,第二种方式需要回表查询的全行数据比较少,这就是mysql的索引下推。

4.4. B+ Tree的优势

//TODO:

2020/04/13 16:59 下午 posted in  数据库

从使用fastjson替换springboot框架默认的json解析工具说起

1. 为什么要用fastjson替换jackson

1.在默认情况下我们在的情况下从返回的数据是

json格式但是在{key,value}中key的值当中用默认的Jackson返还回来会忽略大小写而我们要得到得是不忽略大小写得值因此我

们需要用FastJson替代默认得Jackson

2.我们通常现在为了更快捷得创建类,使用lombok插件得@Data注解生成类得getter,setter及构造方法.jackson是不支持json格式

序列化的,但是FastJson是可以做到这一点的
————————————————
版权声明:本文为CSDN博主「Alin_林」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:
https://blog.csdn.net/weixin_44828552/article/details/89511350

2. 如何替换fastjson

常见的替换方法有以下两种

2.1. 方法一

@SpringBootApplication
public class HelloWorld implement ApplicationRunner{
    public static void main(String[] args){
        SpringApplication.run(HelloWorld.class,args);
    }

    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters(){
        //1.定义fastJson转换器
        FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter();
        FastJsonConfig fastJsonConfig=new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerialzerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty);
        fastConverter.setFastJsonConfig(fastJsonConfig);
        HttpMessageConverter<?> converter = fastConverter;
        return new HttpMessageConverters(converter);
    }   
}

2.2. 方法二

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
        FastJsonConfig config = new FastJsonConfig();
        config.setCharset(Charset.forName("UTF-8"));
//        config.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
//        config.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
        fastJsonConverter.setFastJsonConfig(config);
        List<MediaType> list = new ArrayList<>();
        list.add(MediaType.APPLICATION_JSON_UTF8);
        fastJsonConverter.setSupportedMediaTypes(list);
        converters.add(fastJsonConverter);
    }
}

3. 发散分析

3.1. Jackson的新版本

网络上搜集替换jackson的理由,其中一点是jackson不支持忽略key大小写。

该点已经在2.5.0版本中解决。

ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
CarInfo info = objectMapper.readValue(data, CarInfo.class); 

或在配置文件中

spring.jackson.mapper.accept_case_insensitive_properties=true

3.2. 从替换fastjson,看springboot使用json解析器的逻辑

第二节中的两种方法都涉及到同一个类的使用,那就是FastJsonHttpMessageConverterHttpMessageConverter

将FastJsonHttpMessageConverter 添加到系统的HttpMessageConverter列表中,实际操作就是增加了一种json的解析方法,可以解析的media type是application-json,使用的解析器就是FastJsonHttpMessageConverter。

更多阅读

1. 三种json解析工具对比

fastjson这么快老外为啥还是热衷 jackson? https://blog.csdn.net/Amen_Wu/article/details/79129020

FastJSON、Gson和Jackson性能对比和共同缺点,注意事项
https://blog.csdn.net/qq_28572235/article/details/78604846

参考资料

https://mtyurt.net/post/jackson-case-insensitive-deserialization.html
https://blog.csdn.net/weixin_44828552/article/details/89511350

2019/09/24 17:18 下午 posted in  JSON

数据库分库分表技术

1. 基本概念

1.1. 拆分方式

  • 垂直拆分

    将一个库(表)拆分成多个库(表),每个库(表)的结构和原有的结构不同。

    这实际上可以认为是一种数据库的重新设计。拆分的原则可以是:

    1. 根据业务的具体情况,将热门的数据和冷门的数据分开。达到提高性能的目的。(库或者表的拆分都可以遵循这样一条原则)
    
  • 水平拆分

    根据分片算法,将一个库(表)拆分成多个库(表),每个库(表)的结构和原有的结构相同。

Read more   2019/02/13 21:28 下午 posted in  数据库

用注解,自动扩展启动。

2020/03/12 10:46 上午 posted in  Java

线程安全的对象你用对了吗?

HashMap是线程不安全的,如果我们需要线程安全的使用场景,通常会使用ConcurrentHashMap来代替HashMap来保证线程安全。可是这样就够了吗?

我们用Mybatis源码中的一个例子来说明。

BlockingCache.java

private final ConcurrentHashMap<Object, ReentrantLock> locks;

// MyBatis的这个blocking cache保证同一个时间对于一个查询key只有一个线程可以获得锁。锁放在了locks这个ConcurrentHashMap中。每个线程要操作前需要尝试获取锁。以下就是获取锁的核心逻辑。注意这个方法本身是没有加锁保护的。
private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();
    ReentrantLock previous = locks.putIfAbsent(key, lock);
    return previous == null ? lock : previous;
    return locks.computeIfAbsent(key, (k) -> new ReentrantLock());
}

关键是这一句

locks.putIfAbsent(key, lock);

putIfAbsent() 是啥作用呢? 按照字面意思,就是如果key不存在,则set,否则不插入。为啥会用这样一个看起来有点复杂的接口呢?如果我们没有熟读ConcurrentHashMap提供的接口,我们可能会这么写:

private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = locks.get(key); //  从map里读取一下是否key已经存放了
    if (lock == null) { // 不存在,则新建一个锁,加入到map中
      lock = new ReentrantLock();
      locks.put(key, lock);
    }
    return lock;
}

这段代码是我们日常很有可能写出的逻辑,逻辑清楚,代码也还算简洁。那这段代码会有什么问题呢?

一个有点挫的流程图,来解释一下。这段代码没有同步锁的保护,所以可能有多个线程同一时间都进入了这段代码。A线程先执行了1,2步骤,判断了map中没有key,准备创建线程并添加。这时候发生了线程切换,B线程开始执行这段代码。B线程也去判断map中有没有key,显然也是没有的。然后B线程走完了整个逻辑。然后A线程再一次获取到时间片。最终结果,A线程和B线程都往map中插入了他们自己创建的锁。很明显B线程的锁被覆盖了。

回过头来看一下源码中是怎么写的。

locks.putIfAbsent(key, lock);

源码使用了ConncurrentHashMap的一个接口来达到两个目的,1. 查询一下map中是否包含某个key值。2.不存在则插入。而调用这个接口,ConncurrentHashMap保证了两个步骤的原子性,即要么都做要么都不做。

总结

ConncurrentHashMap虽然是线程安全的,但是用不好一样也会有线程不安全的问题。像这种线程安全的类,他们的单个接口是可以保证线程安全的,但是如果调用了多个接口,又没有用锁来保护,则仍然有可能发生线程不安全的问题。

引申一下,这个问题和Redis用lua脚本来保证多个redis命令的线程安全是有类似的地方。Redis执行单个命令是线程安全的。执行多个组合命令则要使用Lua脚本。这是因为执行Lua脚本命令是安全的。因此可以用Lua脚本来执行多个Redis命令来达到对多个Redis命令的原子性操作。

2020/04/24 14:11 下午 posted in  Java