精华内容
下载资源
问答
  • mysql免费版好用么

    2021-01-18 22:07:56
    有朋友在了解过MySQL想要试试下载安装MySQL,但是纠结下载免费版会不会不好用。这里先讲一下MySQL版本代号说明:1. MySQL Community Server 社区版本,开源免费,但不提供官方技术支持。2. MySQL Enterprise ...

    MySQL是最流行的关系型数据库管理系统,在WEB应用方面MySQL是最好的RDBMS应用软件之一。有朋友在了解过MySQL后想要试试下载安装MySQL,但是纠结下载免费版会不会不好用。

    f42ead585767b63b650b23b3f04f5e87.png

    这里先讲一下MySQL版本代号说明:

    1. MySQL Community Server 社区版本,开源免费,但不提供官方技术支持。

    2. MySQL Enterprise Edition 企业版本,需付费,可以试用30天。

    3. MySQL Cluster 集群版,开源免费。可将几个MySQL Server封装成一个Server。

    4. MySQL Cluster CGE 高级集群版,需付费。

    5. MySQL Workbench(GUITOOL)一款专为MySQL设计的ER/数据库建模工具。它是著名的数据库设计工具DBDesigner4的继任者。MySQLWorkbench又分为两个版本,分别是社区版(MySQL Workbench OSS)、商用版(MySQL WorkbenchSE)。

    6、Generally Available(GA)Release,GA是指软件的通用版本,一般指正式发布的版本。

    7、“essentials”是指精简版,不包含 embedded server and benchmarksuite,有自动安装程序和配置向导,没有MySQL文档。

    8、“noinstall”是指非安装的压缩包的。包含 embedded server and benchmarksuite,没有自动安装程序和配置向导,需手动安装配置,有MySQL文档。

    总结:

    简单的说收费的版本集成了很多的服务,从官方手册上就可以看到community版本和企业版本的区别(也就是收费与不收费的区别)。无论安全性和技术支持上企业版都做到了更好的配置。

    但是MySQL免费版足以为你个人学习提供很好的平台。但如果你是想企业内使用的话,还是建议使用企业版本。

    展开全文
  • CentOS7好用么

    千次阅读 2017-09-25 20:46:43
    CentOS7好用么?我们来对比一下就知道了。 服务器操作系统大多采用Unix和Linux操作系统,而Linux发行版本系统中,多使用CentOS、Redhat、Ubuntu、Gentoo、Debian。而这些发行版本可以大体分为两类,一类是商业公司...

    CentOS7好用么?我们来对比一下就知道了。

    服务器操作系统大多采用Unix和Linux操作系统,而Linux发行版本系统中,多使用CentOS、Redhat、Ubuntu、Gentoo、Debian。而这些发行版本可以大体分为两类,一类是商业公司维护的发行版本,一类是社区组织维护的发行版本,前者以著名的Redhat(RHEL)为代表,后者以Debian为代表。

    在选择系统时,我们希望找到一个可靠的,可预测的系统,并且有强大的软件供应商和开源项目中获得强有力的支持。从可靠性、硬件兼容性和生命周期来对比Redhat与Debian:

    l 可靠性

    Redhat,应该称为Redhat系列,包括RHEL(RedhatEnterprise Linux,收费版本)、CentOS(Community ENTerprise Operating System,开源版本),是由红帽公司测试维护,并在Linux内核稳定分支上进行开发,系统相对稳定。Debian系列,包括Debian和Ubuntu 等,是社区类Linux的典范,分为三个版本分支(branch): stable, testing 和 unstable。其中,unstable为最新的测试版本,其中包括最新的软件包,但是也有相对较多的bug,适合桌面用户。Ubuntu是基于Debian的unstable版本加强而来,一个拥有Debian所有的优点,以及自己所加强的优点的近乎完美的 Linux桌面系统,界面非常友好,容易上手,对硬件的支持非常全面,是最适合做桌面系统的Linux发行版本。

    l 硬件兼容性

    RHEL对硬件的支持很好,主流硬件厂商早就将服务器拿过去测试,一般不存在硬件的兼容性问题。对于Debian来说,由于版权和代码纯洁性考虑,一些硬件驱动和软件被删掉了,导致安装过程有问题

    l 生命周期

    CentOS/RHEL的生命周期是7到10年,基本上可以覆盖硬件的生命周期,也就意味着一个新硬件安装以后,不用再次安装操作系统。而Debian的生命周期是不固定的,一般新版本发布以后,上个版本再维护18个月。而Debian的版本发布时间间隔不稳定,经常会延期。综合起来一个版本的生命周期一般在3~4年。如果选用了 Debian 或者Ubuntu作为服务器,等生命周期过了以后,就没有安全补丁,服务器就会有安全风险。

    基于以上对比,在给服务器选择Linux操作系统时,我们会优先考虑Redhat系统的操作系统。

    由于CentOS源于 Red Hat 企业级Linux(RHEL)的源代码,依照开放源代码规定释出的源代码所编译而成。由于CentOS开源特性,选择CentOS可以降低成本,同时又能够享受RHEL的服务支持。

    CentOS7是在CentOS6基础上发布的新版本,与之前的版本相比,主要的更新包括:

    l 内核更新到3.10.0

    l 支持Linux容器

    l LVM快照支持ext4和XFS

    l 转用systemd、firewalld和GRUB2

    l XFS作为缺省文件系统

    l 支持PTPv2

    l 支持40G 以太网卡

    l 在兼容的硬件上支持以UEFI安全启动模式安装

    这其中最令人瞩目的新特性就是支持Docker技术。作为目前流行的应用虚拟化技术之一,Docker能够将应用程序与系统完全隔离,让其在系统之间实现迁移而不需要停机,提高了应用程序的移动性和灵活性。CentOS7在内核层面支持Docker容器技术,可以提高Docker稳定性和可靠性。

    综上,我们会选择CentOS7来作为服务器的操作系统。

     

    展开全文
  • 所以经过一番思考,我们决定使用线程池来执行这 1000 次的任务,因为线程池可以复用线程资源,无需频繁的新建和销毁线程,也可以通过控制线程池中线程的数量来避免过多线程所导致的 CPU 资源过度争抢和线程频繁...

    作者 | 王磊

    来源 | Java中文社群(ID:javacn666)

    转载请联系授权(微信ID:GG_Stone)

    在 Java 中,如果要问哪个类使用简单,但用好最不简单?我想你的脑海中一定会浮现出一次词——“ThreadLocal”。

    确实如此,ThreadLocal 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果稍有不慎还是导致脏数据、内存溢出、共享变量更新等问题,但即便如此,ThreadLocal 依旧有适合自己的使用场景,以及无可取代的价值,比如本文要介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案。

    使用场景1:本地变量

    我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作。

    ① 2个线程格式化

    当有 2 个线程进行时间格式化时,我们可以这样写:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            // 创建并启动线程1
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 得到时间对象
                    Date date = new Date(1 * 1000);
                    // 执行时间格式化
                    formatAndPrint(date);
                }
            });
            t1.start();
            // 创建并启动线程2
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    // 得到时间对象
                    Date date = new Date(2 * 1000);
                    // 执行时间格式化
                    formatAndPrint(date);
                }
            });
            t2.start();
        }
    
        /**
         * 格式化并打印结果
         * @param date 时间对象
         */
        private static void formatAndPrint(Date date) {
            // 格式化时间对象
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
            // 执行格式化
            String result = simpleDateFormat.format(date);
            // 打印最终结果
            System.out.println("时间:" + result);
        }
    }
    

    以上程序的执行结果为:

    上面的代码因为创建的线程数量并不多,所以我们可以给每个线程创建一个私有对象 SimpleDateFormat 来进行时间格式化。

    ② 10个线程格式化

    当线程的数量从 2 个升级为 10 个时,我们可以使用 for 循环来创建多个线程执行时间格式化,具体实现代码如下:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    
    public class Test {
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 10; i++) {
                int finalI = i;
                // 创建线程
                Thread thread = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 得到时间对象
                        Date date = new Date(finalI * 1000);
                        // 执行时间格式化
                        formatAndPrint(date);
                    }
                });
                // 启动线程
                thread.start();
            }
        }
        /**
         * 格式化并打印时间
         * @param date 时间对象
         */
        private static void formatAndPrint(Date date) {
            // 格式化时间对象
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
            // 执行格式化
            String result = simpleDateFormat.format(date);
            // 打印最终结果
            System.out.println("时间:" + result);
        }
    }
    

    以上程序的执行结果为:


    从上述结果可以看出,虽然此时创建的线程数和 SimpleDateFormat 的数量不算少,但程序还是可以正常运行的。

    ③ 1000个线程格式化

    然而当我们将线程的数量从 10 个变成 1000 个的时候,我们就不能单纯的使用 for 循环来创建 1000 个线程的方式来解决问题了,因为这样频繁的新建和销毁线程会造成大量的系统开销和线程过度争抢 CPU 资源的问题。

    所以经过一番思考后,我们决定使用线程池来执行这 1000 次的任务,因为线程池可以复用线程资源,无需频繁的新建和销毁线程,也可以通过控制线程池中线程的数量来避免过多线程所导致的 CPU 资源过度争抢和线程频繁切换所造成的性能问题,而且我们可以将 SimpleDateFormat 提升为全局变量,从而避免每次执行都要新建 SimpleDateFormat 的问题,于是我们写下了这样的代码:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class App {
        // 时间格式化对象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    
        public static void main(String[] args) throws InterruptedException {
            // 创建线程池执行任务
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                    TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
            for (int i = 0; i < 1000; i++) {
                int finalI = i;
                // 执行任务
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        // 得到时间对象
                        Date date = new Date(finalI * 1000);
                        // 执行时间格式化
                        formatAndPrint(date);
                    }
                });
            }
            // 线程池执行完任务之后关闭
            threadPool.shutdown();
        }
    
        /**
         * 格式化并打印时间
         * @param date 时间对象
         */
        private static void formatAndPrint(Date date) {
            // 执行格式化
            String result = simpleDateFormat.format(date);
            // 打印最终结果
            System.out.println("时间:" + result);
        }
    }
    

    以上程序的执行结果为:


    当我们怀着无比喜悦的心情去运行程序的时候,却发现意外发生了,这样写代码竟然会出现线程安全的问题。从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。

    PS:所谓的线程安全问题是指:在多线程的执行中,程序的执行结果与预期结果不相符的情况

    a) 线程安全问题分析

    为了找到问题所在,我们尝试查看 SimpleDateFormatformat 方法的源码来排查一下问题,format 源码如下:

    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                    FieldDelegate delegate) {
        // 注意此行代码
        calendar.setTime(date);
    
        boolean useDateFormatSymbols = useDateFormatSymbols();
    
        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:
                    toAppendTo.append((char)count);
                    break;
    
                case TAG_QUOTE_CHARS:
                    toAppendTo.append(compiledPattern, i, count);
                    i += count;
                    break;
    
                default:
                    subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                    break;
            }
        }
        return toAppendTo;
    }
    

    从上述源码可以看出,在执行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输入的时间进行转换,那么我们想想一下这样的场景:

    1. 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

    2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;

    3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;

    4. 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

    正常的情况下,程序的执行是这样的:

    非线程安全的执行流程是这样的:

    b) 解决线程安全问题:加锁

    当出现线程安全问题时,我们想到的第一解决方案就是加锁,具体的实现代码如下:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class App {
        // 时间格式化对象
        private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    
        public static void main(String[] args) throws InterruptedException {
            // 创建线程池执行任务
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                    TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
            for (int i = 0; i < 1000; i++) {
                int finalI = i;
                // 执行任务
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        // 得到时间对象
                        Date date = new Date(finalI * 1000);
                        // 执行时间格式化
                        formatAndPrint(date);
                    }
                });
            }
            // 线程池执行完任务之后关闭
            threadPool.shutdown();
        }
    
        /**
         * 格式化并打印时间
         * @param date 时间对象
         */
        private static void formatAndPrint(Date date) {
            // 执行格式化
            String result = null;
            // 加锁
            synchronized (App.class) {
                result = simpleDateFormat.format(date);
            }
            // 打印最终结果
            System.out.println("时间:" + result);
        }
    }
    

    以上程序的执行结果为:

    从上述结果可以看出,使用了 synchronized 加锁之后程序就可以正常的执行了。

    加锁的缺点

    加锁的方式虽然可以解决线程安全的问题,但同时也带来了新的问题,当程序加锁之后,所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了

    有没有既能解决线程安全问题,又能提高程序的执行速度的解决方案呢?

    答案是:有的,这个时候 ThreadLocal就要上场了。

    c) 解决线程安全问题:ThreadLocal

    1.ThreadLocal 介绍

    ThreadLocal 从字面的意思来理解是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能使用自己的变量。

    以上面线程池格式化时间为例,当线程池中有 10 个线程时,SimpleDateFormat 会存入 ThreadLocal 中,它也只会创建 10 个对象,即使要执行 1000 次时间格式化任务,依然只会新建 10 个 SimpleDateFormat 对象,每个线程调用自己的 ThreadLocal 变量。

    2.ThreadLocal 基础使用

    ThreadLocal 常用的核心方法有三个:

    1. set 方法:用于设置线程独立变量副本。没有 set 操作的 ThreadLocal 容易引起脏数据。

    2. get 方法:用于获取线程独立变量副本。没有 get 操作的 ThreadLocal 对象没有意义。

    3. remove 方法:用于移除线程独立变量副本。没有 remove 操作容易引起内存泄漏。

    ThreadLocal 所有方法如下图所示:



    官方说明文档:https://docs.oracle.com/javase/8/docs/api/

    ThreadLocal 基础用法如下:

    /**
     * @公众号:Java中文社群
     */
    public class ThreadLocalExample {
        // 创建一个 ThreadLocal 对象
        private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName + " 存入值:" + threadName);
                    // 在 ThreadLocal 中设置值
                    threadLocal.set(threadName);
                    // 执行方法,打印线程中设置的值
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            try {
                // 得到 ThreadLocal 中的值
                String result = threadLocal.get();
                // 打印结果
                System.out.println(threadName + " 取出值:" + result);
            } finally {
                // 移除 ThreadLocal 中的值(防止内存溢出)
                threadLocal.remove();
            }
        }
    }
    

    以上程序的执行结果为:


    从上述结果可以看出,每个线程只会读取到属于自己的 ThreadLocal 值。

    3.ThreadLocal 高级用法

    ① 初始化:initialValue
    public class ThreadLocalByInitExample {
        // 定义 ThreadLocal
        private static ThreadLocal<String> threadLocal = new ThreadLocal(){
            @Override
            protected String initialValue() {
                System.out.println("执行 initialValue() 方法");
                return "默认值";
            }
        };
    
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 执行方法,打印线程中数据(未设置值打印)
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(threadName + " 得到值:" + result);
        }
    }
    

    以上程序的执行结果为:


    当使用了 #threadLocal.set 方法之后,initialValue 方法就不会被执行了,如下代码所示:

    public class ThreadLocalByInitExample {
        // 定义 ThreadLocal
        private static ThreadLocal<String> threadLocal = new ThreadLocal() {
            @Override
            protected String initialValue() {
                System.out.println("执行 initialValue() 方法");
                return "默认值";
            }
        };
    
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    System.out.println(threadName + " 存入值:" + threadName);
                    // 在 ThreadLocal 中设置值
                    threadLocal.set(threadName);
                    // 执行方法,打印线程中设置的值
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            try {
                // 得到 ThreadLocal 中的值
                String result = threadLocal.get();
                // 打印结果
                System.out.println(threadName + "取出值:" + result);
            } finally {
                // 移除 ThreadLocal 中的值(防止内存溢出)
                threadLocal.remove();
            }
        }
    }
    

    以上程序的执行结果为:

    为什么 set 之后,初始化代码就不执行了?

    要理解这个问题,需要从 ThreadLocal.get() 方法的源码中得到答案,因为初始化方法 initialValueThreadLocal 创建时并不会立即执行,而是在调用了 get 方法只会才会执行,测试代码如下:

    import java.util.Date;
    
    public class ThreadLocalByInitExample {
        // 定义 ThreadLocal
        private static ThreadLocal<String> threadLocal = new ThreadLocal() {
            @Override
            protected String initialValue() {
                System.out.println("执行 initialValue() 方法 " + new Date());
                return "默认值";
            }
        };
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    // 得到当前线程名称
                    String threadName = Thread.currentThread().getName();
                    // 执行方法,打印线程中设置的值
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            System.out.println("进入 print() 方法 " + new Date());
            try {
                // 休眠 1s
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(String.format("%s 取得值:%s %s",
                    threadName, result, new Date()));
        }
    }
    

    以上程序的执行结果为:

    从上述打印的时间可以看出:initialValue 方法并不是在 ThreadLocal 创建时执行的,而是在调用 Thread.get 方法时才执行的。

    接下来来看 Threadlocal.get 源码的实现:

    public T get() {
        // 得到当前的线程
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 判断 ThreadLocal 中是否有数据
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                // 有 set 值,直接返回数据
                return result;
            }
        }
        // 执行初始化方法【重点关注】
        return setInitialValue();
    }
    private T setInitialValue() {
        // 执行初始化方法【重点关注】
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    

    从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值 e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue

    注意事项—类型必须保持一致

    注意在使用 initialValue 时,返回值的类型要和 ThreadLoca 定义的数据类型保持一致,如下图所示:

    如果数据不一致就会造成 ClassCaseException 类型转换异常,如下图所示:

    ② 初始化2:withInitial
    import java.util.function.Supplier;
    
    public class ThreadLocalByInitExample {
        // 定义 ThreadLocal
        private static ThreadLocal<String> threadLocal =
                ThreadLocal.withInitial(new Supplier<String>() {
                    @Override
                    public String get() {
                        System.out.println("执行 withInitial() 方法");
                        return "默认值";
                    }
                });
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    // 执行方法,打印线程中设置的值
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(threadName + " 得到值:" + result);
        }
    }
    

    以上程序的执行结果为:


    通过上述的代码发现,withInitial 方法的使用好和 initialValue 好像没啥区别,那为啥还要造出两个类似的方法呢?客官莫着急,继续往下看。

    ③ 更简洁的 withInitial 使用

    withInitial 方法的优势在于可以更简单的实现变量初始化,如下代码所示:

    public class ThreadLocalByInitExample {
        // 定义 ThreadLocal
        private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "默认值");
        public static void main(String[] args) {
            // 线程执行任务
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    String threadName = Thread.currentThread().getName();
                    // 执行方法,打印线程中设置的值
                    print(threadName);
                }
            };
            // 创建并启动线程 1
            new Thread(runnable, "MyThread-1").start();
            // 创建并启动线程 2
            new Thread(runnable, "MyThread-2").start();
        }
    
        /**
         * 打印线程中的 ThreadLocal 值
         * @param threadName 线程名称
         */
        private static void print(String threadName) {
            // 得到 ThreadLocal 中的值
            String result = threadLocal.get();
            // 打印结果
            System.out.println(threadName + " 得到值:" + result);
        }
    }
    

    以上程序的执行结果为:

    4.ThreadLocal 版时间格式化

    了解了 ThreadLocal 的使用之后,我们回到本文的主题,接下来我们将使用 ThreadLocal 来实现 1000 个时间的格式化,具体实现代码如下:

    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.concurrent.LinkedBlockingQueue;
    import java.util.concurrent.ThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class MyThreadLocalByDateFormat {
        // 创建 ThreadLocal 并设置默认值
        private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
                ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
    
        public static void main(String[] args) {
            // 创建线程池执行任务
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,
                    TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));
            // 执行任务
            for (int i = 0; i < 1000; i++) {
                int finalI = i;
                // 执行任务
                threadPool.execute(new Runnable() {
                    @Override
                    public void run() {
                        // 得到时间对象
                        Date date = new Date(finalI * 1000);
                        // 执行时间格式化
                        formatAndPrint(date);
                    }
                });
            }
            // 线程池执行完任务之后关闭
            threadPool.shutdown();
            // 线程池执行完任务之后关闭
            threadPool.shutdown();
        }
        /**
         * 格式化并打印时间
         * @param date 时间对象
         */
        private static void formatAndPrint(Date date) {
            // 执行格式化
            String result = dateFormatThreadLocal.get().format(date);
            // 打印最终结果
            System.out.println("时间:" + result);
        }
    }
    

    以上程序的执行结果为:


    从上述结果可以看出,使用 ThreadLocal 也可以解决线程并发问题,并且避免了代码加锁排队执行的问题。

    使用场景2:跨类传递数据

    除了上面的使用场景之外,我们还可以使用 ThreadLocal 来实现线程中跨类、跨方法的数据传递。比如登录用户的 User 对象信息,我们需要在不同的子系统中多次使用,如果使用传统的方式,我们需要使用方法传参和返回值的方式来传递 User 对象,然而这样就无形中造成了类和类之间,甚至是系统和系统之间的相互耦合了,所以此时我们可以使用 ThreadLocal 来实现 User 对象的传递。

    确定了方案之后,接下来我们来实现具体的业务代码。我们可以先在主线程中构造并初始化一个 User 对象,并将此 User 对象存储在 ThreadLocal 中,存储完成之后,我们就可以在同一个线程的其他类中,如仓储类或订单类中直接获取并使用 User 对象了,具体实现代码如下。

    主线程中的业务代码:

    public class ThreadLocalByUser {
        public static void main(String[] args) {
            // 初始化用户信息
            User user = new User("Java");
            // 将 User 对象存储在 ThreadLocal 中
            UserStorage.setUser(user);
            // 调用订单系统
            OrderSystem orderSystem = new OrderSystem();
            // 添加订单(方法内获取用户信息)
            orderSystem.add();
            // 调用仓储系统
            RepertorySystem repertory = new RepertorySystem();
            // 减库存(方法内获取用户信息)
            repertory.decrement();
        }
    }
    

    User 实体类:

    /**
     * 用户实体类
     */
    class User {
        public User(String name) {
            this.name = name;
        }
        private String name;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
    }
    

    ThreadLocal 操作类:

    /**
     * 用户信息存储类
     */
    class UserStorage {
        // 用户信息
        public static ThreadLocal<User> USER = new ThreadLocal();
    
        /**
         * 存储用户信息
         * @param user 用户数据
         */
        public static void setUser(User user) {
            USER.set(user);
        }
    }
    

    订单类:

    /**
     * 订单类
     */
    class OrderSystem {
        /**
         * 订单添加方法
         */
        public void add() {
            // 得到用户信息
            User user = UserStorage.USER.get();
            // 业务处理代码(忽略)...
            System.out.println(String.format("订单系统收到用户:%s 的请求。",
                    user.getName()));
        }
    }
    

    仓储类:

    /**
     * 仓储类
     */
    class RepertorySystem {
        /**
         * 减库存方法
         */
        public void decrement() {
            // 得到用户信息
            User user = UserStorage.USER.get();
            // 业务处理代码(忽略)...
            System.out.println(String.format("仓储系统收到用户:%s 的请求。",
                    user.getName()));
        }
    }
    

    以上程序的最终执行结果:


    从上述结果可以看出,当我们在主线程中先初始化了 User 对象之后,订单类和仓储类无需进行任何的参数传递也可以正常获得 User 对象了,从而实现了一个线程中,跨类和跨方法的数据传递

    总结

    使用 ThreadLocal 可以创建线程私有变量,所以不会导致线程安全问题,同时使用 ThreadLocal 还可以避免因为引入锁而造成线程排队执行所带来的性能消耗;再者使用 ThreadLocal 还可以实现一个线程内跨类、跨方法的数据传递。

    参考 & 鸣谢

    《码出高效:Java开发手册》

    《Java 并发编程 78 讲》

    
    往期推荐
    

    ThreadLocal中的3个大坑,内存泄露都是小儿科!


    额!Java中用户线程和守护线程区别这么大?


    线程的故事:我的3位母亲成就了优秀的我!


    展开全文
  • 最近前端们一直反映Swagger看接口信息非常不爽,于是我花了俩小时把Swagger干掉,用上了传说中更好用的YApi。今天就简单分享一下心得体会。Swagger与YApi其实我个人认为S...

    最近前端们一直反映Swagger看接口信息非常不爽,于是我花了俩小时把Swagger干掉,用上了传说中更好用的YApi。今天就简单分享一下心得体会。

    Swagger与YApi

    其实我个人认为Swagger也没啥不好的,后端集成起来方便快捷,就是UI不行,而且对于Java来说代码的侵入性太高了。

    swagger界面

    YApi除了解决了这些问题之外还具有权限管理、团队协作、自动化测试、支持OpenApi规范等等。

    YApi界面

    YApi区别于Swagger的最大不同就是YApi需要导入文档(虽然它也可以通过Swagger轮询导入),而Swagger可以自动发现。

    安装这里就不细说了,官方文档说的很清楚。你可以选择命令行部署、可视化部署,也能使用Docker部署。

    推荐内网部署,毕竟大部分API文档比较敏感。

    文档注释

    YApi的文档解析基于Java注释规范,没有代码侵入!但是这就要求我们要按照Javadoc的规范进行书写文档注释,这是使用YApi的前提。一个接口文档分为以下几个部分。

    接口类注释

    接口类的注释,下面是基本的格式。第一行会作为菜单展示,尽量短小精悍;第二行是接口的描述,用来描述接口的作用和细节。

    /**
     * 接口分类名称
     * <p>
     * 接口备注/描述
     *
     * @author felord
     * @since v1.0
     */
    @RestController
    @RequestMapping("/foo")
    public class FooController {
    // 省略
    }
    
    接口类对应的文档

    还有@module@copyright什么的其实可以不写。

    参数注释

    入参和出参的注释,配合JSR-303有奇效哦。

    /**
     * 账户基本信息
     * 
     * @author felord
     * @since v1.0
     */
    @Data
    public class UserInfoDetail {
    
        /**
         * 用户名
         *
         * 配合JSR303注解声明此字段的约束方式【必填】
         */
        @NotBlank
        private String username;
        /**
         * 真实姓名
         */
        private String realName;
        /**
         * 手机号码
         */
        private String phoneNumber;
        /**
         * 性别 -1 未知 0 女性 1 男性
         *
         * 使用@see来说明该字段的取值来源
         * @see GenderType#value()
         */
        private Integer gender;
        /**
         * 昵称
         */
        private String nickName;
        /**
         * 微信号
         * 使用{@code Deprecated} 表示字典将来会废弃
         */
        @Deprecated
        private String wechatAccount;
        /**
         * 电子信箱
         */
        private String email;
    }
    

    接口方法注释

    入参为复杂对象的话注释用@link引用,@RequestBody会指定Content-Typeapplication/json

        /**
         * 新增用户信息
         *
         * @param userInfoDetail 用户信息参数 {@link UserInfoDetail}
         * @return {@link Boolean}
         */
        @PostMapping("/bar")
        public boolean detail(@RequestBody UserInfoDetail userInfoDetail) {
            return true;
        }
    
    Post请求对应的样式

    如果参数是原始类型类型或者String,可以这样写,@RequestParam有奇效。

    /**
     * 获取用户信息
     *
     * @param name 姓名
     * @param age  年龄
     * @return {@link UserInfoDetail}
     */
    @GetMapping("/sam")
    public UserInfoDetail detailBySamples(@RequestParam String name, Integer age) {
        return new UserInfoDetail();
    }
    
    Get请求对应的样式

    导入文档

    YApi支持SwaggerPostmanJSON等方式导入文档。不过我个人更喜欢使用插件导入,Intellij IDEA中推荐使用easy-yapi。导入的时候定位到对应的Controller,使用快捷键Alt+Ins呼出快捷菜单。

    呼出快捷菜单

    选择Export Yapi ,首次选择会让你输入YApi的服务器地址,还会让你输入对应项目的token字符串。

    token 字符串

    依次填入后,就会解析生成文档并同步到YApi服务器了,结果就是上面截图中的样子。然后你可以配置权限分配给组员使用了,如果有文档更新重复上面的步骤即可,YApi提供了几种策略,你可以选择覆盖也可以选择不覆盖。YApi提供了比Swagger更丰富的功能,具体我还在探索中,如果有什么好玩的,会在后面分享给大家,还请多多关注。

    Spring Boot的 Docker打包插件哪个好用

    2021-05-07

    你以为Spring Boot统一异常处理能拦截所有的异常?

    2021-04-30

    展开全文
  • 原标题:华为手环3的心率健康监测真的好用么?我实测了下 近期,华为推出了旗下专为关注健康,以及初级运动爱好者而设计的手环新品:华为手环3系列,较于上一代华为手环系列而言,华为手环3系列不仅外观有了升级,在...
  • 那么好用的php框架有哪些呢?仁者见仁智者见智,每一个php框架并一定适应每一个人,这里为你总结了几款评价不错的php框架,供大家参考。1、SymfonySymfony更多的适用于高级开发者,即创建企业级应用的开发者,尤其是...
  • 拥有功能如下 自动化SQL语句审核,可对SQL进行自动检测并执行 DDL/DML语句执行自动生成回滚语句 审核/查询 审计功能 支持LDAP登录/钉钉及邮件消息推送 支持自定义审核工作流 支持细粒度权限分配 Yearning 目前兼容...
  • 来源:雪球App,作者: 牛魔王炒牛股,(https://xueqiu.com/1540313194/181750166)今天升级了鸿蒙系统,使用起来还真是传说中的顺滑,比之前的Emui好用。目前发现最牛逼的一点,就是打开APP超级快,而且切换APP的...
  • 当前各行业的业务管理类应用系统建设需求旺盛,快速交付需要好用的工具支持。下面给大家介绍几种,供各位参考。 一、应用框架类 (1)Nutz Fast Work 该项目为Java开源企业级快速开发框架、后台管理系统,拥有...
  • MyBatis Plus到底有多好用

    千次阅读 2020-06-05 14:50:27
    userIds.isEmpty(), User::getUserId, userIds) 改进的写法: 全局配置查询策略 mybatis-plus: global-config: db-config: selectStrategy: NOT_EMPTY 扩展条件SQL类型 public class SqlConditionEx { public ...
  • 在选项对话框中勾选上批量构建时要包含的盘符,进行“全部构建”即可批量构建。默认设置下,本地硬盘上的NTFS盘均被包含在内。 构建只是记录下当时文件信息的一个快照,之后的文件变化不会自动更新。当用户觉得...
  • 开发了多次支付宝与微信支付,很自然产生一种反感,惰性又来了,想在网上找相关的轮子,可是一直没有找到一款自己觉得逞心如意的,要么使用起来太难理解,要么文件结构太杂乱,只有自己撸起袖子干了。 使用: ...
  • 缺点:对我来说珀莱雅 (PROYA)晶采奇幻素颜气质霜的缺点是上脸不够滋润,有搓泥现象。还有就是持久性不是很好。 第六款:安安金纯亮肤裸妆素颜霜 优点:这款素颜霜价格美丽,而且效果也不错,水润,提亮肤色,...
  • 强力推荐,Android Kill(最好用的Android apk反编译软件) Android Killer是一款可以对APK进行反编译的工具,它能够对反编译的Smali文件进行修改,并将修改的文件进行打包
  • 2、说一下APP本身就是19年的版本背着确实不舒服,用过的都知道总是容易只记住图记不住单词,今年更新就挺好,图片上有了释义大大改善这一问题。虽然醒目的蓝色开始看着挺不适应,但是适应适应就好了。 3、第一遍...
  • brew(Mac) 为什么有 brew,因为 mac 平台的 appstore 非常的不好用,审核也很严,因此有很多一些大家公认的“正版”好用的软件,都会在 homebrew 发布,例如 openjdk、qq、maven、go 等,它是 Mac OSX 上的软件包...
  • 你也可以不适用工具手动修改,但是比较麻烦,这个项目很方便我们修改系统默认字体,修改重启就能全部生效! 项目地址:https://github.com/Tatsu-syo/noMeiryoUI 下载地址:...
  • Python 之父 Guido van Rossum 用,也发出了「really love」感叹,向大家墙裂推荐了这一高效工具。那么,这一工具到底效果如何?雷锋网 AI 开发者带大家一探究竟~   更智能的代码补全 据官方介绍,本次更新的「...
  • “ 阅读本文大概需要 3 分钟。 ” 写 Python,很多朋友都用的 PyCharm,包括... 以上就是我自己日常使用,觉得最好用的几个工具和插件,今天一次性全部分享给你,希望对你有用。 觉得本文对你有帮助?请分享给更多人
  • 分享一个好用的网络网络抓包分析工具-NetAnalyzer
  • Microsoft OFFICe不好用

    千次阅读 2016-07-27 10:51:16
    标题差点写成office不好用,但其实不打紧,office就好像微软的代言词,windows即office,office即windows,个人内心是支持was的,即使当年用wps写简历,导致打印不了,错过面试机会,但这些年的改进有目共睹,(很大...
  • 好用的软件及网站收录

    千次阅读 2021-01-23 18:15:15
    好用工具官网 http://www.co2fun.com ...https://down.52pojie.cn/ 白嫖免费的图片cdn缓冲 https://www.processon.com 进去新建一个流程图,然后点插入图片,上传,再右键复制图片 地址,去掉url屁股后面那一坨!!
  • } 可以看出,实际上,编译的代码是继承于Enum类的,并且是泛型。用final修饰,其实也是类,那就是不可以被继承原因。而且INSTANCE也是final修饰的,也是不可变的。但是这样看,上面的都是public方法。那构造方法...
  • 使用真正智能的升级 在接受了数百万个样本图像的训练,千兆像素现在可以以惊人的质量放大图像。 获得清晰的和明确的结果。 千兆像素每个像素执行约200万次操作,以尽可能多的细节和清晰度获得您的结果。 没有更多...
  • 可以说MVP模式的出现主要是将MVC中Control控制层进行解耦,View-视图层中需要展示数据则通过Present-业务层调用Model-模型层去获取,Present-业务层拿到数据以接口形式回传给View-视图层,View-视图层只需要注册...
  • IDEA最好用的插件推荐,吐血整理!

    千次阅读 多人点赞 2020-02-02 09:18:00
    开发中,我们通常会调用其它已经编写好的函数,调用需要填充参数,但是绝大多数情况下,传入的变量名称和该函数的参数名一致,当参数较多时,手动单个填充参数非常浪费时间。 该插件就可以帮你解决这个问题。 安装...
  • 一直想找一个好用一点的前端开发工具,网上说的有很多,不过作为颜值党来说,还是进了这个坑。。。。 二、资源 安装包链接:https://download.csdn.net/download/kris001001/10435638 下载点击安装就可,黑箱...
  • 学习了VIM,我也在原有基础上获得了新的框架。 启用VSCode插件前,我花了大约两周的时间轻而易举地通过了vimtutor。此后又花了一周时间进行开发,通过肌肉记忆来自发使用快捷键浏览代码。那时我正使用任一方式进行...
  • 说到文件同步工具,相比大家知道的应该也不少,比如Dropbox,Nextcloud,Seafile等,但是今天小西要给大家安利的一款知名度不高,但是非常好用的一款:FileYee。 一、软件说明 FileYee是国内苏州图吖网络科技...
  • Seafile和Nextcloud相比较哪个好用

    千次阅读 2019-10-07 18:18:26
    环境部署好,程序的安装比较简单,配置好数据库就行。 总体上,Seafile部署相对容易一点,并且耗时较短,半个小时以内部署完一般没有问题。Nextcloud部署比较费时,光PHP环境就费时不少。 四、性能 这个也是...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,536
精华内容 9,814
关键字:

后好用么