Markdown和代码下载地址,整理不易,欢迎star、fork
volatile是Java提供的轻量级的同步机制,主要有三个特性:
- 保证内存可见性
- 不保证原子性
- 禁止指令重排序
当某个线程在自己的工作内存中将主内存中共享数据的副本 修改并刷新到主内存后,其它线程能够立即
感知到该共享数据发生变化:
/**
* Volatile关键字:【内存可见性】
* - 使用语句(1),注释语句(2) -> 程序一直等待
* - 使用语句(2),注释语句(1) -> 程序正常结束
*
* @author sherman
*/
public class VolatileMemoryVisibility {
//private int i; // (1)
private volatile int i; // (2)
public void changeI() {
this.i = 100;
}
public static void main(String[] args) {
VolatileMemoryVisibility vd = new VolatileMemoryVisibility();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
vd.changeI();
System.out.println("子线程改变了i的值!");
}).start();
while (vd.i == 0) {
}
System.out.println("main线程感知到i的变化!");
}
}
不保证原子性正是volatile轻量级的体现,多个线程对volatile修饰的变量进行操作时,会出现容易
出现写覆盖的情况(i++):
/**
* Volatile关键字:【内存可见性】
* - 使用语句(1),注释语句(2) -> 程序一直等待
* - 使用语句(2),注释语句(1) -> 程序正常结束
*
* @author sherman
*/
public class VolatileMemoryVisibility {
//private int i; // (1)
private volatile int i; // (2)
public void changeI() {
this.i = 100;
}
public static void main(String[] args) {
VolatileMemoryVisibility vd = new VolatileMemoryVisibility();
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
vd.changeI();
System.out.println("子线程改变了i的值!");
}).start();
while (vd.i == 0) {
}
System.out.println("main线程感知到i的变化!");
}
}
计算机执行程序为了提高性能,编译器和处理器常常会进行指令重排。源代码 -> 编译器优化重排 -> 指令并行重排 -> 内存系统重排 -> 最终
执行指令。单线程环境下程序最终执行结果和执行顺序结果一致,多线程环境下线程交替执行,由于编译器优化重排的存在,两个线程使用的变量
一致性无法保证。处理器在进行指令重排的时候必须考虑指令之间的数据依赖性。volatile能够实现禁止指令重排的底层原理:
/**
* Volatile关键字:【禁止指令重排序】
* 高并发环境下DCL(Double Check Lock)单例模式使用volatile防止指令重排序
*
* @author sherman
*/
public class VolatileForbidRearrange {
/**
* 对INSTANCE使用volatile可以防止下面3条语句发生指令重排变成:1 -> 3 -> 2
* 多线程环境下:当发生指令重排时, 进行到第3条语句时, INSTANCE != null但是该内存仅仅是原始内存
* 对象并没有在该原始内存上初始化, 该方法直接return INSTANCE导致当前线程拿到一个非null但是未初始化
* 的对象。如果在该内存上进行相应的访问对象数据操作就会出错
*/
private static volatile VolatileForbidRearrange INSTANCE = null;
private VolatileForbidRearrange() {
}
public static VolatileForbidRearrange getInstance() {
if (INSTANCE == null) {
synchronized (VolatileForbidRearrange.class) {
if (INSTANCE == null) {
// 以下语句实际等同于3条语句:
// 1. rawMemory = memoryAllocate(); // 开辟原始内存
// 2. instance(rawMemory); // 在原始内存上对对象初始化
// 3. 将rawMemory的地址赋值给INSTANCE, 此时INSTANCE != null
INSTANCE = new VolatileForbidRearrange();
}
}
}
return INSTANCE;
}
}
public class AtomicInteger extends Number implements java.io.Serializable {
// ...
private volatile int value;
// ...
}
JMM是一种抽象的概念并不真实存在,描述的是一组规范,定义了Java各个变量(实例字段、静态字段、构成数据的元素)的访问
形式。JMM多线程编程情况下需要保证:内存可见性、原子性、有序性,volatile不满足JMM的原子性。JMM同步规定:
JMM规定所有的变量都存储在主内存中(即计算机的内存条,16g内存),即主内存是共享区域。而每个线程创建时,JVM都会
为其创建一个工作内存,工作内存是每个线程私有的数据区域,不同线程之间无法访问对方的工作空间,线程间通信需要借助
到主内存。各个线程对数据的操作(读取)都是在各自的工作内存中完成的:
CAS(Compare And Swap)算法是一条原子的CPU指令(Atomic::cmpxchg(x, addr, e) == e;),需要三个操作数:
变量的内存地址(或者是偏移量valueOffset) V ,预期值 A 和更新值 B,CAS指令执行时:
当且仅当对象偏移量V上的值和预期值A相等时,才会用更新值B更新V内存上的值,否则不执行更新。但是无论是否更新
了V内存上的值,最终都会返回V内存上的旧值。
synchronized加锁,同一时间段只允许一个线程访问,能够保证一致性但是并发性下降。而是用CAS算法使用do-while不断
判断而没有加锁(实际是一个自旋锁),保证一致性和并发性。
// unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// 获取对象var1,偏移量为var2地址上的值,并赋值给var5
var5 = this.getIntVolatile(var1, var2);
/**
* 再次获取对象var1,偏移量var2地址上的值,并和var5进行比较:
* - 如果不相等,返回false,继续执行do-while循环
* - 如果相等,将返回的var5数值和var4相加并返回
*/
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
// 最终总是返回对象var1,偏移量为var2地址上的值,即上述所说的V。
return var5;
}
可以使用AtomicStampedReference或者AtomicMarkableReference来解决CAS的ABA问题,思路类似于SVN版本号,SpringBoot热部署中trigger.txt
思路:
/**
* ABA问题解决方案,AtomicStampedReference
*
* @author sherman
*/
public class AtomicStampedReferenceABA {
private static AtomicReference<Integer> ar = new AtomicReference<>(0);
private static AtomicStampedReference<Integer> asr =
new AtomicStampedReference<>(0, 1);
public static void main(String[] args) {
System.out.println("=============演示ABA问题(AtomicReference)===========");
new Thread(() -> {
ar.compareAndSet(0, 1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ar.compareAndSet(1, 0);
System.out.println(Thread.currentThread().getName() + "进行了一次ABA操作");
}, "子线程").start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean res = ar.compareAndSet(0, 100);
if (res) {
System.out.println("main成功修改, 未察觉到子线程进行了ABA操作");
}
System.out.println("=============解决ABA问题(AtomicStampReference)===========");
new Thread(() -> {
int curStamp = asr.getStamp();
System.out.println("当前stamp: " + curStamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(0, 1, curStamp, curStamp + 1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(1, 0, asr.getStamp(), asr.getStamp() + 1);
}, "t1").start();
new Thread(() -> {
int curStamp = asr.getStamp();
System.out.println("当前stamp: " + curStamp);
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = asr.compareAndSet(0, 100, curStamp, curStamp + 1);
if (!result) {
System.out.println("修改失败! 预期stamp: " + curStamp + ", 实际stamp: " + asr.getStamp());
}
}, "t2").start();
}
}
/**
* ABA问题解决方案,AtomicMarkableReference
*
* @author sherman
*/
public class AtomicMarkableReferenceABA {
private static AtomicMarkableReference<Integer> amr = new AtomicMarkableReference<>(0, false);
public static void main(String[] args) {
new Thread(() -> {
amr.compareAndSet(0, 1, false, true);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
amr.compareAndSet(1, 0, true, true);
System.out.println("子线程进行了ABA修改!");
}, "子线程").start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean res = amr.compareAndSet(0, 100, false, true);
if (!res) {
System.out.println("修改失败! 当前isMarked: " + amr.isMarked());
}
}
}
在Spring容器刷新方法 refresh() 方法中:obtainFreshBeanFactory()->refreshBeanFactory()【GenericApplicationContext实现类】:
@Override
protected final void refreshBeanFactory() throws IllegalStateException {
// cas算法
// private final AtomicBoolean refreshed = new AtomicBoolean();
if (!this.refreshed.compareAndSet(false, true)) {
throw new IllegalStateException(
"GenericApplicationContext does not support multiple refresh attempts: just call 'refresh' once");
}
this.beanFactory.setSerializationId(getId());
}
Java普通集合类一般情况下都是线程不安全的,没有加锁实现,抛出并发异常主要是由两个变量modCount 和 expectedModCount 不匹配造成的。
ArrayList是线程不安全的,add()方法并没有加锁(synchronized),多线程环境下会抛出ConcurrentModificationException:
public class ArrayListConcurrentDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 30; ++i) {
new Thread(() -> {
list.add(UUID.randomUUID().randomUUID().toString().substring(0, 4));
System.out.println(list);
}).start();
}
}
}
解决方案:
static <T> List<T> synchronizedList(List<T> list, Object mutex) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list, mutex) :
new SynchronizedList<>(list, mutex));
}
static class SynchronizedList<E>
extends SynchronizedCollection<E>
implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public boolean equals(Object o) {
if (this == o)
return true;
synchronized (mutex) {return list.equals(o);}
}
public int hashCode() {
synchronized (mutex) {return list.hashCode();}
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
synchronized (mutex) {return list.indexOf(o);}
}
public int lastIndexOf(Object o) {
synchronized (mutex) {return list.lastIndexOf(o);}
}
public boolean addAll(int index, Collection<? extends E> c) {
synchronized (mutex) {return list.addAll(index, c);}
}
public ListIterator<E> listIterator() {
return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
return list.listIterator(index); // Must be manually synched by user
}
public List<E> subList(int fromIndex, int toIndex) {
synchronized (mutex) {
return new SynchronizedList<>(list.subList(fromIndex, toIndex),
mutex);
}
}
@Override
public void replaceAll(UnaryOperator<E> operator) {
synchronized (mutex) {list.replaceAll(operator);}
}
@Override
public void sort(Comparator<? super E> c) {
synchronized (mutex) {list.sort(c);}
}
private Object readResolve() {
return (list instanceof RandomAccess
? new SynchronizedRandomAccessList<>(list)
: this);
}
}
// CopyOnWriteArrayList.java
public boolean add(E e) {
// 写操作加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 原有容器复制一份
Object[] elements = getArray();
int len = elements.length;
// 创建一个容器,将原来的数据复制到新容器中,并且还有一个位置空余
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 将新元素添加到空余位置
newElements[len] = e;
// 将原来指向旧容器的引用指向新容器
setArray(newElements);
return true;
} finally {
// 写操作完成,解锁
lock.unlock();
}
}
public E set(int index, E element) {
// 更新操作类似
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
E oldValue = get(elements, index);
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
// 读操作不加锁
private E get(Object[] a, int index) {
return (E) a[index];
}
HashSet底层就是一个HashMap,默认的HashSet是一个初始大小为16,负载因子为0.75的HashMap:
/**
* Constructs a new, empty set; the backing HashMap instance has
* default initial capacity (16) and load factor (0.75).
*/
public HashSet() {
map = new HashMap<>();
}
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
所以HashSet的多线程安全问题实际上就是HashMap的多线程安全问题:
/**
* HashSet多线程不安全问题
* HashSet底层就是HashMap,因此这个案例也是HashMap多线程不安全问题的演示
*
* @author sherman
*/
public class HashSetThreadUnsafe {
public static void main(String[] args) {
Set<String> sets = new HashSet<>();
for (int i = 0; i < 100; ++i) {
new Thread(() -> {
sets.add(UUID.randomUUID().toString().substring(0, 4));
System.out.println(sets);
}).start();
}
}
}
解决方案:
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private static final long serialVersionUID = 5457747651344034263L;
private final CopyOnWriteArrayList<E> al;
/**
* Creates an empty set.
*/
public CopyOnWriteArraySet() {
// 构造器内部实例化了一个CopyOnWriteArrayList
al = new CopyOnWriteArrayList<E>();
}
// ...
}
相比于HashSet,HashMap除了可以使用Collections集合类的synchronizedMap方法外,还可以使用
juc包下ConcurrentHashMap类。
具体看一个案例来理解可重入锁:synchronized就是可重入锁,现在问题是synchronized块中能够使用System.out.println()方法?
public void println(String x) {
// println方法内部使用了synchronized
synchronized (this) {
print(x);
newLine();
}
}
/**
* 演示可重入锁
*
* @author sherman
*/
public class LockReentrantDemo1 {
public static void main(String[] args) {
// 程序正常运行输出:hello
new LockReentrantDemo1().lockReentrant();
}
public synchronized void lockReentrant() {
/**
* 注意这个println方法内部就使用了synchronized关键字,锁住了this
* 即synchronized块中仍然能够使用synchronized关键字 -> 可重入的
*/
System.out.println("hello");
}
}
可重入锁的意义有一点类似于事务的传播行为(一个方法运行在另一个开启事务的方法中,那么当前方法的事务行为是什么样的?),
类比来说可重入锁意义就是:一个synchronized(锁)块运行在另一个synchronized(块)中,那么当前synchronized的具体表现行为
是什么,是直接中断?还是阻塞等待?又或者是正常执行,因为两个synchronized锁住的是同一个对象?
可重入锁的含义就是最后一种:正常执行,因为可重入锁,锁的是同一个对象。
具体示例:
/**
* 可重入锁演示
*
* @author sherman
*/
// 演示ReentrantLock是可重入的
class ShareResouce implements Runnable {
private Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
private void get() {
lock.lock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": get()");
set();
} finally {
lock.unlock();
}
}
private void set() {
lock.lock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + ": set()");
} finally {
lock.unlock();
}
}
}
public class LockReentrantDemo2 {
// outer()和inner()方法演示synchronized是可重入的
private synchronized void outer() {
System.out.println(Thread.currentThread().getName() + ": outer method()");
inner();
}
// outer()和inner()方法演示synchronized是可重入的
private synchronized void inner() {
System.out.println(Thread.currentThread().getName() + ": inner method()");
}
public static void main(String[] args) {
// 验证synchronized是可重入的
LockReentrantDemo2 lrd = new LockReentrantDemo2();
new Thread(lrd::outer, "thread-1").start();
new Thread(lrd::outer, "thread-2").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 验证ReentrantLock是可重入的
System.out.println("===================");
new Thread(new ShareResouce(), "thread-3").start();
new Thread(new ShareResouce(), "thread-4").start();
}
}
补充:
在使用ReentrantLock类演示可重入锁时,lock.lock()和lock.unlock()数量一定要匹配,否则:
自旋锁尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是避免线程上下文切换的消耗,缺点是
如果一直自旋会消耗CPU:
/**
* 自旋锁演示
*
* @author sherman
*/
public class LockSpin {
AtomicReference<Thread> ar = new AtomicReference<>();
private void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + ": come in!");
while (!ar.compareAndSet(null, thread)) {
}
}
private void unlock() {
Thread thread = Thread.currentThread();
ar.compareAndSet(thread, null);
System.out.println(thread.getName() + ": get out!");
}
public static void main(String[] args) throws InterruptedException {
LockSpin lockSpin = new LockSpin();
new Thread(() -> {
lockSpin.lock();
try {
Thread.sleep(8000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockSpin.unlock();
}, "线程A").start();
// 保证线程A先进行自旋
Thread.sleep(1000);
new Thread(() -> {
lockSpin.lock();
lockSpin.unlock();
}, "线程B").start();
}
}
/**
* 演示读写锁
*
* @author sherman
*/
class Cache {
private volatile HashMap<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public Object get(String key) {
lock.readLock().lock();
Object res = null;
try {
System.out.println(Thread.currentThread().getName() + ": 正在读取+++");
Thread.sleep(100);
res = map.get(key);
System.out.println(Thread.currentThread().getName() + ": 读取完成---");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return res;
}
public void put(String key, Object value) {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + ": 正在写入>>>");
Thread.sleep(1000);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + ":写入完成<<<");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
}
public class LockReadWrite {
public static void main(String[] args) {
Cache cache = new Cache();
// 写入操作是被一个线程独占的,一旦写线程开始
// 其它线程必须等待其完成后才能继续执行
for (int i = 0; i < 10; i++) {
final int tmp = i;
new Thread(() -> cache.put(tmp + "", tmp + ""), String.valueOf(i)).start();
}
// 读操作可以被多个线程持有
// 其它线程不必等待当前读操作完成才操作
for (int i = 0; i < 10; i++) {
final int tmp = i;
new Thread(() -> cache.get(tmp + ""), String.valueOf(i)).start();
}
}
}
CountDownLatch是一个计数器闭锁,它通过一个初始化定时器latch,在latch的值被减到0之前,
其它线程都会被await()方法阻塞。
以模拟火箭发射过程解释CountDownLatch使用:
/**
* CountDownLatch模拟火箭发射过程:
* 火箭发射之前需要十个线程进行前期检查工作,每个线程耗时0-4s,
* 只有10个线程对应的检查工作全部完成后,火箭才能发射
*
* @author sherman
*/
public class CountDownLatchDemo implements Runnable {
public static final int TASK_NUMBERS = 10;
private static CountDownLatch cdl = new CountDownLatch(TASK_NUMBERS);
public static void main(String[] args) throws InterruptedException {
CountDownLatchDemo countDownLatchDemo = new CountDownLatchDemo();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executorService.submit(countDownLatchDemo);
}
cdl.await();
System.out.println("检查工作检查完毕:fire!");
executorService.shutdown();
}
@Override
public void run() {
try {
// 模拟火箭发射前的各种检查工作
int millis = new Random().nextInt(5000);
Thread.sleep(millis);
System.out.println(Thread.currentThread().getName() + ":检查完毕! 耗时:" + millis + "ms");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 每次检查完毕后都将计数器减1
cdl.countDown();
}
}
}
CyclicBarrier是可循环使用的屏障,它的功能是:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程
到达屏障时,屏障才会被打开,所有被屏障阻塞的方法都会被打开。
A synchronization aid that allows a set of threads to all wait for each other to reach a common
barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that
must occasionally wait for each other. The barrier is called cyclic because it can be re-used
after the waiting threads are released.
示例:模拟集齐七颗龙珠才能召唤神龙:
/**
* CyclicBarrier模拟集齐七颗龙珠才能召唤神龙
* 设置common barrier point为7,每个线程收集到七颗龙珠之前都会被阻塞
* 每个线程都到达common barrier point时候才会召唤神龙
*
* @author sherman
*/
public class CyclicBarrierDemo implements Runnable {
private static CyclicBarrier cb = new CyclicBarrier(7, () -> System.out.println("召唤神龙"));
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + ": 到达同步点(收集到一个龙珠)!");
cb.await();
System.out.println(Thread.currentThread().getName() + ": 阻塞结束,继续执行!");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
CyclicBarrierDemo cbd = new CyclicBarrierDemo();
ExecutorService executorService = Executors.newFixedThreadPool(7);
for (int i = 0; i < 7; i++) {
try {
Thread.sleep(new Random().nextInt(2000));
} catch (InterruptedException e) {
e.printStackTrace();
}
executorService.submit(cbd);
}
executorService.shutdown();
}
}
Semaphore信号量主要有两个目的:
主要方法:
示例:使用Semaphore模拟请车位过程(3个车位,10辆车):
/**
* 使用Semaphore模拟抢车位过程(3个车位,10辆车)
* 任意时刻只有3辆车持有线程
*
* @author sherman
*/
public class SemaphoreDemo {
public static void main(String[] args) {
// 模拟三个车位,十辆车
// 任意时刻只有三辆车持有车位
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + ": 抢到车位");
// 每辆车占有车位[3,8]秒时间
Thread.sleep((new Random().nextInt(6) + 3) * 1000);
System.out.println(Thread.currentThread().getName() + ": 释放车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
阻塞是一个队列,当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞;当队列满时,往队列中添加元素的操作将会被阻塞。
BlockingQueue让我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程:
继承树:
四组API:
与其它BlockingQueue不同,SynchronizedQueue是一个不存储元素的BlockingQueue,每一个put操作必须等待
一个take操作,否则就不能继续添加元素,反之亦然。总之,SynchronizedQueue,生产一个,消费一个。
示例:
使用SynchronizedQueue完成生产者-消费者模式,并且该生产者-消费者模式必须是严格生产一个,消费一个:
/**
* 使用SynchronizedQueue完成生产者-消费者模型,并且是严格生产一个——消费一个
*
* @author sherman
*/
public class SynchronizedQueueDemo {
private static BlockingQueue<String> bq = new SynchronousQueue<>();
public static void main(String[] args) {
new Thread(() -> {
for (int i = 0; i < 10; ++i) {
final int tmp = i;
try {
System.out.println("生产者线程: " + tmp);
bq.put(String.valueOf(tmp));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
final int tmp = i;
try {
Thread.sleep(new Random().nextInt(2000));
System.out.println("消费者线程:" + tmp);
bq.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
实现生产者-消费者模型,要注意:
- 线程操作资源类
- 先判断、在执行、最后返回通知
- while替代if防止虚假唤醒
synchronized-notifyAll-wait实现:
/**
* 生产者-消费者实现方式一:使用synchronized-notifyAll-wait
*
* @author sherman
*/
public class ConsumerAndProducer01 {
// produce和consume都有synchronized修饰,不需要使用volatile保证内存可见性
private int num = 0;
synchronized public void produce() {
while (num != 0) {
try {
// 禁止生产
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始生产
++num;
System.out.println(Thread.currentThread().getName() + ": " + num);
// 返回通知其它所有阻塞的生产者线程
this.notifyAll();
}
synchronized public void consume() {
while (num == 0) {
try {
// 禁止消费
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始消费
--num;
System.out.println(Thread.currentThread().getName() + ": " + num);
// // 返回通知其它所有阻塞的生产者线程
this.notifyAll();
}
/**
* 两个生产者、两个消费者
*/
public static void main(String[] args) {
ConsumerAndProducer01 cs1 = new ConsumerAndProducer01();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
cs1.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "+++生产者线程AAA").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(100);
cs1.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "+++生产者线程BBB").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(200);
cs1.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "---消费者线程CCC").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(200);
cs1.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "---消费者线程DDD").start();
}
}
lock(condition)-signalAll-await实现:
/**
* 生产者-消费者实现方式二:使用lock(condition)-signalAll-await实现
*
* @author sherman
*/
public class ConsumerAndProducer02 {
private int num = 0;
private ReentrantLock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void produce() {
lock.lock();
try {
while (num != 0) {
try {
// 不能生产
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始生产
++num;
System.out.println(Thread.currentThread().getName() + ": " + num);
condition.signalAll();
} finally {
lock.unlock();
}
}
public void consume() {
lock.lock();
try {
while (0 == num) {
try {
// 不能消费
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 开始消费
--num;
System.out.println(Thread.currentThread().getName() + ": " + num);
condition.signalAll();
} finally {
lock.unlock();
}
}
/**
* 两个生产者、两个消费者
*/
public static void main(String[] args) {
ConsumerAndProducer02 cs2 = new ConsumerAndProducer02();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cs2.produce();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "+++生产者线程AAA").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cs2.produce();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "+++生产者线程BBB").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cs2.consume();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "---消费者线程CCC").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
cs2.consume();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "---消费者线程DDD").start();
}
}
阻塞队列实现:
/**
* 生产者-消费者实现方式三:使用阻塞队列
*
* @author sherman
*/
public class ConsumerAndProducer03 {
private volatile boolean isWork = true;
private BlockingQueue<String> blockingQueue;
private AtomicInteger ai = new AtomicInteger();
public ConsumerAndProducer03(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
public void stop() {
isWork = false;
}
public void produce() {
String data;
while (isWork) {
data = ai.incrementAndGet() + "";
try {
boolean res = blockingQueue.offer(data, 2L, TimeUnit.SECONDS);
String name = Thread.currentThread().getName();
if (res) {
System.out.println(name + ":添加元素:" + data + "成功");
} else {
System.out.println(name + ":添加元素:" + data + "失败");
}
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":停止工作");
}
public void consume() {
String res;
while (isWork) {
try {
res = blockingQueue.poll(2L, TimeUnit.SECONDS);
if (res == null || "".equals(res)) {
isWork = false;
System.out.println(Thread.currentThread().getName() + ":超过两秒为获取数据,即将退出");
return;
}
Thread.sleep(200);
System.out.println(Thread.currentThread().getName() + ":取出元素:" + res + "成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
ConsumerAndProducer03 cs3 = new ConsumerAndProducer03(new LinkedBlockingQueue<>(3));
new Thread(cs3::produce, "+++生产者线程AAA").start();
new Thread(cs3::produce, "+++生产者线程BBB").start();
new Thread(cs3::consume, "---生产者线程CCC").start();
new Thread(cs3::consume, "---生产者线程DDD").start();
Thread.sleep(8 * 1000);
System.out.println("终止生产者-消费者线程");
cs3.stop();
}
}
synchronized和lock的区别:
lock(condition)精准唤醒示例:
多个线程之间顺序调用:有三个线程A、B、C,要求:
- A线程打印AAA1次,紧接着B线程打印BBB3次,最后C线程打印CCC5次;
- 保证以上顺序,总共打印10轮;
/**
* 使用lock(condition)实现线程的精准唤醒
*
* 多个线程之间顺序调用:有三个线程A、B、C,要求:
* - A线程打印AAA1次,紧接着B线程打印BBB3次,最后C线程打印CCC5次;
* - 保证以上顺序,总共打印10轮;
*
* @author sherman
*/
public class AccuracyNotify {
private int curThread = 1;
private Lock lock = new ReentrantLock();
private Condition cond1 = lock.newCondition();
private Condition cond2 = lock.newCondition();
private Condition cond3 = lock.newCondition();
public void printOnce() {
lock.lock();
try {
while (1 != curThread) {
cond1.await();
}
System.out.println(Thread.currentThread().getName() + ": AAA");
curThread = 2;
cond2.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printTriple() {
lock.lock();
try {
while (2 != curThread) {
cond2.await();
}
for (int i = 0; i < 3; i++) {
System.out.println(Thread.currentThread().getName() + ": BBB");
}
curThread = 3;
cond3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printFifth() {
lock.lock();
try {
while (3 != curThread) {
cond3.await();
}
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + ": CCC");
}
curThread = 1;
cond1.signal();
System.out.println("=======================");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
AccuracyNotify an = new AccuracyNotify();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
an.printOnce();
}
}, "线程A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
an.printTriple();
}
}, "线程B").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
an.printFifth();
}
}, "线程C").start();
}
}
Java创建线程的4中方式:
Callable接口通过 FutureTask 接口的适配:
FutureTask是Future、Runnable接口的实现类,其构造函数接受Callable接口参数,Future接口主要的方法有:
也正因为Future接口的这些方法,相对于Runnable接口而言,Callable接口的优势在于:
FutureTask类的构造函数接受Callable接口参数,并且FutureTask类本身是Runnable接口的子类,
因此通过FutureTask类可以很好地适配Callable:
/**
* 演示Callable接口的使用
*
* @author sherman
*/
public class CallableDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Enter call() method...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 2571;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new CallableDemo());
/**
* 注意多个线程使用同一个FutureTask对象,对应的call方法只会被执行一次
*/
new Thread(task, "AAA").start();
new Thread(task, "BBB").start();
Integer res = task.get();
// 过早调用get()方法会导致主线程阻塞
while (!task.isDone()) {
// 类似于自旋锁
}
System.out.println(res + 100);
}
}
注意:
线程池最主要的工作在于控制运行线程的数量,从而做到线程复用、控制最大并发数量、管理线程。其具体的优势在于:
线程池相关的继承结构:
使用线程池的三种常见方式:
注意:
ThreadPoolExecutor对构造函数进行了重载,实际内部使用了7个参数:)]
线程池具体工作流程:
当线程池的阻塞队列满了同时线程池中线程数量达到了最大maximumPoolSize时,线程池将会启动相应的拒绝策略来拒绝请求任务。
4种拒绝策略具体为:
注意:
自定义线程池:使用不同的拒绝策略:
/**
* 自定义线程池的各个参数
*
* @author sherman
*/
public class CustomThreadPool {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
// new ThreadPoolExecutor.AbortPolicy()
new ThreadPoolExecutor.CallerRunsPolicy() // 注意使用该拒绝策略,可能会回退给main线程执行
// new ThreadPoolExecutor.DiscardOldestPolicy()
//new ThreadPoolExecutor.DiscardPolicy()
);
try {
for (int i = 0; i < 9; i++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + ": 执行任务");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
executorService.shutdown();
}
}
}
线程池合理配置线程数量需要考虑业务具体是CPU密集型还是IO密集型:
# CPU核数 + 1个线程的线程池
# CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8-0.9
# CPU核数 * 2
两个及以上的线程在执行过程中,因为互相争夺资源而造成一种相互等待的现象,如果外力干涉,那么它们都将无法推进下去:
/**
* 演示死锁的产生
*
* @author sherman
*/
public class DeadLock implements Runnable {
private String mutex1;
private String mutex2;
public DeadLock(String mutex1, String mutex2) {
this.mutex1 = mutex1;
this.mutex2 = mutex2;
}
@Override
public void run() {
synchronized (mutex1) {
System.out.println(Thread.currentThread().getName() + ": 持有" + mutex1 + ", 尝试获取" + mutex2);
try {
// 睡眠一定时间,给别的线程获取资源,产生冲突
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (mutex2) {
System.out.println(Thread.currentThread().getName() + ":持有" + mutex2 + ", 尝试获取" + mutex1);
}
}
}
public static void main(String[] args) {
String mutex1 = "mutex1";
String mutex2 = "mutex2";
new Thread(new DeadLock(mutex1, mutex2), "AAA").start();
new Thread(new DeadLock(mutex2, mutex1), "BBB").start();
}
}
Java提供了类似于ps的命令jps来查看当前Java程序及其进程号,再通过命令jstack能够产看具体进程号的Java程序栈使用情况:
具体可以参看:https://mp.weixin.qq.com/s/WuyxyelaXbU-lg-HVZ95TA
// 开启/关闭打印GC详细信息属性
-XX:+PrintGCDetails
-XX:-PrintGCDetails
// 开启/关闭串行垃圾回收器
-XX:+UseSerialGC
-XX:-UseSerialGC
// 查看某个正在运行的Java程序是否开启了某个JVM参数
jps -l
jinfo -flag [对应的JVM参数:PrintGCDetails] [pid: 17693]
// jinfo -flag [pid:17693] 查看所有默认的JVM参数
// 上面这一步可能在Ubuntu中报错:
修改/etc/sysctl.d/10-ptrace.conf:
kernel.yama.ptrace_scope = 0
examples:
sherman@dell:~/workspace/idea_home$ jps -l
2260 com.intellij.idea.Main
17691 org.jetbrains.jps.cmdline.Launcher
17981 sun.tools.jps.Jps
17693 nwpu.sherman.atsilicon.session2.JVMParametersDemo
3263 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
sherman@dell:~/workspace/idea_home$ jinfo -flag PrintGCDetails 17693
-XX:+PrintGCDetails
sherman@dell:~/workspace/idea_home$ jinfo -flags 2672
Attaching to process ID 2672, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.211-b12
Non-default VM flags: -XX:CICompilerCount=3 -XX:InitialHeapSize=197132288 -XX:MaxHeapSize=3126853632 -XX:MaxNewSize=1042284544 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=65536000 -XX:OldSize=131596288 -XX:+PrintGCDetails -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line: -XX:+PrintGCDetails -javaagent:/home/sherman/dev-tools/ideaIU-2019.1.3/lib/idea_rt.jar=44615:/home/sherman/dev-tools/ideaIU-2019.1.3/bin -Dfile.encoding=UTF-8
// 设置JVM元空间大小
-XX:MetaspaceSize=128m // 元空间默认大小21m左右
// 设置从Young升到老年代区的年龄
-XX:MaxTenuringThreshold=10 // 默认年龄15
java -XX:+PrintFlagsInitial [version] [| grep MetaspaceSize]
java -XX:+PrintFlagsFinal [version] [| grep MetaspaceSize]
sherman@dell:~$ java -XX:+PrintFlagsFinal -version | grep InitialHeapSize
uintx InitialHeapSize := 197132288
// 注意::=代表被JVM修改过或者认为修改过后的值
// 典型配置
-Xms128m -Xmx4096m -Xss1024k -XX:MetaspaceSize=512m -XX:+PrintCommandLineFlags
-XX:+PrintGCDetails -XX:+UseSerialGC
上面图是在JDK1.8之前的,没有元空间的GC日志信息,在JDK1.8及之后,GC日志一共分为4部分:
新生代、老年代、元空间的GC日志格式固定:
名称:->[GC前该区域内存大小]->GC后该区域内存大小(该区域内存总大小)
例如:
Full GC (System.gc()) [PSYoungGen: 2496K->0K(56320K)] [ParOldGen: 0K->2408K(128512K)] 2496K->2408K(184832K), [Metaspace: 3006K->3006K(1056768K)], 0.0059206 secs
Java提供了四种引用,分别是:强引用、软引用、弱引用、虚引用,它们的关系图为:
强引用:
当JVM进行GC时,对于强引用对象,就算出现了OOM也不会对该对象进行回收。强引用是造成Java内存泄露的主要原因之一:
/**
* 演示强引用
*
* @author sherman
*/
public class StrongReference {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = obj1;
obj1 = null;
System.gc();
/**
* obj2属于强引用,不会回收
*/
System.out.println(obj2);
}
}
软引用:
对于软引用关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之内。
如果这次回收后还没有足够的内存,才会抛出异常。即:
软引用通常用在内存敏感的程序中,例如高速缓存中就用到软引用。软引用在Java中用
java.lang.ref.SoftReference来表示:
/**
* 演示软引用
* VM: -Xms5m -Xmx5m -XX:+PrintGCDetails
*
* @author sherman
*/
public class SoftReferenceDemo {
public static void main(String[] args) {
Object obj1 = new Object();
SoftReference<Object> softReference = new SoftReference(obj1);
System.out.println(obj1);
System.out.println(softReference.get());
obj1 = null;
try {
byte[] bytes = new byte[20 * 1024 * 1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(obj1);
System.out.println(softReference.get());
}
}
}
弱引用:
弱引用通过java.lang.ref.WeakReference来完成,当JVM进行GC时,无论内存是否充足,被弱引用关联的对象都会被回收,
即弱引用关联的对象活不到下一次GC时刻:
/**
* 演示弱引用
*
* @author sherman
*/
public class WeakReferenceDemo {
public static void main(String[] args) {
Object obj1 = new Object();
WeakReference<Object> wr = new WeakReference<>(obj1);
System.out.println(obj1);
System.out.println(wr.get());
obj1 = null;
/**
* 弱引用活不到下一次gc,因此即使内存充足,弱引用也会被回收
*/
System.gc();
System.out.println(obj1);
System.out.println(wr.get());
}
}
// output:
// java.lang.Object@1540e19d
// java.lang.Object@1540e19d
// null
// null
虚引用:
虚引用需要java.lang.ref.PhantomReference类来实现,如果一个对象仅持有虚引用,那么它和没有任何引用一样,调用get()方法
总返回null,在任何时候都可能被垃圾回收器回收,虚引用必须和引用队列(ReferenceQueue)联合使用:
/**
* 演示PhantomReference
*
* @author sherman
*/
public class PhantomReferenceDemo {
public static void main(String[] args) throws InterruptedException {
Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
System.out.println(obj);
/**
* PhantomReference任何时候get都是null
*/
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
System.out.println("===============");
/**
* obj=null,gc之后,引用的对象会被添加到引用队列中,
* 因此最后的poll方法能够获取到值
*/
obj = null;
System.gc();
Thread.sleep(100);
System.out.println(obj);
System.out.println(phantomReference.get());
System.out.println(referenceQueue.poll());
}
}
场景:
有一个应用需要读取大量的本地图片:如果每次读取图片都从硬盘读取则会严重影响性能;如果一次都加载到内存中有可能造成内存溢出,
此时可以通过软引用或者弱引用来解决该问题。
设计思路:
用一个HashMap来保存图片路径和相应图片对象关联的软引用之间的映射关系,当系统内存不足时,JVM会自动回收这些缓存图片对象所占用
的空间,从而有效的避免OOM:
Map
WeakHashMap和HashMap一样也是一个散列表,但是它的键是“弱键”,其类型是WeakReference,对于“弱键”,其对应的映射的存在并不会阻止垃圾回收器对该键的丢弃,
也就是说,该弱键是可被终止的。当某个键被终止时,其对应的键值对映射就会从散列表中移除:
/**
* @author sherman
*/
public class WeakHashMapDemo {
public static void main(String[] args) {
Map<Integer, Object> weakHashMap = new WeakHashMap<>();
Map<Integer, Object> hashMap = new HashMap<>();
/**
* 注意这里两个map不能共用一对key-value,会相互影响
*/
Integer key1 = new Integer(250);
String value1 = "value1";
Integer key2 = new Integer(250);
String value2 = "value2";
weakHashMap.put(key1, value1);
hashMap.put(key2, value2);
System.out.println("weakHashMap: " + weakHashMap);
System.out.println("hashMap: " + hashMap);
key1 = null;
key2 = null;
System.gc();
System.out.println("==========================");
System.out.println("weakHashMap: " + weakHashMap);
System.out.println("hashMap: " + hashMap);
}
}
// 从输出结果理解:【对应的映射的存在并不会阻止垃圾回收器对该键的丢弃】的含义
// output:
// weakHashMap: {250=value1}
// hashMap: {250=value2}
// ==========================
// weakHashMap: {}
// hashMap: {250=value2}
注意:虽然StackOverflowError和OutOfMemoryError通常口语上都称为“异常”,但是实际上它们都是Error的子类,属于错误。
StackOverflowError:
栈溢出异常,通常发生在递归调用且未添加终止条件时。使用 -Xss 参数可以更改栈的大小。
OOM:java heap space
当new大对象或者不断new新对象,导致new出来的内存超过了heap的大小,会导致OOM: java heap space异常:
/**
* 演示OOM:java heap size
* VM -Xms5m -Xmx5m -XX:+PrintGCDetails
*
* @author sherman
*/
public class JavaHeapSpace {
public static void main(String[] args) {
// Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
byte[] bytes = new byte[80 * 1024 * 1024];
}
}
OOM:GC overhead limit exceeded:
GC回收时间过长,过长的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC仍然出现这种情况时,才会抛出该异常。
如果多次出现GC回收时间过长情况,但是并不抛出异常则会出现:
GC清理的内存很快又会被再次填满,迫使再次GC,形成恶性循环,CPU使用率一直在100%,而GC却没有任何效果。
/**
* 演示GC overhead limit exceeded异常
*
* -Xms12m -Xmx12m -XX:+PrintGCDetails
*
* 注意上面Xms和Xmx值不能太小,否则还没到达GC limit的限制就直接移除了,抛出java heap space异常
*
* @author sherman
*/
public class GCOverLimit {
public static void main(String[] args) {
int i = 0;
List<String> lists = new ArrayList<>();
try {
while (true) {
// Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
lists.add(String.valueOf(i++).intern());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
OOM:direct buffer memory:
在NIO程序中,经常需要使用ByteBuffer来读取或者写入数据,这是一种基于通道(Channel)和缓冲区(Buffer)的IO方式。它可以使用Native函数库直接
分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存引用进行操作。这样能够在一些场景中显著提高性能,因为可以避免
在Java堆和Native堆中来回复制数据:
但是如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectBuffer对象们就不会被收,这时候堆内存充足,但是本地内存可能已经使用
完毕,再次尝试分配本地内存就会出现OOM,程序直接崩溃:
/**
* 演示Direct Buffer Memory错误
*
* -Xms5m -Xmx5m -XX:+PrintGCDetails -XX:MaxDirectMemorySize=5m
*
* @author sherman
*/
public class DirectBufferMemory {
public static void main(String[] args) {
// 默认应该是1/4系统内存
System.out.println("配置的堆外内存为:" + (sun.misc.VM.maxDirectMemory()) / (double) 1024 / 1024 + "MB");
// Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}
OOM:unable to create new native thread:
高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError:unable to create new native thread,准确的讲该native thread异常
与对应的平台有关。
/**
* 演示OOM:unable to create new native thread
* 注意:在Windows上运行这个程序容易出现假死现象!!!
*
* @author sherman
*/
public class UnableCreateNewNativeThread {
public static void main(String[] args) {
for (int i = 0; ; ++i) {
System.out.println("+++++++" + i + "+++++++");
// Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
new Thread(() -> {
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
# vim /etc/security/limits.d/90-nproc.conf:
* soft nproc 1024
root soft nproc unlimited
OOM:metaspace:
Metaspace是Java8及其以后版本中使用的,用来代替永久代。Metaspace是方法区在HotSpot中的实现,它与永久带最大的区别是:
Metaspace并不在JVM内存中而是直接使用本地内存。也就是说在Java8中,class metadata
(the virtual machines internal presentation of Java class),被存储在叫做Metaspace的Native Memory中。
# windows:java -XX:+PrintFlagsInitial | findstr -i metaspacesize
# linux: java -XX:+PrintFlagsInitial | grep -i metaspacesize
因为Metaspace中存储了类信息,所以不断产生新的类,就会不断向Metaspace中写入数据,就有可能导致Metaspace区域溢出。
GC算法(引用计数、复制、标记清除、标记整理)是内存回收的方法论,垃圾回收器是方法论的落地实现。目前没有完美的的垃圾回收器,
也没有万能的垃圾回收器,只能根据具体的应用选择合适的垃圾回收器。
Serial和Parallel垃圾回收器都会产生STW(Stop The World)问题,CMS并不会产生该问题,但是CMS的GC过程可能更加复杂,导致抢占应用程序的CPU资源。
对于一个正在运行的Java程序,可以通过jps找到Java的pid,然后jinfo flag UseXxxGC pid 来查看
当前运行的程序是否开启了指定的垃圾回收器。
串行垃圾回收器(Serial/Serial Copying):
一个单线程的垃圾回收器并且只使用单个线程进行垃圾回收在进行垃圾回收时,必须暂停其他所有的工作线程(STW)直到它的垃圾回收结束。
串行垃圾回收最稳定高效,对于限定单个CPU的环境来说,没有线程交互的开销可以获得最高的单线程垃圾回收效率。因此,Serial垃圾收集器
依然是JVM运行在Client模式下默认的新生代垃圾收集器。
并行垃圾回收器(ParNew):
使用多个线程进行垃圾回收,是Serial收集器新生代的并行多线程版本,在进行垃圾回收时仍然会出现STW问题直至它的回收工作结束,它是很多JVM运行
在Server模式下的新生代默认垃圾回收器。
并行收集器(Parallel Scavenge):
Parallel Scavenge收集器类似于ParNew,也是一个新生代垃圾并行多线程收集器,使用复制算法,俗称吞吐量(CPU运行用户代码的时间和CPU总消耗时间的比值,
即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))优先收集器。总的来说,相对于ParNew收集器而言:Parallel Scavenge是在新生代和老年代都
是并行多线程处理的垃圾回收器,它是Java8默认的垃圾回收器。
高吞吐量意味着高效利用CPU的时间,它多用于在后台运算而不需要太多的交互任务。
除此之外,相对于ParNew收集器而言,PS收集器存在一个自适应调节策略:JVM会根据当前系统的运行情况收集性能监控信息,动态调节这些参数以提供最合适的
停顿时间(-XX:MaxPauseGCMillis)或最大吞吐量。
Parallel Old收集器:
Parallel Old收集器是PS的老年代版本,使用多线程的标记-整理算法,Parallel Old是JDK1.6才开始提供的。在JDK1.6之前,新生代使用PS收集器只能在
老年代配合Serial Old收集器,只能保证在新生代的吞吐量有限,无法保证整体的吞吐量。
Parallel Old正是为了在老年代同样提供吞吐量优先的垃圾回收器,如果系统对吞吐量要求较高,在JDK1.8及以后版本都是使用Parallel Scavenge&Parallel Old
垃圾收集器组合。
并发标记清除收集器(CMS):
CMS收集器是标记-清除GC算法的落地实现,是一种以获得最短停顿时间为目标的收集器,适合应用在互联网站或者B/S系统的服务器上,这类应用尤其重视服务器
的响应速度,希望系统停顿时间最短CMS非常适合堆内存大, CPU核数多的服务器应用,也是G1收集器出现之前大型应用的首选收集器。
Serial Old收集器:
Serial Old收集器是Serial收集器的老年代版本,同样是单线程收集器,使用标记-整理算法,该收集器主要在Client模式下老年代使用。
在Server模式下,Serial Old收集器的主要作用有:
注意:在Java8中不能使用JVM参数-XX:+UseSerialOldGC来开启Serial Old收集器。
G1收集器:
G1收集器是一种面向服务端的垃圾回收器,应用在多处理器和大容量内存环境中,在实现高吞吐量的同时,尽可能满足垃圾收集暂停时间的要求,用于取代CMS
垃圾收集器,在JDK9中,G1是默认的垃圾回收器。
在G1收集器中,Region之间的对象引用及其他收集器中的新生代和老年代之间的对象引用,JVM都是通过Remembered Set来避免全堆扫描。G1中每个Region
都有一个与之对应的Remembered Set,JVM发现程序对Reference引用的对象进行写操作时,会产生一个写中断(Write Barrier),检查Reference引用的对象
是否处于不同的Region之中(例如检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关用信息记录到被引用对象所属的Region的
Remembered Set中。当进行内存回收时,在GC更节点的枚举范围内加入Remembered Set即可保证不对全堆扫描也不会有遗漏
(注: 这也说明GC Roots中的对象也可以是属于Remembered Set中的对象)。
在G1中还有一个特殊的区域(Humongous)区域,如果一个对象占用的内存超过了分区容量的50%,这样的巨型对象会直接被分配在老年代,但是它会对垃圾回收
产生负面影响。为了解决这个问题,G1专门划分了一个Humongous区域,专门存放巨型对象。如果一个H区不能装下巨型对象,那么G1会寻找连续的H分区来存储,
有时为了能够找到连续的H区,不得不启动一次Full GC。