多线程
1. 常见概念:
1. yield,join,notify和notifyAll
- yield(方法是停止当前线程,让同等优先权的线程或更高优先级的线程有执行的机会。如果没有的话,那么yield方法将不会起作用,并且由可执行状态后马上又被执行。
- join方法是用于在某一个线程的执行过程中调用另一个线程执行,等到被调用的线程执行结束后,再继续执行当前线程。如:t.join();//主要用于等待t线程运行结束,若无此句, main则会执行完毕,导致结果不可预测.
- notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。
- notifyAll会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现
2. 请你谈谈对volatile的理解
Package java.util.concurrent
---> AtomicInteger
Lock
ReadWriteLock
1、volatile是java虚拟机提供的轻量级的同步机制保证可见性、不保证原子性、禁止指令重排
-
保证可见性
当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值
当不添加volatile关键字时示例:
package com.jian8.juc; import java.util.concurrent.TimeUnit; /** * 1验证volatile的可见性 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 */ public class VolatileDemo { public static void main(String[] args) { visibilityByVolatile();//验证volatile的可见性 } /** * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 */ public static void visibilityByVolatile() { MyData myData = new MyData(); //第一个线程 new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t come in"); try { //线程暂停3s TimeUnit.SECONDS.sleep(3); myData.addToSixty(); System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num); } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } }, "thread1").start(); //第二个线程是main线程 while (myData.num == 0) { //如果myData的num一直为零,main线程一直在这里循环 } System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num); } } class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this.num = 60; } }
输出结果:
thread1 come in thread1 update value:60 //线程进入死循环
当我们加上
volatile
关键字后,volatile int num = 0;
输出结果为:thread1 come in thread1 update value:60 main mission is over, num value is 60 //程序没有死循环,结束执行
-
不保证原子性
原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
验证示例(变量添加volatile关键字,方法不添加synchronized):
package com.jian8.juc; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; /** * 1验证volatile的可见性 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰 * 1.2 添加了volatile,可以解决可见性 * * 2.验证volatile不保证原子性 * 2.1 原子性指的是什么 * 不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败 */ public class VolatileDemo { public static void main(String[] args) { // visibilityByVolatile();//验证volatile的可见性 atomicByVolatile();//验证volatile不保证原子性 } /** * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改 */ //public static void visibilityByVolatile(){} /** * volatile不保证原子性 * 以及使用Atomic保证原子性 */ public static void atomicByVolatile(){ MyData myData = new MyData(); for(int i = 1; i <= 20; i++){ new Thread(() ->{ for(int j = 1; j <= 1000; j++){ myData.addSelf(); myData.atomicAddSelf(); } },"Thread "+i).start(); } //等待上面的线程都计算完成后,再用main线程取得最终结果值 try { TimeUnit.SECONDS.sleep(4); } catch (InterruptedException e) { e.printStackTrace(); } while (Thread.activeCount()>2){ Thread.yield(); } System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num); System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger); } } class MyData { // int num = 0; volatile int num = 0; public void addToSixty() { this.num = 60; } public void addSelf(){ num++; } AtomicInteger atomicInteger = new AtomicInteger(); public void atomicAddSelf(){ atomicInteger.getAndIncrement(); } }
执行三次结果为:
//1. main finally num value is 19580 main finally atomicnum value is 20000 //2. main finally num value is 19999 main finally atomicnum value is 20000 //3. main finally num value is 18375 main finally atomicnum value is 20000 //num并没有达到20000
-
禁止指令重排
有序性:在计算机执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种
graph LR 源代码 --> id1["编译器优化的重排"] id1 --> id2[指令并行的重排] id2 --> id3[内存系统的重排] id3 --> 最终执行的指令 style id1 fill:#ff8000; style id2 fill:#fab400; style id3 fill:#ffd557;单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排顺序是必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
重排代码实例:
声明变量:
int a,b,x,y=0
线程1 线程2 x = a; y = b; b = 1; a = 2; 结 果 x = 0 y=0 如果编译器对这段程序代码执行重排优化后,可能出现如下情况:
线程1 线程2 b = 1; a = 2; x= a; y = b; 结 果 x = 2 y=1 这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的
volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化。如果在之零件插入一i奥Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
graph TB subgraph bbbb["对Volatile变量进行读操作时,<br>回在读操作之前加入一条load屏障指令,<br>从内存中读取共享变量"] ids6[Volatile]-->red3[LoadLoad屏障] red3-->id7["禁止下边所有普通读操作<br>和上面的volatile读重排序"] red3-->red4[LoadStore屏障] red4-->id9["禁止下边所有普通写操作<br>和上面的volatile读重排序"] red4-->id8[普通读] id8-->普通写 end subgraph aaaa["对Volatile变量进行写操作时,<br>回在写操作后加入一条store屏障指令,<br>将工作内存中的共享变量值刷新回到主内存"] id1[普通读]-->id2[普通写] id2-->red1[StoreStore屏障] red1-->id3["禁止上面的普通写和<br>下面的volatile写重排序"] red1-->id4["Volatile写"] id4-->red2[StoreLoad屏障] red2-->id5["防止上面的volatile写和<br>下面可能有的volatile读写重排序"] end style red1 fill:#ff0000; style red2 fill:#ff0000; style red4 fill:#ff0000; style red3 fill:#ff0000; style aaaa fill:#ffff00; style bbbb fill:#ffff00;
2、JMM(java内存模型)
JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,他描述的时一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁时同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的成为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是贡献内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先概要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存的变量副本拷贝,因此不同的线程件无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,期间要访问过程如下图:
- 可见性
- 原子性
- 有序性
3、你在那些地方用过volatile
当普通单例模式在多线程情况下:
public class SingletonDemo {
private static SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName() + "\t 构造方法SingletonDemo()");
}
public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
public static void main(String[] args) {
//构造方法只会被执行一次
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
// System.out.println(getInstance() == getInstance());
//并发多线程后,构造方法会在一些情况下执行多次
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonDemo.getInstance();
}, "Thread " + i).start();
}
}
}
其构造方法在一些情况下会被执行多次
解决方式:
-
单例模式DCL代码
DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断
public static SingletonDemo getInstance() { if (instance == null) { synchronized (SingletonDemo.class) { if (instance == null) { instance = new SingletonDemo(); } } } return instance; }
大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次
DCL(双端检锁)机制不一定线程安全,原因时有指令重排的存在,加入volatile可以禁止指令重排
原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能没有完成初始化。instance=new SingleDemo();可以被分为一下三步(伪代码):
memory = allocate();//1.分配对象内存空间 instance(memory); //2.初始化对象 instance = memory; //3.设置instance执行刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化时允许的,如果3步骤提前于步骤2,但是instance还没有初始化完成
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。
所以当一条线程访问instance不为null时,由于instance示例未必已初始化完成,也就造成了线程安全问题。
-
单例模式volatile代码
为解决以上问题,可以将SingletongDemo实例上加上volatile
private static volatile SingletonDemo instance = null;
3. CAS你知道吗
1、compareAndSet----比较并交换
AtomicInteger.conpareAndSet(int expect, int update)
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
第一个参数为期望值,如果期望值一致,进行update赋值,如果期望值不一致,证明数据被修改过,返回fasle,取消赋值
例子:
package com.jian8.juc.cas;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 1.CAS是什么?
* 1.1比较并交换
*/
public class CASDemo {
public static void main(String[] args) {
checkCAS();
}
public static void checkCAS(){
AtomicInteger atomicInteger = new AtomicInteger(5);
System.out.println(atomicInteger.compareAndSet(5, 2019) + "\t current data is " + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2014) + "\t current data is " + atomicInteger.get());
}
}
输出结果为:
true current data is 2019
false current data is 2019
2、CAS底层原理?对Unsafe的理解
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止
-
atomicInteger.getAndIncrement();
public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
-
Unsafe
-
是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存数据。Unsafe类存在于
sun.misc
包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
-
变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的
-
变量value用volatile修饰,保证多线程之间的可见性
-
-
CAS是什么
CAS全称呼Compare-And-Swap,它是一条CPU并发原语
他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。
//unsafe.getAndAddInt public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
var1 AtomicInteger对象本身
var2 该对象的引用地址
var4 需要变动的数据
var5 通过var1 var2找出的主内存中真实的值
用该对象前的值与var5比较;
如果相同,更新var5+var4并且返回true,
如果不同,继续去之然后再比较,直到更新完成
3、CAS缺点
-
循环时间长,开销大
例如getAndAddInt方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销
-
只能保证一个共享变量的原子操作
对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
-
ABA问题
4. 原子类AtomicInteger的ABA问题?原子更新引用?
1、ABA如何产生
CAS算法实现一个重要前提需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
比如线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。
尽管线程1的CAS操作成功,但是不代表这个过程没有问题
2、如何解决?原子引用
示例代码:
package juc.cas;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import java.util.concurrent.atomic.AtomicReference;
public class AtomicRefrenceDemo {
public static void main(String[] args) {
User z3 = new User("张三", 22);
User l4 = new User("李四", 23);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
}
}
@Getter
@ToString
@AllArgsConstructor
class User {
String userName;
int age;
}
输出结果
true User(userName=李四, age=23)
false User(userName=李四, age=23)
3、时间戳的原子引用
新增机制,修改版本号
package com.jian8.juc.cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* ABA问题解决
* AtomicStampedReference
*/
public class ABADemo {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
System.out.println("=====以下时ABA问题的产生=====");
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}, "Thread 1").start();
new Thread(() -> {
try {
//保证线程1完成一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "Thread 2").start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("=====以下时ABA问题的解决=====");
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
}, "Thread 3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
}, "Thread 4").start();
}
}
输出结果:
=====以下时ABA问题的产生=====
true 2019
=====以下时ABA问题的解决=====
Thread 3 第1次版本号1
Thread 4 第1次版本号1
Thread 3 第2次版本号2
Thread 3 第3次版本号3
Thread 4 修改是否成功false 当前最新实际版本号:3
Thread 4 当前最新实际值:100
5. 我们知道ArrayList是线程不安全的,请编写一个不安全的案例并给出解决方案
HashSet与ArrayList一致 HashMap
HashSet底层是一个HashMap,存储的值放在HashMap的key里,value存储了一个PRESENT的静态Object对象
1. 线程不安全
package com.jian8.juc.collection;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 集合类不安全问题
* ArrayList
*/
public class ContainerNotSafeDemo {
public static void main(String[] args) {
notSafe();
}
/**
* 故障现象
* java.util.ConcurrentModificationException
*/
public static void notSafe() {
List<String> list = new ArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, "Thread " + i).start();
}
}
}
报错:
Exception in thread "Thread 10" java.util.ConcurrentModificationException
2. 导致原因
并发正常修改导致
一个人正在写入,另一个同学来抢夺,导致数据不一致,并发修改异常
3. 解决方法:CopyOnWriteArrayList
List<String> list = new Vector<>();//Vector线程安全
List<String> list = Collections.synchronizedList(new ArrayList<>());//使用辅助类
List<String> list = new CopyOnWriteArrayList<>();//写时复制,读写分离
Map<String, String> map = new ConcurrentHashMap<>();
Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
CopyOnWriteArrayList.add方法:
CopyOnWrite容器即写时复制,往一个元素添加容器的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行copy,复制出一个新的容器Object[] newElements,让后新的容器添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements),这样做可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素,所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器
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();
}
}
6. 公平锁、非公平锁、可重入锁、递归锁、自旋锁?手写自旋锁
1、公平锁、非公平锁
-
是什么
公平锁就是先来后到、非公平锁就是允许加塞,
Lock lock = new ReentrantLock(Boolean fair);
默认非公平。-
公平锁是指多个线程按照申请锁的顺序来获取锁,类似排队打饭。
-
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者节现象。
-
-
两者区别
-
公平锁:公平锁,就是很公平,在并发环境中,每个线程在获取锁时,会先查看此锁维护的等待队列,如果为空,或者当前线程就是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己。
-
非公平锁:非公平锁比较粗鲁,上来就直接尝试占有额,如果尝试失败,就再采用类似公平锁那种方式。
-
-
other
对Java ReentrantLock而言,通过构造函数指定该锁是否公平,默认是非公平锁,非公平锁的优点在于吞吐量比公平锁大
对Synchronized而言,是一种非公平锁
2、可重入锁(递归锁)
-
递归锁是什么
指的时同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁所同步着的代码块
-
ReentrantLock/Synchronized 就是一个典型的可重入锁
-
可重入锁最大的作用是避免死锁
-
代码示例
package com.jian8.juc.lock; public static void main(String[] args) { Phone phone = new Phone(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "Thread 1").start(); new Thread(() -> { try { phone.sendSMS(); } catch (Exception e) { e.printStackTrace(); } }, "Thread 2").start(); } } class Phone{ public synchronized void sendSMS()throws Exception{ System.out.println(Thread.currentThread().getName()+"\t -----invoked sendSMS()"); Thread.sleep(3000); sendEmail(); } public synchronized void sendEmail() throws Exception{ System.out.println(Thread.currentThread().getName()+"\t +++++invoked sendEmail()"); } }
package com.jian8.juc.lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockDemo {
public static void main(String[] args) {
Mobile mobile = new Mobile();
new Thread(mobile).start();
new Thread(mobile).start();
}
}
class Mobile implements Runnable{
Lock lock = new ReentrantLock();
@Override
public void run() {
get();
}
public void get() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName()+"\t invoked get()");
set();
}finally {
lock.unlock();
}
}
public void set(){
lock.lock();
try{
System.out.println(Thread.currentThread().getName()+"\t invoked set()");
}finally {
lock.unlock();
}
}
}
#### 3、独占锁(写锁)/共享锁(读锁)/互斥锁
1. **概念**
- **独占锁**:指该锁一次只能被一个线程所持有,**对ReentrantLock和Synchronized而言都是独占锁**
- **共享锁**:只该锁可被多个线程所持有
**ReentrantReadWriteLock**其**读锁是共享锁,写锁是独占锁**
- **互斥锁**:读锁的共享锁可以保证并发读是非常高效的,读写、写读、写写的过程是互斥的
2. **代码示例**
```java
package com.jian8.juc.lock;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 多个线程同时读一个资源类没有任何问题,所以为了满足并发量,读取共享资源应该可以同时进行。
* 但是
* 如果有一个线程象取写共享资源来,就不应该*其他线程可以对资源进行读或写
* 总结
* 读读能共存
* 读写不能共存
* 写写不能共存
*/
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCache myCache = new MyCache();
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.put(tempInt + "", tempInt + "");
}, "Thread " + i).start();
}
for (int i = 1; i <= 5; i++) {
final int tempInt = i;
new Thread(() -> {
myCache.get(tempInt + "");
}, "Thread " + i).start();
}
}
}
class MyCache {
private volatile Map<String, Object> map = new HashMap<>();
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* 写操作:原子+独占
* 整个过程必须是一个完整的统一体,中间不许被分割,不许被打断
*
* @param key
* @param value
*/
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在写入:" + key);
TimeUnit.MILLISECONDS.sleep(300);
map.put(key, value);
System.out.println(Thread.currentThread().getName() + "\t写入完成");
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.writeLock().unlock();
}
}
public void get(String key) {
rwLock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "\t正在读取:" + key);
TimeUnit.MILLISECONDS.sleep(300);
Object result = map.get(key);
System.out.println(Thread.currentThread().getName() + "\t读取完成: " + result);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
public void clear() {
map.clear();
}
}
4、自旋锁
自旋锁了解吗?会有什么问题?
-
spinlock
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
手写自旋锁:
package com.jian8.juc.lock; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; /** * 实现自旋锁 * 自旋锁好处,循环比较获取知道成功位置,没有类似wait的阻塞 * * 通过CAS操作完成自旋锁,A线程先进来调用mylock方法自己持有锁5秒钟,B随后进来发现当前有线程持有锁,不是null,所以只能通过自旋等待,知道A释放锁后B随后抢到 */ public class SpinLockDemo { public static void main(String[] args) { SpinLockDemo spinLockDemo = new SpinLockDemo(); new Thread(() -> { spinLockDemo.mylock(); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e){ e.printStackTrace(); } spinLockDemo.myUnlock(); }, "Thread 1").start(); try { TimeUnit.SECONDS.sleep(3); }catch (Exception e){ e.printStackTrace(); } new Thread(() -> { spinLockDemo.mylock(); spinLockDemo.myUnlock(); }, "Thread 2").start(); } //原子引用线程 AtomicReference<Thread> atomicReference = new AtomicReference<>(); public void mylock() { Thread thread = Thread.currentThread(); System.out.println(Thread.currentThread().getName() + "\t come in"); while (!atomicReference.compareAndSet(null, thread)) { } } public void myUnlock() { Thread thread = Thread.currentThread(); atomicReference.compareAndSet(thread, null); System.out.println(Thread.currentThread().getName()+"\t invoked myunlock()"); } }
5. 乐观锁和悲观锁
7. CountDownLatch/CyclicBarrier/Semaphore使用过吗
1、CountDownLatch(火箭发射倒计时)
-
它允许一个或多个线程一直等待,知道其他线程的操作执行完后再执行。例如,应用程序的主线程希望在负责启动框架服务的线程已经启动所有的框架服务之后再执行
-
CountDownLatch主要有两个方法,当一个或多个线程调用await()方法时,调用线程会被阻塞。其他线程调用countDown()方法会将计数器减1,当计数器的值变为0时,因调用await()方法被阻塞的线程才会被唤醒,继续执行
-
代码示例:
package com.jian8.juc.conditionThread; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; public class CountDownLatchDemo { public static void main(String[] args) throws InterruptedException { // general(); countDownLatchTest(); } public static void general(){ for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t上完自习,离开教室"); }, "Thread-->"+i).start(); } while (Thread.activeCount()>2){ try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName()+"\t=====班长最后关门走人"); } public static void countDownLatchTest() throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(6); for (int i = 1; i <= 6; i++) { new Thread(() -> { System.out.println(Thread.currentThread().getName()+"\t被灭"); countDownLatch.countDown(); }, CountryEnum.forEach_CountryEnum(i).getRetMessage()).start(); } countDownLatch.await(); System.out.println(Thread.currentThread().getName()+"\t=====秦统一"); } }
2、CyclicBarrier(集齐七颗龙珠召唤神龙)
-
CycliBarrier
可循环(Cyclic)使用的屏障。让一组线程到达一个屏障(也可叫同步点)时被阻塞,知道最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CycliBarrier的await()方法。
-
代码示例:
package com.jian8.juc.conditionThread; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class CyclicBarrierDemo { public static void main(String[] args) { cyclicBarrierTest(); } public static void cyclicBarrierTest() { CyclicBarrier cyclicBarrier = new CyclicBarrier(7, () -> { System.out.println("====召唤神龙====="); }); for (int i = 1; i <= 7; i++) { final int tempInt = i; new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t收集到第" + tempInt + "颗龙珠"); try { cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }, "" + i).start(); } } }
3、Semaphore信号量
可以代替Synchronize和Lock
-
信号量主要用于两个目的,一个是用于多个共享资源的互斥作用,另一个用于并发线程数的控制
-
代码示例:
抢车位示例:
package com.jian8.juc.conditionThread; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class SemaphoreDemo { public static void main(String[] args) { Semaphore semaphore = new Semaphore(3);//模拟三个停车位 for (int i = 1; i <= 6; i++) {//模拟6部汽车 new Thread(() -> { try { semaphore.acquire(); System.out.println(Thread.currentThread().getName() + "\t抢到车位"); try { TimeUnit.SECONDS.sleep(3);//停车3s } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "\t停车3s后离开车位"); } catch (InterruptedException e) { e.printStackTrace(); } finally { semaphore.release(); } }, "Car " + i).start(); } } }
8. 阻塞队列
- ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序
- LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue
- SynchronousQueue是一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于
1、队列和阻塞队列
-
首先是一个队列,而一个阻塞队列再数据结构中所起的作用大致如下图
graph LR Thread1-- put -->id1["阻塞队列"] subgraph BlockingQueue id1 end id1-- Take -->Thread2 蛋糕师父--"放(柜满阻塞)"-->id2[蛋糕展示柜] subgraph 柜 id2 end id2--"取(柜空阻塞)"-->顾客线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
当阻塞队列是空是,从队列中获取元素的操作会被阻塞
当阻塞队列是满时,从队列中添加元素的操作会被阻塞
试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。
试图往已满的阻塞队列中添加新元素的线程同样会被阻塞,直到其他的线程从列中移除一个或者多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增
2、为什么用?有什么好处?
-
在多线程领域:所谓阻塞,在某些情况下会挂起线程,一旦满足条件,被挂起的线程又会自动被唤醒
-
为什么需要BlockingQueue
好处时我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须自己控制这些细节,尤其还要兼顾效率和线程安全,而这回给我们程序带来不小的复杂度
3、BlockingQueue的核心方法
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
方法类型 | status |
---|---|
抛出异常 | 当阻塞队列满时,再往队列中add会抛IllegalStateException: Queue full 当阻塞队列空时,在网队列里remove会抛 NoSuchElementException
|
特殊值 | 插入方法,成功true失败false 移除方法,成功返回出队列的元素,队列里没有就返回null |
一直阻塞 | 当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞线程知道put数据或响应中断退出 当阻塞队列空时,消费者线程试图从队列take元素,队列会一直阻塞消费者线程知道队列可用。 |
超时退出 | 当阻塞队列满时,队列会阻塞生产者线程一定时间,超过限时后生产者线程会退 |
4、架构梳理+种类分析
-
种类分析
- ArrayBlockingQueue:由数据结构组成的有界阻塞队列。
-
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为
Integer.MAX_VALUE
)阻塞队列。 - PriorityBlockingQueue:支持优先级排序的*阻塞队列。
- DelayQueue:使用优先级队列实现的延迟*阻塞队列。
- SychronousQueue:不存储元素的阻塞队列,也即单个元素的队列。
- LinkedTransferQueue:由链表结构组成的*阻塞队列。
- LinkedBlockingDeque:由历览表结构组成的双向阻塞队列。
-
SychronousQueue
-
理论:SynchronousQueue没有容量,与其他BlockingQueue不同,SychronousQueue是一个不存储元素的BlockingQueue,每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然。
-
代码示例
package com.jian8.juc.queue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.TimeUnit; /** * ArrayBlockingQueue是一个基于数组结构的有界阻塞队列,此队列按FIFO原则对元素进行排序 * LinkedBlockingQueue是一个基于链表结构的阻塞队列,此队列按FIFO排序元素,吞吐量通常要高于ArrayBlockingQueue * SynchronousQueue是一个不存储元素的阻塞队列,灭个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于 * 1.队列 * 2.阻塞队列 * 2.1 阻塞队列有没有好的一面 * 2.2 不得不阻塞,你如何管理 */ public class SynchronousQueueDemo { public static void main(String[] args) throws InterruptedException { BlockingQueue<String> blockingQueue = new SynchronousQueue<>(); new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "\t put 1"); blockingQueue.put("1"); System.out.println(Thread.currentThread().getName() + "\t put 2"); blockingQueue.put("2"); System.out.println(Thread.currentThread().getName() + "\t put 3"); blockingQueue.put("3"); } catch (InterruptedException e) { e.printStackTrace(); } }, "AAA").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take()); TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take()); TimeUnit.SECONDS.sleep(5); System.out.println(Thread.currentThread().getName() + "\ttake " + blockingQueue.take()); } catch (InterruptedException e) { e.printStackTrace(); } }, "BBB").start(); } }
-
5、用在哪里
-
生产者消费者模式
-
传统版
package com.jian8.juc.queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * 一个初始值为零的变量,两个线程对其交替操作,一个加1一个减1,来5轮 * 1. 线程 操作 资源类 * 2. 判断 干活 通知 * 3. 防止虚假唤起机制 */ public class ProdConsumer_TraditionDemo { public static void main(String[] args) { ShareData shareData = new ShareData(); for (int i = 1; i <= 5; i++) { new Thread(() -> { try { shareData.increment(); } catch (Exception e) { e.printStackTrace(); } }, "ProductorA " + i).start(); } for (int i = 1; i <= 5; i++) { new Thread(() -> { try { shareData.decrement(); } catch (Exception e) { e.printStackTrace(); } }, "ConsumerA " + i).start(); } for (int i = 1; i <= 5; i++) { new Thread(() -> { try { shareData.increment(); } catch (Exception e) { e.printStackTrace(); } }, "ProductorB " + i).start(); } for (int i = 1; i <= 5; i++) { new Thread(() -> { try { shareData.decrement(); } catch (Exception e) { e.printStackTrace(); } }, "ConsumerB " + i).start(); } } } class ShareData {//资源类 private int number = 0; private Lock lock = new ReentrantLock(); private Condition condition = lock.newCondition(); public void increment() throws Exception { lock.lock(); try { //1.判断 while (number != 0) { //等待不能生产 condition.await(); } //2.干活 number++; System.out.println(Thread.currentThread().getName() + "\t" + number); //3.通知 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void decrement() throws Exception { lock.lock(); try { //1.判断 while (number == 0) { //等待不能消费 condition.await(); } //2.消费 number--; System.out.println(Thread.currentThread().getName() + "\t" + number); //3.通知 condition.signalAll(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
-
阻塞队列版
package com.jian8.juc.queue; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; public class ProdConsumer_BlockQueueDemo { public static void main(String[] args) { MyResource myResource = new MyResource(new ArrayBlockingQueue<>(10)); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t生产线程启动"); try { myResource.myProd(); } catch (Exception e) { e.printStackTrace(); } }, "Prod").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t消费线程启动"); try { myResource.myConsumer(); } catch (Exception e) { e.printStackTrace(); } }, "Consumer").start(); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("5s后main叫停,线程结束"); try { myResource.stop(); } catch (Exception e) { e.printStackTrace(); } } } class MyResource { private volatile boolean flag = true;//默认开启,进行生产+消费 private AtomicInteger atomicInteger = new AtomicInteger(); BlockingQueue<String> blockingQueue = null; public MyResource(BlockingQueue<String> blockingQueue) { this.blockingQueue = blockingQueue; System.out.println(blockingQueue.getClass().getName()); } public void myProd() throws Exception { String data = null; boolean retValue; while (flag) { data = atomicInteger.incrementAndGet() + ""; retValue = blockingQueue.offer(data, 2, TimeUnit.SECONDS); if (retValue) { System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "成功"); } else { System.out.println(Thread.currentThread().getName() + "\t插入队列" + data + "失败"); } TimeUnit.SECONDS.sleep(1); } System.out.println(Thread.currentThread().getName() + "\t大老板叫停了,flag=false,生产结束"); } public void myConsumer() throws Exception { String result = null; while (flag) { result = blockingQueue.poll(2, TimeUnit.SECONDS); if (null == result || result.equalsIgnoreCase("")) { flag = false; System.out.println(Thread.currentThread().getName() + "\t超过2s没有取到蛋糕,消费退出"); System.out.println(); return; } System.out.println(Thread.currentThread().getName() + "\t消费队列" + result + "成功"); } } public void stop() throws Exception { flag = false; } }
-
-
线程池
-
消息中间件
9. synchronized和lock有什么区别?
-
原始构成
-
synchronized时关键字属于jvm
monitorenter,底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步或方法中才能掉wait/notify等方法
-
Lock是具体类,是api层面的锁(java.util.),最大的优势就是分为为读和写都提供了锁
-
-
使用方法
- sychronized不需要用户取手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
- ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要lock()和unlock()方法配合try/finally语句块来完成
-
等待是否可中断
- synchronized不可中断,除非抛出异常或者正常运行完成
- ReentrantLock可中断,设置超时方法tryLock(long timeout, TimeUnit unit),或者lockInterruptibly()放代码块中,调用interrupt()方法可中断。
-
加锁是否公平
- synchronized非公平锁
- ReentrantLock两者都可以,默认公平锁,构造方法可以传入boolean值,true为公平锁,false为非公平锁
-
锁绑定多个条件Condition
- synchronized没有
- ReentrantLock用来实现分组唤醒需要要唤醒的线程们,可以精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。
package com.jian8.juc.lock; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * synchronized和lock区别 * <p===lock可绑定多个条件=== * 对线程之间按顺序调用,实现A>B>C三个线程启动,要求如下: * AA打印5次,BB打印10次,CC打印15次 * 紧接着 * AA打印5次,BB打印10次,CC打印15次 * 。。。。 * 来十轮 */ public class SyncAndReentrantLockDemo { public static void main(String[] args) { ShareData shareData = new ShareData(); new Thread(() -> { for (int i = 1; i <= 10; i++) { shareData.print5(); } }, "A").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { shareData.print10(); } }, "B").start(); new Thread(() -> { for (int i = 1; i <= 10; i++) { shareData.print15(); } }, "C").start(); } } class ShareData { private int number = 1;//A:1 B:2 C:3 private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition(); private Condition condition3 = lock.newCondition(); public void print5() { lock.lock(); try { //判断 while (number != 1) { condition1.await(); } //干活 for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } //通知 number = 2; condition2.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print10() { lock.lock(); try { //判断 while (number != 2) { condition2.await(); } //干活 for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } //通知 number = 3; condition3.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15() { lock.lock(); try { //判断 while (number != 3) { condition3.await(); } //干活 for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + "\t" + i); } //通知 number = 1; condition1.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
10. 线程池用过吗?ThreadPoolExecutor谈谈你的理解
1、Callable接口的使用
package com.jian8.juc.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
/**
* 多线程中,第三种获得多线程的方式
*/
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//FutureTask(Callable<V> callable)
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyThread2());
new Thread(futureTask, "AAA").start();
// new Thread(futureTask, "BBB").start();//复用,直接取值,不要重启两个线程
int a = 100;
int b = 0;
//b = futureTask.get();//要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致堵塞,直到计算完成
while (!futureTask.isDone()) {//当futureTask完成后取值
b = futureTask.get();
}
System.out.println("*******Result" + (a + b));
}
}
class MyThread implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("Callable come in");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
}
}
2、为什么使用线程池
-
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动给这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
-
主要特点
线程复用、控制最大并发数、管理线程
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
- 提过响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
- 提高线程的客观理想。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
3、线程池如何使用
-
架构说明
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor
graph BT 类-Executors 类-ScheduledThreadPoolExecutor-->类-ThreadPoolExecutor 类-ThreadPoolExecutor-->类-AbstractExecutorService 类-AbstractExecutorService-.->接口-ExecutorService 类-ScheduledThreadPoolExecutor-.->接口-ScheduledExecutorService 接口-ScheduledExecutorService-->接口-ExecutorService 接口-ExecutorService-->接口-Executor -
编码实现
实现有五种,Executors.newScheduledThreadPool()是带时间调度的,java8新推出Executors.newWorkStealingPool(int),使用目前机器上可用的处理器作为他的并行级别
重点有三种
-
Executors.newFixedThreadPool(int)
执行长期的任务,性能好很多
创建一个定长线程池,可控制线程最大并发数,炒出的线程回在队列中等待。
newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是想到等的,他使用的是LinkedBlockingQueue
-
Executors.newSingleThreadExecutor()
一个任务一个任务执行的场景
创建一个单线程话的线程池,他只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,使用LinkedBlockingQueue
-
Executors.newCachedThreadPool()
执行很多短期异步的小程序或负载较轻的服务器
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲县城,若无可回收,则新建线程。
newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当县城空闲超过60s,就销毁线程
-
-
ThreadPoolExecutor
4、线程池的几个重要参数介绍
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize:线程池中常驻核心线程数
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务
- 当线程池的线程数达到corePoolSize后,就会把到达的任务放到缓存队列当中
-
maximumPoolSize:线程池能够容纳同时执行的最大线程数,必须大于等于1
-
keepAliveTime:多余的空闲线程的存活时间
- 当前线程池数量超过corePoolSize时,档口空闲时间达到keepAliveTime值时,多余空闲线程会被销毁到只剩下corePoolSize个线程为止
-
unit:keepAliveTime的单位
-
workQueue:任务队列,被提交但尚未被执行的任务
-
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
-
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runable的策略
5、线程池的底层工作原理
流程
在创建了线程池之后,等待提交过来的任务请求。
当调用execute()方法添加一个请求任务时,线程池会做出如下判断
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
2.3如果此时队列满了且运行的线程数小于maximumPoolSize,那么还是要创建非核心线程立刻运行此任务
2.4如果队列满了且正在运行的线程数量大于或等于maxmumPoolSize,那么启动饱和拒绝策略来执行
当一个线程完成任务时,他会从队列中却下一个任务来执行
当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:如果当前运行的线程数大于corePoolSize,那么这个线程会被停掉;所以线程池的所有任务完成后他最大会收缩到corePoolSize的大小
11. 线程池用过吗?生产上你如何设置合理参数
1、线程池的拒绝策略
-
什么是线程策略
等待队列也已经排满了,再也塞不下新任务了,同时线程池中的max线程也达到了,无法继续为新任务服务。这时我们就需要拒绝策略机制合理的处理这个问题。
-
JDK内置的拒绝策略
-
AbortPolicy(默认)
直接抛出RejectedExecutionException异常阻止系统正常运行
-
CallerRunsPolicy
”调用者运行“一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
-
DiscardOldestPolicy
抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
-
DiscardPolicy
直接丢弃任务,不予任何处理也不抛异常。如果允许任务丢失,这是最好的一种方案
-
-
均实现了RejectedExecutionHandler接口
2、你在工作中单一的/固定数的/可变的三种创建线程池的方法,用哪个多
一个都不用,我们生产上只能使用自定义的!!!!
为什么?
线程池不允许使用Executors创建,试试通过ThreadPoolExecutor的方式,规避资源耗尽风险
FixedThreadPool和SingleThreadPool允许请求队列长度为Integer.MAX_VALUE,可能会堆积大量请求;;CachedThreadPool和ScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量线程,导致OOM
3、你在工作中时如何使用线程池的,是否自定义过线程池使用
package com.jian8.juc.thread;
import java.util.concurrent.*;
/**
* 第四种获得java多线程的方式--线程池
*/
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(3, 5, 1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
//new ThreadPoolExecutor.AbortPolicy();
//new ThreadPoolExecutor.CallerRunsPolicy();
//new ThreadPoolExecutor.DiscardOldestPolicy();
//new ThreadPoolExecutor.DiscardPolicy();
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
4、合理配置线程池你是如何考虑的?
-
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正多核CPU上才可能得到加速(通过多线程)
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池
-
IO密集型
-
由于IO密集型任务线程并不是一直在执行任务,则应配置经可能多的线程,如CPU核数 * 2
-
IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的 CPU运算能力浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1-阻塞系数) 阻塞系数在0.8~0.9之间
八核CPU:8/(1-0,9)=80
-
12. 死锁的产生及解决方案
-
是什么
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。
graph TD threadA(线程A) threadB(线程B) lockA((锁A)) lockB((锁B)) threadA--持有-->lockA threadB--试图获取-->lockA threadB--持有-->lockB threadA--试图获取-->lockB -
产生死锁的主要原因
- 系统资源不足
- 进程运行推进的顺序不合适
- 资源分配不当
- 导致死锁的根源在于不适当地运用"synchronized"关键词来管理线程对特定对象的访问。
" synchronized关键词的作用是,确保在某个时刻只有一个线程被允许执行特定的代码块,因此,被允许执行的线程首先必须拥有对变量或对象的排他性的访问权。当线程访问对象时,线程会给对象加锁,而这个锁导致其它也想访问同一对象的线程被阻塞,直至第一个线程释放它加在对象上的锁。由于这个原因,在使用" synchronized"关键词时,很容易出现两个线程互相等待对方做出某个动作的情形。
-
死锁示例1
package com.jian8.juc.thread; import java.util.concurrent.TimeUnit; /** * 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那他们都将无法推进下去, */ public class DeadLockDemo { public static void main(String[] args) { String lockA = "lockA"; String lockB = "lockB"; new Thread(new HoldThread(lockA,lockB),"Thread-AAA").start(); new Thread(new HoldThread(lockB,lockA),"Thread-BBB").start(); } } class HoldThread implements Runnable { private String lockA; private String lockB; public HoldThread(String lockA, String lockB) { this.lockA = lockA; this.lockB = lockB; } @Override public void run() { synchronized (lockA) { System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockA + "\t尝试获得:" + lockB); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lockB) { System.out.println(Thread.currentThread().getName() + "\t自己持有:" + lockB + "\t尝试获得:" + lockA); } } } }
死锁实例2:
public class Deadlocker implements Runnable { public int flag = 1; static Object o1 = new Object(), o2 = new Object(); public void run() { System.out.println("flag=" + flag); if (flag == 1) { synchronized (o1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o2) { System.out.println("1"); } } if (flag == 0) { synchronized (o2) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } synchronized (o1) { System.out.println("0"); } } } } } public static void main(String[] args) { Deadlocker td1 = new Deadlocker(); Deadlocker td2 = new Deadlocker(); td1.flag = 1; td2.flag = 0; Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); } }
分析:
当类的对象flag=1时(t1),先锁定o1,睡眠50毫秒,然后锁定o2;
而t1在睡眠的时候另一个flag=0的对象(t2)线程启动,先锁定o2,睡眠50毫秒,等待T1释放o1;
T1睡眠结束后需要锁定o2才能继续执行,而此时o2已被T2锁定;
T2睡眠结束后需要锁定o1才能继续执行,而此时o1已被T1锁定;
T1、T2相互等待,都需要对方锁定的资源才能继续执行,从而死锁。
-
解决
-
使用
jps -l
定位进程号 -
jstack 进程号
找到死锁查看 -
避免死锁的一个通用的经验法则是:当几个线程都要访问共享资源A、B、C时,保证使每个线程都按照同样的顺序去访问它们,比如都先访问A,再访问B和C。如把 Thread t2= new thread(td2);改成 Thread ti2= new thread(td1);
-
还有一种方法是对对象进行 synchronized,加大锁定的粒度,如上面的例子中使得进程锁定当前对象,而不是逐步锁定当前对象的两个子对象o1和o2。这样就在t1锁定o1之后,即使发生休眠,当前对象仍然被t1锁定,t2不能打断t1去锁定o2,等t1休眠后再锁定o2,获取资源,执行成功。然后释放当前对象t2,接着t1继续运行。
public class Deadlocker implements Runnable { public int flag = 1; static Object o1 = new Object(), o2 = new Object(); public synchronized void run() { System.out.println("flag=" + flag); if (flag == 1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("1"); } if (flag == 0) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("0"); } } public static void main(String[] args) { Deadlocker td1 = new Deadlocker(); Deadlocker td2 = new Deadlocker(); td1.flag = 1; td2.flag = 0; Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); } }
代码修改成public synchronized void run(){...},去掉子对象锁定。对于一个成员方法加synchronized 关键字,实际上是以这个成员方法所在的对象本身作为对象锁。此例中,即对td1,td2这两个Deadlocker对象进行加锁。
-
第三种解决死锁的方法是使用实现Lock接口的重入锁类(Reentrantlock),代码如下
public class Deadlocker implements Runnable { public int flag = 1; static Object o1 = new Object(), o2 = new Object(); private final Lock lock = new ReentrantLock(); public boolean checkLock() { return lock.tryLock(); } public void run() { if (checkLock()) { try { System.out.println("flag=" + flag); if (flag == 1) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("1"); } if (flag == 0) { try { Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); } System.out.println("0"); } } finally { lock.unlock(); } } } public static void main(String[] args) { Deadlocker td1 = new Deadlocker(); Deadlocker td2 = new Deadlocker(); td1.flag = 1; td2.flag = 0; Thread t1 = new Thread(td1); Thread t2 = new Thread(td2); t1.start(); t2.start(); } }
代码行lock. tryLock()是测试对象操作是否已在执行中,如果已在执行中则不再执行此对象操作,立即返回false,达到忽略对象操作的效果。
-
13. Synchronized关键字
-
synchronized锁定的是对象而非代码,所处的位置是代码块或方法种使用方法是对代码块使用 synchronized关键字
public void fun(){ synchronized(this); }
-
括号中锁定的是普通对象或Class。
-
对象如果是this,表示在执行该代码块时锁定当前对象,其他线程不能调用该对象的其他锁定代码块,但可以调用其他对象的所有方法(包括锁定的代码块),也可以调用该对象的未锁定的代码块或方法。
-
如果是 Object o1,表示执行该代码块的时候锁定该对象,其他线程不能访问该对象(该对象是空的,没有方法,自然不能调用)
-
如果是类class,那么锁住了该类的class对象,只对静态方法生效。
-
另一种写法是将 synchronized作为方法的修饰符:
public synchronized void fun(){}//这个方法执行的时候锁定该当前对象
-
每个类的对象对应一把锁,每个 synchronized方法都必须获得调用该方法的一个对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
-
如果 synchronized修饰的是静态方法,那么锁住的是这个类的class对象,没有其他线程可以调用该类的这个方法或其他的同步静态方法。
-
实际上, synchronized(this以及非 static的 synchronized方法,只能防止多个线程同时执行同一个对象的这个代码段。
-
synchronized锁住的是括号里的对象,而不是代码。对于非静态的 synchronized方法,锁的就是对象本身。
14. Volatile关键字
-
保证变量的可见性
-
防止指令重排序。
-
- 原子性∶一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。 synchronized可以保证代码片段的原子性。
- 可见性:当一个变量对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。 volatile关键字可以保证共享变量的可见性。
- 有序性:代码在执行的过程中的先后顺序,java在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。 olatile关键字可以禁止指令进行重排序优化。
2. 常见面试题:
1. 有T1、T2、T3三个线程,如何怎样保证T2在T1执行完后执行,T3在T2执行完后执行?
使用join方法。join方法的功能是使异步执行的线程变成同步执行。即调用线程实例的 start方法后,该方法会立即返回,如果调用 start方法后,需要使用一个由这个线程计算得到的值,就必须使用join方法。如果不使用join方法,就不能保证当执行到start方法后面的某条语句时,这个线程一定会执行完。而使用join方法后,直到这个线程退出,程序才会往下执行。
2. 如何保证线程安全?
通过合理的时间调度,避开共享资源的存取冲突。另外,在并行任务设计上可以通过适当的策略,保证任务与任务之间不存在共享资源,设计一个规则来保证一个客户的计算工作和数据访问只会被一个线程或一台工作机完成,而不是把一个客户的计算工作分配给多个线程去完成。
3. 请说明一下sleep() 和 wait() 有什么区别?
最大的不同是在等待时wait会释放锁,而sleep一直持有锁。wait通常被用于线程间交互, sleep通常被用于暂停执行。
其它不同有sleep是 Thread类的静态方法,Wait是 Object方法。
wait, notify和 notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用。
sleep必须捕获异常,而wait, notify和 notifyAll不需要捕获异常。
4. 编写代码,解决生产者和消费者问题?
- 使用wait和notify/notifyAll来实现
/**
* @Author WaleGarrett
* @Date 2020/3/20 22:33
* * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
* * 能够支持2个生产者线程以及10个消费者线程的阻塞调用。
* * 同步容器:多个线程共同访问的时候,不能出问题,就是要加锁了,下面这个是阻塞式的同步容器;
* *
* * 使用wait和notify/notifyAll来实现
*/
public class MyContainer1<T> {
final private LinkedList<T> lists=new LinkedList<>();
final private int MAX=10;
private int count=0;
public synchronized void put(T t){
while(lists.size()==MAX){//被叫醒后再检查一遍,而不是直接从wait之后继续执行,可能还没执行到add时,别的线程已经add一个了
try{
this.wait();//effective java------>大部分时候wait和while一起使用
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll();//通知消费者线程进行消费,不能用notify,可能会叫醒producer,可能会一直wait
}
public synchronized T get(){
T t=null;
while(lists.size()==0){
try {
this.wait();//wait释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t=lists.removeFirst();
count--;
this.notifyAll();//永远使用notifyAll不要使用notify
return t;
}
public static void main(String[] args) {
MyContainer1<String>c=new MyContainer1<>();
//启动消费者线程
for(int i=0;i<10;i++){
new Thread(()->{
for(int j=0;j<5;j++){
System.out.println(c.get());
}
},"c"+i).start();
}
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0;i<2;i++){
new Thread(()->{
for(int j=0;j<25;j++){
c.put(Thread.currentThread().getName()+" "+j);
}
},"p"+i).start();
}
}
}
-
为什么用while而不用if?
假设容器中已经满了,如果用的是if,这个线程A发现list.size()==max已经满了,就this.wait()住了;
如果容器中被拿走了元素,线程A被叫醒了,它会从this.wait()开始继续往下运行,准备执行lists.add(),可是它被叫醒了之后还没有往里扔的时候,
另外一个线程往list里面扔了一个,线程A拿到锁之后不再进行if判断,而是继续执行lists.add()就会出问题了;
如果用while,this.wait()继续往下执行的时候需要在while中再检查一遍,就不会出问题; -
put()方法中为什么使用notifyAll而不是notify?
如果使用notify,notify是叫醒一个线程,那么就有可能叫醒的一个线程又是生产者,整个程序可能不动了,都wait住了;
- 使用Lock和Condition来实现
/**
* @Author WaleGarrett
* @Date 2020/3/21 9:50
* * 面试题:写一个固定容量同步容器,拥有put和get方法,以及getCount方法,
* * 能够支持2个生产者线程以及10个消费者线程的阻塞调用
* *
* * 使用Lock和Condition来实现
* * 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒
*/
public class MyContainer2<T> {
final private LinkedList<T> lists=new LinkedList<>();
final private int MAX=10;
private int count=0;
private Lock lock=new ReentrantLock();
private Condition producer=lock.newCondition();//这两个条件可以帮我们调用wait和notify
private Condition consumer=lock.newCondition();
private void put(T t){
try {
lock.lock();
while(lists.size()==MAX){
producer.await();//不能使用wait,wait需要和synchronized使用
}
lists.add(t);
count++;
consumer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
private T get(){
T t=null;
try {
lock.lock();
while(lists.size()==0){
consumer.await();//await和signal和signalAll一起使用
}
t=lists.removeFirst();
count--;
producer.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
MyContainer2<String>c=new MyContainer2<>();
//启动消费者线程
for(int i=0;i<10;i++){
new Thread(()->{
for(int j=0;j<5;j++){
System.out.println(c.get());
}
},"c"+i).start();
}
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for(int i=0;i<2;i++){
new Thread(()->{
for(int j=0;j<25;j++){
c.put(Thread.currentThread().getName()+" "+j);
}
},"p"+i).start();
}
}
}
- 使用lock和condition好处在于可以精确的通知哪些线程被叫醒,哪些线程不必被叫醒,这个效率显然要比notifyAll把所有线程全叫醒要高很多。
5. 什么是原子操作,Java中的原子操作是什么?
-
所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间切换到另一个线程。
-
java中的原子操作介绍:
-
jdk1.5的包为 javautil. concurrent.atomic
-
这个包里面提供了一组原子类。其基本特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性。
-
即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择另一个线程进入,这只是一种逻辑上的理解。实际上是借助硬件的相关指令来实现的,但不会阻塞线程( synchronized会把别的等待的线程挂,或者说只是在硬件级别上阻塞了)
-
其中的类可以分成4组:
AtomicBoolean, AtomicInteger, AtomicLong, AtomicReference
AtomicIntegerArray, AtomicLongArray
AtomicLongFieldUpdater, AtomicIntegerFieldUpdater, AtomicReferenceFieldUpdater AtomicMarkableReference, AtomicStamped Reference, AtomicReferenceArray
-
Atomic类的作用:
- 使得让对单一数据的操作,实现了原子化
- 使用 Atomic类构建复杂的,无需阻塞的代码
- 访问对2个或2个以上的 atomic变量(或者对单个 atomic变量进行2次或2次以上的操作)通常认为是需要同步的,以达到让这些操作能被作为一个原子单元。
-
Atomicboolean, AtomicInteger, AtomicLong, AtomicReference这四种基本类型用来处理布尔,整数,长整数,对象四种数据。
-
构造函数(两个构造函数)
默认的构造函数:初始化的数据分别是 false,0,0,nu‖
带参构造函数:参数为初始化的数据 -
set()和get()方法∶可以原子地设定和获取 atomic的数据。类似于 volatile,保证数据会在主存中设置或读取。
-
getAndSet()方法
原子的将变量设定为新数据,同时返回先前的旧数据
其本质是get()操作,然后做set()操作。尽管这2个操作都是 atomic,但是他们合并在一起的时候,就不是 atomic。在ava的源程序的级别上,如果不侬赖syη chronized的机制来完成这个工作是不可能的。只有依靠 native方法才可以。
-
compareAndSet()和 weakCompareAndSet()方法:
这两个方法都是 conditional modifier方法。这2个方法接受2个参数,一个是期望数据(expected),一个是新数据(new);如果 atomic里面的数据和期望数据一致,则将新数据设定给 atomic的数据,返回true,表明成功;否则就不设定,并返回 False。 -
对于 AtomicInteger、 AtomicLong还提供了一些特别的方法。 getAndIncrement(),
incrementAnd Get(), getAndDecrement(), decrementAndGet(), addAndGet()
getAndAdd()以实现一些加法,减法原子操作。(注意--i、++i不是原子操作,其中包含有3个操作步骤:第一步,读取i;第二步,加1或減1;第三步:写回内存)
-
-
6. 请你简要说明一下线程的基本状态以及状态之间的关系?
其中Running表示运行状态,Runnable表示就绪状态(万事俱备,只欠CPU),Blocked表示阻塞状态,阻塞状态又有多种情况,可能是因为调用wait()方法进入等待池,也可能是执行同步方法或同步代码块进入等锁池,或者是调用了sleep()方法或join()方法等待休眠或其他线程结束,或是因为发生了I/O中断。
7. 请问当一个线程进入一个对象的synchronized方法A之后,其它线程是否可进入此对象的synchronized方法B?
不能,其他线程只能访问该对象的非同步方法,同步方法则不能进入;因为非静态方法上的synchronized修饰符要求执行方法时要获得对象的锁,如果已经进入A方法,说明对象锁已经被取
8. 请简述一下线程的sleep()方法和yield()方法有什么区别?
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。
9. Java中有几种方法可以实现一个线程?
-
继承Thread类创建线程
-
实现Runnable接口创建线程
-
使用Callable和FutureTask创建线程
- 定义一个Callable接口的实现类
- 创建Callable实现类对象传递给FutureTask构造器(Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。)
- 将FutureTask对象传递给Thread构造器
- Thread对象调用start方法启动线程
- 通过FutureTask对象的get方法获取线程运行的结果
- 使用线程池,例如用Executor框架
使用Executors工具类中的静态工厂方法用于创建线程池
newFixedThreadPool:创建可重用且固定线程数的线程池,
newScheduledThreadPool:创建一个可延迟执行或定期执行的线程池
newCachedThreadPool:创建可缓存的线程池使用execute方法启动线程
使用shutdown方法等待提交的任务执行完成并后关闭线程。
- Spring实现多线程(底层是线程池)
在Spring3之后,Spring引入了对多线程的支持,如果你使用的版本在3.1以前,应该还是需要通过传统的方式来实现多线程的。从Spring3同时也是新增了Java的配置方式,而且Java配置方式也逐渐成为主流的Spring的配置方式。
- 定时器Timer (底层封装了一个TimerThread对象)
严格来说定时器(Timer)不是线程,他只是调度线程的一种工具,它里面封装了一个线程,所以我们可以使用定时器来使用线程。
- 创建Timer 对象
- 调用schedule方法
- 传入TimerTask子类对象
10. Callable和Runnable的区别
Callable定义的方法是call,而Runnable定义的方法是run。
Callable的call方法可以有返回值,而Runnable的run方法不能有返回值。
Callable的call方法可抛出异常,而Runnable的run方法不能抛出异常。
注意:
FutureTask为Runnable的实现类
FutureTask可以视为一个闭锁(门闩),因为只有当线程运行完才会出现结果。
10. 线程同步有几种实现方法,并且这些实现方法具体内容都是什么?
- Synchronized
- Lock
- Volatile
- Atomic
11. 请使用内部类实现线程设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。
Synchronized 相关问题
问题一:Synchronized 用过吗,其原理是什么?
这是一道 Java 面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或者没接触过 Synchronized。
Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,如果你查看被 Synchronized 修饰过的程序块编译后的字节码,会发现,被 Synchronized 修饰过的程序块,在编译前后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令。
这两个指令是什么意思呢?
在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁:
如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;当计数器为 0 时,锁就被释放了。
如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
Java 中 Synchronize 通过在对象头设置标记,达到了获取锁和释放锁的目的。
问题二:你刚才提到获取对象的锁,这个“锁”到底是什么?如何确定对象的锁?
“锁”的本质其实是 monitorenter 和 monitorexit 字节码指令的一个 Reference 类型的参数,即要锁定和解锁的对象。我们知道,使用
Synchronized 可以修饰不同的对象,因此,对应的对象锁可以这么确定。
1.
如果 Synchronized 明确指定了锁对象,比如 Synchronized(变量名)、Synchronized(this) 等,说明加解锁对象为该对象。
2.
如果没有明确指定:
若 Synchronized 修饰的方法为非静态方法,表示此方法对应的对象为锁对象;
若 Synchronized 修饰的方法为静态方法,则表示此方法对应的类对象为锁对象。
注意,当一个对象被锁住时,对象里面所有用 Synchronized 修饰的方法都将产生堵塞,而对象里非 Synchronized 修饰的方法可正常被调用,不受锁影响。
问题三:什么是可重入性,为什么说 Synchronized 是可重入锁?
可重入性是锁的一个基本要求,是为了解决自己锁死自己的情况。
比如下面的伪代码,一个类中的同步方法调用另一个同步方法,假如 Synchronized 不支持重入,进入 method2 方法时当前线程获得锁,method2 方法里面执行 method1 时当前线程又要去尝试获取锁,这时如果不支持重入,它就要等释放,把自己阻塞,导致自己锁死自己。
· 点击图片,放大查看 ·
对 Synchronized 来说,可重入性是显而易见的,刚才提到,在执行 monitorenter 指令时,如果这个对象没有锁定,或者当前线程已经拥
有了这个对象的锁(而不是已拥有了锁则不能继续获取),就把锁的计数器 +1,其实本质上就通过这种方式实现了可重入性。
问题四:JVM 对 Java 的原生锁做了哪些优化?
在 Java 6 之前,Monitor 的实现完全依赖底层操作系统的互斥锁来实现,也就是我们刚才在问题二中所阐述的获取/释放锁的逻辑。
由于 Java 层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间,现代 JDK 中做了大量的优化。
一种优化是使用自旋锁,即在把线程进行阻塞操作之前先让线程自旋等待一段时间,可能在等待期间其他线程已经解锁,这时就无需再让线程执行阻塞操作,避免了用户态到内核态的切换。
现代 JDK 中还提供了三种不同的 Monitor 实现,也就是三种不同的锁:
偏向锁(Biased Locking)
轻量级锁
重量级锁
这三种锁使得 JDK 得以优化 Synchronized 的运行,当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现,这就是锁的升级、降级。
当没有竞争出现时,默认会使用偏向锁。
JVM 会利用 CAS 操作,在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁,因为在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。
如果有另一线程试图锁定某个被偏斜过的对象,JVM 就撤销偏斜锁,切换到轻量级锁实现。
轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。
问题五:为什么说 Synchronized 是非公平锁?
非公平主要表现在获取锁的行为上,并非是按照申请锁的时间前后给等待线程分配锁的,每当锁被释放后,任何一个线程都有机会竞争到锁,这样做的目的是为了提高执行性能,缺点是可能会产生线程饥饿现象。
问题六:什么是锁消除和锁粗化?
锁消除:指虚拟机即时编译器在运行时,对一些代码上要求同步,但被检测到不可能存在共享数据竞争的锁进行消除。主要根据逃逸分析。
程序员怎么会在明知道不存在数据竞争的情况下使用同步呢?很多不是程序员自己加入的。
锁粗化:原则上,同步块的作用范围要尽量小。但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作在循环体内,频繁地进行互斥同步操作也会导致不必要的性能损耗。
锁粗化就是增大锁的作用域。
问题七:为什么说 Synchronized 是一个悲观锁?乐观锁的实现原理又是什么?什么是 CAS,它有什么特性?
Synchronized 显然是一个悲观锁,因为它的并发策略是悲观的:
不管是否会产生竞争,任何的数据操作都必须要加锁、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要被唤醒等操作。
随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略。先进行操作,如果没有其他线程征用数据,那操作就成功了;
如果共享数据有征用,产生了冲突,那就再进行其他的补偿措施。这种乐观的并发策略的许多实现不需要线程挂起,所以被称为非阻塞同步。
乐观锁的核心算法是 CAS(Compareand Swap,比较并交换),它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。
这样处理的逻辑是,首先检查某块内存的值是否跟之前我读取时的一样,如不一样则表示期间此内存值已经被别的线程更改过,舍弃本次操作,否则说明期间没有其他线程对此内存值操作,可以把新值设置给此块内存。
CAS 具有原子性,它的原子性由 CPU 硬件指令实现保证,即使用 JNI 调用 Native 方法调用由 C++ 编写的硬件级别指令,JDK 中提供了 Unsafe 类执行这些操作。
问题八:乐观锁一定就是好的吗?
乐观锁避免了悲观锁独占对象的现象,同时也提高了并发性能,但它也有缺点:
1.
乐观锁只能保证一个共享变量的原子操作。如果多一个或几个变量,乐观锁将变得力不从心,但互斥锁能轻易解决,不管对象数量多少及对象颗粒度大小。
2.
长时间自旋可能导致开销大。假如 CAS 长时间不成功而一直自旋,会给 CPU 带来很大的开销。
3.
ABA 问题。CAS 的核心思想是通过比对内存值与预期值是否一样而判断内存值是否被改过,但这个判断逻辑不严谨,假如内存值原来是 A,后来被一条线程改为 B,最后又被改成了 A,则 CAS 认为此内存值并没有发生改变,但实际上是有被其他线程改过的,这种情况对依赖过程值的情景的运算结果影响很大。解决的思路是引入版本号,每次变量更新都把版本号加一。
可重入锁 ReentrantLock 及其他显式锁相关问题
问题一:跟 Synchronized 相比,可重入锁 ReentrantLock 其实现原理有什么不同?
其实,锁的实现原理基本是为了达到一个目的:
让所有的线程都能看到某种标记。
Synchronized 通过在对象头中设置标记实现了这一目的,是一种 JVM 原生的锁实现方式,而 ReentrantLock 以及所有的基于 Lock 接口的实现类,都是通过用一个 volitile 修饰的 int 型变量,并保证每个线程都能拥有对该 int 的可见性和原子修改,其本质是基于所谓的 AQS 框架。
问题二:那么请谈谈 AQS 框架是怎么回事儿?
AQS(AbstractQueuedSynchronizer 类)是一个用来构建锁和同步器的框架,各种 Lock 包中的锁(常用的有 ReentrantLock、ReadWriteLock),以及其他如 Semaphore、CountDownLatch,甚至是早期的 FutureTask 等,都是基于 AQS 来构建。
1.
AQS 在内部定义了一个 volatile int state 变量,表示同步状态:当线程调用 lock 方法时 ,如果 state=0,说明没有任何线程占有共享资源的锁,可以获得锁并将 state=1;如果 state=1,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待。
2.
AQS 通过 Node 内部类构成的一个双向链表结构的同步队列,来完成线程获取锁的排队工作,当有线程获取锁失败后,就被添加到队列末尾。
o
Node 类是对要访问同步代码的线程的封装,包含了线程本身及其状态叫 waitStatus(有五种不同 取值,分别表示是否被阻塞,是否等待唤醒,是否已经被取消等),每个 Node 结点关联其 prev 结点和 next 结点,方便线程释放锁后快速唤醒下一个在等待的线程,是一个 FIFO 的过程。
o
Node 类有两个常量,SHARED 和 EXCLUSIVE,分别代表共享模式和独占模式。所谓共享模式是一个锁允许多条线程同时操作(信号量 Semaphore 就是基于 AQS 的共享模式实现的),独占模式是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待(如 ReentranLock)。
3.
AQS 通过内部类 ConditionObject 构建等待队列(可有多个),当 Condition 调用 wait() 方法后,线程将会加入等待队列中,而当
Condition 调用 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。
4.
AQS 和 Condition 各自维护了不同的队列,在使用 Lock 和 Condition 的时候,其实就是两个队列的互相移动。
问题三:请尽可能详尽地对比下 Synchronized 和 ReentrantLock 的异同。
ReentrantLock 是 Lock 的实现类,是一个互斥的同步锁。
从功能角度,ReentrantLock 比 Synchronized 的同步操作更精细(因为可以像普通对象一样使用),甚至实现 Synchronized 没有的高级功能,如:
等待可中断:当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,对处理执行时间非常长的同步块很有用。
带超时的获取锁尝试:在指定的时间范围内获取锁,如果时间到了仍然无法获取则返回。
可以判断是否有线程在排队等待获取锁。
可以响应中断请求:与 Synchronized 不同,当获取到锁的线程被中断时,能够响应中断,中断异常将会被抛出,同时锁会被释放。
可以实现公平锁。
从锁释放角度,Synchronized 在 JVM 层面上实现的,不但可以通过一些监控工具监控 Synchronized 的锁定,而且在代码执行出现异常时,JVM 会自动释放锁定;但是使用 Lock 则不行,Lock 是通过代
码实现的,要保证锁定一定会被释放,就必须将 unLock() 放到 finally{} 中。
从性能角度,Synchronized 早期实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大。
但是在 Java 6 中对其进行了非常多的改进,在竞争不激烈时,Synchronized 的性能要优于 ReetrantLock;在高竞争情况下,Synchronized 的性能会下降几十倍,但是 ReetrantLock 的性能能维持常态。
问题四:ReentrantLock 是如何实现可重入性的?
ReentrantLock 内部自定义了同步器 Sync(Sync 既实现了 AQS,又实现了 AOS,而 AOS 提供了一种互斥锁持有的方式),其实就是加锁的时候通过 CAS 算法,将线程对象放到一个双向链表中,每次获取锁的时候,看下当前维护的那个线程 ID 和当前请求的线程 ID 是否一样,一样就可重入了。
问题五:除了 ReetrantLock,你还接触过 JUC 中的哪些并发工具?
通常所说的并发包(JUC)也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:
提供了 CountDownLatch、CyclicBarrier、Semaphore 等,比 Synchronized 更加高级,可以实现更加丰富多线程操作的同步结构。
提供了 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者通过类似快照机制实现线程安全的动态数组 CopyOnWriteArrayList 等,各种线程安全的容器。
提供了 ArrayBlockingQueue、SynchorousQueue 或针对特定场景的 PriorityBlockingQueue 等,各种并发队列实现。
强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等。
问题六:请谈谈 ReadWriteLock 和 StampedLock。
虽然 ReentrantLock 和 Synchronized 简单实用,但是行为上有一定局限性,要么不占,要么独占。实际应用场景中,有时候不需要大量竞争的写操作,而是以并发读取为主,为了进一步优化并发操作的粒度,Java 提供了读写锁。
读写锁基于的原理是多个读操作不需要互斥,如果读锁试图锁定时,写锁是被某个线程持有,读锁将无法获得,而只好等待对方操作结束,这样就可以自动保证不会读取到有争议的数据。
ReadWriteLock 代表了一对锁,下面是一个基于读写锁实现的数据结构,当数据量较大,并发读多、并发写少的时候,能够比纯同步版本凸显出优势:
·
读写锁看起来比 Synchronized 的粒度似乎细一些,但在实际应用中,其表现也并不尽如人意,主要还是因为相对比较大的开销。
所以,JDK 在后期引入了 StampedLock,在提供类似读写锁的同时,还支持优化读模式。优化读基于假设,大多数情况下读操作并不会和写操作冲突,其逻辑是先试着修改,然后通过 validate 方法确认是否进入了写模式,如果没有进入,就成功避免了开销;如果进入,则尝试获取读锁。
·
问题七:如何让 Java 的线程彼此同步?你了解过哪些同步器?请分别介绍下。
JUC 中的同步器三个主要的成员:CountDownLatch、CyclicBarrier 和 Semaphore,通过它们可以方便地实现很多线程之间协作的功能。
CountDownLatch 叫倒计数,允许一个或多个线程等待某些操作完成。看几个场景:
跑步比赛,裁判需要等到所有的运动员(“其他线程”)都跑到终点(达到目标),才能去算排名和颁奖。
模拟并发,我需要启动 100 个线程去同时访问某一个地址,我希望它们能同时并发,而不是一个一个的去执行。
用法:CountDownLatch 构造方法指明计数数量,被等待线程调用 countDown 将计数器减 1,等待线程使用 await 进行线程等待。一个简单的例子:
CyclicBarrier 叫循环栅栏,它实现让一组线程等待至某个状态之后再全部同时执行,而且当所有等待线程被释放后,CyclicBarrier 可以被重复使用。CyclicBarrier 的典型应用场景是用来等待并发线程结束。
CyclicBarrier 的主要方法是 await(),await() 每被调用一次,计数便会减少 1,并阻塞住当前线程。当计数减至 0 时,阻塞解除,所有在此 CyclicBarrier 上面阻塞的线程开始运行。
在这之后,如果再次调用 await(),计数就又会变成 N-1,新一轮重新开始,这便是 Cyclic 的含义所在。CyclicBarrier.await() 带有返回值,用来表示当前线程是第几个到达这个 Barrier 的线程。
举例说明如下: ·
Semaphore,Java 版本的信号量实现,用于控制同时访问的线程个数,来达到限制通用资源访问的目的,其原理是通过 acquire() 获取一个许可,如果没有就等待,而 release() 释放一个许可。
如果 Semaphore 的数值被初始化为 1,那么一个线程就可以通过 acquire 进入互斥状态,本质上和互斥锁是非常相似的。但是区别也非常明显,比如互斥锁是有持有者的,而对于 Semaphore 这种计数器结构,虽然有类似功能,但其实不存在真正意义的持有者,除非我们进行扩展包装。
问题八:CyclicBarrier 和 CountDownLatch 看起来很相似,请对比下呢?
它们的行为有一定相似度,区别主要在于:
CountDownLatch 是不可以重置的,所以无法重用,CyclicBarrier 没有这种限制,可以重用。
CountDownLatch 的基本操作组合是 countDown/await,调用 await 的线程阻塞等待 countDown 足够的次数,不管你是在一个线
程还是多个线程里 countDown,只要次数足够即可。 CyclicBarrier 的基本操作组合就是 await,当所有的伙伴都调用了 await,才会继续进行任务,并自动进行重置。
CountDownLatch 目的是让一个线程等待其他 N 个线程达到某个条件后,自己再去做某个事(通过 CyclicBarrier 的第二个构造方法 public CyclicBarrier(int parties, Runnable barrierAction),在新线程里做事可以达到同样的效果)。而 CyclicBarrier 的目的是让 N 多线程互相等待直到所有的都达到某个状态,然后这 N 个线程再继续执行各自后续(通过 CountDownLatch 在某些场合也能完成类似的效果)。
Java 线程池相关问题
问题一:Java 中的线程池是如何实现的?
在 Java 中,所谓的线程池中的“线程”,其实是被抽象为了一个静态内部类 Worker,它基于 AQS 实现,存放在线程池的 HashSet
而需要执行的任务则存放在成员变量 workQueue(BlockingQueue
这样,整个线程池实现的基本思想就是:从 workQueue 中不断取出需要执行的任务,放在 Workers 中进行处理。
问题二:创建线程池的几个核心构造参数?
Java 中的线程池的创建其实非常灵活,我们可以通过配置不同的参数,创建出行为不同的线程池,这几个参数包括:
corePoolSize:线程池的核心线程数。
maximumPoolSize:线程池允许的最大线程数。
keepAliveTime:超过核心线程数时闲置线程的存活时间。
workQueue:任务执行前保存任务的队列,保存由 execute 方法提交的 Runnable 任务。
问题三:线程池中的线程是怎么创建的?是一开始就随着线程池的启动创建好的吗?
显然不是的。线程池默认初始化后不启动 Worker,等待有请求时才启动。
每当我们调用 execute() 方法添加一个任务时,线程池会做如下判断:
如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;
如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常 RejectExecutionException。
当一个线程完成任务时,它会从队列中取下一个任务来执行。 当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断。
如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
问题四:既然提到可以通过配置不同参数创建出不同的线程池,那么 Java 中默认实现好的线程池又有哪些呢?请比较它们的异同。
- SingleThreadExecutor 线程池
这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。
corePoolSize:1,只有一个核心线程在工作。
maximumPoolSize:1。
keepAliveTime:0L。
workQueue:new LinkedBlockingQueue(),其缓冲队列是*的。 - FixedThreadPool 线程池
FixedThreadPool 是固定大小的线程池,只有核心线程。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
FixedThreadPool 多数针对一些很稳定很固定的正规并发线程,多用于服务器。
corePoolSize:nThreads
maximumPoolSize:nThreads
keepAliveTime:0L
workQueue:new LinkedBlockingQueue(),其缓冲队列是*的。 - CachedThreadPool 线程池
CachedThreadPool 是*线程池,如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60 秒不执行任务)线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。
线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。SynchronousQueue 是一个是缓冲区为 1 的阻塞队列。
缓存型池子通常用于执行一些生存期很短的异步型任务,因此在一些面向连接的 daemon 型 SERVER 中用得不多。但对于生存期短的异步任务,它是 Executor 的首选。
corePoolSize:0
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:60L
workQueue:new SynchronousQueue(),一个是缓冲区为 1 的阻塞队列。 - ScheduledThreadPool 线程池
ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在 DEFAULT_KEEPALIVEMILLIS 时间内回收。
corePoolSize:corePoolSize
maximumPoolSize:Integer.MAX_VALUE
keepAliveTime:DEFAULT_KEEPALIVE_MILLIS
workQueue:new DelayedWorkQueue()
问题六:如何在 Java 线程池中提交线程?
线程池最常用的提交任务的方法有两种: - execute():ExecutorService.execute 方法接收一个 Runable 实例,它用来执行一个任务: ·
- submit():ExecutorService.submit() 方法返回的是 Future 对象。可以用 isDone() 来查询 Future 是否已经完成,当任务完成时,它具有一个结果,可以调用 get() 来获取结果。也可以不用 isDone() 进行检查就直接调用 get(),在这种情况下,get() 将阻塞,直至结果准备就绪。
Java 内存模型相关问题
问题一:什么是 Java 的内存模型,Java 中各个线程是怎么彼此看到对方的变量的?
Java 的内存模型定义了程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。
此处的变量包括实例字段、静态字段和构成数组对象的元素,但是不包括局部变量和方法参数,因为这些是线程私有的,不会被共享,所以不存在竞争问题。
Java 中各个线程是怎么彼此看到对方的变量的呢?Java 中定义了主内存与工作内存的概念:
所有的变量都存储在主内存,每条线程还有自己的工作内存,保存了被该线程使用到的变量的主内存副本拷贝。
线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存的变量。不同的线程之间也无法直接访问对方工作内存的变量,线程间变量值的传递需要通过主内存。
问题二:请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?
关键字 volatile 是 Java 虚拟机提供的最轻量级的同步机制。当一个变量被定义成 volatile 之后,具备两种特性:
1.
保证此变量对所有线程的可见性。当一条线程修改了这个变量的值,新值对于其他线程是可以立即得知的。而普通变量做不到这一点。
2.
禁止指令重排序优化。普通变量仅仅能保证在该方法执行过程中,得到正确结果,但是不保证程序代码的执行顺序。
Java 的内存模型定义了 8 种内存间操作:
lock 和 unlock
把一个变量标识为一条线程独占的状态。
把一个处于锁定状态的变量释放出来,释放之后的变量才能被其他线程锁定。
read 和 write
把一个变量值从主内存传输到线程的工作内存,以便 load。
把 store 操作从工作内存得到的变量的值,放入主内存的变量中。
load 和 store
把 read 操作从主内存得到的变量值放入工作内存的变量副本中。
把工作内存的变量值传送到主内存,以便 write。
use 和 assgin
把工作内存变量值传递给执行引擎。
将执行引擎值传递给工作内存变量值。
volatile 的实现基于这 8 种内存间操作,保证了一个线程对某个 volatile 变量的修改,一定会被另一个线程看见,即保证了可见性。
问题三:既然 volatile 能够保证线程间的变量可见性,是不是就意味着基于 volatile 变量的运算就是并发安全的?
显然不是的。基于 volatile 变量的运算在并发下不一定是安全的。volatile 变量在各个线程的工作内存,不存在一致性问题(各个线程的工作内存中 volatile 变量,每次使用前都要刷新到主内存)。
但是 Java 里面的运算并非原子操作,导致 volatile 变量的运算在并发下一样是不安全的。
问题四:请对比下 volatile 对比 Synchronized 的异同。
Synchronized 既能保证可见性,又能保证原子性,而 volatile 只能保证可见性,无法保证原子性。
ThreadLocal 和 Synchonized 都用于解决多线程并发访问,防止任务在共享资源上产生冲突。但是 ThreadLocal 与 Synchronized 有本质的区别。
Synchronized 用于实现同步机制,是利用锁的机制使变量或代码块在某一时该只能被一个线程访问,是一种 “以时间换空间” 的方式。
而 ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,根除了对变量的共享,是一种 “以空间换时间” 的方式。
问题五:请谈谈 ThreadLocal 是怎么解决并发安全的?
ThreadLocal 这是 Java 提供的一种保存线程私有信息的机制,因为其在整个线程生命周期内有效,所以可以方便地在一个线程关联的不同业务模块之间传递信息,比如事务 ID、Cookie 等上下文相关信息。
ThreadLocal 为每一个线程维护变量的副本,把共享数据的可见范围限制在同一个线程之内,其实现原理是,在 ThreadLocal 类中有一个 Map,用于存储每一个线程的变量的副本。
问题六:很多人都说要慎用 ThreadLocal,谈谈你的理解,使用 ThreadLocal 需要注意些什么?
使用 ThreadLocal 要注意 remove!
ThreadLocal 的实现是基于一个所谓的 ThreadLocalMap,在 ThreadLocalMap 中,它的 key 是一个弱引用。
通常弱引用都会和引用队列配合清理机制使用,但是 ThreadLocal 是个例外,它并没有这么做。
这意味着,废弃项目的回收依赖于显式地触发,否则就要等待线程结束,进而回收相应 ThreadLocalMap!这就是很多 OOM 的来源,所以通常都会建议,应用一定要自己负责 remove,并且不要和线程池配合,因为 worker 线程往往是不会退出的。
并发容器
一 JDK 提供的并发容器总结
JDK 提供的这些容器大部分在 java.util.concurrent
包中。
- ConcurrentHashMap: 线程安全的 HashMap
- CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.
- ConcurrentLinkedQueue: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
- BlockingQueue: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
- ConcurrentSkipListMap: 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
二 ConcurrentHashMap
我们知道 HashMap 不是线程安全的,在并发场景下如果要保证一种可行的方式是使用 Collections.synchronizedMap()
方法来包装我们的 HashMap。但这是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
所以就有了 HashMap 的线程安全版本—— ConcurrentHashMap 的诞生。在 ConcurrentHashMap 中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
关于 ConcurrentHashMap 相关问题,我在 Java 集合框架常见面试题 这篇文章中已经提到过。下面梳理一下关于 ConcurrentHashMap 比较重要的问题:
三 CopyOnWriteArrayList
3.1 CopyOnWriteArrayList 简介
public class CopyOnWriteArrayList<E>
extends Object
implements List<E>, RandomAccess, Cloneable, Serializable
在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。
这和我们之前在多线程章节讲过 ReentrantReadWriteLock
读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 CopyOnWriteArrayList
类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList
读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。那它是怎么做的呢?
3.2 CopyOnWriteArrayList 是如何做到的?
CopyOnWriteArrayList
类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了。
从 CopyOnWriteArrayList
的名字就能看出CopyOnWriteArrayList
是满足CopyOnWrite
的 ArrayList,所谓CopyOnWrite
也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了。
3.3 CopyOnWriteArrayList 读取和写入源码简单分析
3.3.1 CopyOnWriteArrayList 读取操作的实现
读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,因此可以保证数据安全。
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
3.3.2 CopyOnWriteArrayList 写入操作的实现
CopyOnWriteArrayList 写入操作 add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#add})
*/
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();//释放锁
}
}
四 ConcurrentLinkedQueue
Java 提供的线程安全的 Queue 可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。
从名字可以看出,ConcurrentLinkedQueue
这个队列使用链表作为其数据结构.ConcurrentLinkedQueue 应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现。
ConcurrentLinkedQueue 内部代码我们就不分析了,大家知道 ConcurrentLinkedQueue 主要使用 CAS 非阻塞算法来实现线程安全就好了。
ConcurrentLinkedQueue 适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。
五 BlockingQueue
5.1 BlockingQueue 简单介绍
上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:
下面主要介绍一下: ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,这三个 BlockingQueue 的实现类。
5.2 ArrayBlockingQueue
ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。
ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
5.3 LinkedBlockingQueue
LinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做*队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE。
相关构造方法:
/**
*某种意义上的*队列
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
/**
*有界队列
* Creates a {@code LinkedBlockingQueue} with the given (fixed) capacity.
*
* @param capacity the capacity of this queue
* @throws IllegalArgumentException if {@code capacity} is not greater
* than zero
*/
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
5.4 PriorityBlockingQueue
PriorityBlockingQueue 是一个支持优先级的*阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo()
方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator
来指定排序规则。
PriorityBlockingQueue 并发控制采用的是 ReentrantLock,队列为*队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。
简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是*队列(take 方法在队列为空的时候会阻塞)。
推荐文章:
《解读 Java 并发队列 BlockingQueue》
https://javadoop.com/post/java-concurrent-queue
六 ConcurrentSkipListMap
下面这部分内容参考了极客时间专栏《数据结构与算法之美》以及《实战 Java 高并发程序设计》。
为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。
对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map。
跳表的本质是同时维护了多个链表,并且链表是分层的,
最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。
跳表内的所有链表的元素都是排序的。查找时,可以从*链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。
查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。
从上面很容易看出,跳表是一种利用空间换时间的算法。
使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。