并发

xmy...大约 15 分钟

进程、线程的基础知识请查阅操作系统部分,这里只介绍Java中的线程哦~

Java线程模型

在Java中,启动一个main函数就代表启动了一个JVM进程,main函数所在的线程是这个进程的主线程,该JVM进程中的所有线程共享该JVM的堆和方法区,同时JVM在每个线程创建时为其分配各自的PC、虚拟机栈和本地方法栈,每个线程有一个Thread对象维护其上下文。

image-20210903102622040
image-20210903102622040

Java中线程有六个状态

状态含义
NEW线程对象刚被创建时的状态
RUNNABLE包含了操作系统中的运行和就绪状态,调用了start方法后就从NEW进入该状态
BLOCKED运行过程中需要调用的对象正在被其它线程占用时,进入该对象EntrySet里,同时转为BLOCKED状态
WAITING等待状态,进入该状态后需要等待被唤醒
TIMED_WAITING和等待状态一样,但若没人唤醒,超过时间参数自动唤醒
TERMINATED线程执行完毕,即run方法返回了
image-20210903120621985
image-20210903120621985

哪个线程调用了Object.wait()/线程对象.join(),哪个线程就进入WAITING状态,同时该线程进入Object的Monitor的WaitSet里

哪个线程调用了Object.notify(),就把这个Object的Monitor的WaitSet里随机一个线程唤醒

哪个线程调用了Object.notifyAll(),就把Object的Monitor的WaitSet里所有线程唤醒

如果上面的方法带时间参数,就不是进入WAITING而是TIMED_WAITING状态

并发控制中的锁一般有两种,悲观锁乐观锁,一般来说悲观锁是基于Monitor实现的,乐观锁是基于CAS+自旋来实现的,这二者在Java中分别对应synchronized关键字AQS

synchronized

synchronized修饰的方法或代码块同一时间只能被一个线程执行

一般有三种使用方法:

  1. 修饰实例方法:调用某对象的该方法前获取该对象实例的锁

  2. 修饰静态方法:调用某对象的该方法前获取该类的锁。

    两个线程分别执行同一个对象synchronized修饰的实例方法和静态方法时不会发生互斥,因为锁的资源不同,一个锁了对象实例,一个锁了类。

  3. 锁对象,修饰代码块:synchronized(对象的引用)锁的是对象实例,synchronized(类.class)锁的是类

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

synchronized不能修饰构造方法,也没必要修饰,构造方法本身就是线程安全的

底层原理

尝试获取对象的monitor,monitor已被其他线程占用时,获取失败,该线程进入EntrySet。占有monitor时调用wait()进入WaitSet。调用notify()时从WaitSet里随机选一个线程唤醒,调用notifyAll时唤醒WaitSet里所有线程

AQS

AQS全称是AbstractQueuedSynchronizer,它是Java中用来构建锁和同步器的基础框架,可以用于实现诸如ReentrantLock、Semaphore、CountDownLatch等多种同步工具。

image-20210906170322803
image-20210906170322803

AQS主要依赖于一个双向链表和一个volatile类型的整数state来实现同步控制。该整数state用来表示同步状态,一般情况下,state=0表示没有线程占用同步资源,state>0表示有线程占用同步资源,state<0表示同步资源已经被争用了多次,比如ReentrantLock可以允许一个线程多次获得锁,每次state值减一。

AQS的主要方法有下面几个:

  • acquire():该方法用来获取同步状态,如果同步状态被占用,则线程将被加入等待队列中。

  • acquireInterruptibly():与acquire()类似,但是该方法允许中断操作。

  • tryAcquire():该方法用来尝试获取同步状态,如果成功则返回true,否则返回false。

  • release():该方法用来释放同步状态,并唤醒等待队列中的线程。

  • acquireShared():该方法用来获取共享式同步状态,如果同步状态被占用,则线程将被加入等待队列中。

  • releaseShared():该方法用来释放共享式同步状态,并唤醒等待队列中的线程。

AQS实现同步的关键在于,它提供了一个基于FIFO队列的等待队列,通过将等待线程加入等待队列中,然后在释放同步状态的时候,从等待队列中唤醒等待线程,从而实现了同步机制。

AQS的实现主要有两种方式:独占式(Exclusive)和共享式(Shared)。独占式是指只有一个线程可以占用同步资源,比如ReentrantLock,而共享式是指多个线程可以同时占用同步资源,比如CountDownLatch。在AQS中,这两种方式的实现是基本相同的,区别在于获取和释放同步状态的方式不同。

以上是AQS的基本实现方式,它是Java中构建锁和同步器的核心框架,为各种同步工具的实现提供了强大的基础支持。

并发容器

Java中为保证并发下的线程安全,设计了一些并发容器,常见的有CopyOnWriteArrayList和ConcurrentHashMap

CopyOnWriteArrayList

CopyOnWriteArrayList的实现原理主要分为两个方面,一是利用可重入锁实现线程安全,二是通过复制数组实现读写分离。

在CopyOnWriteArrayList中,每次写操作都会先获取可重入锁,然后将当前数组复制一份,进行修改后再将新的数组赋值给原来的引用。在修改完成后释放锁。由于读操作不会对原数组进行修改,所以读操作可以直接对原来的数组进行读取,无需加锁。这样就实现了读写分离的效果,可以在不影响正在进行的读操作的情况下进行写操作。

在CopyOnWriteArrayList的实现中,由于每次写操作都需要复制整个数组,所以写操作的性能比较低。但是在读多写少的场景中,CopyOnWriteArrayList的并发性能比较好,因为读操作不会加锁,可以同时进行。

需要注意的是,虽然CopyOnWriteArrayList是线程安全的,但是它并不能保证数据的实时一致性。由于写操作的结果只会对新的数组产生影响,所以在多线程环境中,读取到的数据可能不是最新的。因此,CopyOnWriteArrayList适用于读多写少且对实时性要求不高的场景。

ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它可以在多线程环境下并发地进行读写操作,而不需要像传统的HashTable那样在读写时加锁。

ConcurrentHashMap的实现原理主要基于分段锁和CAS操作。它将整个哈希表分成了多个Segment(段),每个Segment都类似于一个小的HashMap,它拥有自己的数组和一个独立的锁。在ConcurrentHashMap中,读操作不需要锁,可以直接对Segment进行读取,而写操作则只需要锁定对应的Segment,而不是整个哈希表,这样可以大大提高并发性能。

在Concurr