JAVA并发编程的艺术-读书笔记

1、并发编程的挑战

多线程并不一定能带来性能提升,相反过多的线程导致线程创建和上下文切换有时会比单线程性能更低

无锁并发编程:根据数据id进行取模,不同的线程处理不同段的数据

死锁:资源互相等待,线程因为一些异常没有释放锁

避免死锁:避免一个线程同时获取多个锁、lock.tryLock(timeOut)、避免一个线程在锁内获取多个资源、数据库加解锁必须在同一个数据库连接里

2、java并发机制的底层实现原理

volatile:volatile是轻量级的sychronized,使用恰当的话他比sychronized的使用成本更低

如何实现可见性:多核处理器下,当对含有volatile的变量进行操作,jvm会向处理器发送命令,将这个变量写回主存,然后其他cpu通过嗅探在总线上的数据发现自己的这个变量已经失效,那么下次操作的时候就会从主存读取。

synchronized原理:

通过持有monitor对象,执行monitorenter和monitorexit,monitorenter放在方法入口处,monitorexit放在方法结束和异常处。当一个monitor被持有后,他处于锁定状态、

Java对象头:

mark word:锁标志位、hashcode、分代年龄

类信息地址指针

array lenth:数组长度

锁升级与对比:

为什么不能锁降级:为了提高获取锁和释放锁的效率

1.偏向锁:hotspot作者研究发现大部分情况下,锁不存在多线程竞争,并且总是由同一个线程获取,因此引入偏向锁,当一个线程拿到锁后,对象头markword会记录该线程id,下次进入的时候只需简单对比markword中的线程id,如果是当前线程,则获取锁成功,否则判断对象头是否开启了偏向锁,如果开启了则尝试将线程id指向当前线程

偏向锁的撤销:当有其他线程想进行CAS替换markword信息,则开始撤销偏向锁,等到全局安全点,把markword的线程id置位空

偏向锁默认开启,可通过jvm参数关闭

2.轻量级锁

线程执行同步块之前,jvm先将markword中的锁信息拷贝到栈帧,当线程执行完后,尝试CAS将锁记录从栈帧拷贝回markword,如果此时存在其他线程尝试获取锁,则膨胀为重量级锁

原子操作的实现原理:

处理器如何实现:总线锁和缓存锁,总线锁开销大,缓存锁是缓存变量所在主存中的地址,当cpu1锁定了某个缓存行,那cpu2就不能同时缓存该行

Java如何实现:锁或CAS

CAS三大问题:ABA、循环开销大、只能操作一个变量

ABA问题:版本号->AtomicStampReference

循环开销大:如果jvm支持CPU的pause指令,那么效率上会有一定提升,pause可以使cpu流水线延迟执行指令

只能操作单变量:可以吧多个变量放在一个对象,使用AtomicRefrence

3.Java内存模型

java通过内存模型来控制线程间通信,线程间通信步骤:1.线程A将变量刷新到主存2.线程B将主存中的变量拿到自己的内存中。

为了防止指令重排引起的线程安全问题,jvm通过在指令间增加内存屏障防止指令重排

happens-before原则:定义了一些进程、线程、锁相关的执行顺序。A happens-beforeB,B happens-beforeC,那么A happens-before C,对于一个监视器的解锁,happens-before于对监视器的加锁

as-if-serial语义:不管怎么排序,单线程程序的执行结果不能被改变。

volatile内存语义:保证单个变量读写的原子性,通过在读写操作之间加入内存屏障

锁的内存语义:线程A释放锁时,会将变量写入主存,然后通过主存发送消息给B,当B获取锁后,JMM会把本地变量失效,然后到主存中读取共享变量。

ReentrantLock使用AQS进行同步,volatile修饰的state变量,公平锁和非公平锁释放时,最后都要写state变量,公平锁获取时,先读state变量,非公平锁获取时,先CAS修改state变量

final域的内存语义:禁止吧final域的写重排序到构造函数外,确保在读到final域前是已经初始化完成的

happens-before的两类重排序:对于会更改程序结果的重排序,JMM不允许编译器和处理器进行重排序,对于不会更改程序结果的重排序,JMM不做要求

双重检查锁定与延迟初始化:双重检锁单例模式中,instance要加volatile修饰,否则会因为重排序出现线程安全问题,还有一种Holder单例模式,通过private static修饰holder 类和instance,可以保证延迟加载和线程安全。

延迟初始化降低了初始化创建实例的开销,但是却增加了访问的开销,大多数时候,正常初始化优于延迟初始化。

4.Java并发编程基础

线程优先级:priority,可以设置1-10,但是有些操作系统会忽略优先级设置

线程状态:new:刚创建还未start(),runnable:java将操作系统中就绪和运行统称为runnable,blocked,wait,time_wait,teminated

注意:在synchronized前的线程是BLOCKED状态,在Lock接口前的线程是WAITED状态,因为Lock接口的阻塞使用了LockSupport中的方法

线程暂停方法:suspend(),resume(),stop(),但是这些方法是过期的,因为可能会导致死锁或者线程工作在不确定状态下的问题,建议使用等待/唤醒机制。

等待/唤醒流程:线程A拿到锁,调用wait方法进入WaitQueue,此时是WAIT状态,然后线程B拿到锁,调用notify方法,此时A从WaitQueue进入SynchronizedQueue,BLOCKED状态,线程B释放锁后,线程A拿到锁并继续执行。

ThreadLocal:以threadLocal对象为key,任意对象为值的存储结构。

5.Java中的锁

Lock相比synchronized,需要手动获取锁和释放锁,并且可以设置锁的释放时间,注意获取锁要在try块之前,否则获取锁异常后,异常抛出时也会使得锁失效

AQS:队列同步器,由FIFO队列和volatile state组成

同步队列:FIFO双向列表,当一个线程获取锁失败,则被构造成节点使用CAS加入同步队列尾部,并自旋。可以用acquire获取同步状态,该方法对中断不敏感,也就是当线程处于同步队列时,即使中断了也不会移出队列。

独占式state获取和释放:移出队列的条件时前驱节点称为头结点且成功获取到同步状态,在释放同步状态时,会唤醒后继节点。

共享式state获取和释放:成功获取同步状态并退出自旋的条件是tryAcquireShard方法返回值大于0,当释放同步状态后,会唤醒后续处于等待状态的节点。

独占式超时state获取和释放:和不超时的主要区别是,线程超过了设置的nanoTime后,自动返回

重入锁:ReentrantLock中,获取state值前会判断线程,如果重新获取state的线程是之前的线程,则state+1,解锁之后state-1

公平锁与非公平锁:ReentrantLock之所以默认非公平锁,是因为同个线程重新获取state的几率非常大,这样就防止了同步队列中线程频繁更换,导致频繁上下文切换,但是非公平锁也会造成线程饥饿。

读写锁的实现:在ReentrantLock中,state用来表示一个线程获取锁的次数,在读写锁中,这个变量要维护多个读进程和一个写进程的状态,因此需要按位切割使用该变量,高16位是读,低16位是写

写锁的获取和释放:写锁是一个可重入的排它锁,释放和ReentrantLock一样,也是减少state值。

读锁的获取和释放:读锁是一个可重入的共享锁,每次被获取就增加读状态,释放就减少读状态

锁降级:一个线程先获取写锁,然后获取读锁,最后释放写锁的过程。这个过程是为了保证数据的可见性。

LockSupport工具:LoclSuppoet提供了阻塞和唤醒功能,java6中在原有的park(),park(nanos)上增加了park(Obkject blocker),park(Obkect brocker,long nanos),这个参数便于dump打印出更详细的对象信息,而java5的park()是无法提供对象信息的。

Condition接口:任何java对象都有一组监视器方法,wait(),notify(),notifyAll(),可以与synchronized搭配实现等待/通知模式,Condition接口也提供了一些列方法和Lock实现等待/通知模式。Condition是AQS的一个内部类

6.Java并发容器和框架

ConcurrentHashMap:使用segment数组和hashentry数组组成,segmentShift和segmentMask需要在定位segment的hash算法上使用,

定位segment:将key进行再散列,目的是为了元素更平均的分布在segment上,减少hash冲突

get:先经过一次再散列,然后用这个散列值进行散列定位到segment,然后在通过散列算法定位到元素,get是不加锁的,原因是他会对要使用的元素用volatile修饰,在happens-before语义中,保证了同一时刻进行读写,get也能拿到最新值。

put:先定位到segment,然后加锁,插入前先判断是否需要扩容,如果需要扩容,则将segment扩容2倍,然后将旧值进行再散列。值得注意的是,segment的扩容比hashmap的扩容更切当,因为hashmap是新增元素后才判断需要扩容,如果新增完后没有再put元素,此次就是无效扩容,而且concurrentHashMap不是整个容器进行扩容,而是单个segment进行扩容

size:使用一个volatile修饰的count维护,如果在计算大小过程中元素数量发生改变,则用一个modeCount记录。

7.Java中13个原子操作类

AtomicInteger实现原理:使用了unsafe类的compareAndSwapInt方法,如果是符合预期的,则修改,否则自旋重试,

8.Java中的并发工具类

countDownLatch、cyclicBarrier区别:cyclicBarrier可以用reset方法进行重置,可以用isBroken方法判断元素是否被中断,可以用getNumberWaitng方法判断阻塞的线程数

semapphore:线程数控制

Exchanger:用于线程间数据交换,当两个线程到达同步点,可以交换数据、

9.Java中的线程池

处理流程:核心线程->队列->最大线程->拒绝策略

如果当前线程数小于corePoolSize,则创建线程,当时创建核心线程需要获取全局锁,所以最好是先预热核心线程数

线程池会将线程封装成worker,而worker会不断地从队列中获取任务执行

关闭线程池:shutdown和shutdownNow,原理是遍历工作线程调用interrupt()方法,shutdown()是将线程池设置为SHUTDOWNz状态,然后中断没有正在执行任务的线程,shutdownNow()是将线程池设置为STOP状态,然后将所有进行中或没有任务的线程都中断。

合理配置线程池:依赖数据库的线程池,可以将线程数设置大,可以减少等待数据库返回数据时CPU空转时间。建议使用有界队列,否则容易造成内存溢出

10.Executor框架

Execotor接口下有ThreadPoolExecutor、ScheduledThreadPoolExecutor

SingleThreadPoolExecutor:只有一个工作线程,适合用来需要顺序执行任务的场景

FixThreadPoolExecutor:固定线程数

CachedThreadPoolExecutor:无固定大小

ScheduledThreadPoolEeecutor:定时

Future:保存异步任务结果

Runnable:不返回结果

Callable:返回结果

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值