并发编程学习笔记

一:JMM(Java内存模型)

Java内存模型(Java Memory Model简称JMM)是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。
JVM运行程序的实体是线程,而每个线程创建时 JVM都会为其创建一个工作内存(有些地方称为栈空间),用于
存储线程私有的数据,而Java 内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都
可以访问, 但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自 己的工
作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作 主内存中的变量,工作内
存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个 线程的私有数据区域,因此不同的线程间无
法访问对方的工作内存,线程间的通信(传值)必 须通过主内存来完成。
可以用以下图来做一个表示:
 
二:数据同步的八大原子性操作
(1) lock(锁定) :作用于主内存的变量,把一个变量标记为一条线程独占状态
(2) unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后
的变量才可以被其他线程锁定
(3) read(读取) :作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存
中,以便随后的load动作使用
(4) load(载入) :作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工
作内存的变量副本中
(5) use(使用) :作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6) assign(赋值) :作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内
存的变量
(7) store(存储) :作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存
中,以便随后的write的操作
(8) write(写入) :作用于工作内存的变量,它把store操作从工作内存中的一个变量的值
传送到主内存的变量中
 
 
三:并发编程的可见性,原子性,有序性问题
原子性:
原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不
会被其他线程影响。
可见性:
可见性指的是当一个线程修改了某个共享变量 的值,其他线程是否能够马上得知这个修改的值。
有序性
有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序 现象。
 
四:JMM是如何保证可见性,原子性,有序性
1.可见性
 
volatile关键字保证可见性(底层会给对应的字段加上lock锁,触发缓存一致性协议(即MESI)当对应的字段大于64字节时)。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中 读取新值。synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个 线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

2.有序性

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然, synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行 同步代码,自然就保证了有序性。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为happens-before 原则。

volatile能实现有序性是因为Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对

CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。
单列模式(懒汉式)中就很好的用到了这个关键字
1 memory = allocate (); //1. 分配对象内存空间
2 instance (memory); //2. 初始化对象
3 instance = memory; //3. 设置 instance 指向刚分配的内存地址,此时 instance不为空
 
但是步骤2与步骤3可能会发生指令重排
1 memory = allocate (); //1. 分配对象内存空间
3 instance = memory; //3. 设置 instance 指向刚分配的内存地址,此时 instance不为空
2 instance (memory); //2. 初始化对象
这样的话,在多线程的环境下,就会存在线程安全的问题了。
当第二个操作是volatile写时,不管第一个操作是什么,都不 能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。
as-if-serial语义
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语
义。
 
happens-before 原则
1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2. 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4. 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
5. 传递性 A先于B ,B先于C 那么A必然先于C
6. 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。
7. 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。
8. 对象终结规则对象的构造函数执行,结束先于finalize()方法
 
 
 
五: synchronized详解
 
5.1  synchronized原理详解
 
(1)synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可
重入的。
加锁的方式:
1、同步实例方法,锁是当前实例对象
2、同步类方法,锁是当前类对象
3、同步代码块,锁是括号里面的对象
synchronized关键字被编译成字节码后会被翻译成 monitorenter monitorexit 两条指令分别在同步块逻辑代码的起始位置
与结束位置。 monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁
 
 
锁的膨胀升级过程
synchronized是基于JVM 内置锁实现,通过内部对象 Monitor (监视器锁)实现,基于进入与退出 Monitor 对象实现方法与代码
块同步,监视器锁的实现依赖底层操作系统的 Mutex lock (互斥锁)实现,它是一个重量级锁性能较低。但在1.5之后进行了优化
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。 JDK 1.6 中默认是开启偏向锁和轻量级锁 ,可以通过-XX:-UseBiasedLocking来禁用偏向锁。 下图为锁的升级全过。
 

偏向锁:

偏向锁的核心 思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,
无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。 但是对于锁竞争比较激烈的场合,偏向锁就 失效了,因为这样场合极有可能每次申请锁的线程都是不相同的。
 
轻量级锁:
轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意 轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场 合,就会导致轻量级锁膨胀为重量级锁。
 
自旋锁:
自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或
100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。
 
锁消除:
Java虚拟机在JIT编译时,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,从而节省毫无意义的请求锁时间,
 
六:线程池
  6.1:什么时候适合使用线程池以及优点:
      1.  单个任务处理时间比较短 ;需要处理的任务数量很大
      2. 线程池优势
          重用存在的线程,减少线程创建,消亡的开销,提高性能, 提高线程的可管理性
          提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
      

  6.2 线程的实现方式

       (1)继承Thread类

               1. 定义一个Thread类的子类,重写run方法,将相关逻辑实现,run()方法 就是线程要执行的业务逻辑方法

               2. 创建自定义的线程子类对象

               3. 调用子类实例的star()方法来启动线
 
      (2)实现Runnable接口
 
              1. 定义Runnable接口实现类MyRunnable,并重写run()方法
              2. 创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
              3. 调用线程对象的start()方法
 
     (3)实现callable接口
 

              1. 创建实现Callable接口的类myCallable

              2. 以myCallable为参数创建FutureTask对象

             3. 将FutureTask作为参数创建Thread对象

             4. 调用线程对象的start()方法

6.3 线程池中常用的方法

     1, execute (Runnable command):履行Ruannable类型的任务,
     2, submit (task):可用来提交Callable或Runnable任务,并返回代表此任务的Future对象
     3, shutdown ():在完成已提交的任务后封闭办事,不再接管新任务,
     4, shutdownNow ():停止所有正在履行的任务并封闭办事。
     5, isTerminated ():测试是否所有任务都履行完毕了。
     6, isShutdown ():测试是否该ExecutorService已被关闭。
 
6.4 线程池的几种状态
 
1、RUNNING
(1) 状态说明:线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行
处理。
(2) 状态切换:线程池的初始化状态是RUNNING。换句话说,线程池被一旦被创建,就处于RUNNING状态,并且线程池中的任务数为0!
2、 SHUTDOWN
(1) 状态说明:线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务。
(2) 状态切换:调用线程池的shutdown()接口时,线程池由RUNNING -> SHUTDOWN。
3、STOP
(1) 状态说明:线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
(2) 状态切换:调用线程池的shutdownNow()接口时,线程池由(RUNNING or SHUTDOWN ) -> STOP。
4、TIDYING
(1) 状态说明:当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
(2) 状态切换:当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。 当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
5、 TERMINATED
(1) 状态说明:线程池彻底终止,就变成TERMINATED状态。
(2) 状态切换:线程池处在TIDYING状态时,执行完terminated()之后,就会由 TIDYING - > TERMINATED。
进入TERMINATED的条件如下:
线程池不是RUNNING状态;线程池状态不是TIDYING状态或TERMINATED状态;如果线程池状态是SHUTDOWN并且workerQueue为空;workerCount为0; 设置TIDYING状态成功。
 
 
 
6.5 线程池的创建

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

(1)corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于corePoolSize;如果当前线程数为corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
(2)maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于maximumPoolSize;
(3)keepAliveTime
线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime;
(4)unit:keepAliveTime的单位;
  (5)  workQueue
用来保存等待被执行的任务的阻塞队列,且任务必须实现Runable接口,在JDK中提供了如下阻塞队列:
1、ArrayBlockingQueue:基于数组结构的有界阻塞队列,按FIFO排序任务;
2、LinkedBlockingQuene:基于链表结构的阻塞队列,按FIFO排序任务,吞吐量通常要高于ArrayBlockingQuene;
3、SynchronousQuene:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高LinkedBlockingQuene;
4、priorityBlockingQuene:具有优先级的无界阻塞队列;
(6)threadFactory
它是ThreadFactory类型的变量,用来创建新线程。默认使用 Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。
(7)handler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了4种策略:
1、AbortPolicy:直接抛出异常,默认策略;
2、CallerRunsPolicy:用调用者所在的线程来执行任务;
3、DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
4、DiscardPolicy:直接丢弃任务;
上面的4种策略都是ThreadPoolExecutor的内部类。 当然也可以根据应用场景实现RejectedExecutionHandler接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。
 
6.6 线程池的原理
 
execute方法执行流程如下:
 
源码里的核心方法:
execute方法,代码里的大致内容可由下图来表示:
 
addWorker方法

addWorker方法的主要工作是在线程池中创建一个新的线程并执行,firstTask参数 用于指定新增的线程执行的第一个任务,core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize。

 

runWorker方法

  1. while循环不断地通过getTask()方法获取任务;
  2. getTask()方法从阻塞队列中取任务;
  3. 如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态;
  4. 调用task.run()执行任务;
  5. 如果task为null则跳出循环,执行processWorkerExit()方法;
  6. runWorker方法执行完毕,也代表着Worker中的run方法执行完毕,销毁线程。

从execute方法开始,Worker使用ThreadFactory创建新的工作线程,runWorker通过getTask获取任务,然后执行任务,如果getTask返回null,进入processWorkerExit方法,整个线程结束,如图所示:

 

七:并发容器之ThreadLocal详解

定义:ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个ThreadLocalMap 对象,简单说 ThreadLocal 就是一种以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

原理:线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。

注意点:在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。

使用场景:为每个线程分配一个 JDBC 连接 Connection。

ThreadLocal造成内存泄漏的原因:

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来, ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set() get() remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用 remove() 方法。
 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值