1、对volatile的理解
一、volatile是Java虚拟机提供的轻量级的同步机制
①保证可见性
②不保证原子性
③禁止指令重排
二、JMM
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式
JMM关于同步的规定:
①线程解锁前,必须把共享变量的值刷新回主内存
②线程加锁前,必须读取主内存的最新值到自己的工作内存
③加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有的地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(值传递)必须通过主内存来完成。
(2.1)可见性
第一时间看到的(及时通知)
class MyData {
int number = 0;
public void addT060() {
this.number = 60;
}
}
/**
1、验证volatile的可见性
1.1 假如int number = 0; number变量之前根本没有添加volatile关键字修饰,没有可见性
*/
public class VolatileDemo {
public static void main(String[] args) { //main是一切方法的运行入口
MyData myData = new MyData(); //资源类
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
//暂停一会儿线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addT060();
System.out.println(Thread.currentThread().getName() + "\t updated number value:" + myData.number);
},"AAA").start();
//第2个线程就是我们的main线程
while(myData.number == 0) {
//main线程就一直在这里等待循环,直到number值不再等于零
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
}
}
上述代码运行不会停,因为没有保证可见性,main线程一直以为number变量为0,没有被通知number已经更改为60
给number变量添加修饰符volatile
,修改后刷新到主内存,通知其他线程变量已更改
各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量X对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
添加了volatile,可以解决可见性问题
(2.2)原子性
验证volatile不保证原子性
原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
volatile不保证原子性的案例演示
public void addPlusPlus() {
number++;
}
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
//需要等待上面20个线程都全部计算完成后,再用main线程取得最终的结果值看是多少?
while(Thread.activeCount() > 2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:" + myData.number);
}
number++在多线程下是非线程安全的,如何不加synchronized解决?
写的时候覆盖了另外一个线程写入的值,没有拿到最新值又去写
虽然number++在写java代码时只有一行代码,但是在字节码文件中它是三条指令,在多条线程写回主内存时可能会存在写覆盖情况,还没有通知到变量已修改,就写入主内存
n++被拆分成了3个指令:
- 执行getfield拿到原始n;
- 执行iadd进行加1操作;
- 执行putfield写把累加后的值写回
如何解决原子性?
- 加sync
- 使用JUC下的AtomicInteger(原理是CAS),不会出现写覆盖
AtomicInteger atomicInteger = new AutomicInteger();
public void addAtomic() {
atomicInteger.getAndIncrement();
}
(2.3)volatileDemo代码演示可见性+原子性代码
(2.4)有序性
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
- 源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令
- 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
- 处理器在进行重排序时必须要考虑指令之间的数据依赖性
- 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier),是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存
对volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量
工作内存与主内存同步延迟现象导致的可见性问题:
可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
对于指令重排导致的可见性问题和有序性问题:
可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化
三、使用过volatile的场景:
(单例模式:通过单例模式的方法创建的类在当前进程中只有一个实例)
单例模式要点:①某个类只能有一个实例;②它必须自行创建这个实例;③它必须自行向整个系统提供这个实例
单例模式实现要点:①单例模式的类只提供私有的构造函数;②类定义中含有一个该类的静态私有对象;③该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象
(3.1)单例模式DCL代码
public class SingletonDemo {
private static volatile SingletonDemo instance = null;
private SingletonDemo() {
System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo()");
}
//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 SingletonDemo();
可以分为以下3步完成(伪代码)
memory = allocate();
1、分配对象内存空间
instance(memory);
2、初始化对象
instance = memory;
3、设置instance指向刚分配的内存地址,此时instance!=null
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变
因此这种重排优化是允许的
memory = allocate();
1、分配对象内存空间
instance = memory;
3、设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!(多线程时可能会取到空值)
instance(memory);
2、初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造就了线程安全问题
(3.2)单例模式volatile分析
2、CAS
compareAndSet
比较并交换
期望值和主物理内存的值做比较,如果没人动过,则更新该值,否则从主物理内存重新获得该值
底层原理:自旋锁、UnSafe类
atomicInteger.getAndIncrement()
方法的源代码:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, volueOffset, 1);
}
UnSafe类是什么?
1、UnSafe是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法
注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
2、变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。
3、变量value用volatile修饰,保证了多线程之间的内存可见性
CAS的全称为Compare-And-Swap,它是一条CPU并发原语
它的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的,CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用Unsafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}//valueOffset内存偏移量
//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;如果不同,继续取值然后再比较,直到更新完成
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上):
1、AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存
2、线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起
3、线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK
4、这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了
5、线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功
简单版小总结
CAS(CompareAndSwap)
比较当前工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较直到主内存和工作内存中的值一致为止
CAS应用
CAS有3个操作数,内存值V,旧的预期值A,要修改的更新值B
当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
CAS缺点
- 循环时间长开销很大
可以看到getAndAddInt方法执行时,有个do while,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销
- 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作
但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性
- 引出来ABA问题???
3、原子类AtomicInteger的ABA问题,原子更新引用
CAS——>Unsafe——>CAS底层思想——>ABA——>原子引用更新——>如何规避ABA问题
ABA:狸猫换太子
弹幕:A被换成B又换成A,所以比较的时候是一致的
CAS会导致”ABA“问题
CAS算法实现一个重要前提需要提取内存中某时刻的数据并在当下时刻比较并交换,那么在这个时间差里会导致数据的变化
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功
尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的
@Getter
@ToString
@AllArgConstructor
class User {
String userName;
int age;
}
public class AtomicReferenceDemo {
public static void main(String[] args) {
User z3 = new User("z3", 22);
User li4 = new User("li4", 25);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(z3);
System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());
}
}
解决ABA问题?理解原子引用+新增一种机制,那就是修改版本号(类似时间戳)
public class ABADemo{ //ABA问题的产生 AtomicStampedReference
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
}, "t1").start();
new Thread(() -> {
//暂停1秒钟t2线程,保证上面的t1线程完成了一次ABA操作
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
}, "t2").start();
}
}
ABA问题的解决:用带时间戳的原子引用
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
}, "t3").start();
4、ArrayList线程不安全的案例
集合类不安全的问题
public class ContainerNotSafeDemo {
public static main(String[] args) {
List<String> list = Arrays.asList("a", "b", "c");
list.forEach(System.out::println);
}
}
java.util.ConcurrentModificationException
并发修改异常
导致原因:并发争抢修改异常,参考我们的花名册签名情况:一个人正在写入,另外一个同学过来抢夺,导致数据不一致异常。并发修改异常(一个人在写,另一个想读)
解决方案:
new Vector<>();
不许用
Collections.synchronizedList(new ArrayList<>());
不许用,影响并发性能
new CopyOnWriteArrayList<>()
写时复制:读写分离的思想
写时复制:
CopyOnWrite容器即写时复制的容器,往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy,复制出一个新的容器Object[] newElements,然后新的容器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();
}
}
HashSet底层是HashMap
ArrayList线程不安全
Set线程不安全
Map线程不安全concurrentHashMap
基本类型传的是复印件
5、公平锁/非公平锁/可重入锁/递归锁/自旋锁
公平锁和非公平锁
Reentrantlock()可重入锁默认是非公平锁
公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到
非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁;在高并发的情况下,有可能会造成优先级反转或者饥饿现象
并发包中ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或非公平锁,默认是非公平锁
关于两者区别:
- 公平锁:Threads acquire a fair lock in the order in which they requested it
- 公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己
- 非公平锁:a nonfair lock permits barging: threads requesting a lock can jump ahead of the queue of waiting threads if the lock happens to be available when it is requested
- 非公平锁比较粗鲁,上来就尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式
- 题外话:Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。
- 对于Synchronized而言,也是一种非公平锁
可重入锁(也叫做递归锁)
指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
也就是说,线程可以进入任何一个它已经拥有的锁所同步的代码块
大门、厕所
public sync void method1() {
method02();
}
public sync void method2() {
}
ReentrantLock/Synchronized就是一个典型的可重入锁
可重入锁最大的作用是避免死锁
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();
}
}
加锁几次,解锁几次,必须匹配就行了
自旋锁(spinlock)
是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU
自旋锁好处:循环比较获取知道成功为止,没有类似wait的阻塞
通过CAS操作完成自旋锁,A线程先进来调用myLock方法自己持有锁5秒钟,B随后进来后发现当前线程持有锁,不是null,所以只能通过自旋等待,直到A释放锁后B随后抢到
public class SpinLockDemo {
//原子引用线程
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()");
}
}
独占锁(写锁)/共享锁(读锁)/互斥锁
独占锁:指该锁一次只能被一个线程所持有,对ReentrantLock和Synchronized而言都是独占锁
共享锁:指该锁可被多个线程所持有
对ReentrantReadWriteLock其读锁是共享锁,其写锁是独占锁
读锁的共享锁可保证并发读是非常高效的,读写、写读、写写的过程是互斥的
多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该可以同时进行
但是如果有一个线程想去写共享资源,就不应该再有其它线程对该资源进行读或写
小总结:读-读能共存
读-写不能共存
写-写不能共存
写操作:原子+独占,整个过程必须是一个完整的统一体,中间不许被分割,被打断
缓存:
class MyCache{//资源类
private volatile Map<String, Object> map = new HashMap<>();//保证可见性
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
public void put(String key, Object value) {
rwLock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"\t 正在写入:" + key);
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 {
Object result = map.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
rwLock.readLock().unlock();
}
}
}
6、CountDownLatch/CyclicBarrier/Semaphore
CountDownLatch
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"\t上完自习,离开教室");
countDownLatch.countDown();
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println(Thread.currentThread().getName()+"\t ******班长最后关门走人");
}
public enum CountryEnum {
ONE(1, "齐"), TWO(2,"楚"), THREE(3, "燕"), FOUR(4, "赵"), FIVE(5, "魏"), SIX(6, "韩");
@Getter private Integer retCode;
@Getter private String retMessage;
CountryEnum(Integer retCode, String retMessage) {
this.retCode = retCode;
this.retMessage = retMessage;
}
public static CountryEnum forEach_CountryEnum(int index){
CountryEnum[] myArray = CountryEnum.values();
for (CountryEnum element : myArray) {
if (index == element.getRetCode()) {
return element;
}
}
return null;
}
}
public static void main(String[] args) throws Exception {
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 ******秦帝国,一统华夏");
}
让一些线程阻塞直到另一些线程完成一系列操作后才被唤醒
CountDownLatch主要有两个方法,当一个或多个线程调用await方法时,调用线程会被阻塞。
其它线程调用countDown方法会将计数器减1(调用countDown方法的线程不会阻塞),当计数器的值变为零时,因调用await方法被阻塞的线程会被唤醒,继续执行
弹幕:现实中最好把countDown方法放进final中
CyclicBarrier
和CountDownLatch相反:集齐七颗龙珠可召唤神龙
弹幕:底层也是减法实现的,可以理解为做加,等大家都做到某一步,再开始新的循环
CyclicBarrier的字面意思是可循环(Cyclic)使用的屏障(Barrier),它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活,线程进入屏障通过CyclicBarrier的await()方法
public static void main(String[] args) {
//CyclicBarrier(int parties, Runnable barrierAction)
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.currentThead().getName()+"\t 收集到第:"+tempInt+"龙珠");
try {
cyclicBarrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}, String.valueOf(i)).start();
}
}
Semaphore
信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数的控制
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);//模拟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); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(Thread.currentThread().getName()+"\t停车3秒后离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}, String.valueOf(i)).start();
}
}
7、阻塞队列
消息中间件的底层原理
ArrayBlockingQueue
:是一个基于数组结构的有界阻塞队列,此队列按FIFO(先进先出)原则对元素进行排序
LinkedBlockingQueue
:一个基于链表结构的阻塞队列,此队列按FIFO(先进先出)排序元素,吞吐量通常要高于ArrayBlockingQueue
SynchronousQueue
:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockQueue
线程1往阻塞队列中添加元素,而线程2从阻塞队列中移除元素
阻塞队列相当于消费者和生产者,消费者从阻塞队列中拿元素,生产者往阻塞队列中生产元素
- 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞
- 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞
- 试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列中插入新的元素
- 同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程从列中移除一个或多个元素或者完全清空队列后使队列重新变得空闲起来并后续新增
在多线程领域:所谓阻塞,在某些情况下会挂起线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒
为什么需要BlockingQueue?好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为这一切BlockingQueue都给你一手包办了(不需要wait和notify)
在concurrent包发布以前,在多线程环境下,我们每个程序员都必须去自己控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度
种类分析
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
DelayQueue:使用优先级队列实现的延迟无界阻塞队列
SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列(生产一个消费一个,生产一个消费一个)
LinkedTransferQueue:由链表结构组成的无界阻塞队列
LinkedBlockingDeuqe:由链表结构组成的双向阻塞队列
BlockingQueue的核心方法:
方法类型 |
抛出异常 |
特殊值 |
阻塞 |
超时 |
插入 |
add(e) |
offer(e) |
put(e) |
offer(e,time,unit) |
移除 |
remove() |
poll() |
take() |
poll(time,unit) |
检查 |
element() |
peek() |
不可用 |
不可用 |
抛出异常
当阻塞队列满时,再往队列里add插入元素会抛IllegalStateException:Queue full
当阻塞队列空时,再往队列里remove移除元素会抛NoSuchElementException
特殊值
插入方法,成功true失败false
移除方法,成功返回出队列的元素,队列里面没有就返回null
一直阻塞
当阻塞队列满时,生产者线程继续往队列里put元素,队列会一直阻塞生产线程直到put数据or响应中断退出
当阻塞队列空时,消费者线程试图从队列里take元素,队列会一直阻塞消费者线程直到队列可用
超时退出
当阻塞队列满时,队列会阻塞生产者线程一定时间,超时后限时后生产者线程会退出
SynchronousQueue没有容量
与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue
每一个put操作必须要等待一个take操作,否则不能继续添加元素,反之亦然
线程通信之生产者消费者传统版
sync——lock
wait——await
notify——signal
高内聚,低耦合,线程操纵资源类
判断,干活,唤醒通知
严防多线程并发状态下的虚假唤醒
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();
}
}
public class ProdConsumer_TraditionDemo {
public static void main(String[] args) {
ShareData shareData = new ShareData();
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
shareData.increment();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "AAA").start();
new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
shareData.decrement();
} catch (Exception e) {
e.printStackTrace();
}
}
}, "BBB").start();
}
}
synchronized和Lock有什么区别?用新的Lock有什么好处?
1、原始构成
synchronized是关键字属于JVM层面,monitorenter(底层是通过monitor对象来完成,其实wait/notify等方法也依赖于monitor对象只有在同步块或方法中才能调wait/notify等方法,monitorexit
Lock是具体类(java.util.concurrent.Locks.Lock)是api层面的锁
2、使用方法
synchronized不需要用户去手动释放锁,当synchronized代码执行完后系统会自动让线程释放对锁的占用
ReentrantLock则需要用户去手动释放锁若没有主动释放锁,就有可能导致出现死锁现象,需要Lock()和unlock()方法配合try/finally语句块来完成
3、等待是否可中断
synchronized不可中断,除非抛出异常或者正常运行完成
ReentrantLock可中断①设置超时方法tryLock(Long timeout, TimeUnit unit);②lockInterruptibly()放代码块中,调用interrupt()方法可中断
4、加锁是否公平
synchronized非公平锁
ReentrantLock两者都可以,默认非公平锁,构造方法可用传入boolean值,true为公平锁,false为非公平锁
5、锁绑定多个条件Condition
synchronized没有
ReentrantLock用来实现分组唤醒需要唤醒的线程们,可用精确唤醒,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程
题目:多线程之间按顺序调用,实现A->B->C三个线程启动,要求如下:
AA打印5次,BB打印10次,CC打印15次
紧接着
AA打印5次,BB打印10次,CC打印15次
…
求10轮
class ShareResource {
private int number = 1;//A:1 B:2 C:3
private Lock lock = new ReentrantLock();
private Condition c1 = lock.newCondition();
private Condition c2 = lock.newCondition();
private Condition c3 = lock.newCondition();
public void print5() {
lock.lock();
try {
//1、判断
while (number != 1) {
c1.await();
}
//2、干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3、通知
number = 2;
c2.signal();
}
}
public void print10() {
lock.lock();
try {
//1、判断
while (number != 2) {
c2.await();
}
//2、干活
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3、通知
number = 3;
c3.signal();
}
}
public void print15() {
lock.lock();
try {
//1、判断
while (number != 3) {
c3.await();
}
//2、干活
for (int i = 1; i <= 5; i++) {
System.out.println(Thread.currentThread().getName()+"\t"+i);
}
//3、通知
number = 1;
c1.signal();
}
}
}
public class synAndReentrantLockDemo() {
public static void main(String[] args) {
ShareResource shareResource = new ShareResource();
new Thread(() -> {
for (int i = 0; i <= 10; i++) {
shareResource.print5();
}
}, "A").start();
new Thread(() -> {
for (int i = 0; i <= 10; i++) {
shareResource.print10();
}
}, "B").start();
new Thread(() -> {
for (int i = 0; i <= 10; i++) {
shareResource.print15();
}
}, "C").start();
}
}
传接口不许传具体的类
class MyResource {
private volatile boolean FLAG = true;//默认开启,进行生产+消费,volatile保证线程之间的可见性
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, 2L, 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(2L, TimeUnit.SECONDS);
if (null == result || result.equalsIgnoreCase("")) {
FLAG = false;
System.out.println(Thread.currentThread().getName()+"\t 超过2秒钟没有取到蛋糕,消费退出");
return;
}
System.out.println(Thread.currentThread().getName()+"\t 消费队列蛋糕"+result+"成功");
}
}
public void stop() throws Exception {
this.FLAG = false;
}
}
public class ProdConsumer_BlockQueueDemo {
public static void main(String[] args) throws Exception {
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("5秒钟时间到,大老板叫停,活动结束");
myResource.stop();
}
}
8、线程池,对ThreadPoolExecutor的理解
查看CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行
主要特点:线程复用:控制最大并发数:管理线程
第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
线程池底层:阻塞队列
class MyThread implements Runnable {
@Override
public void run() {
}
}
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return null;
}
}
一个有泛型,一个没泛型;一个有返回值,一个没有返回值,泛型中写的类型就是返回值的类型;一个实现run()方法,一个实现call()方法
class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws InterruptedException, ExecutionException {
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread());
Thread t1 = new Thread(futureTask, "AAA");
t1.start();
int result01 = 100;
//while(!futureTask.isDone()){}
int result02 = futureTask.get();//要求获得Callable线程的计算结果,如果没有计算完成就要去强求,会导致阻塞,得等计算完成
System.out.println("*******result:" + (result01 + result02));
}
}
Java中的线程池是通过Executor框架实现的,该框架中用到了Executor、Executors、ExecuctorService、ThreadPoolExecutor这几个类
线程池的底层是ThreadPoolExecutor这个类
Executors.newScheduledThreadPool()
Executors.newWorkStealingPool(int)
java8新增,使用目前机器上可用的处理器作为它的并行级别
重点
Executors.newFixedThreadPool(int)
执行长期的任务,性能好很多
Executors.newSingleThreadExecutor()
一个任务一个任务执行的场景
Executors.newCachedThreadPool()
适用:执行很多短期异步的小程序或者负载较轻的服务器
//第四种获得/使用java多线程的方式,线程池
public class MyThreadPoolDemo {
public static void main(String[] args) {
//ExecutorService threadPool = Executors.newFixedThreadPool(5);//一池5个处理线程
//ExecutorService threadPool = Executors.newSingleThreadExecutor();//一池1个处理线程
ExecutorService threadPool = Executors.newCachedThreadPool();//一池N个处理线程
//三种线程池底层原理都是ThreadPoolExecutor
try {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName()+"\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
}
主要特点如下:
1、创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
2、newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue
@NotNull public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}
主要特点如下:
1、创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
2、newSingleThreadExecutor将corePoolSize和maximumPoolSize都设置为1,它使用的是LinkedBlockingQueue
@NotNull public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
}
主要特点如下:
1、创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
2、newCachedThreadPool将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,使用的SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程
七大参数
1、corePoolSize:线程池中的常驻核心线程数
①在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
②当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中
2、maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止
默认情况下,只有当线程池中的线程数大于corePoolSize时KeepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize
4、unit:keepAliveTime的单位
5、workQueue:任务队列,被提交但尚未被执行的任务
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程一般用默认的即可
7、handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝请求执行的runnable的策略
核心线程数满了——>阻塞队列——>阻塞队列满了——>增加到最大线程数——>开启拒绝策略(AbortPolicy/DiscardPolicy/CallerRunsPolicy/DiscardOldestPolicy)
1、在创建了线程池后,等待提交过来的任务请求
2、当调用execute()方法添加一个请求任务时,线程池会做如下判断:
2.1 如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务
2.2 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
2.3 如果这时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务
2.4 如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
3、当一个线程完成任务时,它会从队列中取下一个任务来执行
4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
所以线程池的所有任务完成后它最终会收缩到corePoolSize的大小
9、生产中如何设置合理的线程池参数
线程池的拒绝策略
等待队列也已经满了,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务,这时候我们就需要拒绝策略机制合理的处理这个问题
JDK内置的拒绝策略:
AbortPolicy
(默认):直接抛出RejectedExecutionException异常阻止系统正常运行
CallerRunsPolicy
:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
DiscardPolicy
:直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案
以上内置拒绝策略均实现了RejectedExecutionHandler接口
×你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用哪个多?
都不用,我们生产上只能使用自定义的
Executors中JDK已经给你提供了,为什么不用?
阿里巴巴开发手册
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
说明:Executors返回的线程池对象的弊端如下:
1)FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2)CachedThreadPool和ScheduledThreadPool:
允许的创建线程数量为Interger.MAX_VALUE,可能会创建大量的线程,从而导致OOM
线程池的手写改造和拒绝策略
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(2, 5, 1L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
}
}
合理配置线程你是如何考虑的?
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
而在单核CPU,无论你开几个模拟的多线程任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量
一般公式:CPU核数+1个线程的线程池
IO密集型
1、由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
2、IO密集型,即该任务需要大量的IO,即大量的阻塞
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力浪费在等待
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1 - 阻塞系数)
阻塞系数在0.8~0.9之间
比如8核CPU:8/(1 - 0.9) = 80个线程数
10、死锁编码及定位分析
死锁
死锁是指两个或两个以上的进程在执行过程中,因抢夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法推进下去。如果系统资源充足,进程的资源请求都能得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
产生死锁主要原因:
系统资源不足
进程运行推进的顺序不合适
资源分配不当
class HoldLockThread implements Runnable {
private String lockA;
private String lockB;
public HoldLockThread(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);
sychronized (lockB) {
System.out.println(Thread.currentThread().getName()+"\t 自己持有:" + lockB + "\t 尝试获得:" + lockA);
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
String lockA = "lockA";
String lockB = "lockB";
new Thread(new HoldLockThread(lockA, lockB), "ThreadAAA").start();
new Thread(new HoldLockThread(lockB, lockA), "ThreadBBB").start();
}
}
死锁解决
jps命令定位进程号
jstack找到死锁查看
11、Java里锁的认识
Linux ps -ef | grep xxxx
windows下的java运行程序,也有类似ps的查看进程的命令,但是目前我们需要查看的只是java
jps -l
jstack 9636(进程编号)