一.进程与线程概念
- 进程的概念
进程是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位。简单来说,当你运行一个程序,操作系统就会为这个程序创建一个或多个进程,每个进程都有自己独立的内存空间、系统资源(如文件描述符、I/O 端口等)以及执行状态。
- 线程的概念
线程是进程中的一个执行单元,是 CPU 调度和分派的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源,但每个线程都有自己独立的执行栈和程序计数器,用于记录线程的执行状态,多个线程可以在同一进程中并发执行,操作系统通过线程调度算法来分配 CPU 时间片,使得多个线程看起来像是同时运行。注意这里是并发不是并行,并行与并发区别如下图:
二.线程的操作(开启、等待、唤醒、停止)
1.线程的状态
通过上述图可以看出,线程差不多就6种状态,分别是新建/运行/阻塞/等待/超时等待/终止。
2.线程操作
1.线程的开启
new Thread(new Runnable(){
@override
public void run(){
}
}).start();
注意这里的start并不是直接就运行了,这个要依赖cpu的线程调度执行。
2.线程的等待和唤醒
private static final int maxSize = 5;
private static final List<Integer> integers = new LinkedList<>();
private static final Object lock = new Object();
@Test
public void testWaitNotify() {
Thread consumer = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("consumer start");
while (true) {
synchronized (lock) {
try {
while (integers.size() == 0) {
System.out.println("consumer 为空 等待唤醒");
lock.wait();//释放锁
}
System.out.println("consumer 移除: " + integers.remove(0));
lock.notify();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
});
Thread producer = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("producer start");
while (true) {
synchronized (lock) {
try {
while (integers.size() >= maxSize) {
System.out.println("producer 等待唤醒");
lock.wait();//释放锁
}
integers.add(1);
lock.notify();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
});
producer.start();
consumer.start();
try {
producer.join();
consumer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
3.线程的停止
public void testThreadStop() {
thread = new Thread(new Runnable() {
@Override
public void run() {
while (!thread.isInterrupted()) {
i++;
if (i == 2) {
System.out.println("stop it--->>>"+i);
//thread.stop();//抛异常退出
//thread.interrupt(); //打断退出
//定义变量退出
try {
Thread.sleep(3*1000);
System.out.println("stop it--->>> sleep over");
} catch (InterruptedException e) {
System.out.println("stop it--->>> sleep error");
thread.interrupt();//如果在休眠期间 线程被打断,可能就会抛异常,
e.printStackTrace();
}
}
}
}
});
thread.start();
try {
Thread.sleep(2*1000);
thread.interrupt();//在外部中断这个线程,导致线程在休眠时异常。isInterrupted()就会是false,直到正确的打断线程
} catch (InterruptedException e) {
e.printStackTrace();
}
while (true);
}
4.线程的插入
public void testThread2() {
final Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2");
}
});
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread");
try {
thread2.join(); //在thread线程中插入thread2
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread over");
}
});
thread.start();
thread2.start();
try {
thread.join(); //在主线程中插入thread
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.线程的小知识点
线程大小在虚拟机栈种的默认设置大概是1M:所以请务必尽量使用线程池来控制线程的创建和操作。
线程的终止可能会抛出终端异常:
如果线程内部做了sleep、wait等可能抛出中断异常的操作,如果方法未执行完成时,强行在外部中断这个线程,中断的标志位就会为false,中断异常也会抛出。
上下文的切换的概念是什么?
如果我们的处理器是1.6ghz的,1条指令的周期大概是0.6ns,上下文切换是由于进程时CPU时间轮转机制,来回的让出资源从而引起的上下文切换,时间大概为20000个cpu周期,我们可以使用协程来帮助我们来减少上下文的切换,这个后面可以参考kotlin中的协程。
wait()执行后会释放锁,线程会处于等待状态,直到有人唤醒,notify或notifyAll并不会释放锁,只是唤醒其他线程。
三.线程同步
1.synchronized和volatile
synchronized关键字是用来做线程同步的, 它避免了多个线程访问操作同一个变量从而导致变量最后的计算不确定性,它是悲观锁,具有独占性,并且他也是非公平锁,因为某个线程拿到锁执行结束释放锁后,它和其他线程拿到此锁的机会一致,并不排队。
1. synchronized的使用及其坑点
1.锁对象
2.锁class对象
3.锁方法
4.示例代码
public class SynchronizedTest {
/**
// * 1.锁方法 锁的是当前调用此方法的对象的方法
// */
// int i = 0;
// private synchronized void addOne() {
//// 锁class对象 synchronized (SynchronizedTest.class){
// for (int j = 0; j < 10_000; j++) {
// i++;
// }
//// }
// }
//
//
//
// private synchronized void addOne2() {
//// 锁class对象 synchronized (SynchronizedTest.class){
// for (int j = 0; j < 10_000; j++) {
// i++;
// }
//// }
// }
/**
* 2.锁类方法,锁的是class对象
*/
// static int i = 0;
// private static synchronized void addOne() {
//// 锁class对象 synchronized (SynchronizedTest.class){
// for (int j = 0; j < 10_000; j++) {
// i++;
// }
//// }
// }
//
//
// /**
// * 1.锁静态方法 锁的是当前调用此方法的对象的方法 不管是不是实例对象调用
// */
// private static synchronized void addOne2() {
//// 锁class对象 synchronized (SynchronizedTest.class){
// for (int j = 0; j < 10_000; j++) {
// i++;
// }
//// }
// }
/**
// * 1.对象
// */
int i = 0;
private Object lock =new Object();
private void addOne() {
synchronized (lock){
for (int j = 0; j < 10_000; j++) {
i++;
}
}
}
private void addOne2() {
// 锁class对象
synchronized (lock) {
for (int j = 0; j < 10_000; j++) {
i++;
}
}
}
@Test
public void testSyn() {
final SynchronizedTest synchronizedTest = new SynchronizedTest();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.addOne();
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronizedTest.addOne2();
}
});
thread1.start();
thread.start();
try {
thread1.join();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i = " + synchronizedTest.i);
}
}
2. synchronized基本原理
锁对象时我们看下class字节码,主要是有一对MoniterEnter和Out来标记是否上锁,同时jvm的对象头中也有个锁标志。
锁class对象是在方法外加了个synchronized来控制线程的调用。
3. synchronized的优化
1.轻量级锁
如果在竞争拿锁的线程并没有拿到锁,如果要挂起/阻塞,就重新做一次自旋(拿锁),如果拿到了就可以继续执行。这种使用自旋一次的拿锁的过程就叫轻量级锁。
2.重量级锁
如果在竞争拿锁的线程并没有拿到锁,如果要挂起/阻塞,就重新做多次(<10次,大概是1次上下文切换的时间差不多3-5us)自旋(拿锁),如果拿到了就可以继续执行。通过这种自旋依旧拿不到就会膨胀为重量级锁。
3.偏向锁
大部分情况,我们在拿锁的时候就认为当前对象就是当前线程的锁,它总是偏向第一个拿锁的线程,无需cas来判断就叫偏向锁。
4.对象头的标志位示意图
5.锁的撤销与升级
当我们锁从偏向锁向轻量级锁升级时,会撤销轻量级锁,可能就会同GC一样触发STW。
4. volatile的作用
1.原子性:防止指令重排序;
2.修改可见性:线程读时,总可见看见其他线程的修改,但volatile++不具有原子性。
volatile int i = 0; //无法做到同步源子性
private void addOne() {
for (int j = 0; j < 10_000; j++) {
i++;
}
}
private void addOne2() {
for (int j = 0; j < 10_000; j++) {
i++;
}
}
@Test
public void testSyn() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
addOne();
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
addOne2();
}
});
thread1.start();
thread.start();
try {
thread1.join();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i = " + i);
}
volatile+cas一般替换synchronized
但是它不具有同步锁性,只是在某个线程修改后,将数据刷新到主内存中,其他线程读取的时候能拿到最新的,它适应于一写多读。比我们在DCL单例时常用此volite。
public class VolatileTest {
//加入volatile防止指令重排序
private volatile static VolatileTest instance;
private VolatileTest (){}
public VolatileTest getInstance(){
if(null==instance){ //第一次判空,在同步前,减少同步操作
synchronized (VolatileTest.class){
if(null==instance){ //第二次判空,在null的基础上创建对象
//1分配内存
//2调用构造函数
//3.对象指向给instance
instance=new VolatileTest();
}
}
}
return instance;
}
}
5. synchronized坑点
1.synchronized的错误使用
锁Interger的坑,如果我们的锁是Interger,并且这个数值是变得,就无法上锁。
Integer i = 0;
private void addOne() {
synchronized (i){//会锁失败,integer对象会变
for (int j = 0; j < 10_000; j++) {
i++;
}
}
}
private void addOne2() {
synchronized (i) {
for (int j = 0; j < 10_000; j++) {
i++;
}
}
}
@Test
public void testSynError() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
addOne();
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
addOne2();
}
});
thread1.start();
thread.start();
try {
thread1.join();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i = " +i);
}
2.死锁
死锁如何产生的?
至少两个线程抢占2个以上资源;每个线程拿到一把锁后等待另个线程释放另一把锁,并且不释放已经拿到的锁。
示例代码分析
public class SynchronizedDeadest {
//测试死锁
Object lockA = new Object();
Object lockB = new Object();
//死锁测试
Integer i = 0;
private void addOne() {
synchronized (lockA) {
System.out.println("addOne wait lockB");
synchronized (lockB) {
System.out.println("addOne GET lockB");
}
}
}
private void addTwo() {
synchronized (lockB) {
System.out.println("addTwo wait lockA");
synchronized (lockA) {
System.out.println("addTwo GET lockA");
}
}
}
@Test
public void testSynError() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
addOne();
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
addTwo();
}
});
thread1.start();
thread.start();
try {
thread1.join();
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.CAS、AQS、LOCK
1.CAS与其原理
compareandswap的简称,它是无锁化线程同步。它的原理是当内存种的变量与期望种的一致,则会进行相关修改,如果不一致则继续比较,它是乐观锁,原子的操作。
2.CAS的坑
1.ABA的问题
如果其他线程修改了A变成B,然后再改回A,这个就会导致ABA的问题。
如何解决ABA的问题?
方式1:使用 AtomicStampedReference来解决, 它不仅记录对象的引用,还维护一个时间戳(stamp)或版本号。每次修改都会递增版本号,即使值从A变回A,版本号也会不同,从而可以检测到中间的变化。
// 创建一个 AtomicStampedReference,初始值为 "A",初始版本号为 0
private static AtomicStampedReference<String> atomicStampedReference =
new AtomicStampedReference<>("A", 0);
@Test
public void testCasABA() {
// 线程 1 模拟正常的更新操作
Thread thread1 = new Thread(() -> {
try {
// 获取当前的版本号
int[] stampHolder = new int[1];
String value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("线程 1 获取到的值: " + value + ", 版本号: " + stamp);
// 模拟一些耗时操作
Thread.sleep(2000);
// 尝试更新值
boolean result = atomicStampedReference.compareAndSet(value, "C", stamp, stamp + 1);
System.out.println("线程 1 更新结果: " + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 线程 2 模拟 ABA 操作
Thread thread2 = new Thread(() -> {
try {
// 获取当前的版本号
int[] stampHolder = new int[1];
String value = atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("线程 2 获取到的值: " + value + ", 版本号: " + stamp);
// 将值从 A 改为 B
boolean result1 = atomicStampedReference.compareAndSet(value, "B", stamp, stamp + 1);
System.out.println("线程 2 将值从 A 改为 B 的结果: " + result1);
// 获取新的版本号
stamp = atomicStampedReference.getStamp();
// 将值从 B 改回 A
boolean result2 = atomicStampedReference.compareAndSet("B", "A", stamp, stamp + 1);
System.out.println("线程 2 将值从 B 改回 A 的结果: " + result2);
} catch (Exception e) {
e.printStackTrace();
}
});
// 启动线程
thread1.start();
thread2.start();
// 等待线程执行完毕
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 输出最终结果
int[] stampHolder = new int[1];
String finalValue = atomicStampedReference.get(stampHolder);
int finalStamp = stampHolder[0];
System.out.println("最终值: " + finalValue + ", 最终版本号: " + finalStamp);
}
方式2:使用AtomicMarkableReference来标记值是否已经修改了。
private static AtomicMarkableReference<String> atomicMarkableReference = new AtomicMarkableReference<>("A", false);
@Test
public void testCasABAAtomicMark() {
// 线程 1 模拟正常的更新操作
Thread thread1 = new Thread(() -> {
boolean[] markHolder = new boolean[1];
String value = atomicMarkableReference.get(markHolder);
boolean mark = markHolder[0];
System.out.println("线程 1 获取到的值: " + value + ", 标记: " + mark);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//当线程 1 执行 compareAndSet 操作时,由于布尔标记与预期不一致,更新操作失败,从而避免了 ABA 问题。
boolean result = atomicMarkableReference.compareAndSet(value, "C", mark, true);
System.out.println("线程 1 更新结果: " + result);
});
// 线程 2 模拟 ABA 操作
Thread thread2 = new Thread(() -> {
boolean[] markHolder = new boolean[1];
String value = atomicMarkableReference.get(markHolder);
boolean mark = markHolder[0];
System.out.println("线程 2 获取到的值: " + value + ", 标记: " + mark);
boolean result1 = atomicMarkableReference.compareAndSet(value, "B", mark, true);
System.out.println("线程 2 将值从 A 改为 B 的结果: " + result1);
mark = atomicMarkableReference.isMarked();
boolean result2 = atomicMarkableReference.compareAndSet("B", "A", mark, true);
System.out.println("线程 2 将值从 B 改回 A 的结果: " + result2);
});
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean[] markHolder = new boolean[1];
String finalValue = atomicMarkableReference.get(markHolder);
boolean finalMark = markHolder[0];
System.out.println("最终值: " + finalValue + ", 最终标记: " + finalMark);
}
方式3:采用悲观锁,如Synchronized,ReetrantLock
2.自旋开销问题
CAS通常与自旋循环结合使用,即线程不断尝试CAS操作直到成功。在高并发场景下,如果竞争激烈,线程可能会陷入长时间的自旋,消耗大量CPU资源,导致系统性能下降。可以 使用Thread.yield() 提示调度器让出CPU。
@Test
public void testCasYield() {
AtomicInteger atomicInteger = new AtomicInteger(0);
// 创建一个线程来进行CAS操作
Thread thread = new Thread(() -> {
int expectedValue = atomicInteger.get();
int newValue = expectedValue + 1;
// 自旋进行CAS操作
while (!atomicInteger.compareAndSet(expectedValue, newValue)) {
// 自旋失败,让出CPU
Thread.yield();
// 重新获取当前值
expectedValue = atomicInteger.get();
newValue = expectedValue + 1;
}
System.out.println("CAS操作成功,最终值为: " + atomicInteger.get());
});
// 启动线程
thread.start();
try {
// 等待线程执行完毕
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
3.AQS及其基本原理
1.AQS介绍及原理
AQS是java并发包中的抽象同步框架,主要封装了线程等待、唤醒、排队机制,底层使用的是volatile修饰的state来控制或判断线程的占用状态,0表示没有人占,1表示已上锁,如果拿不到就会被放到双向队列中排队,怎么加锁,怎么释放锁,需要通过方法重写来实现,比如ReentrantLock、CountDownLatch等。
2.ReentrantLock
它是依托于aqs实现的,默认是非公平锁,如果是公平锁(释放锁后再次其他非队列中线程想要获取锁时,如果队列中有等待线程,则需要排队,非公平锁默认不需要排队),需要在构造参数中设置入参。
读写锁
ReentrantReadWriteLock 是 Java 并发包(java.util.concurrent)中提供的一个读写锁实现,它基于 AQS(AbstractQueuedSynchronizer)实现,遵循“读共享、写互斥”的原则,允许多个线程同时进行读操作,但在写操作时会独占锁,以提高并发性能。
public class ReentrantLockTest {
/**
* 遵循“读共享、写互斥”的原则,允许多个线程同时进行读操作,但在写操作时会独占锁,以提高并发性能。
*
* 读共享:多个线程可以同时获取读锁进行读操作,提高了读操作的并发性能。
* 写互斥:同一时刻只能有一个线程获取写锁进行写操作,并且在写锁被持有时,其他线程不能获取读锁或写锁。
* 可重入:读锁和写锁都支持可重入,即同一个线程可以多次获取读锁或写锁。
* 锁降级:写锁可以降级为读锁,即持有写锁的线程在不释放写锁的情况下可以获取读锁,然后再释放写锁。
*/
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
private int sharedData = 0;
// 读操作方法
public int readData() {
readLock.lock();
try {
System.out.println("getLock--->");
return sharedData;
} finally {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
readLock.unlock();
System.out.println("release---> over");
}
}
// 写操作方法
public void writeData(int newData) {
writeLock.lock();
try {
sharedData = newData;
} finally {
writeLock.unlock();
}
}
@Test
public void testReadWrite() {
// 写操作
new Thread(() -> writeData(10)).start();
// 读操作
new Thread(() -> System.out.println("读取的数据---: " +readData())).start();
new Thread(() -> System.out.println("读取的数据--->>>: " +readData())).start();
while (true);
}
}
3.Condition
Condition 实例是由 Lock 对象创建的,它可以和 Lock 配合使用,将一个 Lock 对象分割成多个等待队列, 每个队列对应一个 Condition 实例。这使得线程可以在不同的条件下等待和被唤醒,提高了线程间协作的灵活性和效率。
- 等待队列的创建
当通过 Lock 对象的 newCondition() 方法创建 Condition 实例时,实际上是创建了一个新的等待队列。每个 Condition 实例都有自己独立的等待队列,这个队列是一个 单向链表 ,队列中的每个节点代表一个等待在该 Condition 上的线程。 - 线程进入等待队列
当线程调用 Condition 的 await() 方法时,会发生以下操作:
**释放锁:线程会释放当前持有的 Lock 锁,以允许其他线程获取该锁。
****创建节点:为当前线程创建一个新的节点,并将其加入到 Condition 的等待队列尾部。
**线程阻塞:线程进入阻塞状态,等待被唤醒。
- 线程从等待队列唤醒
当其他线程调用 Condition 的 signal() 或 signalAll() 方法时,会发生以下操作:
signal() 方法:会从 Condition 的等待队列头部取出一个节点,并将其转移到 AQS 的同步队列中。被转移的线程会在合适的时候竞争获取锁,获取到锁后会从 await() 方法返回继续执行。
signalAll() 方法:会将 Condition 等待队列中的所有节点依次转移到 AQS 的同步队列中,这些线程都会在合适的时候竞争获取锁。
- 多等待队列的管理
由于每个 Condition 实例都有自己独立的等待队列,不同的 Condition 实例之间的等待队列是相互隔离的。这使得线程可以根据不同的条件进入不同的等待队列,实现了多等待队列的功能。例如,在一个生产者 - 消费者模型中,可以创建两个 Condition 实例,一个用于生产者在缓冲区满时等待,另一个用于消费者在缓冲区空时等待,这样就可以实现生产者和消费者线程的精细控制。
/**
* Condition 实例是由 Lock 对象创建的,它可以和 Lock 配合使用,将一个 Lock 对象分割成多个等待队列,
* 每个队列对应一个 Condition 实例。这使得线程可以在不同的条件下等待和被唤醒,提高了线程间协作的灵活性和效率。
*
* 通过使用 Condition,实现了生产者和消费者线程之间的精细协作,避免了线程的盲目等待和唤醒
*/
private static final int MAX_SIZE = 5;
private final Queue<Integer> queue = new LinkedList<>();
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();//队列不满
private final Condition notEmpty = lock.newCondition();//队列不为空
// 生产者方法
public void produce() throws InterruptedException {
lock.lock();
try {
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
notFull.await(); //当队列已满时,生产者线程会调用 notFull.await() 方法进入等待状态,直到队列有空闲位置
}
int item = (int) (Math.random() * 100);
queue.add(item);
System.out.println("生产者生产了: " + item);
notEmpty.signal();//当生产了一个元素后,会调用 notEmpty.signal() 方法唤醒一个等待的消费者线程 notEmpty.await()会被唤醒
} finally {
lock.unlock();
}
}
// 消费者方法
public void consume() throws InterruptedException {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
notEmpty.await(); //当队列为空时,消费者线程会调用 notEmpty.await() 方法进入等待状态,直到队列中有元素
}
int item = queue.poll();
System.out.println("消费者消费了: " + item);
notFull.signal();//当消费了一个元素后,会调用 notFull.signal() 方法唤醒一个等待的生产者线程 notFull.await() 会被唤醒
} finally {
lock.unlock();
}
}
@Test
public void testCondition() throws InterruptedException {
// 生产者线程
Thread producerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
produce();
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者线程
Thread consumerThread = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
producerThread.join();
consumerThread.join();
}
4.LOCK一些概念
重入锁
重入锁也被称为递归锁,指的是同一个线程在持有锁的情况下,可以多次获取该锁而不会被阻塞。每一次获取锁都会使锁的持有计数加 1,每一次释放锁则使持有计数减 1,只有当持有计数为 0 时,锁才会真正被释放,其他线程才能获取该锁。
不可重入锁
不可重入锁则相反,当一个线程已经持有该锁时,如果它再次尝试获取该锁,就会被阻塞,直到锁被释放。也就是说,不可重入锁不允许同一个线程在未释放锁的情况下再次获取该锁。
互斥锁
互斥锁是一种排他性的锁,其核心作用是确保在任意时刻,只有一个线程能够访问特定的临界区资源。当一个线程持有互斥锁时,其他任何试图获取该锁的线程都将被阻塞,直到持有锁的线程主动释放锁
共享锁
共享锁,也称为读锁,允许多个线程同时读取同一份资源,但在写入时必须获取独占锁。它的设计遵循“读共享,写互斥”的原则。
5.自定义锁
步骤1:实现Lock接口,重写方法;
步骤2:内部类继承自AQS,实现获取锁和释放锁的操作;
步骤3:再实现lock的接口的类中,调用内部实现aqs的方法进行判断和调用。
class MyLock implements Lock {
private final SelfAqs selfAqs = new SelfAqs();
@Override
public void lock() {
//最终会调用到 selfAqs.tryAcquire(1)
selfAqs.acquire(1);
}
//线程再等待锁的过程种可以被打断
@Override
public void lockInterruptibly() throws InterruptedException {
selfAqs.acquireInterruptibly(1);
}
//尝试获取锁
@Override
public boolean tryLock() {
return selfAqs.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return selfAqs.tryAcquireNanos(1,unit.toNanos(time));
}
@Override
public void unlock() {
selfAqs.release(1);
}
//配合wait的notify
@Override
public Condition newCondition() {
return selfAqs.condition();
}
public boolean isLocked(){
return selfAqs.isLocked();
}
//是否有等待线程
public boolean hasWait(){
return selfAqs.hasQueuedThreads();
}
/**
* 内部类继承自ABS
*/
class SelfAqs extends AbstractQueuedSynchronizer {
//AQS原理就是修改cas状态
//获取锁
@Override
protected boolean tryAcquire(int arg) {
//通过cas来修改状态
if (compareAndSetState(0, 1)) {
//设置执行者是当前线程 不可重入的独占非公平锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
//释放锁
@Override
protected boolean tryRelease(int arg) {
if (getState() == 0) {
throw new IllegalMonitorStateException("不可以释放锁");
}
setState(0);
return true;
}
//创建condition 使用AQS默认的condition
Condition condition(){
return new ConditionObject();
}
public boolean isLocked(){
return getState()==1;
}
}
}
Lock lock=new MyLock();
@Test
public void testSelLock() throws InterruptedException {
Thread a=new Thread(()->{
test();
});
a.setName("A");
Thread b=new Thread(()->{
test();
});
b.setName("B");
a.start();
a.join();
b.start();
b.join();
}
public void test(){
try {
System.out.println(Thread.currentThread().getName()+" acquire lock before");
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName()+" acquire get lock");
}
System.out.println(Thread.currentThread().getName()+" acquire lock---->>after");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
}
四.java并发辅助类
1.Fork/Join
- 任务拆分和任务密取
// 继承 RecursiveTask 类,用于计算数组元素之和
class SumTask extends RecursiveTask<Integer> {
private static final int THRESHOLD = 10;
private int[] array;
private int start;
private int end;
public SumTask(int[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= THRESHOLD) {
System.out.println("start:"+start+",end:"+end);
// 任务足够小,直接计算结果
int sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
System.out.println("no need fork ");
return sum;
} else {
// 任务过大,拆分成两个子任务
int mid = (start + end) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);
System.out.println("fork left start:"+start+",end:"+end+",mid:"+mid);
invokeAll(leftTask,rightTask);// 任务执行
System.out.println("continue->>>");
return leftTask.join() + rightTask.join();// 合并子任务的结果
}
}
}
@Test
public void testForkJoin() {
// 创建一个包含 100 个元素的数组
int len=100;
int[] array = new int[len];
for (int i = 0; i < len; i++) {
array[i] = i + 1;
}
// 创建 ForkJoinPool 实例
ForkJoinPool forkJoinPool = new ForkJoinPool();
// 创建 SumTask 任务实例
SumTask sumTask = new SumTask(array, 0, array.length);
// 执行任务并获取结果
int result = forkJoinPool.invoke(sumTask);
System.out.println("数组元素之和为: " + result);
forkJoinPool.shutdown();
}
2.CountDownLatch、CyclicBarrier、Semaphore、Exchage与Callable
- CountDownLatch
它是java并发包中的一个常用辅助类,它可以设置一组或者一个线程完成操作时才可以继续往下操作。
示例代码:
CountDownLatch countDownLatch=new CountDownLatch(5);
@Test
public void testCountDownLatch(){
Thread thread= new Thread(new Runnable() {
@Override
public void run() {
countDownLatch.countDown();
System.out.println("count 1:"+countDownLatch.getCount());
countDownLatch.countDown();
System.out.println("count 2:"+countDownLatch.getCount());
countDownLatch.countDown();
System.out.println("count 3:"+countDownLatch.getCount());
countDownLatch.countDown();
System.out.println("count 4:"+countDownLatch.getCount());
countDownLatch.countDown();
System.out.println("count 5:"+countDownLatch.getCount());
}
});
thread.start();
System.out.println("wait it start");
try {
//阻塞当前线程 必须等到countDown减少到0时才会往下执行
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("wait it over");
}
}
- CyclicBarrier
CyclicBarrier 是 Java 并发包 java.util.concurrent 中的一个同步辅助类,允许一组线程相互等待,直到所有线程都到达一个公共屏障点(barrier point),然后所有线程再继续执行后续操作。并且它可以被重复使用,这也是它被称为“循环”屏障的原因。
示例代码:
//多线程 汇总
CyclicBarrier cyclicBarrier=new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
//等到屏障都到达后才执行,可以不设置
System.out.println("after barrier recycle it---->>>>");
}
});
@Test
public void testCyclicBarrier() throws InterruptedException {
Thread thread= new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread do it wait");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
System.out.println(" thread after do it wait");
//可以循环在操作一次
try {
cyclicBarrier.await();
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("main do it wait");
try {
cyclicBarrier.await();
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
System.out.println("main after do it wait");
//可以循环在操作一次
try {
cyclicBarrier.await();
} catch (BrokenBarrierException | InterruptedException e) {
e.printStackTrace();
}
}
- Semaphore(信号量)
Semaphore 是 java.util.concurrent 包下的一个类,用于控制同时访问特定资源的线程数量,它通过使用信号量机制来实现并发控制。
@Test
public void testSemaphore() {
// 创建一个具有 3 个许可的 Semaphore 默认是非公平模式。
Semaphore semaphore = new Semaphore(3);
// 创建 5 个线程来模拟对资源的访问
for (int i = 0; i < 5; i++) {
new Thread(new Worker(semaphore, i)).start();
}
while (true);
}
class Worker implements Runnable {
private final Semaphore semaphore;
private final int id;
public Worker(Semaphore semaphore, int id) {
this.semaphore = semaphore;
this.id = id;
}
@Override
public void run() {
try {
// 尝试获取许可
System.out.println("线程 " + id + " 正在尝试获取许可...");
semaphore.acquire();
System.out.println("线程 " + id + " 已获取到许可,开始工作...");
// 模拟工作
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可
System.out.println("线程 " + id + " 工作完成,释放许可。");
semaphore.release();
}
}
}
- Exchange
Exchange是 java.util.concurrent.Exchanger 类。它是 Java 并发包(java.util.concurrent)中的一个同步工具,用于在两个线程之间进行数据交换。
Exchanger<Integer> exchanger=new Exchanger<>();
@Test
public void testExchange() throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
int ex=exchanger.exchange(2);
System.out.println("ex before:2 after ex:"+ex);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
int ex=exchanger.exchange(4);
System.out.println("ex2 before:4 after ex:"+ex);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
thread2.start();
thread.join();
thread2.join();
}
- Callable
Callable 是一个位于 java.util.concurrent 包中的泛型接口[3]。它与 Runnable 接口类似,都用于定义可以被其他线程执行的任务,但 Callable 提供了更强大的功能
@Test
public void testFuture() {
FutureTask<Integer> futureTask = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 1;
}
});
new Thread(futureTask).start();//传入runnable接口,会执行到Callable中的call方法
Integer integer = null;
try {
integer = futureTask.get();//会阻塞当前线程,直到call有结果原理是cas+状态
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(integer);
}
五.ThreadLocal
1.ThreadLocal是什么?
threadlocal是每个线程提供一个变量的副本,保证线程隔离,保证线程的安全性。
ThreadLocal<String> threadLocal=new ThreadLocal<>();
@Test
public void testThreadLocal(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1:"+threadLocal.get());
threadLocal.set("张三");
System.out.println(threadLocal.get());
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread2:"+threadLocal.get());
threadLocal.set("李四");
System.out.println(threadLocal.get());
}
});
thread.start();
thread2.start();
try {
thread.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main thread:"+threadLocal.get());
}
2.ThradLocal的特性
1.每个线程都有自己的ThreadLocalMap;
2.ThreadLocal是线程隔离的;
3.当我们使用threadLocal使用set变量时,如果当前线程id没有对应的ThreadLocalMap,它会帮助你创建一个,如果有的话,就会取出;
4.我们set某个变量,实际上就是将数据插入ThreadLocalMap中的数组table添加对应类型中,如果有对应类型实则就是替换。
3.Threadlocal的内存泄漏问题
每个线程都持有对应treadLocalmap变量,threadLocalMap持有threadLocal键和对应value:
当发生GC回收时,在entry持有的弱引用会被回收时,线程本身内部任然持有map,map中的key虽然是null,但是它的value没有被回收,所以需要是使用完成后remove下。
4.ThreadLocal线程的不安全
对于共享的变量可能就是不安全了,并不是线程独享咯。
private static Integer a=new Integer(0);//对于共享的变量 可能threadLocal包裹之后就并不线程安全了
private static ThreadLocal<Integer> integerThreadLocal=new ThreadLocal<>();
@Test
public void testThreadLocalUnsafe(){
// ThreadLocal<Integer> threadLocal=new ThreadLocal<>();
// threadLocal.set();
for (int i = 0; i <5 ; i++) {
new Thread(new ThreadLocalTest()).start();
}
while (true);
}
@Override
public void run() {
a=a+1;
integerThreadLocal.set(a);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName()+"_a:"+integerThreadLocal.get());
}
六.线程池及其原理
1.为什么需要线程池?
通过前面我们知道,线程的创建时需要消耗资源的,我们通过线程来控制线程的创建,以及任务的调度。
2.线程池的参数及原理。
我们看看构造方法
public ThreadPoolExecutor(int corePoolSize,//cpu密集型的默认配置cpu+1 为什么?
//合理利用资源,保证CPU充分利用,避免线程等待导致CPU不会被重复利用
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize(核心线程数)
含义:线程池中的核心线程数量。当提交的任务数量小于 corePoolSize 时,线程池会创建新的线程来执行这些任务,即使此时线程池中有空闲的线程。
示例:如果 corePoolSize 设置为 5,那么当有 3 个任务提交时,线程池会创建 3 个线程来执行这些任务。 - maximumPoolSize(最大线程数)
含义:线程池允许创建的最大线程数量。当提交的任务数量超过 workQueue 的容量时,线程池会创建新的线程,直到线程数量达到 maximumPoolSize。
示例:如果 corePoolSize 为 5,workQueue 容量为 10,maximumPoolSize 为 15。当有 18 个任务提交时,首先会有 5 个线程执行任务,另外 10 个任务会放入 workQueue 中,剩下的 3 个任务会创建新的线程来执行,此时线程池中的线程数量为 8。 - keepAliveTime(线程存活时间)
含义:当线程池中的线程数量超过 corePoolSize 时,多余的空闲线程在被销毁之前等待新任务的最长时间。
示例:如果 keepAliveTime 设置为 60,unit 设置为 TimeUnit.SECONDS,那么当线程池中的线程数量超过 corePoolSize 且某个线程空闲了 60 秒后,该线程会被销毁。 - unit(时间单位)
含义:keepAliveTime 的时间单位,它是 TimeUnit 枚举类型的实例,常见的时间单位有 TimeUnit.NANOSECONDS(纳秒)、TimeUnit.MICROSECONDS(微秒)、TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)等。
示例:如果 keepAliveTime 为 5,unit 为 TimeUnit.MINUTES,则表示空闲线程的存活时间为 5 分钟。 - workQueue(任务队列)
含义:用于存储等待执行的任务的阻塞队列。当提交的任务数量超过 corePoolSize 时,这些任务会被放入 workQueue 中等待执行。
常见类型:
ArrayBlockingQueue:有界阻塞队列,基于数组实现,需要指定队列的容量。
LinkedBlockingQueue:无界阻塞队列,基于链表实现,默认情况下容量为 Integer.MAX_VALUE。
SynchronousQueue:同步队列,不存储元素,每个插入操作必须等待另一个线程的移除操作,反之亦然。
PriorityBlockingQueue:支持优先级排序的无界阻塞队列,根据任务的优先级来决定执行顺序。 - threadFactory(线程工厂)
含义:用于创建线程的工厂。通过自定义线程工厂,可以为线程设置名称、优先级等属性,方便线程的管理和调试。
示例:
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "my-thread-" + threadNumber.getAndIncrement());
return t;
}
};
7. handler(拒绝策略)
含义:当线程池中的线程数量达到 maximumPoolSize 且 workQueue 已满时,新提交的任务会被拒绝,此时会调用 handler 来处理被拒绝的任务。
常见策略:
ThreadPoolExecutor.AbortPolicy:默认的拒绝策略,直接抛出 RejectedExecutionException 异常。
ThreadPoolExecutor.CallerRunsPolicy:由提交任务的线程来执行该任务。
ThreadPoolExecutor.DiscardPolicy:直接丢弃该任务,不做任何处理。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃 workQueue 中最旧的任务,然后尝试重新提交新的任务。
8.原理介绍
- 优先创建核心线程:如果当前线程池中运行的线程数量少于
corePoolSize,无论线程池内的其他线程是否空闲,线程池都会创建一个新的核心线程来直接执行新提交的任务**** 。 - 尝试将任务加入工作队列 :如果当前线程池中的线程数已经达到或超过 ****
corePoolSize,线程池会尝试将该任务添加到指定的 ****workQueue****中。如果此时有核心线程空闲,它会从队列中取出任务并执行。 - 创建非核心线程 :如果向 ****
workQueue****添加任务失败(例如,队列已满),并且当前线程池中的线程总数小于 ****maximumPoolSize,线程池会创建一个新的“非核心”线程来执行这个任务。 - 执行拒绝策略:如果以上所有步骤都无法成功处理任务(即线程数已达到
maximumPoolSize且任务队列也满了),那么线程池将根据预先设定的RejectedExecutionHandler(拒绝策略)来处理这个无法执行的任务。常见的策略包括抛出异常、由调用者线程执行任务等
3.安卓中四种的线程池
1.缓存线程池
我们来看下它的构造方法
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,它没有核心线程,并且它的阻塞队列是同步队列,它的最大线程为2147483647,这种适用于大量的短期的异步任务,来提高程序性能;
2.固定线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到,它的核心线程和最大线程一致,所以一直会有线程存活,并且阻塞队列是无界的,这种适用于需要控制并发线程总数,并且负载比较稳定的场合
3.单线程线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到,它是一个单线程的无界任务队列,所以它适用于需要保证任务串行执行,避免并发问题的场景。
4.调度线程池
private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
new DelayedWorkQueue());
}
它具有定时或周期性执行任务的功能。核心线程数是固定的,但非核心线程数没有限制。当非核心线程处于空闲状态时,会立即被终止回收。它可以用来安排命令在给定的延迟后运行,或者定期执行。适用于需要执行定时任务或周期性任务的场景,例如轮询服务器、定时刷新数据等。
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task executed with fixed delay");
};
// 初始延迟 1 秒,每次任务执行完成后延迟 2 秒再执行下一次任务
executor.scheduleWithFixedDelay(task, 1, 2, TimeUnit.SECONDS);
Runnable task2 = () -> System.out.println("Task executed at fixed rate");
// 初始延迟 1 秒,之后每隔 2 秒执行一次任务
executor.scheduleAtFixedRate(task2, 1, 2, TimeUnit.SECONDS);
executor.shutdown();//适用于希望等待已提交的任务执行完毕后再关闭线程池的场景,能保证任务的完整性。
executor.shutdownNow();//适用于需要立即停止线程池的情况,比如应用程序需要快速关闭,但可能会导致正在执行的任务被中断。