并发编程基础
Java线程模型
发生了系统调用的锁,就是重量锁
MMU: 虚拟地址映射
线程类型
用户线程:使用Java代码创建的线程。
内核线程:操作系统对应的线程
对应关系
一对一
一个用户线程对应一个内核线程
优点:简单,几乎所有对线程的操作都交给了内核线程。
缺点:
对用户线程的大部分操作会映射到内核线程上,引起用户态和内核态的频繁切换。
创建大量线程对系统性能有影响。
多对一
多个用户线程对应一个内核线程
优点:用户线程的很多操作对内核来说是透明的,不需要进行频繁的用户态和内核态的切换,线程的创建、调度、同步非常快。
缺点:
其中一个线程阻塞,其他用户线程也无法执行。
内核不知道用户有哪些线程,无法像内核线程一样实现完整的调度、优先级等。
多对多
用户态和内核态
linux系统虚拟地址映射
物理地址空间 :物理地址就是真实地址,对应真实的内存条。内存像一个数组,每个存储单元被分配了一个地址,就是物理地址。
虚拟地址空间 : 每个进程拥有一个巨大的连续内存空间,甚至比内存空间更大,这是一个“假象”。
CPU使用虚拟地址像内存寻址,通过内存管理单元MMU硬件,把虚拟地址转换成真实的物理地址。
CPU有四种不同的执行级别0-3,linux用0表示内核态,3表示用户态。
切换方式
- 系统调用 (关注重点)
- 异常(不是Java中的异常)
- 外围设备中断
CPU上下文
CPU寄存器和程序计数器就是CPU的上下文,都是CPU在运行任务前,必须的依赖环境。
CPU寄存器:CPU内置的容量小、速度极快的内存。
程序计数器:用来存储CPU正在执行的指令位置,或者即将执行的下一条指令位置。
CPU上下文切换
将前一个任务的CPU上下文保存,然后加载新任务的上下文到寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新的任务。
CPU上下文切换类型
进程上下文切换
系统调用:从用户态到内核态的转变,通过系统调用来完成。 属于同进程内的CPU上下文切换。一次系统调用过程,发生两次CPU上下文切换(用户态 —> 内核态 —> 内核态)
进程间上下文切换:进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间资源,还包括了内核堆栈、寄存器等内核空间资源。比系统调用多了一步,在保存内核态资源之前,需要把该进程的用户态资源保存下来,加载了下一进程的内核态后,需要刷新新进程的虚拟内存和用户栈。
进程上下文切换场景:
- CPU轮转,进程分配的时间片耗尽。
- 系统资源不足
- 调用sleep等方法将自己主动挂起
- 有优先级更高的进程运行
线程上下文切换
- 两个线程属于不同进程: 资源不共享,切换过程和进程上下文切换一样。
- 属于同一进程:虚拟内存共享,只需要切换线程的私有数据、寄存器等不共享的数据。
并发相关知识
对象头
基本对象布局:对象头(96bit)+ [实例数据] + [对齐填充数据]
实例数据就是定义的全局变量所占的内存。
对象的大小必须为8字节(byte)的整数倍。1byte = 8 bit
添加这个依赖,可以打印出对象的信息ClassLayout.parseClass(A.class).toPrintable();
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
大小端模式
小端模式:高字节存在高地址,低字节存在低地址。所以打印出来是反的。
如 1的字节码为:00000000 00000000 00000000 00000001
打印出来就是:00000001 00000000 00000000 00000000
对象头:
mark world 占64bit
Klass point 指向对象的元数据。开启了指针压缩,4位。没开启,就占8位
偏向锁 01 轻量锁 00 重量锁 10 GC 11
偏向锁:
101 可偏向
001 不可偏向 计算了hashCode之后就不能偏向了
公平锁和非公平锁
总结:
一朝排队,永远排队
基于JDK1.8的解释:
非公平锁
首先会在加锁的时候去抢锁(公平锁不会上来就拿锁)
如果加锁失败,判断锁是否被人持有了,没有被人持有的话,会直接去进行加锁(公平锁会判断是否有人排队),成功进入代码块。失败则进入队列
进入队列后,如果前面那个是head,则再次尝试加锁,失败则park,进行真正的排队。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 第1步 加锁的时候就去获取锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
/**
*acquire(1)方法会调用到该方法,再次尝试获取锁
*
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 第2步 锁没别人持有,会去再次尝试拿锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 重入锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
...
}
公平锁
- 调用lock的时候不会去尝试加锁,回去查看队列中有没有排队的节点Node,如果有则进入队列(并不等于排队),
- 会再次进行查看前面那个节点是否为head,如果是,则再次尝试拿锁。失败则排队park
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 会判断是否有节点在排队
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
是否开启了偏向模式?涉及到偏向的撤销。
偏向延迟的时间是4秒钟。JVM自身的代码,基本上不存在偏向锁,所以把偏向锁给禁用掉了,因为偏向的撤销很耗费性能。而过了4秒钟之后,JVM认为自己已经执行完成了,而用户写的代码不知道是不是偏向锁,所以开启偏向锁。自己可以在启动的时候设置JVM的参数,设置启动延迟为0秒。-XX:BiasedLockingStartupDelay=0 -XX:+UseBiasedLocking //启用偏向锁
轻量锁00加锁成功一定是要无锁的状态
pthread_mutex_lock 重量锁
JMH 基准测试 java性能测试
public class A {
static boolean isRunning = true;
static int i;
void test(){
log.info("t1.start-----");
while (isRunning){
// 代码中不做什么事情
i++;
i = i + 3;
}
log.error("t1.end-----");
}
public static void main(String[] args) throws InterruptedException {
A a = new A();
new Thread(a::test,"t1").start();
Thread.sleep(200);
// 睡眠过后,更改这个值,不会让t1线程停止运行。isRunning改成volite可以实现,不是因为不可见性,而是因为禁止了指令重排
// 其实是Jvm对代码进行了优化,发现while循环中并没有做什么操作,创建了一个临时变量等于isRunning,来控制了循环
isRunning = false;
}
}
happend before JVM对代码进行了优化,如果发现
两条CAS指令不能保证原子性
synchronize发生异常,如果没有进行try catch 会释放锁。 monitorExit指令
Lock特点
- 可打断(lock.lockInterruptibly() 获取锁),可重入
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 支持读写锁
打断之后是在异常中处理的,自己可以在打断的异常中做自己的逻辑处理。
线程的顺序执行:
- wait和notify实现(while循环)
- LockSupport.park 和 LockSupport.unpark(t1)
- Join(),future()
并发编程基础知识
高并发的好处
- 充分利用CPU资源
- 加快响应用户时间
- 代码模块化,异步化,简单化
多线程注意事项
- 线程之间的安全性
- 线程之间的死锁
- 线程对服务器资源的消耗
线程的启动与中止
启动:
- 继承Thread类,调用start方法
- 实现Runnable,交给Thread运行
- 实现Callable,通过FutureTask把Callable包装成Runnable,交给Thread运行,可以通过FutureTask拿到Callable运行后的返回值。
中止:
- run方法执行完成,或者抛出一个未处理的异常导致线程提前结束
- 调用suspend()、resume() 和 stop()这些过期方法,不建议使用,调用后不会释放占有的资源
建议使用中断式的操作,调用线程的interrupt()方法对其进行中断操作,这个是协作式的。该线程通过isInterrupted()方法进行判断是否被中断。调用静态方法Thread.interrupted()会将中断标识改为false,也可以作为判断。
处于死锁的线程无法被中断
线程
线程中的方法
run()方法,本质是就是一个成员方法, 可重复执行,也可被单独调用
start()方法,只有执行了start()方法,才真正启动线程,只能执行一次。
yield()方法,让出CPU的执行权,但是让出时间不可设定,也不会释放锁资源。
join()方法,把指定线程A加入到当前线程,知道A线程执行完成后,才会继续执行当前线程。两个交替的线程可以合并为顺序执行。
sleep()方法,调用后,当前线程会休眠,不会释放锁。
setPriority()方法,设置线程的优先级,高优先级的线程分配的时间片数量要多于低优先级的线程。默认优先级为5,范围为1-10。高优先级的线程并不能保证一定会比低优先级的线程先执行。因为线程的调度最终取决于操作系统。
守护线程
Daemon守护线程是一种支持型线程,主要用作程序中后台调度和支持性工作。当Java虚拟机中不存在非Daemon线程的时候,Java虚拟机就会退出。使用Thread.setDaemon(true)将线程设置为守护线程。虚拟机退出时守护线程的finally块不一定会执行,所以守护线程不能依靠finally块来确保执行关闭或清理资源。垃圾回收线程就是守护线程。
等待通知机制
线程A调用了对象o的wait()方法进入等待状态,而线程B调用了对象o的notify()或者notifyAll()方法,线程A收到通知后,从o.wait()方法返回,执行后续操作。这几个方法都是针对对象的,都是Object中的方法。
等待通知范式:
// 等待方遵循的范式
synchronized(对象){// 1.获取对象的锁
while(条件不满足){// 2.条件不满足,调用对象的wait方法,被通知后仍要检查条件
对象.wait();// 会在这进行阻塞
}
对应的逻辑处理 // 3.条件满足则执行对应的逻辑
}
// 通知方范式
synchronized(对象){// 1.获得对象的锁
改变条件 // 2.改变条件
对象.notifyAll(); // 3.通知所有等待在线程上的线程
}
wait()、notify()、notifyAll()方法,线程必须要获得该对象的锁,只能在同步方法或者同步代码块中调用。进入wait方法后,当前线程就会释放锁。调用了notifyAll()后其它线程就会去竞争锁。但是锁释放是在同步代码块执行完成后释放,所以一般notifyAll方法都是在同步代码块的最后一行执行。
线程的并发工具类
Fork-Join
分而治之,设计思想是将一个难以解决的大问题,分割成一些规模较小的相同问题,各个击破,分而治之,最后进行join汇总。快速排序,归并排序,二分查找,大数据中M/R都是分而治之的思想。
CountDownLatch
闭锁,能够使一个线程等待其它线程完成各自的工作后再执行。通过一个计数器来实现,计数器的初始值为初十任务的数量,每完成一个任务后,计数器减1,当计数器到达0时,表示所以任务已完成。在闭锁上等待 CountDownLatch.await()方法的线程就可以恢复执行任务。
static CountDownLatch countDownLatch = new CountDownLatch(5);
public static void countDownLatchTest() {
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
System.out.println("子线程运行... " + finalI);
countDownLatch.countDown();// 计数器减1
}).start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
countDownLatchTest();
try {
countDownLatch.await();// 等计数器为0之后,停止阻塞,恢复执行
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程运行完成...");
}
并非说任务数量和线程数量一点要相等,一个线程可以多吃调用countDownLatch.countDown();进行计数器减1
CycilcBarrier
可循环使用的屏障。让一组线程到达一个屏障时被阻塞,直到最后一个线程到达屏障时,屏障才会开放,所有被屏蔽拦截的线程才会继续执行。
CyclicBarrier 默认的构造方法是 CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用 CyclicBarrier.await 方法告诉 CyclicBarrier 我已经到达了屏障,然 后当前线程被阻塞。
CyclicBarrier 还提供一个更高级的构造函数 CyclicBarrie(r int parties,Runnable barrierAction),用于在线程到达屏障时,优先执行 barrierAction,方便处理更复 杂的业务场景。
Semaphore
信号量用来控制同时访问特定资源的线程数量。可以用于做流量控制。Semaphore 的构造方法 Semaphore(int permits)接受一个整型的数字, 表示可用的许可证数量。线程使用 Semaphore的 acquire()方法获取一个许可证,使用完之后调用 release()方法归还许可证。还可以用 tryAcquire()方法尝试获取许可证。
Semaphore 还提供一些其他方法,具体如下。
intavailablePermits():返回此信号量中当前可用的许可证数。
intgetQueueLength():返回正在等待获取许可证的线程数。
booleanhasQueuedThreads():是否有线程正在等待获取许可证。
void reducePermits(int reduction):减少 reduction 个许可证,是个protected 方法。
Collection getQueuedThreads():返回所有等待获取许可证的线程集合,是个protected 方法。
Exchange
Exchanger交换者是一个用于线程间协作的工具类。用于进行线程间数据交换。提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。通过exchange()方法交换数据,第一个线程先执行exchange方法,会等待第二个线程执行exchange方法,两个线程都到达同步点时,就可以交换数据。
原子操作CAS
如果有两个操作A和B,如果从执行 A 的线程来看,当另一个线程执行 B 时,要么将 B 全部执行完,要么完全不执行 B,那么 A 和 B 对彼此来说是原子的。
锁实现原子操作存在的问题:被阻塞的线程优先级很高,获得锁的线程一直不释放锁,大量线程竞争支援,导致CPU资源浪费,死锁问题。
CAS基本思路:内存地址V上的值和期望的值A相等,则给其赋予新值B,否则不做任何事,只返回原值。
CAS实现原子操作的3大问题
-
ABA问题
一个值原来是A,后面变成了B,又修改成了A,使用CAS进行检查时发现其值没有发生变化,但是实际上是已经变化过了的。
解决思路就是使用版本号,在变量面前加上版本号。
-
循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
只能保证一个共享变量的原子操作
Jdk中原子相关操作类
- AtomicInteger
- AtomicIntegerArray
- AtomicReference
- AtomicStampedRefrence 利用版本戳形式记录了每次改变的版本号,避免ABA问题
- AtomicMarkableReference 带有标记位的引用类型
- AtomicIntegerFieldUpdater
- AtomicLongFieldUpdater
- AtomicReferenceFieldUpdater
显示锁
synchronized关键字会隐式的获取锁,它将锁的获取和释放固化了,就是先获取再释放。
显示锁提供了隐式锁很多没有的功能,比如尝试非阻塞的获取锁,能被中断的获取锁,超时获取锁等。
Lock的标准用法
lock.lock();// 获取锁
try{
...
}finally{
lock.unlock();// 释放锁
}
在finally语句块中执行unlock()方法,保证最终能够释放锁。
Lock的常用API
公平和非公平锁
先对锁获取的请求一定先被满足,那么这个锁就是公平锁。也就是等待时间最长的线程优先获取锁。反之就是非公平锁。非公平锁效率比公平锁高。
恢复一个被挂起的线程与该线程真正开始执行之间存在严重的延迟,有可能在这个延迟时间段内有另外一个线程获取到锁并且执行完成,被挂起的线程还没有被唤醒。正因为这个情况,非公平锁比公平锁效率更高。
ReentrantLock可重入锁
定义:同一个线程对于已经获得到的锁,可以多次继续申请到该锁的使用权。Synchronized隐式的支持可重入。ReentrantLock实现Lock接口和序列化接口,构造函数传入ture表示使用公平锁。默认为非公平锁。
读写锁ReentrantReadWriteLock
排它锁:在同一时刻,只允许一个线程进行访问。
读写锁在同一个时刻可以允许多个读线程访问。但是写线程访问时,所有的读线程和其它线程均被阻塞。
维护了一个读锁和一个写锁,通过分离读锁和写锁,提升并发性。
Condition接口
Condition 接口提供 了类似 Object 的监视器方法,与 Lock 配合可以实现等待/通知模式。
使用范式:
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void conditionWaiter() throws InterruptedException{
lock.lock();
try {
condition.await();
} finally {
lock.unlock();
}
}
public void conditionSignal() {
lock.lock();
try {
condition.signal();
}finally {
lock.unlock();
}
}