juc并发编程相关

一、线程的三种创建方式

方式一:直接继承Thread类

  • 定义一个类继承Thread类,并重写run()方法,run()方法的方法体就是线程要完成的任务,因此把run()称为线程的执行体;

  • 创建该类的实例对象,即创建了线程对象;

  • 调用线程对象的start()方法来启动线程;

方式二:实现Runnable接口

  • 定义一个类实现Runnable接口,重写Thread类的run()方法;

  • 创建该类的实例对象obj;

  • 将obj作为构造器参数传入Thread类实例对象,这个对象才是真正的线程对象;

  • 调用线程对象的start()方法启动该线程;

实际的线程对象依然是Thread实例,这里的Thread实例负责执行其target的run()方法; 实际的线程对象依然是Thread实例,这里的Thread实例负责执行其target的run()方法,实际的线程对象依然是Thread实例,这里的Thread实例负责执行其target的run()方法; 从JAVA8开始,Runnable接口使用了@FunctionlInterface修饰,也就是说Runnable接口是函数式接口,可使用lambda表达式创建对象,使用lambda表达式就可以不像上述代码一样还要创建一个实现Runnable接口的类,然后再创建类的实例*

方式三:实现Callable接口

从JAVA5开始,JAVA提供提供了Callable接口,该接口是Runnable接口的增强版,Callable接口提供了一个call()方法可以作为线程执行体,但call()方法比run()方法功能更强大,体现在两个方面: (1)call()方法可以有返回值;
(2)call()方法可以声明抛出异常

Thread就是把run方法包装成了执行体,那么能否把任意方法都包装成线程的执行体呢?于是,JAVA5提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,该类实现了Future接口和Runnable接口,所以同时也解决了Callable对象不能作为Thread类的target这一问题。

继承Thread类和实现Runnable接口的区别:

  • Thread是类,是多个线程分别完成自己的任务(一对一),每个线程都有一个相关联的唯一对象;Runnable是接口,是多个线程共同完成一个任务(多对一),多线程可以共享同一个Runnable实例。

  • 继承Thread实现方式实际上是重写了 run() 方法,由于线程的资源和 Thread 实例捆绑在一起,所以不同的线程的资源不会进行共享。

  • 实现Runnable接口实现方式就是静态代理的方式,线程资源与 Runable 实例捆绑在一起,Thread 只是作为一个代理类,所以资源可以进行共享。

  • 实现Runnable使你的类更灵活。如果您继承Thread类,那么您所做的操作总是处于一个线程中。然而如果你采用实现Runnable接口,您可以在一个线程中运行它,或者将它传递给某种执行器(executor),或者只是将它作为一个单线程应用程序中的任务传递给它。

线程的优先级:在操作系统中,线程可以划分优先级,线程优先级越高,获得 CPU 时间片的概率就越大,但线程优先级的高低与线程的执行顺序并没有必然联系,优先级低的线程也有可能比优先级高的线程先执行。

线程的优先级分为 1~10 一共 10 个等级,所有线程默认优先级为 5,如果优先级小于 1 或大于 10,则会抛出java.lang.IllegalArgumentException 异常。

线程优先级继承特性:在 Java 中,线程的优先级具有继承性,如果线程 A 启动了线程 B,则线程 B 的优先级与线程 A 的优先级是一样的。

getPriority() : 返回线程的优先级值
setPriority(int newPriority) : 设置线程的优先级值,可以写具体的值,也可以写等级名

二、线程池

2.1 概述

        线程池是一种可以统一管理和维护线程、复用线程、并将线程的创建和任务的执行解耦开来的一种技术。

        上图为线程的生命周期,众所周知, 频繁的开启线程或者停止线程,线程需要重新被 cpu 从就绪到运行状态调度,需要发生 CPU的上下文切换,效率非常低。

        CPU 的上下文切换:
                在多任务的操作系统中,任务量是远远大于CPU的数量的,那么系统在极短的时间将CPU分配给他们,产生了多个任务同时进行的假象;切换指的是CPU得知道任务从哪儿开始加载、从哪儿运行,那就要谈到CPU寄存器和程序计数器。。。那么CPU的上下文指的是程序计数器所指指令的下一条指令。

2.2 使用线程池的优势

        线程池的核心技术在于它的复用机制,提前创建好一些线程一直处于运行状态,实现复用,限制线程创建数量。
1、降低资源消耗。复用线程减去了线程的创建和销毁资源的消耗
2、提高响应速度。当有任务到达,无需线程创建可立即运行
3、管理和维护线程。线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因 为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
4、还能提供一些其他功能:如延时定时线程池

2.3JDK提供的四种线程池创建方式

Executors.newCachedThreadPool(); 可缓存线程池
Executors.newFixedThreadPool();可定长度 限制最大线程数 Executors.newScheduledThreadPool() ; 可定时
Executors.newSingleThreadExecutor(); 单例

底层都是基于 ThreadPoolExecutor 构造函数封装的,实际项目不会直接使用!!

四、线程池的底层如何实现复用?

前面有提到线程池会提前创建好线程一直处于运行状态,这是利用死循环实现的。将线程任务缓存到并发队列集合中交给线程去完成任务。有点像mq。。。当线程池满,则执行饱和策略(拒绝策略),拒绝策略分很多种类型,有直接丢弃任务、执行任务、忽视任务、队列中剔除一个任务、自定义方案。。。

实际最多执行任务数 = 核心线程数 + 缓存队列容量 + 最大线程数 - 核心线程数


注意!线程池创建得线程不会一直运行。
        配置核心线程数 corePoolSize 为 2 、最大线程数 maximumPoolSize 为 5 我们可以通过配置超出 corePoolSize 核心线程数后创建的线程的存活时间例如为 60s 在 60s 内没有核心线程一直没有任务执行,则会停止该线程。
        对于核心线程,可以使用ThreadPoolExecutor类的allowCoreThreadTimeOut(boolean value)方法来设置核心线程是否可以被回收。如果将参数设置为true,表示核心线程也可以被回收;如果设置为false,则表示核心线程不能被回收。默认情况下,核心线程不能被回收。


四、 Java锁

4.1 悲观锁和乐观锁

悲观锁:悲观的人总会把事情往坏的方向发展。一个共享数据加了悲观锁,那么线程每次操作数据前都会假设其他线程也可能操作这个数据,所以每次操作前都会上锁,这样其他线程想要操作这个数据就拿不到锁只能阻塞了。

  • Java 语言中 synchronizedReentrantLock等就是典型的悲观锁

乐观锁:乐观的人总会把事情往好的方向发展。一个线程操作数据前不会上锁,而是每次判断是否有其他线程去更新这个数据。

  • 乐观锁可以使用版本号机制CAS算法实现。在 Java 语言中 java.util.concurrent.atomic包下的原子类就是使用CAS 乐观锁实现的。

适用场景:

  • 乐观锁适用于写少的场景,因为不用上锁、释放锁,省去了锁的开销,从而提升了吞吐量。

  • 如果是写多读少的场景,即冲突比较严重,线程间竞争激励,使用乐观锁就是导致线程不断进行重试,这样可能还降低了性能,这种场景下使用悲观锁就比较合适。

4.2 独占锁和共享锁

独占锁:只能被一个线程所持有。如果一个线程对数据加上排他锁后,那么其他线程不能再对该数据加任何类型的锁。获得独占锁的线程即能读数据又能修改数据。

  • JDK中的synchronizedjava.util.concurrent(JUC)包中Lock的实现类就是独占锁。

共享锁:是指锁可被多个线程所持有。如果一个线程对数据加上共享锁后,那么其他线程只能对数据再加共享锁,不能加独占锁。其他线程获得共享锁的线程只能读数据,不能修改数据。

  • 在 JDK 中 ReentrantReadWriteLock 就是一种共享锁。

4.3 互斥锁和读写锁

互斥锁:是独占锁的一种常规实现,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。

读写锁:读锁可以在没有写锁的时候被多个线程同时持有,而写锁是独占的。写锁的优先级要高于读锁,一个获得了读锁的线程必须能看到前一个释放的写锁所更新的内容。

  • 在 JDK 中定义了一个读写锁的接口:ReadWriteLock

  • ReentrantReadWriteLock 实现了ReadWriteLock接口

4.4 公平锁和非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,这里类似排队买票,先来的人先买,后来的人在队尾排着,这是公平的。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁,在高并发环境下,有可能造成优先级翻转,或者饥饿的状态(某个线程一直得不到锁)。

  • 在 java 中 synchronized 关键字是非公平锁,ReentrantLock默认也是非公平锁。

4.5 可重入锁

又称之为递归锁,是指同一个线程在外层方法获取了锁,在进入内层方法会自动获取锁。

  • 对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁。对于Synchronized而言,也是一个可重入锁。

4.6 自旋锁

自旋锁的目的是为了减少线程被挂起的几率,因为线程的挂起和唤醒也都是耗资源的操作。

在 Java 中,AtomicInteger 类有自旋的操作,我们看一下代码:

public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);
    } while (!compareAndSwapInt(o, offset, v, v + delta));
    return v;
}

在JDK1.6又引入了自适应自旋,这个就比较智能了,自旋时间不再固定,由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定。

什么是死锁?

指两个或两个以上的进程在执行过程中,互相竞争资源或相互通信中造成阻塞,若无外力干预,它们都将无法进行下去。此时称系统处于死锁状态。 ​ 死锁不仅会造成大量的资源浪费,还会造成整个系统崩溃。 ​ 死锁产生的四个必要条件: (1)(资源)互斥使用 (2)不可抢占 (3)请求和保持:资源在请求其他资源的同时保持对原有资源的占有 (4)循环等待,如P1占有P2的资源、P2占有P3的资源、P3占有P4的资源

破坏四个必要条件之一就可以预防死锁!

4.7 AQS

简介:AQS就是AbstractQueuedSynchronizer抽象类,AQS提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架,是java.util.concurrent.locks包下,除了java自带的synchronized关键字之外的锁机制。JUC下的很多内容都是基于AQS实现部分功能,如ReentrantLock,ThreadPoolExecutor,阻塞队列,CountDownLatch等

AQS本质上提供了两种锁机制:独占锁和共享锁

核心:

  • AQS提供了一个有volatile修饰且采用CAS方式修改的int类型的state变量,来保证state的一个线程安全,未获取到锁的线程会通过内置的FIFO队列CLH完成资源获取的排队工作,如果是公平锁,当前一个线程释放锁后,那么就唤醒队列中第一个Node节点来获取锁;非公平而言,无论队列是否有Node都会通过CAS来修改state变量。

    • ReentrantLock:(可重入独占式锁):state初始化为0,表示未锁定状态,A线程lock()时,会调用tryAcquire()独占锁并将state+1.之后其他线程再想tryAcquire的时候就会失败,直到A线程unlock()到state=0为止,其他线程才有机会获取该锁。A释放锁之前,自己也是可以重复获取此锁(state累加),这就是可重入的概念。 注意:获取多少次锁就要释放多少次锁,保证state是能回到零态的。;

    • CountDownLatch:(共享锁)任务分N个子线程去执行,state就初始化 为N,N个线程并行执行,每个线程执行完之后countDown()一次,state就会CAS减一。当N子线程全部执行完毕,state=0,会unpark()主调用线程,主调用线程就会从await()函数返回,继续之后的动作。

其次AQS维护了一个双向链表,有head、tail,并且每个节点都是Node对象。抢不到锁的线程就会被封装成一个Node节点进入线程等待队列。

static final class Node {
    //AQS 定义了两种资源共享方式:
    // Share:共享,多个线程可以同时执行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
    static final Node SHARED = new Node();
    // Exclusive:独占,只有一个线程能执行,如ReentrantLock
    static final Node EXCLUSIVE = null;
    // 给当前Node做一个状态的声明
    // 表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
    static final int CANCELLED =  1;
    // 表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
    static final int SIGNAL    = -1;
    // 表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
    static final int CONDITION = -2;
    // 表示共享模式下无条件传播.共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
    static final int PROPAGATE = -3;
​
    
    volatile int waitStatus;
    
    volatile Node prev;
    volatile Node next;
    .
    .
    .
}

4.8 深入Synchronized底层实现原理

Synchronized(重量级锁)被编译后会生成两个字节码指令monitorenter、monitorexit,依赖这两个字节码指令来实现线程同步。。Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的,monitor本质是依赖于底层操作系统的Mutex Lock(互斥锁)来实现的。操作系统实现线程之前的切换需要从用户态转换到核心态,成本高耗时长,因此Synchronized效率低。

Synchronized给对象加锁实则修改对象头结构中Mark Word中的最后两位——锁标志位。判断对象是否上锁,其实就是判断这个数值。1.6 为了减少获得锁和释放锁带来的性能消耗,将锁划分一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。

  • 无锁状态:单线程不需要加锁,或者说多线程也不会出现竞争

  • 偏向锁:资源会被竞争,但不想通过锁定资源方式来保证线程安全,就出现了CAS。CAS设想如果当前对象能认识线程就好了。。如何实现?当锁标志位是01时,去判断Mark Word的倒数第三位为1就是偏向锁,而Mark Word前23bit就是线程id。

  • 轻量级锁:当多个线程竞争锁时,升级为轻量级锁,锁标志位为00。当线程发现锁标志位为00就会在自己的虚拟机栈(JVM内存结构,线程私有)中开辟一块Lock Record的内存空间来存放Mark Word的副本指向对象的owner指针,同时Mark Word前30位会生成指针指向线程虚拟机栈中的Lock Record,这时就实现线程和对象绑定。

  • 重量级锁:当多个线程自旋等待对象锁释放时升级为重量级锁,如前面Synchronized所述。

4.9 Synchronized与Lock的区别?

  • synchronized 是一个 关键字,使用C++实现的,没办法控制锁的开始、锁结束,也没办法中断线程的执行;lock 是一个实现类,,例如ReentrantLock 就是Lock的一个实现类。

  • synchronized 可以修饰方法和代码块,并且只有在代码执行异常或执行结束才会释放锁;而Lock锁的粒度是通过lock()和unlock()来决定的,Lock还提供了非阻塞竞争锁的方法trylock()一般避免死锁,我们把unlock()写在finally块中。

  • synchronized悲观锁机制,jdk1.6之后做了优化性能与Lock差不多;Lock是乐观锁机制,线程以CAS的方式,尝试将锁的状态从0修改成1,就是尝试获取锁,获取到了就把当前线程设置给AQS的属性exclusiveOwnerThread,也就是指明当前锁的拥有者是当前线程

  • 在1.6之后synchronized 引入锁升级概念,性能上二者差不多

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

(空白格)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值