多线程(笔记十二)

本文详细介绍了Java多线程的相关知识,包括进程与线程的概念,线程的五种和六种状态,synchronized与ReentrantLock的使用,以及线程池的工作原理和拒绝策略。此外,还讨论了volatile关键字的作用,无锁的CAS操作,以及公平锁与非公平锁的区别。最后,文章提到了锁的优化策略,如偏向锁、轻量级锁和自旋锁,以及乐观锁与悲观锁的概念及其在实际应用中的选择。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

java多线程

进程与线程

进程

当一个程序被运行,就开启了一个进程程序由指令和数据组成

线程

  • 一个进程内可分为多个线程
  • 一个线程就是一个指令流,cpu调度的最小单位

java多线程的基本使用

  1. 继承Thread类 (可以说是 将任务和线程合并在一起)
  2. 实现Runnable接口 (可以说是 将任务和线程分开了)
  3. 实现Callable接口 (利用FutureTask执行任务)

上下文切换

一个线程被暂停剥夺使用权,另外一个线程被选中开始或者继续运行的过程就叫做上下文切换

内核(操作系统的核心)在CPU上对进程或者线程进行切换

如果线程数多,单个核又会并发的调度线程,运行时会有上下文切换的概念

以下几种情况会发生上下文切换。

  1. 线程的cpu时间片用完
  2. 垃圾回收
  3. 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法

线程的阻塞

  1. BIO阻塞,即使用了阻塞式的io流
  2. sleep(long time) 让线程休眠进入阻塞状态
  3. join() 方法的线程进入阻塞
  4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态
  5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gA4ALxtY-1632392761788)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210829150843072.png)]

操作系统层面分为五种状态

五种状态

img

1.初始状态创建线程对象时的状态

2.就绪状态调用start()方法后进入就绪状态,也就是准备好被cpu调度执行

3.运行状态: 线程获得到cpu的时间片,执行run方法

4.阻塞状态: 线程被阻塞,放弃cpu的时间片等待解除阻塞重新回到就绪状态争抢时间片

5.终止状态: 线程执行完成抛出异常后的状态

六种状态

  1. NEW 线程对象被创建

  2. Runnable 线程调用了start()方法后进入该状态包含了三种情况

    1. 就绪状态 :等待cpu分配时间片

    2. 运行状态:进入Runnable方法执行任务

    3. 阻塞状态:BIO 执行阻塞式io流时的状态

    4. Blocked 没获取到锁时的阻塞状态

    5. WAITING 调用wait()、join()等方法后的状态

    6. TIMED_WAITING 调用 sleep(time)、wait(time)、join(time)等方法后的状态

    7. TERMINATED 线程执行完成或抛出异常后的状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CNufmAu-1632392761795)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210821205231628.png)]

start() 让线程启动,进入就绪状态,等待cpu分配时间片

yield()线程的礼让,使得获取到cpu时间片的线程进入就绪状态,让给优先级高的重新争抢时间片

wait()/wait(long timeout)获取到锁的线程进入阻塞状态,wait方法只会释放当前对象的锁, 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
notify()随机唤醒被wait()的一个线程(唤醒一个waitSet里的线程
notifyAll();唤醒被wait()的所有线程(唤醒waitSet中所有的线程),重新争抢时间片

如果存在多线程共享

  • 多线程只有读操作,则线程安全
  • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

synchronized

同步锁也叫对象锁,是锁在对象上的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PEDi28fW-1632392761797)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210829151918363.png)]

同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题

如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

synchronized实际上使用对象锁保证临界区的原子性

// 加在方法上 实际是对this对象加锁
private synchronized void a() {
}

// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b(){
    synchronized (this){

    }

}

// 加在静态方法上 实际是对类对象加锁
private synchronized static void c() {

}

// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d(){
    synchronized (TestSynchronized.class){
        
	}
}

加锁是加在对象上,一定要保证是同一对象,加锁才能生效

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KyDoDHxX-1632392761800)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210829152824942.png)]

wait 和 sleep的区别?

二者都会让线程进入阻塞状态,有以下区别

  1. wait会立即释放锁 sleep不会释放锁
  2. wait是Object的方法 Sleep是Thread的方法
  3. wait后线程的状态是Watting ,sleep后线程的状态为 Time_Waiting

生产者消费者

/**
* 生产
*

public void put(Message message) {
synchronized (list) {
    while (list.size() == capacity) {
        log.info("队列已满,生产者等待");
        try {
            list.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    list.addLast(message);
    log.info("生产消息:{}", message);
    // 生产后通知消费者
    list.notifyAll();
}
    }
    
public Message take() {
    synchronized (list) {
        while (list.isEmpty()) {
            log.info("队列已空,消费者等待");
            try {
                list.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Message message = list.removeFirst();
        log.info("消费消息:{}", message);
        // 消费后通知生产者
        list.notifyAll();
        return message;
    }
}

Reentrant Lock

可重入锁 : 一个线程获取到对象的锁后,执行方法内部在需要获取锁的时候是可以获取到的

private static final ReentrantLock LOCK = new ReentrantLock();

private static void m() {
LOCK.lock();
try {
log.info("begin");
// 调用m1()
m1();
} finally {
// 注意锁的释放
LOCK.unlock();
}
}
public static void m1() {
LOCK.lock();
try {
log.info("m1");
m2();
} finally {
// 注意锁的释放
LOCK.unlock();
}
}

优点

1.支持获取锁的超时时间

2.获取锁时可被打断

3.可设为公平锁

4.有多个waitSet,可以指定唤醒

// 默认非公平锁,参数传true 表示未公平锁
ReentrantLock lock = new ReentrantLock(false);
// 尝试获取锁
lock()
// 释放锁 应放在finally块中 必须执行到
unlock()
try {
// 获取锁时可被打断,阻塞中的线程可被打断
LOCK.lockInterruptibly();
} catch (InterruptedException e) {
return;
}
// 尝试获取锁 获取不到就返回false
LOCK.tryLock()
// 支持超时时间 一段时间没获取到就返回false
tryLock(long timeout, TimeUnit unit)
// 指定条件变量 休息室 一个锁可以创建多个休息室
Condition waitSet = ROOM.newCondition();
// 释放锁 进入waitSet等待 释放后其他线程可以抢锁
yanWaitSet.await()
// 唤醒具体休息室的线程 唤醒后 重写竞争锁
yanWaitSet.signal()

java内存模型(JMM)

  1. 原子性 保证指令不会受到上下文切换的影响

    即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

  2. 可见性 保证指令不会受到cpu缓存的影响

    可见性是指当多个线程访问同一个变量时一个线程修改了这个变量的值**,其他线程能够立即看得**到修改的值。

  3. 有序性 保证指令不会受并行优化的影响

    即程序执行的顺序按照代码的先后顺序执行

volatile

该关键字解决了可见性和有序性,volatile通过内存屏障来实现的

  • 写屏障,编译器不会对volatile写前面的任意内存操作重排序;

会在对象写操作之后加写屏障,会对写屏障的之前的数据都同步到主存,并且保证写屏障的执行顺序在写屏障之前

  • 读屏障,编译器不会对volatile读后面的任意内存操作重排序;

会在对象读操作之前加读屏障,会在读屏障之后的语句都从主存读,并保证读屏障之后的代码执行在读屏障之后

注意: volatile不能解决原子性,即不能通过该关键字实现线程安全。

volatile应用场景:一个线程读取变量,另外的线程操作变量,加了该关键字后保证写变量后,读变量的线程可以及时感知。(多处理器总线嗅探)每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己缓存行对应的内存地址呗修改,就会将当前处理器的缓存行设置无效状态

无锁-cas compare and swap 比较并交换

CAS不是锁机制,它是线程安全的操作类.

CAS(Compare And Swap)是一种有名的无锁算法。CAS算法是乐观锁的一种实现。CAS有3个操作数,内存所存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B并返回true,否则返回false

如果一直都不是这个A值,则循环(死循环,自旋)里不断的进行CAS操作

cas底层是cpu层面的( CPU并发原语),即不使用同步锁也可以保证操作的原子性。

private AtomicInteger balance;

// 模拟cas的具体操作
@Override
public void withdraw(Integer amount) {
    while (true) {
        // 获取当前值
        int pre = balance.get();
        // 进行操作后得到新值
        int next = pre - amount;
        // 比较并设置成功 则中断 否则自旋重试
        if (balance.compareAndSet(pre, next)) {
            break;
        }
    }
}

无锁的效率是要于之前的锁的,由于无锁不会涉及线程的上下文切换

cas是乐观锁的思想,sychronized是悲观锁的思想

cas适合很少有线程竞争的场景,如果竞争很强,重试经常发生,反而降低效率(重复自旋)

juc并发包下包含了实现了cas的原子类

ABA问题

cas存在ABA问题,即比较并交换时,如果原值为A,其他线程将其修改为B,在有其他线程将其修改为A

此时实际发生过交换,但是比较和交换由于值没改变可以交换成功

解决方式

AtomicStampedReference/AtomicMarkableReference

上面两个类解决ABA问题,原理就是为对象增加版本号,每次修改时增加版本号,就可以避免ABA问题

或者增加个布尔变量标识,修改后调整布尔变量值,也可以避免ABA问题

线程池

线程池的好处

  1. 降低资源消耗,通过池化思想,减少创建线程和销毁线程的消耗,控制资源
  2. 提高响应速度,任务到达时,无需创建线程即可运行
  3. 提供更多更强大的功能,可扩展性高

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {

}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fPeaHfro-1632392761804)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210904113507897.png)]

线程池的状态

线程池通过一个int变量高3位来表示线程池的状态低29位来存储线程池的数量

线程池的主要流程

1.创建线程池后,线程池的状态是Running,该状态下才能有下面的步骤提交任务时,线程池会创建线程去处理任务

2.当线程池的工作线程达到corePoolSize时,继续提交任务会进入阻塞队列

3.当阻塞队列装满时继续提交任务,会创建救急线程来处理

4.当线程池中的工作线程数达到maximumPoolSize时,会执行拒绝策略

5.当线程取任务的时间达到keepAliveTime还没有取到任务,工作线程数大于corePoolSize时,会回收该线程

拒绝策略

  1. 调用者抛出RejectedExecutionException (默认策略)
  2. 让调用者运行任务
  3. 丢弃此次任务
  4. 丢弃阻塞队列中最早的任务加入该任务

ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务

ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。

ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务

1.newFixedThreadPool(固定大小线程池。)

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

核心线程数 = 最大线程数 没有救急线程
阻塞队列无界 可能导致oom

2.newCachedThreadPool(无界线程池)

public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
核心线程数是0,最大线程数无限制 ,救急线程60秒回收
队列采用 SynchronousQueue 实现 没有容量,即放入队列后没有线程来取就放不进去
可能导致线程数过多cpu负担

3.newSingleThreadExecutor(大小为1的固定线程池)

public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

核心线程数和最大线程数都是1,没有救急线程,无界队列 可以不停的接收任务
将任务串行化 一个个执行, 使用包装类是为了屏蔽修改线程池的一些参数 比如 corePoolSize
如果某线程抛出异常了,会重新创建一个线程继续执行
可能造成oom

4.newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}

任务调度的线程池 可以指定延迟时间调用,可以指定隔一段时间调用

线程池的关闭

shutdown()

会让线程池状态为shutdown,不能接收任务,但是会将工作线程和阻塞队列里的任务执行完再关闭 相当于优雅关闭

shutdownNow()

会让线程池状态为stop, 不能接收任务,会立即中断执行中的工作线程,并且不会执行阻塞队列里的任务, 会返回阻塞队列的任务列表

公平锁和非公平锁的区别

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁

优点:所有的线程

缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。(插队)

优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。

缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。

AbstractQueuedSynchronizer(AQS)

到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!

类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch…。

模板方法设计模式
父类中写好主要的功能 模块,在子类中覆盖这些方法
https://www.cnblogs.com/java-my-life/archive/2012/05/14/2495235.html

volatile int state(同步状态): 代表共享资源

getState:获取当前的同步状态

setState:设置当前同步状态, 非安全

compareAndSetState使用CAS设置状态,保证状态设置的原子性

独占式只有一个线程能执行,如ReentrantLock

独占式获取的方法(流程 方法)
void acquire(int arg)
void acquireInterruptibly(int arg)
响应中断
boolean tryAcquireNanos(int arg, long nanosTimeout)
尝试获取,有超时设置
独占式释放(流程 方法)
release()

共享式多个线程可同时执行,Semaphore/CountDownLatch
共享式获取方法(流程 方法)
void acquireShared(int arg)
void acquireSharedInterruptibly(int arg)
响应中断
boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
尝试获取,有超时设置
共享式释放(流程 方法)
releaseShared()

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。

偏向锁

在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁

核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即重新获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。

偏向锁失败后,并不会立即膨胀为重量级锁,而是先膨胀为轻量级锁

轻量级锁

轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”

轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

悲观锁与乐观锁

悲观锁

共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

乐观锁适用于多读的应用类型,这样可以提高吞吐量,可以使用版本号机制CAS算法实现

Java中java.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的。

redis中

WATCH命令实现

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oGlHXJw5-1632392761806)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210907094145676.png)]

WATCH命令就是一个乐观锁,它可以在EXEC命令执行之前监视任意数量的数据库键,并在EXEC命令执行时检查监视的键是否被修改了,如果被修改了服务器将拒绝执行事物,并向客户端返回空

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MeEw8Mba-1632392761807)(C:\Users\涛大爷的笔记本\AppData\Roaming\Typora\typora-user-images\image-20210907095210928.png)]

Redis事务三特性

Ø 单独的隔离操作

n 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断

Ø 没有隔离级别的概念

n 队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

Ø 不保证原子性 (指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。)

n 事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

  1. 版本号机制
    一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

  2. CAS算法
    即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

需要读写的内存值 V
进行比较的值 A
拟写入的新值 B
当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试

乐观锁的缺点

ABA 问题是乐观锁一个常见的问题

v==A 变量V初次读取的时候是A值,准备赋值的时候检查到它仍然是A值,可能被其他线程修改过

解决方法compareAndSet 方法(如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值)

循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

只能保证一个共享变量的原子操作

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作.所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作。

CAS与synchronized的使用情景
简单的来说CAS适用于写比较少的情况下(多读场景,冲突一般较少),synchronized适用于写比较多的情况下(多写场景,冲突一般较多

对于资源竞争较少(线程冲突较轻)的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现不需要进入内核不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。
对于资源竞争严重(线程冲突严重)的情况,CA 自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值