并发和并行

  • 并发

两个及两个以上的作业在同一时间段内执行。

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

  • 并行

两个及两个以上的作业在同一时刻执行。

指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。

并发与并行最关键的区别点是:是否同时执行。

同步和异步

  • 同步

发出调用之后,没有得到结果之前,该调用不可以返回,需一直等待。

  • 异步

发出调用之后,不用等待返回结果,该调用直接返回,可以执行下一步。

JAVA线程的生命周期

  • NEW 初始状态

线程被创建出来,还没有调用start()

  • RUNNABLE 运行状态

线程被调用了start 等待运行的状态

  • BLOCKED 阻塞状态

当线程进入 synchronized 方法快或调用 wait后(被 notify)重新进入 synchronized 方法快,但是锁被其他线程占有,那么该线程就会进入 BLOCKED 状态

  • WAITING 等待状态

线程需要等待其他线程做出一些特定的操作(通知或中断),如调用了wait()

  • TIME_WAITING 超时等待状态

指定的等待时间,而不是像WAITING状态那样一直等待,调用了sleep(long millis)或wait(long millis)可以讲线程置于 TIME_WAITING 状态。当超时时间结束后,线程将会返回到 RUNNABLE 状态。

  • TERMINATED 终止状态

表示线程已经运行完毕,执行完了run()

线程上下文切换

线程在执行过程中会有自己的运行条件和状态(也就是上下文)。

线程从占用CPU的状态中退出,这时会保存线程的上下文,并加载下一个将要占用CPU的线程上下文,这就完成了线程上下文切换。

也就是当前正在执行的线程发生了切换。

保存的上下文用于下次线程占用CPU时可以恢复执行。

  • 什么时候线程会从占用CPU的状态中退出?

  1. 线程主动退出CPU占用,如调用了sleep(),wait()等

  2. 调用阻塞类型的系统中断,如请求IO,线程被阻塞

  3. 时间片用完,这个与操作系统相关,操作系统为了防止一个线程或进程长期占用CPU而导致其它线程与进程没有资源。

  4. 被终止或结束运行

多线程概念与问题

为什么要使用多线程?

从计算机来讲,线程是程序执行的最小单位,线程之间的切换和调用的成本低于进程。

从项目上来讲,多线程并发可以提高系统整体的并发能力与性能,可以支持更多的用户。

多线程会带来什么问题?

多线程开发的目的是为了提高程序的执行效率和运行速度,但是在使用过程中会遇到内存泄漏,死锁,线程不安全等问题。

什么是线程不安全?

多线程环境下对同一份数据的访问是否能够保证其正确性和一致性。

安全代表着无论多少格线程同时访问一份数据,都不会出现数据混乱、错误或者丢失等问题。

什么是死锁?

多个线程同时被阻塞,它们中一个或全部都在等待某个资源释放,由于线程被无限期的阻塞,导致程序无法正常终止。

死锁的四个必要条件

  1. 互斥:该资源任意时刻只由一个线程占用

  2. 请求与保持:一个线程因请求资源而阻塞时,对已获得的资源保持不放

  3. 不剥夺:线程已获得的资源在未使用完之前,不能被其他线程强行剥夺,只有自己使用完毕后才会的都释放

  4. 循环等待:若干线程之间,形成一种头尾相接的循环等待资源关系

简单示例:

线程1持有A资源,线程2持有B资源,它们同时都在申请对方的资源,而线程没有执行完,资源没有被释放,导致它们互相在等待对方执行完毕释放资源,这就进入了死锁状态。

如何预防和避免死锁?

破坏形成死锁的必要条件即可预防死锁的发生。

破坏请求与保持:一次性申请所有的资源

破坏不剥夺条件:占用部分资源的线程,申请其他资源,如果申请不到,可以主动释放持有资源

破坏循环等待条件:按照顺序申请资源,释放资源则按反顺序释放

在之前线程1持有A,线程2持有B,它们同时申请对方资源导致死锁的问题。

就可以让线程1与线程2都持有AB(一次性申请所有的资源),这样执行过程中一定有一方拿不到AB资源从而进入等待。

而另一方拿到了AB资源可以正常执行,然后释放资源。

sleep() 方法和 wait() 方法对比

sleep()与wait()的区别

  • 共同点

两者都可以暂停线程的执行。

  • 区别

  1. sleep()没有释放锁,而wait()释放了锁(当前线程占有的对象锁)

  2. wait()通常被用于线程间交互/通信,sleep()通常被用于线程暂停执行

  3. wait()方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()或notifyAll(),当然,可以使用wait(long timeout)超时后线程会自动苏醒

  4. sleep()方法执行完成后,线程会自动苏醒

  5. sleep()是 Thread 类的静态本地方法,wait()是Object类的本地方法

为什么wait()方法不定义在Thread中?

wait()是让获得对象锁的线程实现等待,会自动释放当前线程占有的对象锁

每个对象(Object)都拥有对象锁,既然要释放当前线程占有的对象锁,并让其进入WAITING状态,自然操作对应的对象(Object),而非当前的线程(Thread)

为什么sleep()方法定义在Thread中是一样的道理

sleep()是让当前线程暂停执行,操作的是线程,不涉及到对象类,也不需要获取对象锁。

可以调用Thread类的run方法码?

new一个Thread,线程进入了初始状态,调用start(),线程进入就绪状态,分配到时间片后就会开始运行。

start()会执行线程相应的准备工作,然后自动执行run()方法的内容,这是多线程。

但是直接执行run()方法会把run()方法当成一个main线程(主线程)下的普通方法去执行,并不会在其他线程中执行它,所以这就不是多线程了。

调用start()方法可以启动线程并使线程进入运行状态,然后自动执行run()方法

直接调用run()方法则是在主线程下执行,并不会以多线程的方式执行

什么是悲观锁

悲观锁是一种思想,它总是假设每次都是最坏的情况。

认为共享资源每次被访问都会发生问题,所以在每次对资源的操作都会加锁。

这样当一个线程拿到资源后,其他线程获取该资源都会进入阻塞状态,需要等待该资源被释放。

java 中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。

public void performSynchronisedTask() {
    synchronized (this) {
        // 需要同步的操作
    }
}

private Lock lock = new ReentrantLock();
lock.lock();
try {
   // 需要同步的操作
} finally {
    lock.unlock();
}

在高并发的场景下,激烈的锁竞争会造成大量大线程阻塞,线程阻塞会导致系统频繁的触发线程上下文切换,增加系统的性能开销。

并且悲观锁会存在死锁问题,影响程序的运行。

什么是乐观锁?

乐观锁是一种思想,它总是假设每次都是最好的情况。

认为共享资源每次都被访问都不会出现问题,线程可以不停的执行,无需加锁,也无需等待。

只有在提交修改的时候去验证对应资源是否被其他线程修改了

java 中 java.util.concurrent.atomic 包下面的原子变量类(比如AtomicInteger、LongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 // 代价就是会消耗更多的内存空间(空间换时间) 
LongAdder sum = new LongAdder();
sum.increment();

高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。

但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。

乐观锁一般会使用版本号机制或CAS算法实现。

版本号机制

比如数据库中加一个数据的版本号version字段。

然后修改的时候先获取,提交修改时对比版本号是否一致。

如果一致,修改版本号的值,然后提交修改。

如果不一致,表示中途被其他线程修改,应该要驳回本次请求。

CAS算法

CAS的全称时 Compare And Swap(比较与交换),用于实现乐观锁,被广泛的应用各大框架中。

CAS的思想就是用一个预期值和要更新的变量值进行比较,两值相等才会更新数据。

CAS是一个原子操作,底层依赖于一条CPU的原子指令。

CAS涉及到三个操作数:

  1. V:要更新的变量值(Var)

  2. E:预期值(Expected)

  3. N:拟写入的新值(New)

仅当V的值等于E时,CAS通过原子方式来将N的值更新到V。

如果不相等,说明V已经被其他线程更新,本线程应该放弃更新。

简单案例

int a = 1;

需要将 i 的值更新为 6

那么 V = a,E = 1,N = 6

V与E进行比较,相等,说明 a 还没有被其他线程修改,可以将值更新为6

如果不相等,说明 a 已经被其他线程修改,本次放弃更新。

CAS算法的ABA问题

使用CAS算法时,因为V读取到的值是预期值,可能已经发生修改了,比如1修改为5然后又被修改为1,那么CAS操作就会认为它从来没有被修改过,这就是ABA问题。

ABA问题的解决思路是在变量前面追加上版本上或者时间戳。

synchronized 有什么用?

synchronized 是JAVA的一个关键字,用于控制多线程对共享资源的访问,实现线程同步。

需要注意的是 synchronized 在使用中锁住的范围。

// 修饰实例方法 (锁当前对象实例)
// 即同一时间只有一个线程可以访问该实例的此方法
synchronized void method() {
	//业务代码
}
// 修饰静态方法 (锁当前类)
// 即同一时间只有一个线程可以访问该类的所有实例的此静态方法
synchronized static void method() {
    //业务代码
}
// 修改代码块
// synchronized(object) 表示进入同步代码库前要获得指定对象的锁。
// synchronized(类.class) 表示进入同步代码前要获得指定 Class 的锁
public void method() {
	// 同步代码块,对当前实例加锁(对象锁)
    synchronized (this) {
    }
	// 同步代码块,指定Class加锁
	synchronized (类.class) {
    }
}

总结:

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块上都是是给 Class 类上锁

synchronized 关键字加到实例方法上是给对象实例上锁

注意:

尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能

构造方法不能使用 synchronized 关键字修饰,因为构造方法本身就属于线程安全

ReentrantLock 有什么用?

ReentrantLock 实现了 Lock 接口,是一个可重入且独占式的锁,和 synchronized 关键字类似。不过,ReentrantLock 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。

ReentrantLock 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。

// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

公平锁和非公平锁有什么区别?

  • 公平锁

锁被释放之后,先申请的线程先得到锁。

性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。

  • 非公平锁

锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。

性能更好,但可能会导致某些线程永远无法获取到锁

共享锁和独占锁有什么区别?

  • 共享锁

一把锁可以被多个线程同时获得

  • 独占锁

一把锁只能被一个线程获得