java并发编程

一,同步

  1 synchronized 关键字

    synchronized 锁什么?锁对象。

    可能锁对象包括: this, 临界资源对象(多线程大家都能找到的对象),Class 类对象。

    区别在于所的范围不同:一个类有多个实例(this),但是只有一个class(.class),根据要求级别不同使用不同的锁对象;

    加锁的目的:是为了保证操作的原子性,代码不可分割;  

    1.1同步方法

      synchronized T methodName(){}

      同步方法锁定的是当前对象。当多线程通过同一个对象引用多次调用当前同步方法时,需同步执行。

    1.2同步代码块

      同步代码块的同步粒度更加细致,是商业开发中推荐的编程方式。可以定位到具体的同步位置,而不是简单的将方法整体实现同步逻辑。在效率上,相对更高。

      1.2.1 锁定临界对象

        T methodName(){
        synchronized(object){}
        }
        同步代码块在执行时,是锁定 object 对象。当多个线程调用同一个方法时,锁定对象不变的情况下,需同步执行。

      1.2.2 锁定当前对象

        T methodName(){
        synchronized(this){}
        }

 

      

      

      同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。     

      代码辅助理解:

/**
 * synchronized关键字
 * 同步方法 - 同步方法和非同步方法的调用
 * 同步方法只影响锁定同一个锁对象的同步方法。不影响其他线程调用非同步方法,或调用其他锁资源的同步方法。
 */public class Test_04 {
    Object o = new Object();
    public synchronized void m1(){ 
        System.out.println("public synchronized void m1() start");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("public synchronized void m1() end");
    }
    public void m3(){
        synchronized(o){
            System.out.println("public void m3() start");
            try {
                Thread.sleep(1500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("public void m3() end");
        }
    }    
    public void m2(){
        System.out.println("public void m2() start");
        try {
            Thread.sleep(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("public void m2() end");
    }
   
    public static class MyThread01 implements Runnable{
        public MyThread01(int i, Test_04 t){
            this.i = i;
            this.t = t;
        }
        int i ;
        Test_04 t;
        public void run(){
            if(i == 0){
                t.m1();
            }else if (i > 0){
                t.m2();
            }else {
                t.m3();
            }
        }
    }
    
    public static void main(String[] args) {
        Test_04 t = new Test_04();
        new Thread(new Test_04.MyThread01(0, t)).start();
        new Thread(new Test_04.MyThread01(1, t)).start();
        new Thread(new Test_04.MyThread01(-1, t)).start();
    }
    
}

说明:Test04中有三个方法,m1 m2 m3 1和3加锁,但是不是同一把锁,一个是o一个是this,执行的结果,发现,相互之间没有等待之类的,而是正常执行,
所以方法之间同步不同步,互不影响,锁对象不同,互不影响

        上边代码执行结果

        

      脏读:

        逻辑中访问同一个数据,逻辑还没有执行完,就去读取那个数据,数据是脏数据;

        看代码说话
/**
 * synchronized关键字
 * 同步方法 - 多方法调用原子性问题(业务)
 * 同步方法只能保证当前方法的原子性,不能保证多个业务方法之间的互相访问的原子性。
 * 注意在商业开发中,多方法要求结果访问原子操作,需要多个方法都加锁,且锁定统一个资源。
 * 
 * 一般来说,商业项目中,不考虑业务逻辑上的脏读问题。
 */
package concurrent.t01;

import java.util.concurrent.TimeUnit;

public class Test_05 {
    private double d = 0.0;
    public synchronized void m1(double d){
        try {
            // 相当于复杂的业务逻辑代码。
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.d = d;
    }
    
    public double m2(){
        return this.d;
    }
    
    public static void main(String[] args) {
        final Test_05 t = new Test_05();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1(100);
            }
        }).start();
        System.out.println(t.m2());
        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.m2());
    }
    
}
说明:
  类中两个方法相当于是一个设置值一个读取值,睡眠是模拟现实逻辑执行过程的时间,
  当1给成员属性设置值的时候,和没有完成设置,m2就去读取,当然读取到的是0.0;

      异常时锁处理: 

/**
 * synchronized关键字
 * 同步方法 - 锁与异常
 * 当同步方法中发生异常的时候,自动释放锁资源。不会影响其他线程的执行。
 */
package concurrent.t01;

import java.util.concurrent.TimeUnit;

public class Test_08 {
    int i = 0;
    synchronized void m(){
        System.out.println(Thread.currentThread().getName() + " - start");
        while(true){
            i++;
            System.out.println(Thread.currentThread().getName() + " - " + i);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            if(i == 5){
                i = 1/0;
            }
        }
    }
    
    public static void main(String[] args) {
        final Test_08 t = new Test_08();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        }, "t1").start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        }, "t2").start();
    }
}

        执行结果:两个线程使用的是同一把锁,线程一一直在执行,当遇到异常的时候,抛出异常,同时释放锁资源,线程二获得锁资源,开始执行。

        

    1.3锁的底层实现

      Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。同步方法 并不是由 monitor enter 和 monitor exit 指令来实现同步的,而是由方法调用

      指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的。

      1.3.1 对象内存简图

        

        对象头:存储对象的 hashCode、锁信息或分代年龄或 GC 标志,类型指针指向对象的类
        元数据,JVM 通过这个指针确定该对象是哪个类的实例等信息。
        实例变量:存放类的属性数据信息,包括父类的属性信息
        填充数据:由于虚拟机要求对象起始地址必须是 8 字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐

        当在对象上加锁时,数据是记录在对象头中。当执行 synchronized 同步方法或同步代码块时,会在对象头中记录锁标记,锁标记指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每个

        对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持

        有后,它便处于锁定状态。

        在 Java 虚拟机(HotSpot)中,monitor 是由 ObjectMonitor 实现的。ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList,以及_Owner 标记。其中_WaitSet是用于管理等待队列(wait)线程的,

        _EntryList 是用于管理锁池阻塞线程的,_Owner 标记用于记录当前执行线程。线程状态图如下:

      

        当多线程并发访问同一个同步代码时,首先会进入_EntryList,当线程获取锁标记后,monitor 中的_Owner 记录此线程,并在 monitor 中的计数器执行递增计算(+1),代表锁定,其他线程在

        _EntryList 中继续阻塞。若执行线程调用 wait 方法,则 monitor 中的计数器执行赋值为 0 计算,并将_Owner 标记赋值为 null,代表放弃锁,执行线程进如_WaitSet 中阻塞。若执行线程调用

        notify/notifyAll 方法,_WaitSet 中的线程被唤醒,进入_EntryList 中阻塞,等待获取锁标记。若执行线程的同步代码执行结束,同样会释放锁标记,monitor 中的_Owner标记赋值为 null,且计数器

        赋值为 0 计算。

    1.4锁的种类

      Java 中锁的种类大致分为偏向锁,自旋锁,轻量级锁,重量级锁。

      锁的使用方式为:先提供偏向锁,如果不满足的时候,升级为轻量级锁,再不满足,升级为重量级锁。自旋锁是一个过渡的锁状态,不是一种实际的锁类型。锁只能升级,不能降级。

      1.4.1 重量级锁

        在 1.3 中解释的就是重量级锁。

      1.4.2 偏向锁

        是一种编译解释锁。如果代码中不可能出现多线程并发争抢同一个锁的时候,JVM 编译代码,解释执行的时候,会自动的放弃同步信息。消除 synchronized 的同步代码结果。使用锁标记的形式记录

        锁状态。在 Monitor 中有变量 ACC_SYNCHRONIZED。当变量值使用的时候,代表偏向锁锁定。可以避免锁的争抢和锁池状态的维护。提高效率。

      1.4.3 轻量级锁

        过渡锁。当偏向锁不满足,也就是有多线程并发访问,锁定同一个对象的时候,先提升为轻量级锁。也是使用标记 ACC_SYNCHRONIZED 标记记录的。ACC_UNSYNCHRONIZED 标记记录未获取

        到锁信息的线程。就是只有两个线程争抢锁标记的时候,优先使用轻量级锁。两个线程也可能出现重量级锁。

      1.4.4 自旋锁

        是一个过渡锁,是偏向锁和轻量级锁的过渡。当获取锁的过程中,未获取到。为了提高效率,JVM 自动执行若干次空循环,再次申请锁,而不是进入阻塞状态的情况。称为自旋锁。自旋锁提高效率

        就是避免线程状态的变更。

  2 volatile 关键字

        变量的线程可见性。在 CPU 计算过程中,会将计算过程需要的数据加载到 CPU 计算缓存中,当 CPU 计算中断时,有可能刷新缓存,重新读取内存中的数据。在线程运行的过程中,如果某变量被其

        他线程修改,可能造成数据不一致的情况,从而导致结果错误。而 volatile修饰的变量是线程可见的,当 JVM 解释 volatile 修饰的变量时,会通知 CPU,在计算过程中,每次使用变量参与计算时,都

        会检查内存中的数据是否发生变化,而不是一直使用 CPU 缓存中的数据,可以保证计算结果的正确。volatile 只是通知底层计算时,CPU 检查内存数据,而不是让一个变量在多个线程中同步。

        只保证可见性不保证原子性

/**
 * volatile关键字
 * volatile的可见性
 * 通知OS操作系统底层,在CPU计算过程中,都要检查内存中数据的有效性。保证最新的内存数据被使用。
 * 
 */
package concurrent.t01;

import java.util.concurrent.TimeUnit;

public class Test_09 {
    
    volatile boolean b = true;
    
    void m(){
        System.out.println("start");
        while(b){}
        System.out.println("end");
    }
    
    public static void main(String[] args) {
        final Test_09 t = new Test_09();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m();
            }
        }).start();
        
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        t.b = false;
    } 
}
说明:在没有加volatile关键字的时候因为true会死循环,但是我们的main方法中设置了false,却没有成功,
    是因为,cpu将文件加载到cpu缓存中,就不会再去读内存中的数据,导致我们设置的false没有起作用,那volatile的作用就是让cpu强制的去内存中读取一下,数据;

 

          只保证可见性不保证原子性

/**
 * volatile关键字
 * volatile的非原子性问题
 * volatile, 只能保证可见性,不能保证原子性。
 * 不是枷锁问题,只是内存数据可见。
 */
package concurrent.t01;

import java.util.ArrayList;
import java.util.List;

public class Test_10 {
    
    volatile int count = 0;
    /*synchronized*/ void m(){
        for(int i = 0; i < 10000; i++){
            count++;
        }
    }
    
    public static void main(String[] args) {
        final Test_10 t = new Test_10();
        List<Thread> threads = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            threads.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    t.m();
                }
            }));
        }
        for(Thread thread : threads){
            thread.start();
        }
        for(Thread thread : threads){
            try {
                thread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println(t.count);
    }
}

说明:理论上结果应该是100k结果就不一定了哦。因为没有保证原子性,一个线程执行过程中另一个线程进来捣乱一下,

 

  3 wait&notify

 

  4 AtomicXxx 类型组

      原子类型。
      在 concurrent.atomic 包中定义了若干原子类型,这些类型中的每个方法都是保证了原子操作的。多线程并发访问原子类型对象中的方法,不会出现数据错误。在多线程开发中,如果某数据需要多个线程

      同时操作,且要求计算原子性,可以考虑使用原子类型对象注意:原子类型中的方法是保证了原子操作,但多个方法之间是没有原子性的。

      如:AtomicInteger i = new AtomicInteger(0);     if(i.get() != 5) i.incrementAndGet();在上述代码中,get 方法和 incrementAndGet 方法都是原子操作,但复合使用时,无法保证原子性,仍旧可能出现数据

      错误。

/**
 * AtomicXxx
 * 同步类型
 * 原子操作类型。 其中的每个方法都是原子操作。可以保证线程安全。
 */
package concurrent.t01;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Test_11 {
    AtomicInteger count = new AtomicInteger(0);
    void m(){
        for(int i = 0; i < 10000; i++){
            /*if(count.get() < 1000)*/
                count.incrementAndGet();
        }
    }
    
    public static void main(String[] args) {
        final Test_11 t = new Test_11();
        List<Thread> threads = new ArrayList<>();
        for(int i = 0; i < 10; i++){
            threads.add(new Thread(new Runnable() {
                @Override
                public void run() {
                    t.m();
                }
            }));
        }
        for(Thread thread : threads){
            thread.start();
        }
        for(Thread thread : threads){
            try {
                thread.join();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        System.out.println(t.count.intValue());
    }
}
说明:这个时候,结果就是100k了。

 

  5 CountDownLatch 门闩

      门闩是 concurrent 包中定义的一个类型,是用于多线程通讯的一个辅助类型。门闩相当于在一个门上加多个锁,当线程调用 await 方法时,会检查门闩数量,如果门闩数量大于 0,线程会阻塞等待。当线

      程调用 countDown 时,会递减门闩的数量,当门闩数量为 0 时,await 阻塞线程可执行。

/**
 * 门闩 - CountDownLatch
 * 可以和锁混合使用,或替代锁的功能。
 * 在门闩未完全开放之前等待。当门闩完全开放后执行。
 * 避免锁的效率低下问题。
 */
package concurrent.t01;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class Test_15 {
    CountDownLatch latch = new CountDownLatch(5);
    
    void m1(){
        try {
            latch.await();// 等待门闩开放。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m1() method");
    }
    
    void m2(){
        for(int i = 0; i < 10; i++){
            if(latch.getCount() != 0){
                System.out.println("latch count : " + latch.getCount());
                latch.countDown(); // 减门闩上的锁。
            }
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("m2() method : " + i);
        }
    }
    
    public static void main(String[] args) {
        final Test_15 t = new Test_15();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        }).start();
    }
    
}

 

        

  6 锁的重入

      在 Java 中,同步锁是可以重入的。只有同一线程调用同步方法或执行同步代码块,对同一个对象加锁时才可重入。当线程持有锁时,会在 monitor 的计数器中执行递增计算,若当前线程调用其他同步代

      码,且同步代码的锁对象相同时,monitor 中的计数器继续递增。每个同步代码执行结束,monitor 中的计数器都会递减,直至所有同步代码执行结束,monitor 中的计数器为 0 时,释放锁标记,_Owner

      标记赋值为 null。

/**
 * synchronized关键字
 * 同步方法 - 调用其他同步方法
 * 锁可重入。 同一个线程,多次调用同步代码,锁定同一个锁对象,可重入。
 */
package concurrent.t01;

import java.util.concurrent.TimeUnit;

public class Test_06 {
    
    synchronized void m1(){ // 锁this
        System.out.println("m1 start");
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        m2();
        System.out.println("m1 end");
    }
    synchronized void m2(){ // 锁this
        System.out.println("m2 start");
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("m2 end");
    }
    
    public static void main(String[] args) {
        
        new Test_06().m1();
        
    }
}
--------------------------------------------------------------------------------------------------------------------
/** * synchronized关键字 * 同步方法 - 继承 * 子类同步方法覆盖父类同步方法。可以指定调用父类的同步方法。 * 相当于锁的重入。 */ package concurrent.t01; import java.util.concurrent.TimeUnit; public class Test_07 { synchronized void m(){ System.out.println("Super Class m start"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Super Class m end"); } public static void main(String[] args) { new Sub_Test_07().m(); } } class Sub_Test_07 extends Test_07{ synchronized void m(){ System.out.println("Sub Class m start"); super.m(); System.out.println("Sub Class m end"); } }

 

  7 ReentrantLock

      重入锁,建议应用的同步方式。相对效率比 synchronized 高。量级较轻。synchronized 在 JDK1.5 版本开始,尝试优化。到 JDK1.7 版本后,优化效率已经非常好了。在绝对效率上,不比 reentrantLock

      差多少。使用重入锁,必须必须必须手工释放锁标记。一般都是在 finally 代码块中定义释放锁标记的 unlock 方法。

/**
 * ReentrantLock
 *  重入锁
 */
package concurrent.t03;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test_01 {
    Lock lock = new ReentrantLock();
    
    void m1(){
        try{
            lock.lock(); // 加锁
            for(int i = 0; i < 10; i++){
                TimeUnit.SECONDS.sleep(1);
                System.out.println("m1() method " + i);
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock(); // 解锁
        }
    }
    
    void m2(){
        lock.lock();
        System.out.println("m2() method");
        lock.unlock();
    }
    
    public static void main(String[] args) {
        final Test_01 t = new Test_01();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        }).start();
    }
}

 

    尝试锁:
/**
 * 尝试锁
 */
package concurrent.t03;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test_02 {
    Lock lock = new ReentrantLock();
    
    void m1(){
        try{
            lock.lock();
            for(int i = 0; i < 10; i++){
                TimeUnit.SECONDS.sleep(1);
                System.out.println("m1() method " + i);
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    void m2(){
        boolean isLocked = false;
        try{
            // 尝试锁, 如果有锁,无法获取锁标记,返回false。
            // 如果获取锁标记,返回true
            // isLocked = lock.tryLock();
            
            // 阻塞尝试锁,阻塞参数代表的时长,尝试获取锁标记。
            // 如果超时,不等待。直接返回。
            isLocked = lock.tryLock(5, TimeUnit.SECONDS); 
            
            if(isLocked){
                System.out.println("m2() method synchronized");
            }else{
                System.out.println("m2() method unsynchronized");
            }
        }catch(Exception e){
            e.printStackTrace();
        }finally{
            if(isLocked){
                // 尝试锁在解除锁标记的时候,一定要判断是否获取到锁标记。
                // 如果当前线程没有获取到锁标记,会抛出异常。
                lock.unlock();
            }
        }
    }
    
    public static void main(String[] args) {
        final Test_02 t = new Test_02();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        }).start();
    }
}

 

  

/**
 * 可打断
 * 
 * 阻塞状态: 包括普通阻塞,等待队列,锁池队列。
 * 普通阻塞: sleep(10000), 可以被打断。调用thread.interrupt()方法,可以打断阻塞状态,抛出异常。
 * 等待队列: wait()方法被调用,也是一种阻塞状态,只能由notify唤醒。无法打断
 * 锁池队列: 无法获取锁标记。不是所有的锁池队列都可被打断。
 *  使用ReentrantLock的lock方法,获取锁标记的时候,如果需要阻塞等待锁标记,无法被打断。
 *  使用ReentrantLock的lockInterruptibly方法,获取锁标记的时候,如果需要阻塞等待,可以被打断。
 * 
 */
package concurrent.t03;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test_03 {
    Lock lock = new ReentrantLock();
    
    void m1(){
        try{
            lock.lock();
            for(int i = 0; i < 5; i++){
                TimeUnit.SECONDS.sleep(1);
                System.out.println("m1() method " + i);
            }
        }catch(InterruptedException e){
            e.printStackTrace();
        }finally{
            lock.unlock();
        }
    }
    
    void m2(){
        try{
            lock.lockInterruptibly(); // 可尝试打断,阻塞等待锁。可以被其他的线程打断阻塞状态
            System.out.println("m2() method");
        }catch(InterruptedException e){
            System.out.println("m2() method interrupted");
        }finally{
            try{
                lock.unlock();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    
    public static void main(String[] args) {
        final Test_03 t = new Test_03();
        new Thread(new Runnable() {
            @Override
            public void run() {
                t.m1();
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                t.m2();
            }
        });
        t2.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        t2.interrupt();// 打断线程休眠。非正常结束阻塞状态的线程,都会抛出异常。
    }
}

 

      7.1公平锁(syn是不公平的)

      

/**
 * 公平锁
 */
package concurrent.t03;

import java.util.concurrent.locks.ReentrantLock;

public class Test_04 {
    
    public static void main(String[] args) {
        TestReentrantlock t = new TestReentrantlock();
        //TestSync t = new TestSync();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        t1.start();
        t2.start();
    }
}

class TestReentrantlock extends Thread{
    // 定义一个公平锁
    private static ReentrantLock lock = new ReentrantLock(true);
    public void run(){
        for(int i = 0; i < 5; i++){
            lock.lock();
            try{
                System.out.println(Thread.currentThread().getName() + " get lock");
            }finally{
                lock.unlock();
            }
        }
    }
    
}

class TestSync extends Thread{
    public void run(){
        for(int i = 0; i < 5; i++){
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + " get lock in TestSync");
            }
        }
    }
}

  8 ThreadLocal

      remove 问题

      

二、 同步容器

解决并发情况下的容器线程安全问题的。给多线程环境准备一个线程安全的容器对象。
线程安全的容器对象: Vector, Hashtable。线程安全容器对象,都是使用 synchronized
方法实现的。
concurrent 包中的同步容器,大多数是使用系统底层技术实现的线程安全。类似 native。
Java8 中使用 CAS。
1 Map/Set
1.1ConcurrentHashMap/ConcurrentHashSet
底层哈希实现的同步 Map(Set)。效率高,线程安全。使用系统底层技术实现线程安全。
量级较 synchronized 低。key 和 value 不能为 null。
1.2ConcurrentSkipListMap/ConcurrentSkipListSet
底层跳表(SkipList)实现的同步 Map(Set)。有序,效率比 ConcurrentHashMap 稍低。

实现的。
public ThreadPoolExecutor
(int corePoolSize, // 核心容量,创建线程池的时候,默认有多少线程。也是线程池保持
的最少线程数
int maximumPoolSize, // 最大容量,线程池最多有多少线程
long keepAliveTime, // 生命周期,0 为永久。当线程空闲多久后,自动回收。
TimeUnit unit, // 生命周期单位,为生命周期提供单位,如:秒,毫秒
BlockingQueue<Runnable> workQueue // 任务队列,阻塞队列。注意,泛型必须是
Runnable
);
使用场景: 默认提供的线程池不满足条件时使用。如:初始线程数据 4,最大线程数
200,线程空闲周期 30 秒。

四、 JVM 优化
1 JVM 简单结构图

引擎。应用在服务端程序。(如:tomcat)
Java HotSpot Client 模式和 Server 模式的区别
当虚拟机运行在-client 模式的时候,使用的是一个代号为 C1 的轻量级编译器, 而-server
模式启动的虚拟机采用相对重量级,代号为 C2 的编译器. C2 比 C1 编译器编译的相对彻底,服
务起来之后,性能更高
JDK 安装目录/jre/lib/(x86、i386、amd32、amd64)/jvm.cfg
文件中的内容,-server 和-client 哪一个配置在上,执行引擎就是哪一个。如果是 JDK1.5
版本且是 64 位系统应用时,-client 无效。
--64 位系统内容
-server KNOWN
-client IGNORE
--32 位系统内容
-server KNOWN
-client KNOWN
注意:在部分 JDK1.6 版本和后续的 JDK 版本(64 位系统)中,-client 参数已经不起作用
了,Server 模式成为唯一
2 堆结构及对象分代
2.1什么是分代,分代的必要性是什么
Java 虚拟机根据对象存活的周期不同,把堆内存划分为几块,一般分为新生代、老年
代和永久代(对 HotSpot 虚拟机而言),这就是 JVM 的内存分代策略。
堆内存是虚拟机管理的内存中最大的一块,也是垃圾回收最频繁的一块区域,我们程序
所有的对象实例都存放在堆内存中。给堆内存分代是为了提高对象内存分配和垃圾回收的效
率。试想一下,如果堆内存没有区域划分,所有的新创建的对象和生命周期很长的对象放在
一起,随着程序的执行,堆内存需要频繁进行垃圾收集,而每次回收都要遍历所有的对象,
遍历这些对象所花费的时间代价是巨大的,会严重影响我们的 GC 效率。
有了内存分代,情况就不同了,新创建的对象会在新生代中分配内存,经过多次回收仍
然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象
存活时间短,只需要在新生代区域中频繁进行 GC,老年代中对象生命周期长,内存回收的
频率相对较低,不需要频繁进行回收,永久代中回收效果太差,一般不进行垃圾回收,还可
以根据不同年代的特点采用合适的垃圾收集算法。分代收集大大提升了收集效率,这些都是
内存分代带来的好处。
2.2分代的划分
Java 虚拟机将堆内存划分为新生代、老年代和永久代,永久代是 HotSpot 虚拟机特有的
概念(JDK1.8 之后为 metaspace 替代永久代),它采用永久代的方式来实现方法区,其他的
虚拟机实现没有这一概念,而且 HotSpot 也有取消永久代的趋势,在 JDK 1.7 中 HotSpot 已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、
静态变量等数据,与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
内存简图如下:

言,Java 虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
3 垃圾回收算法及分代垃圾收集器
3.1垃圾收集器的分类
3.1.1 次收集器
Scavenge GC,指发生在新生代的 GC,因为新生代的 Java 对象大多都是朝生夕死,所以
Scavenge GC 非常频繁,一般回收速度也比较快。当 Eden 空间不足以为对象分配内存时,会
触发 Scavenge GC。
一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对
Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理
Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大
部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁
进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
当年轻代堆空间紧张时会被触发
相对于全收集而言,收集间隔较短
3.1.2 全收集器
Full GC,指发生在老年代的 GC,出现了 Full GC 一般会伴随着至少一次的 Minor GC(老
年代的对象大部分是 Scavenge GC 过程中从新生代进入老年代),比如:分配担保失败。Full
GC 的速度一般会比 Scavenge GC 慢 10 倍以上。当老年代内存不足或者显式调用 System.gc()
方法时,会触发 Full GC。
当老年代或者持久代堆空间满了,会触发全收集操作
可以使用 System.gc()方法来显式的启动全收集
全收集一般根据堆大小的不同,需要的时间不尽相同,但一般会比较长。

3.1.3 垃圾回收器的常规匹配

3.2常见垃圾回收算法
3.2.1 引用计数(Reference Counting)
比较古老的回收算法。原理是此对象有一个引用,即增加一个计数,删除一个引用则减
少一个计数。垃圾回收时,只用收集计数为 0 的对象。此算法最致命的是无法处理循环引用
的问题。
3.2.2 复制(Copying)
此算法把内存空间划为两个相等的区域,每次只使用其中一个区域。垃圾回收时,遍历
当前使用区域,把正在使用中的对象复制到另外一个区域中。此算法每次只处理正在使用中
的对象,因此复制成本比较小,同时复制过去以后还能进行相应的内存整理,不会出现“碎
片”问题。当然,此算法的缺点也是很明显的,就是需要两倍内存空间。简图如下:

3.2.3 标记-清除(Mark-Sweep)
此算法执行分两阶段。第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍
历整个堆,把未标记的对象清除。此算法需要暂停整个应用,同时,会产生内存碎片。简图
如下:

3.2.4 标记-整理(Mark-Compact)
此算法结合了“标记-清除”和“复制”两个算法的优点。也是分两阶段,第一阶段从
根节点开始标记所有被引用对象,第二阶段遍历整个堆,把清除未标记对象并且把存活对象
“压缩”到堆的其中一块,按顺序排放。此算法避免了“标记-清除”的碎片问题,同时也
避免了“复制”算法的空间问题。简图如下:

3.3分代垃圾收集器
3.3.1 串行收集器(Serial)
Serial 收集器是 Hotspot 运行在 Client 模式下的默认新生代收集器, 它的特点是:只用一
个 CPU(计算核心)/一条收集线程去完成 GC 工作, 且在进行垃圾收集时必须暂停其他所有
的工作线程(“Stop The World” -后面简称 STW)。可以使用-XX:+UseSerialGC 打开。
虽然是单线程收集, 但它却简单而高效, 在 VM 管理内存不大的情况下(收集几十 M~一
两百 M 的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内。

3.3.2 并行收集器(ParNew)
ParNew 收集器其实是前面 Serial 的多线程版本, 除使用多条线程进行 GC 外, 包括 Serial
可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与 Serial 完全一样(也
是 VM 启用 CMS 收集器-XX: +UseConcMarkSweepGC 的默认新生代收集器)。由于存在线程切换的开销, ParNew 在单 CPU 的环境中比不上 Serial, 且在通过超线程技
术实现的两个 CPU 的环境中也不能 100%保证能超越 Serial. 但随着可用的 CPU 数量的增加,
收集效率肯定也会大大增加(ParNew 收集线程数与 CPU 的数量相同, 因此在 CPU 数量过大的
环境中, 可用-XX:ParallelGCThreads=<N>参数控制 GC 线程数)。

3.3.3 Parallel Scavenge 收集器
与 ParNew 类似, Parallel Scavenge 也是使用复制算法, 也是并行多线程收集器. 但与其
他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge 更关注系统吞吐量:
系统吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞
吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成
程序的运算任务. Parallel Scavenge 提供了如下参数设置系统吞吐量:

3.3.4 Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本, 同样是单线程收集器,使用“标记-整理”算法

3.3.5 Parallel Old 收集器
Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多线程和“标记-整理”算
法, 吞吐量优先, 主要与 Parallel Scavenge 配合在注重吞吐量及 CPU 资源敏感系统内使用;

3.3.6 CMS 收集器(Concurrent Mark Sweep)
CMS(Concurrent Mark Sweep)收集器是一款具有划时代意义的收集器, 一款真正意义上
的并发收集器, 虽然现在已经有了理论意义上表现更好的 G1 收集器, 但现在主流互联网企
业线上选用的仍是 CMS(如 Taobao、微店).
CMS是一种以获取最短回收停顿时间为目标的收集器(CMS又称多并发低暂停的收集器),
基于”标记-清除”算法实现, 整个 GC 过程分为以下 4 个步骤:
1. 初始标记(CMS initial mark)
2. 并发标记(CMS concurrent mark: GC Roots Tracing 过程)
3. 重新标记(CMS remark)
4. 并发清除(CMS concurrent sweep: 已死对象将会就地释放, 注意:此处没有压缩)
其中 1,3 两个步骤(初始标记、重新标记)仍需 STW. 但初始标记仅只标记一下 GC Roots
能直接关联到的对象, 速度很快; 而重新标记则是为了修正并发标记期间因用户程序继续运
行而导致标记产生变动的那一部分对象的标记记录, 虽然一般比初始标记阶段稍长, 但要远
小于并发标记时间.

CMS 特点:
1. CMS 默认启动的回收线程数=(CPU 数目+3)4
当 CPU 数>4 时, GC 线程一般占用不超过 25%的 CPU 资源, 但是当 CPU 数<=4 时, GC 线程
可能就会过多的占用用户 CPU 资源, 从而导致应用程序变慢, 总吞吐量降低.
2.无法处理浮动垃圾, 可能出现 Promotion Failure、Concurrent Mode Failure 而导致另一
次 Full GC 的产生: 浮动垃圾是指在 CMS 并发清理阶段用户线程运行而产生的新垃圾. 由于
在 GC 阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致 CMS
不 能 像 其 他收 集 器那 样 等到 老 年 代几 乎 填满 了 再进 行 收 集. 因此 CMS 提供了
-XX:CMSInitiatingOccupancyFraction 参 数 来 设 置 GC 的 触 发 百 分 比 ( 以 及

-XX:+UseCMSInitiatingOccupancyOnly 来启用该触发百分比), 当老年代的使用空间超过该比例
后 CMS 就会被触发(JDK 1.6 之后默认 92%). 但当 CMS 运行期间预留的内存无法满足程序需
要, 就会出现上述 Promotion Failure 等失败, 这时 VM 将启动后备预案: 临时启用 Serial Old
收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停
顿的时间就是大家都不愿看到的了).
3.最后, 由于 CMS 采用”标记-清除”算法实现, 可能会产生大量内存碎片. 内存碎片过
多 可 能 会 导 致 无 法 分 配 大 对 象 而 提 前 触 发 Full GC. 因 此 CMS 提供了
-XX:+UseCMSCompactAtFullCollection 开关参数, 用于在 Full GC 后再执行一个碎片整理过程.
但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此 CMS
还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction 用于设置在执行 N 次不进行内存整
理的 Full GC 后, 跟着来一次带整理的(默认为 0: 每次进入 Full GC 时都进行碎片整理).

3.3.7 分区收集- G1 收集器
G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗 CPU 的服务
器治理大内存.
- G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector
(CMS).
-XX:+UseG1GC 启用 G1 收集器.
与其他基于分代的收集器不同, G1 将整个 Java 堆划分为多个大小相等的独立区域
(Region), 虽然还保留有新生代和老年代的概念, 但新生代和老年代不再是物理隔离的了,
它们都是一部分 Region(不需要连续)的集合.如:

每块区域既有可能属于 O 区、也有可能是 Y 区, 因此不需要一次就对整个老年代/新生
代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多
很多. 虽然在清理这些区块时 G1 仍然需要暂停应用线程, 但可以用相对较少的时间优先回
收垃圾较多的 Region. 这种方式保证了 G1 可以在有限的时间内获取尽可能高的收集效率.
G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果
存活时间达到阀值, 这部分对象就会被提升到老年代.如图:

其特定是:
一整块堆内存被分为多个 Regions.
存活对象被拷贝到新的 Survivor 区或老年代.
年轻代内存由一组不连续的 heap 区组成, 这种方法使得可以动态调整各代区域尺寸.
Young GC 会有 STW 事件, 进行时所有应用程序线程都会被暂停.
多线程并发 GC.
G1 老年代 GC 特点如下:
并发标记阶段
1 在与应用程序并发执行的过程中会计算活跃度信息.
2 这些活跃度信息标识出那些 regions 最适合在 STW 期间回收(which regions will be best
to reclaim during an evacuation pause).
3 不像 CMS 有清理阶段.
再次标记阶段
1 使用 Snapshot-at-the-Beginning(SATB)算法比 CMS 快得多.
2 空 region 直接被回收.
拷贝/清理阶段(Copying/Cleanup Phase)
1 年轻代与老年代同时回收.
2 老年代内存回收会基于他的活跃度信息.

4 JVM 优化
4.1JDK 常用 JVM 优化相关命令

4.1.1 jps
jps - l
显示线程 id 和执行线程的主类名
jps -v
显示线程 id 和执行线程的主类名和 JVM 配置信息

 

4.1.2 jstat
jstat -参数 线程 id 执行时间(单位毫秒) 执行次数
jstat -gc 4488 30 10

SXC -survivor 初始空间大小,单位字节。
SXU - survivor 使用空间大小, 单位字节。
EC - eden 初始空间大小
EU - eden 使用空间大小
OC - old 初始空间大小
OU - old 使用空间大小
PC - permanent 初始空间大小
PU - permanent 使用空间大小
YGC - youngGC 收集次数
YGCT - youngGC 收集使用时长, 单位秒
FGC - fullGC 收集次数
FGCT - fullGC 收集使用时长
GCT - 总计收集使用总时长 YGCT+FGCT

4.1.3 jvisualvm
一个 JDK 内置的图形化 VM 监视管理工具

4.1.4 visualgc 插件

重启 jvisualvm 工具

4.2JVM 常见参数
配置方式:java [options] MainClass [arguments]
options - JVM 启动参数。 配置多个参数的时候,参数之间使用空格分隔。
参数命名: 常见为 -参数名
参数赋值: 常见为 -参数名=参数值 | -参数名:参数值
4.2.1 内存设置
-Xms:初始堆大小,JVM 启动的时候,给定堆空间大小。
-Xmx:最大堆大小,JVM 运行过程中,如果初始堆空间不足的时候,最大可以扩展到多
少。
-Xmn:设置年轻代大小。整个堆大小=年轻代大小+年老代大小+持久代大小。持久代一
般固定大小为 64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,
Sun 官方推荐配置为整个堆的 3/8。
-Xss: 设置每个线程的 Java 栈大小。JDK5.0 以后每个线程 Java 栈大小为 1M,以前每
个线程堆栈大小为 256K。根据应用的线程所需内存大小进行调整。在相同物理内存下,减
小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限
生成,经验值在 3000~5000 左右。
-XX:NewSize=n:设置年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为 3,表示年轻代与年老代比值为 1:
3,年轻代占整个年轻代+年老代和的 1/4
-XX:SurvivorRatio=n:年轻代中 Eden 区与两个 Survivor 区的比值。注意 Survivor 区有两个。
如:3,表示 Eden:Survivor=3:2,一个 Survivor 区占整个年轻代的 1/5
-XX:MaxPermSize=n:设置持久代大小

XX:MaxTenuringThreshold:设置垃圾最大年龄。如果设置为 0 的话,则年轻代对象不经
过 Survivor 区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设
置为一个较大值,则年轻代对象会在 Survivor 区进行多次复制,这样可以增加对象再年轻代
的存活时间,增加在年轻代即被回收的概率。
4.2.2 内存设置经验分享
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt 还是 64-bit)限制;
系统的可用虚拟内存限制;系统的可用物理内存限制。32 位系统 下,一般限制在 1.5G~2G;
64 为操作系统对内存无限制。
Tomcat 配置方式: 编写 catalina.bat|catalina.sh,增加 JAVA_OPTS 参数设置。windows
和 linux 配置方式不同。windows - set "JAVA_OPTS=%JAVA_OPTS% 自定义参数";linux -
JAVA_OPTS="$JAVA_OPTS 自定义参数"
常见设置:
-Xmx3550m -Xms3550m -Xmn2g -Xss128k 适合开发过程的测试应用。要求物理内存大于
4G。
-Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4
-XX:MaxPermSize=160m -XX:MaxTenuringThreshold=0 适合高并发本地测试使用。且大数据对
象相对较多(如 IO 流)
环境: 16G 物理内存,高并发服务,重量级对象中等(线程池,连接池等),常用对象
比例为 40%(运行过程中产生的对象 40%是生命周期较长的)
-Xmx10G -Xms10G -Xss1M -XX:NewRatio=3 -XX:SurvivorRatio=4 -XX:MaxPermSize=2048m
-XX:MaxTenuringThreshold=5
4.2.3 收集器设置
收集器配置的时候,次收集器和全收集器必须匹配。具体匹配规则参考 3.1.3
-XX:+UseSerialGC:设置串行收集器,年轻带收集器, 次收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParNewGC:设置年轻代为并行收集。可与 CMS 收集同时使用。JDK5.0 以上,JVM
会根据系统配置自行设置,所以无需再设置此值。
-XX:+UseParallelOldGC:设置并行年老代收集器,JDK6.0 支持对年老代并行收集。
-XX:+UseConcMarkSweepGC:设置年老代并发收集器,测试中配置这个以后,-XX:NewRatio
的配置失效,原因不明。所以,此时年轻代大小最好用-Xmn 设置。
-XX:+UseG1GC:设置 G1 收集器
4.2.4 垃圾回收统计信息
类似日志的配置信息。会有控制台相关信息输出。 商业项目上线的时候,不允许使用。
一定使用 loggc
-XX:+PrintGC
-XX:+Printetails
-XX:+PrintGCTimeStamps

Xloggc:filename
4.2.5 并行收集器设置
-XX:ParallelGCThreads=n:设置并行收集器收集时最大线程数使用的 CPU 数。并行收集线
程数。
-XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间,单位毫秒。可以减少 STW 时间。
-XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为 1/(1+n)并发收
集器设置
-XX:+CMSIncrementalMode:设置为增量模式。适用于单 CPU 情况。
-XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应
的 Survivor 区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并
行收集器时,一直打开。
-XX:CMSFullGCsBeforeCompaction=n:由于并发收集器不对内存空间进行压缩、整理,所
以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次 GC 以后对内
存空间进行压缩、整理。
-XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可
以消除碎片
4.2.6 收集器设置经验分享
关于收集器的选择 JVM 给了三种选择:串行收集器、并行收集器、并发收集器,但是
串行收集器只适用于小数据量的情况,所以这里的选择主要针对并行收集器和并发收集器。
默认情况下,JDK5.0 以前都是使用串行收集器,如果想使用其他收集器需要在启动时加入相
应参数。JDK5.0 以后,JVM 会根据当前系统配置进行判断。
常见配置:
并行收集器主要以到达一定的吞吐量为目标,适用于科学计算和后台处理等。
-Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
使用 ParallelGC 作为并行收集器, GC 线程为 20(CPU 核心数>=20 时),内存问题根据
硬件配置具体提供。建议使用物理内存的 80%左右作为 JVM 内存容量。
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20
-XX:+UseParallelOldGC
指定老年代收集器,在JDK5.0之后的版本,ParallelGC对应的全收集器就是ParallelOldGC。
可以忽略
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
指定 GC 时最大暂停时间。单位是毫秒。每次 GC 最长使用 100 毫秒。可以尽可能提高
工作线程的执行资源。
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:MaxGCPauseMillis=100
-XX:+UseAdaptiveSizePolicy

UseAdaptiveSizePolicy 是提高年轻代 GC 效率的配置。次收集器执行效率。
并发收集器主要是保证系统的响应时间,减少垃圾收集时的停顿时间。适用于应用服务
器、电信领域、互联网领域等。
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
指定年轻代收集器为 ParNew,年老代收集器 ConcurrentMarkSweep,并发 GC 线程数为
20(CPU 核心>=20),并发 GC 的线程数建议使用(CPU 核心数+3)/4 或 CPU 核心数【不推
荐使用】。
-Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC
-XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
CMSFullGCsBeforeCompaction=5 执行 5 次 GC 后,运行一次内存的整理。
UseCMSCompactAtFullCollection 执行老年代内存整理。可以避免内存碎片,提高 GC 过
程中的效率,减少停顿时间。
4.2.7 简单总结
年轻代大小选择
响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情
况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对
象。
吞吐量优先的应用:尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要
求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用。
年老代大小选择
响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考
虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可以会造成内存碎片、高回
收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。
最优化的方案,一般需要参考以下数据获得:
并发垃圾收集信息
持久代并发收集次数
传统 GC 信息
花在年轻代和年老代回收上的时间比例
减少年轻代和年老代花费的时间,一般会提高应用的效率
吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老
代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期
存活对象。
较小堆引起的碎片问题,因为年老代的并发收集器使用标记、清除算法,所以不会对堆
进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但
是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的
空间,那么并发收集器将会停止,然后使用传统的标记、整理方式进行回收。如果出现“碎
片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。
-XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次 Full GC
后,对年老代进行压缩

4.2.8 测试代码

 

  

posted @ 2018-10-21 21:09  凤凰山小旋风  阅读(179)  评论(0编辑  收藏  举报