Java微服务架构师_Java 并发编程_005

博客围绕并发编程展开,聚焦Java多线程中SimpleDateFormat的线程安全问题。分析了该问题产生的原因,即Calendar对象线程不安全,多线程调用parse方法时非原子性操作导致错误。还给出解决方案,先使用synchronized同步,后进一步优化采用ThreadLocal消除锁竞争。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

并发编程有必要先了解 目前业务场景常见的问题:
问题1: 线程安全是核心问题

线程编程中的常见线程安全问题: SimpleDateFormat 线程安全问题复现:

在这里插入图片描述

在这里插入图片描述
内部类图结构:
在这里插入图片描述

每个SimpleDataFormat实例里面都有一个Calendar对象,通过后面源码分析,会发现SimpleDataFormat之所以线程不安全,就是因为Calender是线程不安全的,后者之所以是线程不安全的,是因为其中存放的日期数据的变量都是线程不安全的,比如fields、time等

代码分析:
在这里插入图片描述
点击进入parse:

@Override
    public Date parse(String text, ParsePosition pos)
    {
        checkNegativeNumberExpression();

        int start = pos.index;
        int oldStart = start;
        int textLength = text.length();

        boolean[] ambiguousYear = {false};

        CalendarBuilder calb = new CalendarBuilder();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                if (start >= textLength || text.charAt(start) != (char)count) {
                    pos.index = oldStart;
                    pos.errorIndex = start;
                    return null;
                }
                start++;
                break;

            case TAG_QUOTE_CHARS:
                while (count-- > 0) {
                    if (start >= textLength || text.charAt(start) != compiledPattern[i++]) {
                        pos.index = oldStart;
                        pos.errorIndex = start;
                        return null;
                    }
                    start++;
                }
                break;

            default:
                // Peek the next pattern to determine if we need to
                // obey the number of pattern letters for
                // parsing. It's required when parsing contiguous
                // digit text (e.g., "20010704") with a pattern which
                // has no delimiters between fields, like "yyyyMMdd".
                boolean obeyCount = false;

                // In Arabic, a minus sign for a negative number is put after
                // the number. Even in another locale, a minus sign can be
                // put after a number using DateFormat.setNumberFormat().
                // If both the minus sign and the field-delimiter are '-',
                // subParse() needs to determine whether a '-' after a number
                // in the given text is a delimiter or is a minus sign for the
                // preceding number. We give subParse() a clue based on the
                // information in compiledPattern.
                boolean useFollowingMinusSignAsDelimiter = false;

                if (i < compiledPattern.length) {
                    int nextTag = compiledPattern[i] >>> 8;
                    if (!(nextTag == TAG_QUOTE_ASCII_CHAR ||
                          nextTag == TAG_QUOTE_CHARS)) {
                        obeyCount = true;
                    }

                    if (hasFollowingMinusSign &&
                        (nextTag == TAG_QUOTE_ASCII_CHAR ||
                         nextTag == TAG_QUOTE_CHARS)) {
                        int c;
                        if (nextTag == TAG_QUOTE_ASCII_CHAR) {
                            c = compiledPattern[i] & 0xff;
                        } else {
                            c = compiledPattern[i+1];
                        }

                        if (c == minusSign) {
                            useFollowingMinusSignAsDelimiter = true;
                        }
                    }
                }
                start = subParse(text, start, tag, count, obeyCount,
                                 ambiguousYear, pos,
                                 useFollowingMinusSignAsDelimiter, calb);
                if (start < 0) {
                    pos.index = oldStart;
                    return null;
                }
            }
        }

        // At this point the fields of Calendar have been set.  Calendar
        // will fill in default values for missing fields when the time
        // is computed.

        pos.index = start;

        Date parsedDate;
        try {
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

解析日期字符串并把解析好的数据放入CalendarBuilder的实例calb中。CalendarBuilder是一个建造者模式,用来存放后面需要的日期数据。
在这里插入图片描述
使用calb中解析好的日期数据设置calendar
在这里插入图片描述
calb.establish的代码如下:

Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                            && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }

// (1)重置日期对象cal的属性值
        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // (2) 使用calb中的属性设置calb
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        // (3)返回设置好的cal对象。
        return cal;
    }

从以上代码可以看出 (1)(2)(3)并不是原子性操作,当多个线程调用parse方法时,比如线程A执行代码(1)和代码(2),也就是设置好了cal对象,但在执行代码(3)之前,线程B执行了代码(3),清空了cal对象,由于多个线程使用的是一个cal对象,所以线程A执行代码(3)返回的可能就是被线程B清空的对象,当然也有可能线程B执行了代码(2),设置了之前被线程A修改的cal对象,从而导致线程A程序出现错误。

解决方案:
出现线程安全的原因是因为多线程执行时代码(1)(2)(3)三个步骤不是一个原子性操作,那么容易想到的是对这个步骤进行同步处理,使这3步骤成为原子性操作,可以使用synchronized进行同步

/**
 * SimpleDateFormat
 * 如何解决线程安全问题
 */

public class SolutionSimpleDateFormat {
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i=0;i<10; i++){
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (sdf){
                        try {
                            System.out.println(sdf.parse("2018-12-13 15:17:27"));
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    }
                }
            });
            thread.start();
        }
    }
}

多线程同步意味着多个线程要进行竞争锁,在高并发场景下会导致系统响应性能下降。 所以我们可以再进一步优化,采用ThreadLocal

这样每个线程只需要使用一个SimpleDateFormat实例,消除了锁竞争的性能问题

参考代码如下:

package com.gary.javastack.concurrent.aqs.safe;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * SimpleDateFormat
 * 如何解决线程安全问题
 */

public class SolutionSimpleDateFormat2 {
    static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };


    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {

                    try {
                        System.out.println(sdf.get().parse("2018-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    } finally {
                        // 使用完毕记得清除,避免内存泄露
                        sdf.remove();
                    }

                }
            });
            thread.start();
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值