线程与线程池,实例比较。,线程池实例比较

线程池:

图片 1

int count = 200000;
        long startTime = System.currentTimeMillis();
        final List<Integer> l = new LinkedList<Integer>();
        ThreadPoolExecutor tp = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(count));
        final Random random = new Random();
        for (int i = 0; i < count; i++) {
            tp.execute(new Runnable() {

                @Override
                public void run() {
                    l.add(random.nextInt());
                }
            });
        }
        tp.shutdown();
        try {
            tp.awaitTermination(1, TimeUnit.DAYS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis() - startTime);
        System.out.println(l.size());

图片 2

输出结果:

1 172
2 200000

线程:

图片 3

int count = 200000;
        long startTime = System.currentTimeMillis();
        final List<Integer> l = new LinkedList<Integer>();
        final Random random = new Random();
        for (int i = 0; i < count; i++) {
            Thread thread = new Thread(){
                @Override
                public void run(){
                    l.add(random.nextInt());
                }
            };
            thread.start();
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(System.currentTimeMillis() - startTime);
        System.out.println(l.size());

图片 4

输出结果:

1 33556
2 200000

总结:差异在于线程池是复用线程的,而不使用线程池是每次都要去创建线程。线程中执行工作很简单,创建线程的开销占整个时间的比例较大。

复制去Google翻译翻译结果  

线程池:
int count = 200000 ; long startTime = System.currentTimeMillis(); final
ListInteger l = new LinkedListInte…

java虚拟机在运行时会把它管理的内存分为几个不同的数据区域,基本上可以分成两个部分,一个是由所有线程共享的数据区域,另外一个数据区域就是线程自身的数据区域。其中,共享的数据区域包括方法区和堆,线程自身的数据区域有程序计数器、虚拟机栈和本地方法栈。

3.9 创建线程以及线程池时候要指定与业务相关的名字,以便于追溯问题

日常开发中当一个应用中需要创建多个线程或者线程池时候最好给每个线程或者线程池根据业务类型设置具体的名字,以便在出现问题时候方便进行定位,下面就通过实例来说明不设置时候为何难以定位问题,以及如何进行设置。

图片 5image.png

3.9.1创建线程需要带线程名

下面通过简单的代码来说明不指定线程名称为何难定位问题,代码如下:

 public static void main(String[] args) {
       //订单模块
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存订单的线程");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                throw new NullPointerException();
            }
        });
     //发货模块
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存收获地址的线程");
            }
        });

        threadOne.start();
        threadTwo.start();

    }

 

如上代码分别创建了线程one和线程two并且启动执行运行上面代码可能会输出如下:

 

图片 6

image.png

从运行接口可知Thread-0抛出了NPE异常,那么单看这个日志根本无法判断是订单模块的线程抛出的异常,首先我们分析下这个Thread-0是怎么来的,这要看下创建线程时候的代码:

 public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null);
    }

 

可知如果调用了没有指定线程名字的方法创建了线程,内部会使用"Thread-" + nextThreadNum()作为线程的默认名字,其中nextThreadNum代码如下:

  private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

 

可知threadInitNumber是static变量,nextThreadNum是static方法,所以线程的编号是全应用唯一的并且是递增的,另外这里由于涉及到了多线程递增threadInitNumber也就是执行读取-递增-写入操作,而这个是线程不安全的所以使用了方法级别的synchronized进行同步。

当一个系统中有多个业务模块而每个模块中有都是用了自己的线程,除非抛出与业务相关的异常,否者比如上面抛出的NPE异常,根本没法判断是哪一个模块出现了问题,现在修改代码如下:

  static final String THREAD_SAVE_ORDER = "THREAD_SAVE_ORDER";
    static final String THREAD_SAVE_ADDR = "THREAD_SAVE_ADDR";

    public static void main(String[] args) {
        // 订单模块
        Thread threadOne = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存订单的线程");
                throw new NullPointerException();
            }
        }, THREAD_SAVE_ORDER);
        // 发货模块
        Thread threadTwo = new Thread(new Runnable() {
            public void run() {
                System.out.println("保存收货地址的线程");
            }
        }, THREAD_SAVE_ADDR);

        threadOne.start();
        threadTwo.start();

    }

 

如上代码在创建线程的时候给线程指定了一个与具体业务模块相关的名字,下面运行结果输出为:

 

图片 7

image.png

从运行结果就可以定位到是保存订单模块抛出了NPE异常,一下子就可以定位到问题。

程序计数器

程序计数器可以理解为当前线程执行字节码的指示器,即可以记录下一条要执行的字节码,此内存区域是唯一一个在java虚拟机规范中没有规定任何OutOfMemoryError的区域。

3.9.2创建线程池时候也需要指定线程池的名称

同理下面通过简单的代码来说明不指定线程池名称为何难定位问题,代码如下:

static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());

    public static void main(String[] args) {

        //接受用户链接模块
        executorOne.execute(new  Runnable() {
            public void run() {
                System.out.println("接受用户链接线程");
                throw new NullPointerException();
            }
        });
        //具体处理用户请求模块
        executorTwo.execute(new  Runnable() {
            public void run() {
                System.out.println("具体处理业务请求线程");
            }
        });

        executorOne.shutdown();
        executorTwo.shutdown();
    }

 

运行代码输出如下结果:

图片 8

同理我们并不知道是那个模块的线程池抛出了这个异常,那么我们看下这个pool-1-thread-1是如何来的。其实是使用了线程池默认的ThreadFactory,翻看线程池创建的源码如下:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }

   public static ThreadFactory defaultThreadFactory() {
   return new DefaultThreadFactory();
    }


 static class DefaultThreadFactory implements ThreadFactory {
        //(1)
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        //(2)
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        //(3)
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
           //(4)
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

 

如上代码DefaultThreadFactory的实现可知:

  • 代码(1)poolNumber是static的原子变量用来记录当前线程池的编号是应用级别的,所有线程池公用一个,比如创建第一个线程池时候线程池编号为1,创建第二个线程池时候线程池的编号为2,这里pool-1-thread-1里面的pool-1中的1就是这个值
  • 代码(2)threadNumber是线程池级别的,每个线程池有一个该变量用来记录该线程池中线程的编号,这里pool-1-thread-1里面的thread-1中的1就是这个值
  • 代码(3)namePrefix是线程池中线程的前缀,默认固定为pool
  • 代码(4)具体创建线程,可知线程的名称使用namePrefix + threadNumber.getAndIncrement()拼接的。

从上知道我们只需对实现ThreadFactory并对DefaultThreadFactory的代码中namePrefix的初始化做手脚,当需要创建线程池是传入与业务相关的namePrefix名称就可以了,代码如下:

 // 命名线程工厂
    static class NamedThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        NamedThreadFactory(String name) {

            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
            if (null == name || name.isEmpty()) {
                name = "pool";
            }

            namePrefix = name + "-" + poolNumber.getAndIncrement() + "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

 

然后创建线程池时候如下:

   static ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-ACCEPT-POOL"));
    static ThreadPoolExecutor executorTwo = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(), new NamedThreadFactory("ASYN-PROCESS-POOL"));

 

然后运行执行结果如下:

图片 9

image.png

ASYN-ACCEPT-POOL-1-thread-1就可以知道是接受链接线程池抛出的异常。

虚拟机栈

java虚拟机栈是用于存储局部变量表、操作数栈、动态链接、方法出口等,这里的局部变量表存储了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用类型和returnAddress类型。这里的long和double类型的数据占用了2个局部变量空间,其余的数据类型只占用1个。在这个区域,会出现两种异常情况:一是StackOverFlowError,这是当线程请求的栈深度大于虚拟机所允许的深度所抛出的异常,二是OutOfMemoryError,这是当无法申请到足够的内存时会抛出的异常。

3.9.3总结

本节通过简单的例子介绍了为何不给线程或者线程池起名字会给问题排查带来麻烦,然后通过源码原理介绍线程和线程池名称是默认名称是如何来的,以及如何自定义线程池名称,以便问题追溯。

本地方法栈

本地方法栈和虚拟机栈是类似的,区别在于虚拟机栈是为虚拟机执行java方法服务的,本地方法栈是为native方法服务的,对于sun
hotspot虚拟机,就直接把本地方法栈和虚拟机栈合并到一起。

java堆

在虚拟机规范中,所有的对象实例以及数组都是在堆上分配的,但是现在随着jit编译期的发展以及逃逸分析技术的成熟,这个规定也不是绝对的了。java堆是垃圾收集器主要的收集区域,也就是我们平常gc的主要收集区域。

方法区

方法区一般用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。如果是HotSpot虚拟机,会把方法区称为“永生代”,但是像HotSpot这种做法,就更容易导致内存溢出的问题。在JDK1.7中,就把原本放在永生代的字符串常量池移出了。

运行时常量池

运行时常量池是方法区的一部分,运行时常量池对于Class文件常量池的一个重要特征是具备动态性,即常量不一定在编译期就产生,在运行期间也可以将新的常量放入池中,例如String类的intern()方法。

对于java基本类型(boolean、byte、char、short、int、float、long、double),应该使用“==”来比较,比较的是他们的值。对于复合类型,使用“==”比较的是它们的在内存中的存放地址,使用equals方法是比较对象在堆内存的地址,但在一些诸如String、Integer、Date类中把Object中的这个方法覆盖了,作用被覆盖为比较内容是否相同。由于jdk1.7已经把常量池从方法区移除来了,这里只总结jdk1.7之后的区别。(也会加上jdk6的比较,但是笔者没有去验证,仅引用其他人的结论)在jdk6,字符串常量池是在方法区,也就是说,对于字符串常量,是在方法区分配的内存,而在jdk7之后的版本,是在java堆分配的内存,跟对象实例是在同一个地方分配的内存。还有要记住一点,通过双引号定义的字符串是直接在字符串常量池产生的,而通过new
Stirng()方法产生的字符串是在java堆分配空间的。

利用“==”比较双引号定义的String类型 /** * "=="对于复合类型是比较地址 */ public void method1(){ String s1 = "hello"; String s2 = "hello"; System.out.println; }结果:true,因为s1和s2引用的是同一个字符串,地址是一样的利用“equals”比较双引号定义的String类型 /** * "equals"本来是比较地址,但是Sting重写了,变成比较值了 */ public void method2(){ String s1 = "hello"; String s2 = "hello"; System.out.println(s1.equals; }结果:true,equals比较的是值,固两者是一样的。双引号定义的字符串和new String()字符串的比较 public void method3(){ String s1 = "hello"; String s2 = new String; System.out.println; System.out.println(s1.equals; }结果:false和true,我们重点来看这种情况。1、对于第一句话,是先在栈中创建了一个String类型的对象引用变量s1,然后检查常量池是否有“hello”这个字符串了,如果有,就让s1指向“hello”,如果没有,就先在常量池创建“hello”字符串,然后让s1指向“hello”。2、对于第二句话,也是先在栈中创建了一个String类型的对象引用变量s2,然后在堆内存中创建一个对象(new string),同时,如果字符串常量池没有该字符串,也会在常量池生成一个“hello”字符串。在这里说明一下,对于jdk7,因为常量池已经在堆内存了,所以常量池不一定会放字符串本身,也可能是一个引用,引用堆内存里面的字符串,这点,在下面的intern()方法会着重说明。

上面第三点所分配的内存示意图

图片 10image.png首先,先在栈创建了一个String类型的对象引用变量s1,然后检查常量池是否有“hello”这个字符串了,现在没有,然后就在常量池创建“hello”;接着,在栈创建了一个String类型的对象引用变量s2,在堆内存中创建一个对象,该对象存的就是“hello”字符串,所以s1和s2的地址是不一样的,但是值是一样的,使用“==”来判断地址的时候,就会输出false,而使用equlas来判断值的时候,就会输出true。

首先,我们先要知道intern()方法的作用,intern()方法的目的在于复用字符串对象以节省内存,我们可以简单地理解为,对一个字符串使用intern()方法的时候,它会去字符串常量池查找该字符串是否已经存在,如果已经存在,就会直接返回常量池中的该字符串。

图片 11image.png我们可以直接看Stirng.intern()方法的api文档的解释,上图红框部分的意思是,当调用intern()方法的时候,如果常量池里已经包含一个字符串,并且该字符串等于调用intern()方法的字符串对象(两者是通过equals方法来判断是否相等的),就返回常量池中的字符串,否则,就把该字符串对象加入到常量池中,并且返回该字符串对象的引用,因此,对于两个字符串s和t,s.intern()
== t.intern()当且仅当s.equals为真

jdk6的intern()方法详解

String s = new String; String s2 = "1"; s.intern(); System.out.println; String s3 = new String + new String; String s4 = "11"; s3.intern(); System.out.println; 

在jdk6中,上面的结果都是false,解释如下:先看前面四句话:s在堆内存创建了一个“1”对象,然后在字符串常量池也生成了一个“1”;s2发现字符串常量池已经有“1”了,然后就会直接指向“1”;s.intern()方法,会使得s的object对象指向常量池中的“1”所以,s和s2的地址是不一样的,所以第一个输出false。再看后四句话:s3先在堆内存创建一个“1”对象,然后发现在字符串常量池已经有“1”了,就不会再次生成“1”,此时常量池是没有“11”字符串的。s4会在字符串常量池生成了一个“11”,然后让s4指向“11”s3.intern()方法,会使得s3的object对象指向常量池中的“11”所以,s3和s4的地址是也不一样的,所以第二个输出false。

图片 12image.png

如果我们把intern()方法跟前面的代码调换,在jdk6中,返回的结果是一样的,都是false,这个就不分析了,这里跟前面不同的一点在于,调用s3.intern()方法时,由于常量池没有“11”字符串,就会在常量池创建“11”字符串。

String s = new String; s.intern(); String s2 = "1"; System.out.println; String s3 = new String + new String; s3.intern(); String s4 = "11"; System.out.println; 

结论:这里无论调用还是不调用intern()方法,结果都是false,因为地址都是不一样的,但是在jdk7就不一样了。

jdk7的intern()方法详解

String s = new String; String s2 = "1"; s.intern(); System.out.println; String s3 = new String + new String; String s4 = "11"; s3.intern(); System.out.println; 

在jdk7中,上面的情况也是返回两个false。从图片可以看出来,跟jdk6的情况基本上是一样的,没什么区别,这里就不注重解释,关键是下面把intern()方法跟上面代码调换一行的情况,就大大不同了。

图片 13image.png

如果我们把intern()方法跟前面的代码调换,在jdk7中,返回的结果就不一样了。

String s = new String; s.intern(); String s2 = "1"; System.out.println; String s3 = new String + new String; s3.intern(); String s4 = "11"; System.out.println; 

此时,第一个会输出false,但是,第二个会输出true,为什么呢,我们从下图来看看解释。第一个输出跟前面一样,没什么区别,我们重点看看第二个输出true的情况:s3先在堆内存创建一个“1”对象,然后发现在字符串常量池已经有“1”了,就不会再次生成“1”调用s3.intern()方法,如果是jdk6,就会在字符串常量池创建一个“11”字符串了,但是,对于jdk7,由于常量池就在堆内存,此时可以直接使用堆内存里已经存在的“11”字符串,就是s3的object对象,也就是说,字符串常量池不会创建“11”字符串,它只会创建一个引用,该引用就是s3的object对象。s4也是直接指向了该引用,不会在常量池创建“11”字符串。所以,s3和s4的地址是一样的,所以输出true。

图片 14image.png结论:在jdk7中,字符串常量池已经不需要时刻保存一份字符串了,相反的,它可以保存一份引用,该引用指向堆内存的一个字符串对象即可。

对于java内存方面的知识就介绍到这里了,笔者对java内存的理解不深,上面所说如果有错误,欢迎指出。

参考资料

Author

发表评论

电子邮件地址不会被公开。 必填项已用*标注