尚硅谷大厂面试题第二季(上)
1、 volatile关键字理解
volatile是Java提供的轻量级的同步机制,主要有三个特性:
- 保证内存可见性
- 不保证原子性
- 禁止指令重排序
1.1 保证内存可见性
当某个进程在自己的工作内存中将主内存中共享数据的副本修改并刷新到主内存后,其他线程能够立即感知到该共享数据发生变化:
/** volatile关键字:【内存可见性】
* - 使用语句(1),注释语句(2) -> 程序一直等待
* - 使用语句(2),注释语句(1) -> 程序正常结束
*/
public class ColatileMemoryVisibility{
//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的变化!");
}
}
1.2 不保证原子性
不保证原子性正式Volatile轻量级的体现,多个线程对volatile修饰的变量进行操作时,容易出现写覆盖的情况(i++):
/**
* Volatile关键字:【内存可见性】
* - 使用语句(1),注释语句(2) -> 程序一直等待
* - 使用语句(2),注释语句(1) -> 程序正常结束
*/
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的变化!");
}
}
1.3 禁止指令重排序
计算机执行程序为了提高性能,编译器和处理器常常会进行指令重排。源代码->编译器优化重排->指令并行重排->内存系统重排->最终执行指令。单线程环境下程序最终执行结果和执行顺序结果一致,多线程环境下线程交替执行一次,由于编译器优化重排的存在,两个线程使用的变量一致性无法保证。因此,处理器在进行指令重排的时候必须考虑指令之间的数据依赖性。volatile能够实现禁止指令重排的地层学原理:
- 内存屏障(Memory Barrier):他是一个CPU指令。由于编译器和CPU能够执行指令重排,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,任何指令都不能和该条Memory Barrier指令进行重排序,即通过后插入内存屏障指令能够禁止在内存屏障前后的指令执行重排序。
- 内存屏障的另一个作用是强制刷新各种CPU的缓存数据,因此任何CPU上的线程能够读取到这些数据的最新颁布的。以上两点正好对应了volatile关键字禁止指令重排序和内存可见性的特点。
- 对volatile变量进行写操作时,会在写操作之后加入一条store屏障指令,将工作内存中的共享变量copy刷新回主内存中;对volatile变量进行读操作时,会在读操作之前加入load的屏障指令,从主内存中读取共享变量。
/**
* Volatile关键字:【禁止指令重排序】
* 高并发环境下DCL(Double Check Lock)单例模式使用volatile防止指令重排序
*/
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;
}
}
1.4 volatile使用场景
- 高并发环境下DCL单例模式使用volatile,见上面实例
- JUC包下AtomicXxx类:原子类AtomicXxx都有一个成员变量value,该value变量被声明为volatile,保证AtomicXxx的内存可见性,而原子性由CAS算法&Unsafe类保证,结合这两点才能让AtomicXxx类很好地替代synchronized关键字。
public class AtomicInteger extends Number implements java.io.Serializable {
// ...
private volatile int value;
// ...
}
2、 Java内存模型(JMM)
2.1 JMM概念
JMM(Java内存模型Java Memory Model,简称JMM)是一种抽象的概念并不真实存在,描述的是一组规范,定义了Java各个变量(实例字段、静态字段、构成数据的元素)的访问形式。JMM多线程编程情况下需要保证:内存可见性、原子性、有序性,volatile不满足JMM的原子性。JMM同步规定:
- 线程加锁之前,必须读取主内存中的值到线程私有的工作内存中
- 线程解锁之前,必须将线程各自工作内存中的值刷新回主内存中*
- 加锁和解锁必须是同一把锁
2.2 JMM工作原理
JMM规定所有的变量都存储在主内存中(即计算机的内存条,16g内存),即主内存是共享区域。而每个线程创建时,JVM都会
为其创建一个工作内存,工作内存是每个线程私有的数据区域,不同线程之间无法访问对方的工作空间,线程间通信需要借助
到主内存。各个线程对数据的操作(读取)都是在各自的工作内存中完成的:
- 首先将主内存中的变量copy到各自的工作内存
- 完成操作之后,在将各自工作内存中的变量刷新回主内存空间,而不是每个线程在主内存空间直接操作共享数据
3、 CAS算法
CAS(Compare And Swap)算法是一条原子的CPU指令(Atomic::cmpxchg(x,addr,e)==e;),需要三个操作数;变量的内存地址(或者是偏移量valueOffset)V,预期值A和更新值B,CAS指令执行时:当且仅当对象偏移量V上的值和预期值A相等时,才会用更新值B更新V内存上的值,否则不执行更新。但是无论是否更新了V内存的值,最终都会返回V内存上的旧值。
3.1 为什么使用CAS代替synchronized
Synchronized加锁,同一时间段只允许一个线程访问,能够保持一致性但是并发性下降。而CAS算法使用do-while不断判断有没有加锁(实际上是一个自旋锁),同时保证了一致性与并发性。
- 原子性保证:CAS算法依赖于rt.jar包下的sun.misc.Unsfel类,该类中的所有方法都是native修饰的,直接调用操作系统底层资源执行相应的任务。
// 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;
}
- 内存可见性和禁止指令重排的保证:AtomicXxx中的成员变量value是由volatile修饰的:private volatile int value;
3.2 CAS算法的缺点
- do-while循环,如果CAS失败就会一直进行尝试,即一直在自旋,导致CPU开销很大;
- 只能保证一个共享变量的原子操作,如果操作多个共享变量则需要加锁实现;
- ABA问题:如果一个线程初次读取时的值为A,并且在准备赋值的时候检查该值仍然是A,但是可能在这两次操作操作之间,有另外一个线程将变量的值改成了B,然后又将该值改回为A,那么CAS会误认为该变量没有变化过。
3.3 ABA问题解决方案
可以使用AtomicStampedReference或者AtomicMarkableReference来解决CAS的ABA问题,思路类似于SVN版本号,SpringBoot热部署中trigger.txt。思路:
- AtomicStampedReference解决方案:每次修改都会让stamp值加一,类似于版本控制号。
/**
* ABA问题解决方案,AtomicStampedReference
*/
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();
}
}
- AtomicMarkableReference:如果不关心引用变量中途被修改了多少次,而只是关心是否被修改过,可以使用AtomicMarkableReference:
/**
* ABA问题解决方案,AtomicMarkableReference
*/
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());
}
}
}
3.3 补充:CAS算法实际使用
在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());
}
4、 集合类并发异常
Java普通集合类一般情况下都是线程不安全的,没有加锁实现,抛出并发异常主要是由两个modCount和expectedModCount不匹配造成的。
4.1 ArrayList线程安全问题
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(); } }}
解决方案:
- 使用Vector类(使用了synchrinized,加了锁,影响性能,效率极低
- 使用Colections.synchronizedList(new ArrayList<>()):内部直接将接受的List对象的调用方法都是直接调用输入List对象的方法,但是加了Synchronized,类似装饰器模式,也是对输入List的一种增强:
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:写时复制是一种读写分离,在并发读的时候不需要加锁,因为它能够保证并发读的情况下不会添加任何元素。而在并发写的情况下,需要先加锁,但是并不直接对当前容器进行写操作。而是先将先前容器进行复制获取一个新的容器,进行完并发写操作之后,当之前指向原容器的引用指向当前新容器。也就是说,并发读和并发写时针对不同结合,因此不会产生并发异常。
// CopyOnWriteArrayList.javapublic 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];}
4.2 HashSet线程安全问题
HashSet底层就是一个HashMap,默认的HashSet是一个初始大小为16,负载因子为0.75的HashMap:
/** * Constructs a new, empty set; the backing <tt>HashMap</tt> 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 Mapprivate static final Object PRESENT = new Object();public boolean add(E e) { return map.put(e, PRESENT)==null;}
所以HashSet的多线程安全问题实际上就是HashMap的多线程安全问题:
/** * HashSet多线程不安全问题 * HashSet底层就是HashMap,因此这个案例也是HashMap多线程不安全问题的演示 */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(); } }}
解决方案:
- Collections集合类的static方法SynchronizedSet
- CopyOnWriteArraySet:也是写时复制思想,但是内部还是使用CopyOnWriteArrayList实现:
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>(); } // ...}
4.3 HashMap多线程安全的解决方案
相比于HashSet,HashMap除了可以使用Collections集合类的synchronizedMap方法外,还可以使用juc包下ConcurrentHashMap类。
5、 Java多线程锁
5.1 公平锁与非公平锁
- 定义:
- 公平锁:多个线程按照申请锁的顺序来获取锁,按照FIFO规则从等待队列中拿到等待线程获取响应锁;
- 非公平锁:多个线程并不是按照申请锁的顺序来获取锁,有可能出现后申请锁的线程先申请到锁。在高并发环境下,非公平锁有可能造成优先级反转或者饥饿的现象。如果非公平锁抢占失败,就要继续采取类似公平锁的机制。非公平锁的优点在于吞吐量大。
- 常见的非公平锁:
- ReentrantLock可以通过指定构造函数的boolean类型来获取公平/非公平锁,默认情况下是非公平锁。
- 对于synchronized而言,也是一种非公平锁。
5.2 可重入锁(递归锁)
- 定义:可重入锁的定义是类比递归的定义来理解。指在同一个线程外层函数获得锁之后,内层递归函数仍然能够获取该锁的代码,即进入内层函数时会自动获取锁。也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块。一个同步方法内部仍然存在一个同步方法,那么可以进入内层同步方法,且内存同步方法和外层同步方法持有的是同一把锁。
- 具体看一个案例来理解可重入锁:synchronized就是可重入锁,现在问题是synchronized块中能够使用System.out.println()方法吗?
public void println(String x) { // println方法内部使用了synchronized synchronized (this) { print(x); newLine(); }}/** * 演示可重入锁 */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锁住的是同一个对象?
可重入锁的含义就是最后一种:正常执行,因为可重入锁,锁的是同一个对象。
- 典型的可重入锁:ReentrantLock && synchronize关键字
- 作用:最大作用是防止死锁,因为多层嵌套的是同一个对象,另一个含义是:嵌套方法持有同一把锁。
- 具体事例:
/** * 可重入锁演示 */// 演示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()数量一定要匹配,否则:
- 当lock.lock()数量>lock.unlock:程序一直运行
- 当lock.lock()数量<lock.unlock()抛出java.langLLLegalMonitorStateException异常
5.3 自旋锁(SpinLock)
自旋锁尝试获取的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是避免线程上下文切换的消耗,缺点是如果一直自旋会消耗CPU;
/** * 自旋锁演示 */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(); }}
5.4 读写锁
- 写锁(独占锁):指该锁一次只能被一个线程所持有,ReentrantLock和Synchronized都是独占锁;
- 读锁(共享锁):指该锁可以被多个线程所持有
- 读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程都是互斥的。
/** * 演示读写锁 */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(); } }}
5.5 CountDownLatch
CountDownLatch是一个计数器闭锁,它通过一个初始化定时器latch,在latch的值被减到0之前,其他线程都会被await()方法阻塞。以模拟火箭发射过程解释CountDownLatch使用:
/** * CountDownLatch模拟火箭发射过程: * 火箭发射之前需要十个线程进行前期检查工作,每个线程耗时0-4s, * 只有10个线程对应的检查工作全部完成后,火箭才能发射 */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(); } }}
5.6 CyclicBarrier
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时候才会召唤神龙 */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(); }}
5.7 Semaphore
Semaphore信号量主要有两个目的:
- 用于多个共享资源的互斥使用;
- 用于并发数量的控制(是synchronized的加强版,当并发数量为1时就退化成synchronized);
主要方法:
- Semaphore(int permits):构造函数,允许控制的并发数量;
- acquire():请求一个信号量,导致信号量的数量减一;
- release():释放一个信号量,信号量加一;
示例:使用Semaphore模拟请车位过程(3个车位,10辆车):
/** * 使用Semaphore模拟抢车位过程(3个车位,10辆车) * 任意时刻只有3辆车持有线程 */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(); } }}
6、 阻塞队列
阻塞是一个队列,当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞;当队列满时,往队列中添加元素的操作将会被阻塞。
6.1 BlockingQueue
BlockingQueue让我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程。继承树:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列🌟
- LinkedBlockingQueue:由链表结构组成的有界阻塞(默认大小Interger.Max_VALUE())队列🌟
- ProiorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先队列实现延迟无界阻塞队列
- SynchronizedQueue:不存储元素的阻塞队列,也即单个元素的队列🌟
- LinkedTransferQueue:由链表组成的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列🌟
四组API:
- 抛出异常组:
- add():当阻塞队列满时,抛出异常
- remove():当队列为空时,抛出异常
- element():取出队列开头元素
- null&false返回值:
- offer():当阻塞队列满时,返回false
- poll():当阻塞队列为空时,返回null
- peek():返回阻塞队列开头元素,如果队列为空,返回null
- 一直阻塞:
- put():当阻塞队列满时,队列会一直阻塞直到队列有数据被拿走或者响应中断
- take():当阻塞队列为空时,队列会一直阻塞直到队列中有新数据被放入或者响应中断
- 超时退出:
- offer(e,time,unit):当阻塞队列满时,会阻塞生产者线程一段时间,超出时间限制后生产者线程退出
- poll(time,unit):当阻塞队列为空时,会阻塞消费者一段时间,超出时间限制后消费者线程退出
6.2 SynchronizedQueue
与其他BlockingQueue不同,SynchronizedQueue是一个不存储元素的BlockingQueue,每一个put操作必须等待一个take操作,否则就不能继续添加元素,反之亦然。总之SynchronizedQueue,生产一个,消费一个。
示例:使用SynchronizedQueue完成生产者-消费者模式,并且该生产者-消费者模式必须是严格生产一个,消费一个:
/** * 使用SynchronizedQueue完成生产者-消费者模型,并且是严格生产一个——消费一个 */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(); }}
6.3 三种生产者-消费者实现方式
实现生产者-消费者模型,要注意:
- 1.线程操作资源类
- 2.先判断,再执行,最后返回通知
- 3.while替代if防止虚假唤醒
Synchronized-notifyAll-wait实现:
/** * 生产者-消费者实现方式一:使用synchronized-notifyAll-wait */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实现 */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(); }}
阻塞队列实现:
/** * 生产者-消费者实现方式三:使用阻塞队列 */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(); }}
7、 synchronized & lock
synchronized和lock的区别:
- 原始构成方面:
- synchronized属于JVM层面,底层通过monitorenter和monitorexit两个指令实现;
- lock是JUC提供的具体类,是API层面的东西;
- 用法方面:
- synchronized不需要用户手动释放锁,当synchronized代码执行完毕之后会自动让线程释放持有的锁;
- lock需要一般使用try-finally模式去手动释放锁,否则容易出现死锁或者程序不终止现象;
- 等待是否可中断:
- synchronized是不可中断的,除非抛出异常或者程序正常退出;
- lock可中断:
- 设置超时方法tryLock(time,unit);
- 使用lockInterruptibly,调用iterrupt()方法可中断;
- 是否公平锁:
- synchronized是非公平锁;
- lock默认是非公平锁,但是可以通过构造函数传入boolean类型值更改是否为公平锁;
- 锁是否能绑定多个条件(condition)🌟:
- synchronized没有condition的说法,要么唤醒所有线程,要么随机唤醒一个线程;
- lock可以使用condition实现分组唤醒需要唤醒的线程,实现精准唤醒;
lock(condition)精准唤醒示例:
多个线程之间顺序调用:有三个线程A、B、C,要求:
- A线程打印AAA一次,紧接着B线程打印BBB三次,最后C线程打印CCC五次;
- 保证以上顺序,总打印十轮;
/** * 使用lock(condition)实现线程的精准唤醒 * 多个线程之间顺序调用:有三个线程A、B、C,要求: * - A线程打印AAA1次,紧接着B线程打印BBB3次,最后C线程打印CCC5次; * - 保证以上顺序,总共打印10轮; */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(); }}
8、 Callable接口
8.1 Callable接口介绍
Java创建线程池的四种方式:
- 继承Thread类:浪费仅有一次的继承机会;
- 实现Runnable接口:无返回值
- 实现Callable接口:可以有返回值,可抛出异常,可中断
- 通过线程池创建:线程复用,但是实际开发中应该自己实现线程池
Callable接口通过Future Task接口的适配:
FutureTask是Future Task接口的适配:
FutureTask是Future、Runnable接口的实现类,其构造函数接受Callabble接口参数,FUture接口主要的方法有:
- isCancel()
- isDone()
- get()
也正因为Future接口的这些方法,相对于 Runnable接口而言,Callable接口的优势在于:
- 判断线程任务是否完成或者取消
- 能够中断任务
- 能够获得返回结果,且可以抛出异常
8.2 Callable接口使用
FutureTask类的构造函数接受Callable接口参数,并且FutureTask类本身是Runnable接口的子类,因此通过FutureTask类可以很好地适配Callable:
/** * 演示Callable接口的使用 */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); }}
注意:
- get()方法尽量推迟使用,过早使用可能会阻塞主线程;
- 判断call()方法是否已经结束可以通过while&isDone方法,类似于自旋锁形式
- ⭐️多个线程同用一个Future Task对象,对应的Call()方法只会被执行一次
9、 线程池
9.1 线程池优势
线程池最主要的工作在于控制运行线程的数量,从而做到线程复用、控制最大并发数量、管理线程。其具体的优势在于:
- 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗;
- 提高响应速度:当任务到达时,任务可以不需要等到线程创建就能执行;
- 提高线程的可管理性:线程是稀缺资源,不能无限创建,否则会消耗系统资源、降低系统的稳定性,使用线程可以进行统一分配,调优和监控;
9.2 线程池使用
线程池相关的继承结构:
使用线程池的三种常见方式:
- Executors.newFixedThreadPoll(int):创建一个固定线程数量的线程池,可控制线程最大并发数,超出的线程需要在队列中等待。注意他内部corePoolSize和maximumPoolSuze的值是相等,并且使用的是LinkedBlockingQueue:
public static ExecutorService newFixedThreadPool(int nThreads){ return new ThreadPoolExecutor(nThreads,nThreads,0L,TImeUnit.MILLSECONDS, new LinkedBlockingQueue<Runnable>());}
- Executors.newSingleThreadExecutor():创建一个单线程的线程池,他只有唯一的线程来执行任务,保证所有任务按照指定顺序执行。注意它内部corePoolSize和maximumPoolSize的值都为1,它使用的是LinkedBlockingQueue:
@NotNull public static ExecutorService newSingleThreadExecutorService newSingleThreadExecutor(){ return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,oL,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));}
- Executors.newCachedThreadPool():创建一个可缓存的线程池,如果线程长度超过处理需要,可灵活回收空闲线程,若无可回收线程,则创建新线程。注意它内部将corePoolSize值设置为Integer.MAX_VALUE,并且使用的是SynchronizedQueue,keepAliveTime值为60,即当线程空闲时间超过60秒,就销毁线程:
@NotNull public static ExecutorService newCachedThreadPool(){ return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());}
注意:
- 以上三种创建线程的方式内部都是由ThreadPoolExecutor这个类完成的,该类的构造方法有五个参数,称为线程池的五大参数(还有另外两个参数);
- 线程池使用完毕之后需要关闭,应该配合try-finally代码块,将线程池关闭的代码放在finallly代码块中。
9.3 线程池的七大参数
ThreadPoolExecutor对构造函数进行了重载,实际内部使用率七个参数:
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecution handler){}
- corePoolSize:线程池中常驻核心线程池
- maximumPoolSize:线程池中能够容纳同时执行最大线程数,该值必须大于等于一
- keepAliceTime:多余线程的最大存活时间
- unit:keepAliveTime的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:生成线程池中工作线程的线程工厂,一般使用默认即可
- handler:拒绝策略,表示当任务队列满并且工作线程大于等于线程池的最大线程数时,对即将到来的线程的拒绝策略。
9.4 线程池底层原理
线程池具体工作流程:
- 在创建线程后,等待提交过来的任务请求
- 当调用execute().submit()方法添加一个请求任务时,线程池会做出以下判断:
- 如果正在运行的线程数量小于corePoolSize,会立刻创建线程运行该任务;
- 如果正在运行的线程数量大于等于corePoolSize,会将任务放在阻塞队列中
- 如果队列也满足但是正在运行的线程数量小于maximumPoolSize,线程池会进行扩展,将线程池中的线程数拓展到最大线程数。
- 如果队列满并且运行的线程数量大于等于maximumPoolSize,那么线程池会启动相应的拒绝策略来拒绝相应的任务请求;
- 当一个线程完成任务时,它会从队列中取下一个任务来执行
- 当一个线程空闲时间超过给定的keepAliceTime时,线程会做出判断;
- 如果当前运行线程大于corePoolSize,那么该线程将会被停止。也就是说,当线程池的所有任务都完成之后,它会收缩到corePoolSize的大小
9.5 线程池的拒绝策略
当线程池的阻塞队列满了同时线程池中线程数量达到了最大maximumPoolSize时,线程池将会启动相应的拒绝策略来拒绝请求任务。四种拒绝策略具体为:
- AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
- CallerRunsPolicy:调用者运行的一种机制,该策略既不会抛弃任务,也不会抛出异常而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果任务允许丢失,那么该策略是最好的方案
注意:
- 以上四种拒绝策略均实现了RejectedExecutionHandler接口
- 实际开发中不允许使用内置的线程池:必须明确地通过THreadPoolExecutor方式,指定相应的线程池参数创建自定义线程或者使用其他框架提供的线程池。因为内置线程池的第五个参数阻塞队列允许的请求队列长度为Integer.MAX_Value,可能造成大量请求堆积,导致OOM:
自定义线程池:使用不同的拒绝策略:
/** * 自定义线程池的各个参数 */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(); } }}
9.6 线程池配置合理线程数量
线程池合理配置线程数量需要考虑业务具体是CPU密集型还是IO密集型:
- CPU密集型:该任务需要大量运算,而没有阻塞,CPU一直在全速运行,CPU密集型只有在真正的多核CPU上才能进行加锁。CPU密集型任务配置应该尽可能少的线程数量,一般公式为:
CPU核数 + 1个线程的线程池
- IO密集型:任务需要大量的IO操作,即大量的阻塞,在单线程上进行IO密集型的任务会浪费大量的CPU运算能力在等待操作上。所以IO密集型任务中使用多线程可以大大加速程序运行:
CPU核数 / (1 - 阻塞系数)·(阻塞系数在0.8 - 0.9)CPU核数 * 2
10、 死锁的产生和定位
10.1 死锁的产生
两个及以上的线程在执行过程中,因为互相争夺资源而造成一种相互等待的现象。如果外力干涉,那么它们都将无法进行下去:
/** * 演示死锁的产生 */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(); }}
10.2 死锁的定位
Java提供了类似于PS的命令jps来查看当前Java程序及其进程号,再通过命令hstack能够查看具体进程号的Java程序栈使用情况:
- jps -l:有一个pid为18064的Java线程
- jstack pid:使用jstack 18064继续查看该Java进程具体信息: