java高阶-并发编程知识点-大杂烩

3 进程/线程(并发)

3.1 线程基础

线程存在于操作系统中,不仅仅是Java

  • 进程:系统调用的最小单位

    分配 cpu/内存/磁盘io

  • 线程:cpu调用的最小单位

    内核数与线程数: 一比一 (逻辑处理器,2倍)

    RR调度: cpu时间片轮转 机制

  • 并行(各自执行):同时运行的任务数

  • 并发(交替执行):单位时间内,处理的任务数

os限制: linux 分配1000个线程, windows 分配2000个 (线程池来控制线程数量)。

句柄: 系统分配了一段连续的内存空间, 指向这个内存空间的叫做句柄。

jdk 线程是协作式, 不是强制式

Thread.currentThread() : 获取当前线程

创建线程:一种:Thread; 二种:Runnable

  • stop() (过时): 不建议使用,方式野蛮,直接停止线程操作。如写文件时,中途停止,文件未写完整。

  • interrupt()(推荐): 标识线程中断,标识位, 实际不一定是停止线程。

  • isInterrupted(): 判断是否停止 while(isInterrupted()){…} , 不建议使用 设置boolean 标识 while(cancel){…}

  • interrupted(): 判断是否停止 (会修改标识为 true)

sleep捕获异常,需要interupt(), 外部不会让线程中断。

static class T extends Thread{
        @Override
        public void run() {
            System.out.println("run...2-1 "+ isInterrupted());
            //没有打断
            while (!isInterrupted()){
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    //需要打断,否则while会一直执行下去
                    interrupt();
                    System.out.println("run...sleep   "+ isInterrupted());
                }
                System.out.println("run...1   "+ isInterrupted());
            }
            System.out.println("run...2-2   "+ isInterrupted());
        }
    }
  • run() : 主线程执行

  • start() : 启动一个子线程执行

  • yield(): 从cpu中让出执行权,转为就绪状态。 不会让出锁

  • join(): 获取执行权, 但是有顺序的执行(串行) (如何让2个线程有顺序的执行?使用join)

  • setPriority(): 线程优先级

  • setDaemon(): 设置为守护线程。 当前线程完毕后,守护线程停止

    当前线程外, 设置了setDaemon的线程 都是守护线程。

    线程状态交互:

在这里插入图片描述

线程间的共享

多个线程对同一个资源访问 ===> 加锁 (方法加锁,对象加锁)

静态方法上内加锁===>

注: 相同锁, 多线程串行; 锁不同,多线程并行

死锁:

  • 多个操作者(线程)
  • 争夺多个资源

volatile: 最轻量的线程同步机制,可见性。(旧值变成新值时,马上被其他线程看到) ,不能保证线程安全。 一写多读的场景下使用。

​ - 可见性:对一个volatile变量的读,能看到其他线程对这个变量最后的写入。(强制刷新值同步回主内存,强制其他读线程去读。没有写的情况下,是各自工作线程从主内存中拷贝一个副本并进行各自操作

​ - 只能保证可见性,不能保持原子性(复合操作(自增)。 要么一起成功,要么都不成功)

synchronized: 独占锁, 保证 可见性+原子性

ThreadLocal: 每个线程都有遍历的副本,线程隔离。 拥有一个线程独有的 TheadLocalMap (内含Entry[])

​ threadLocal使用后, 不remove,会发生内存泄漏 。

​ 没有使用好,也会线程不安全。

可见性与原子性

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

    由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。

    要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁

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

    • “时间片”:CPU资源的分配都是以线程为单位的,并且是分时调用,操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为**“时间片”**。

    • 线程切换问题原理:线程切换为什么会带来bug呢?因为操作系统做任务切换,可以发生在任何一条CPU 指令执行完!,CPU 指令,而不是高级语言里的一条语句。

      count++;  //一条java语句,对应多条 cpu指令
      

volatile:把对volatile变量的单个读/写,看成是使用同一个对这些单个读/写操作做了同步

  • 可见性:对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

  • 原子性:对任意单个volatile变量的读/写具有原子性。能保证执行完及时把变量刷到主内存

  • 但对于count++这种非原子性、多指令的情况。由于线程切换,线程A刚把count=0加载到工作内存,线程B也拿到0(执行++ =>1),这样就会导致线程A和B执行完的结果都是1,都写到主内存中,主内存的值还是1不是2

  • **原理:**该修饰的变量进行写时,会使用cpu提供的Lock前缀指令,可以理解为CPU指令级的一种锁

    • 将当前处理器缓存行的数据写回系统内存
    • 这个写回操作会使其他cpu缓存了该内存地址的数据无效。重新来读取。

引用分类

  • 强引用
  • 软引用:softReference gc时, 内存不足时, 才会回收。
  • 弱引用:weakReference gc时,不管内存是否充足,都会回收。
  • 虚引用:

线程之间的协作:

  • wait() : object的方法, 写在syn 同步块或者方法中。 //释放锁
  • notify()/ notifyAll() : object的方法, 写在syn 同步块或者方法中
//等待
syn(对象){
	while(条件不满足){
		对象.wait()  //释放锁
	}
	//业务
}
//通知
syn(对象){
	//业务, 改变条件
	对象.notify()/notifyAll()
}

object o = new object()

o = null; (代表栈指向空,但是对应的堆对象还存在,等待gc回收)

fork/join: 分而治之 , 大任务拆分(fork)成若干小任务, 再将小任务的直接结果汇总(join)

3.2 CAS (compare and swap)(无锁算法)

原子操作: 不可分,要么全部做,要么一个都不做

  • 目的:实现原子操作(synchronized也能实现原子操作)

    synchronized缺点:

    • 被阻塞的线程优先级很高很重要怎么办?
    • 获得锁的线程一直不释放锁怎么办?
    • 如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争。会出现如死锁等情况。
    • 锁机制是一种比较粗糙,粒度比较大的机制,相对于像计数器这样的需求有点儿过于笨重。
  • 释义1:比较再交换,java.util.concurrent.*,其下面的类直接或者间接使用CAS算法实现,区别synchronouse同步锁的一种乐观锁。

  • 释义2:CPU指令级的操作,只有一步原子操作,非常快。避免了请求操作系统来裁定锁的问题,直接在CPU执行。

  • 原理: 利用现代处理器都支持的CAS的指令, 循环这个指令,直到成功为止。 (自旋:类似死循环,长时间不成功会带来cpu高开销)。

  • get变量值(旧值)—>计算后得到新值—> compare内存中变量值旧值---->如果相等-----旧值swap为新值

    ​ ---->如果不相等,从头再来一次

    ​ (如果一个线程一直不相等,则最后一次也会相等)

  • **思想:**获取当前变量最新值A(预期值),然后进行CAS操作。此时如果内存中变量的值V(内存值V)等于预期值A,说明没有被其他线程修改过,我就把变量值改成B(更新值);如果不是A,便不再修改。(被其他线程修改过,则重新循环)

eg: count++ 多个线程来操作 A:0–>1

CAS接收三个参数(1. count的内存地址; 2. 期望的值(旧值,count 0); 3. 新值(1))

当执行时,比较交换是不能被外部线程打断: 原子性

缺点:

  • ABA问题: 中间已经改过了。
  • 开销问题:不停重试。
  • 只能保证一个共享变量的原子操作。 (如果内部有个变量修改,就需要syn)

JDK中相关原子操作类(java.util.concurrent.atomic,提供一种高效的CAS操作):

  • 更新基本类型类: AtomicBoolean, AtomicInteger, AtomicLong
  • 更新数组类: AtomicIntegerArray, AtmoIntegerArray, AtomicReferenceArray
  • 更新引用类型:AtomicReference, AtomicMarkableReference, AtomicStampedReference

syn: (一个线程操作,其他线程都得等待)

ConcurrentHashMap (线程安全,解决并发问题)

  • jdk 1.8以前: Segment+ReentrantLock
  • jdk 1.8以后: CAS+Synchronized

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

解决多线程并发问题:

  • 加synchronized 。锁机制
  • threadLocal。副本,4个方法,get/set/remove/initialValue
  • concurrent下的原子操作类。 concuuurentHashMap,AtomicBoolean…
  • volatile+CAS操作:替换synchronized

阻塞队列

  • 队列满了:再添加,会阻塞

  • 队列空了:去取,会阻塞

BlockingQueue: 阻塞方法(put()/ take()),有阻塞方法也有非阻塞方法。

  • 线程take()取值时,如果取不到值,会阻塞在那里。
  • ArrayBlockingQueue : 由数组构成的有界阻塞
  • LinkedBlockingQueue:由链表构成的有界阻塞
  • PriorityBlockingQueue:支持优先级排序构成的无界阻塞
  • DelayQueue:支持优先级排序构成的无界阻塞 (有效时间, 缓存时间控制)

生产者/消费者:

  • 中间建立一个容器(阻塞队列),生产者和消费者各自执行各自(解决生产者和消费者的强耦合问题)
  • 阻塞队列就相当于一个缓冲区,平衡生产者和消费者的处理能力。
方法抛出异常返回值一直阻塞超时退出
插入方法addofferputoffer(e,time,unit)
移除方法removepolltakepoll(time,unit)
获取方法elementpeek--

抛出异常:

  • 当队列满时,如果再往队列里插入元素,会抛出IllegalStateException(“Queuefull”)异常。
  • 当队列空时,从队列里获取元素会抛出NoSuchElementException异常。

3.3 线程池原理

作用:

  • 降低资源消耗。重复利用已创建的线程 降低线程创建销毁造成的消耗。
  • 提高响应速度。只有执行时间(已创建线程的情况下)。
  • 提高线程的可管理性。统一分配、调优和监控。

一个线程所需要的资源时间

  • 创建时间
  • 任务执行时间 (线程池就只包含了该时间,不需要重复创建/销毁)
  • 销毁时间

Exceutor–ExecutorService–ThreadPoolExecutor

ThreadPoolExecutor(
  int corePoolSize,   //核心线程池数量
  int maximumPoolSize,  //线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize (立即执行)
  long keepAliveTime,  //核心线程 保活时间
  TimeUnit unit,  //保活时间单位
  BlockingQueue<Runnable> workQueue, //阻塞队列, 超过核心数量后,加入到该队列中
  ThreadFactory threadFactory,  //设置,线程名、守护线程。Executors静态工厂里默认的threadFactory,线程的命名规则是“pool-数字-thread-数字”。
  RejectedExecutionHandler handler // 超过最大线程池数量,拒绝
)
  //eg: 核心线程数 3, 最大线程数 6, 阻塞队列 10
  //1、2、3(核心线程数),4--13(阻塞队列),14、15、16 (最大线程数 6-3=3)的执行顺序,1、2、3、14、15、16、4--13

线程池工作机制

  • 如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)
  • 如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
  • 如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。
  • 如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

怎么让线程一直进行?

当前线程执行run方法中,可以使用BlockingQueue阻塞队列进行 take()阻塞

在这里插入图片描述

任务特性

  • cpu密集型:cpu纯计算, 从内存中取出来计算。 线程数:不要超过cpu核心数+1。 速度快

    Runtime.getRuntime().availableProcessors()
    
  • io密集型:网络通信/ 读写磁盘。 线程数:机器的cpu核心数*2 速度慢

  • 混合型:

摘自《Jeff Dean在Google全体工程大会的报告》

操作响应时间
打开一个站点几秒
数据库查询一条记录(有索引)十几毫秒
1.6G的CPU执行一条指令0.6纳秒
从机械磁盘顺序读取1M数据2-10毫秒
从SSD磁盘顺序读取1M数据0.3毫秒
从内存连续读取1M数据250微秒
CPU读取一次内存100纳秒
1G网卡,网络传输2Kb数据20微秒

1秒=1000毫秒 1毫秒=1000微秒 1微秒=1000纳秒

AQS:AbstractQueuedSynchronizer 抽象队列同步器 (state锁状态值)

  • 作用:用来构建锁或者其他同步组件的基础框架。使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

  • **使用:**继承AQS,管理同步状态值state

  • CLH队列锁(Craig, Landin, and Hagersten (CLH) locks):基于链表的自旋锁。

  • 内部一个state状态值。包含模版方法模式。 同步工具类的内部类来继承AQS

设计模式:模版方法模式

doSomthing(){

​ method1();

​ method2();

​ …

}

abstract public void method1();

compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

非公平锁:不排队 拿锁。 抢占锁

ReentrantLock: 可重入锁,线程每进入一次会进入累加,释放一次进行累减。 基于AQS实现

cpu---->高速缓存---->内存

基于cpu读取速度快, 内存读取相对慢的情况,java内存模型引入了主内存和工作内存。

java内存模型: JMM

java线程从主内存中拷贝一个副本到各自的工作内存中进行相应操作。

3.4 synchronized的实现原理

**描述:**基于进入和退出Monitor对象来实现方法同步和代码块同步,都可以通过成对的MonitorEnter和MonitorExit指令来实现。常量池中多了ACC_SYNCHRONIZED标示符。

  • JVM根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置。
  • 如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor
  • 在方法执行期间,其他任何线程都无法获得同一个monitor对象。

javap -v xxxx.class : 反编译class文件

monitorenter/monitorexit 指令实现 syn同步代码块

锁的状态 比较 (会随着竞争情况逐渐升级)

  • 无锁状态
  • 偏向锁 : 加锁/解锁不需要额外消耗,和非同步方法执行接近。适用于一个线程访问同步块(减少不必要的CAS操作)
  • 轻量级锁:由偏向锁升级来,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。得不到锁会使用自旋消耗cpu,不会阻塞。 追求响应时间
  • 重量级锁:线程阻塞,响应时间慢。不实用自旋,不消耗cpu。 追求吞吐量

synchronized是加锁在某一个具体的对象上, 如对象/ class/ 变量

DCL:双重检查锁定(单例模式)

//最终懒汉式 单例模式 ===类加载机制,保证线程安全
public class SingleObject {
    private SingleObject(){}
    
    static class Instance{
        private static SingleObject singleObject = new SingleObject();
    }
    
    public static SingleObject getInstance(){
        return Instance.singleObject;
    }
}

指令重排序

  • sleep/yield :依然拥有锁 (sleep会抛出异常,所有会中断)

  • wait : 让出锁的执行权

  • 线程顺序执行(T1/T2/T3):T3方法中调用t2.join,再调用t1.join。 依次从t1–>t2–>t3

3.5 并发编程

工具包:java.util.concurrent.*

│  AbstractExecutorService.java
│  ArrayBlockingQueue.java    //<-----
│  BlockingDeque.java
│  BlockingQueue.java
│  ConcurrentHashMap.java    //<-----
│  ConcurrentLinkedDeque.java
│  ConcurrentMap.java
│  DelayQueue.java
│  Executor.java
│  ExecutorService.java    //<-----
│  ForkJoinPool.java
│  ForkJoinWorkerThread.java
│  Future.java
│  LinkedBlockingDeque.java
│  LinkedBlockingQueue.java
│  RejectedExecutionException.java
│  RejectedExecutionHandler.java
│  ScheduledExecutorService.java
│  ...
│
├─atomic
│      AtomicBoolean.java     //<-----
│      AtomicInteger.java    //<----- 
│      AtomicIntegerArray.java    //<-----
│      AtomicLong.java
│      AtomicLongArray.java
│      AtomicReference.java
│      AtomicReferenceArray.java
│      AtomicStampedReference.java
│
└─locks
        AbstractOwnableSynchronizer.java
        AbstractQueuedLongSynchronizer.java
        AbstractQueuedSynchronizer.java    //<-----
        Lock.java
        ReadWriteLock.java
        ReentrantLock.java
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值