Java并发编程的艺术—笔记

前言

本文内容摘抄自: Java并发编程的艺术

线程上下文切换
  • 单核处理器也支持多线程执行代码,CPU时间片分配算法来循环执行任务,一次上下文切换:当时间片切换到下一个任务时,会保存当前任务状态,所以任务从保存到再加载的过程就是一次上下文切换

  • 测量工具:

    • Lmbench3可以测量上下文切换时长
    • vmstat可测量上下文切换次数,其中显示的cs表示上下文切换次数
    • jstack命令可以dump线程信息
减少上下文切换方法
  • 无锁并发编程、CAS算法、是用最少线程和使用协程

  • 协程:在单线程中实现多任务调度、且维持多个任务间的切换

  • 避免死锁方法:

    • 避免一个线程同时获取多个锁
    • 保证每个锁只占用一个资源
    • 使用定时锁lock.tryLock(timeout)来替代内部锁机制
    • 对于数据库锁,加锁和解锁必须在一个数据库连接里

    并不是并发越多越好,这取决于资源限制

volatile

为保证内存可见,Java编译器会在适当的位置插入内存屏障指令来禁止特定类型的处理器指令重排,load store

volatile禁止处理器的指令重排和令CPU缓存失效

  • 不会引起线程的上下文切换和调度,可以理解为轻量级的synchronized , 涉及Java内存模型(主内存、工作内存) ,保证共享变量的可见性当变量改变时,所有线程都会同步状态

  • 原理:经过volatile修饰的代码,会在汇编代码中出现,lock前缀指令

    • 将当前处理器缓存行回写到主内存中
    • 回写操作使得其他CPU中缓存的内存地址失效
对象在JVM中表现形式

对象在内存中的布局分为:对象头实例数据填充数据

对象头中包含两部分: MarkWord 和 类型指针.

如果是数组对象的话, 对象头还有一部分是存储数组的长度.

多线程下synchronized的加锁就是对同一个对象的对象头中的MarkWord中的变量进行CAS操作.

MarkWord

Mark Word用于存储对象自身的运行时数据, 如HashCode, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID等等.
占用内存大小与虚拟机位长一致(32位JVM -> MarkWord是32位, 64位JVM->MarkWord是64位).

synchronized

重量级锁,JDK1.6得到优化,引入‘偏向锁和轻量级锁’,所以锁共有4种状态:从低到高:无锁状态、偏向锁、轻量级锁和重量级锁,状态随竞争不断升级,不能降级

Java中的每个对象都可以作为锁. 具体变现为以下3中形式.

  1. 对于普通同步方法, 锁是当前实例对象.
  2. 对于静态同步方法, 锁是当前类的Class对象.
  3. 对于同步方法块, 锁是synchronized括号里配置的对象.

一个线程试图访问同步代码块时, 必须获取锁. 在退出或者抛出异常时, 必须释放锁.

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步, 但是两者的实现细节不一样.

  1. 代码块同步: 通过使用monitorenter和monitorexit指令实现的.
  2. 同步方法: ACC_SYNCHRONIZED修饰

monitorenter指令是在编译后插入到同步代码块的开始位置, 而monitorexit指令是在编译后插入到同步代码块的结束处或异常处.

线程执行到monitorenter指令时,尝试获取对应的monitor所有权,即尝试获取对象的锁

Java的monitor机制,临界区:使用 synchronized 关键字,PV原语:Object 的 wait / notify 等元素

当前线程必须获取到了obj的Monitor,才能去调用其wait方法,即wait必须放在被synchronized修饰的代码中。

notify有两个方法notify和notifyAll,前者只能唤醒一个正在等待这个对象的monitor的线程,具体由JVM决定,后者则会唤醒所有正在等待这个对象的monitor的线程

偏向锁

对象中的对象头中的markWord中的标识位,当其他线程尝试竞争偏向锁时才释放锁

轻量级锁

轻量级锁解锁时, 会使用原子的CAS操作将当前线程的锁记录替换回到对象头, 如果成功, 表示没有竞争发生; 如果失败, 表示当前锁存在竞争, 锁就会膨胀成重量级锁.

CAS

JVM中的CAS操作利用的是处理器提供的CMPXCHG指令实现的。,但是有三个问题

  • ABA问题,解决思路:版本号 JDK中的Atomic包提供compareAndSet方法
  • 循环时间长而开销大
  • 只能保证一个共享变量的原子操作,

使用锁机制保证原子操作

除了偏向锁,JVM实现的锁都是采用循环CAS来实现的。

JVM内存模型

线程之间:数据交换问题 /

进程之间:相互通信问题

在命令式编程中,线程之间的通信机制是:共享内存和消息传递

ReentrantLock 依赖于Java的AQS同步器框架
class ReentrantLockExample{
    int a=0;
    ReentrantLock lock=new ReentrantLock();
    public void writer(){
        lock.lock();
        try{
            a++;
        }finally{
            lock.unlock();
        }
    }
    public void reader(){
        lock.lock();
        try{
            a++....
        }finally{
            lock.unlock();
        }
    }
}
双重锁检测与延迟初始化

有时需要采用延迟初始化来降低初始化类和创建对象的开销,双重锁检测机制是常见的延迟初始化方式

//俄汉模式
public class Singleton{
    private static Singleton instance;
    static{
    	instance=new Singleton();
	}
	public static Singleton getInstance(){
        return instance;
	}
}
//懒汉模式
public class Singleton{
	private static Singleton instance;
	public static synchronized Singleton getInstance{
        if(instance==null){
            instance=new Singleton();
        }
        return instance;
	}
}

//双重锁检测机制
public class Singleton{
    private static Singleton instance;
    public static Singleton getInstance{
        if(instance==null){
            synchronized(Singleton.class){
                if(singleton==null){
                	singleton=new Singleton();
				}
            }
        }
        return singleton;
    }
}

JDK1.5才出现的枚举,可避免多线程同步问题和防止反序列化重新创建新对象

双重锁检测机制存在问题,

  • 分配对象的内存空间,
  • 初始化对象,
  • 设置instance指向刚分配的内存地址

2和3可能会出现指令重排,所以得利用volatile修饰。

Java并发编程基础

ThreadLocal
重入锁
  • 线程再次获取锁
  • 锁的最终释放

Java.lang.Obejct上有 wait() wait(long timeout) notify() notifyAll()synchronized配合

Condition接口 和Lock配合

调用Lock.newCondition()获取Condition对象

condition.await() 和condition.signal() 对应等待和释放

JUC包

ConcurrentHashMap是由Segment数组和HashEntry数组组成的,

  • Segment是一种可重入锁,在ConcurrentHashMap扮演锁的角色,
  • HashEntry存储键值对

ConcurrentHashMap的3种操作,get操作、put操作和size操作

get操作不加锁,因为统计Segment大小的count字段和HashEntry的value被修饰为volatile变量

put操作加锁,首先定位到哪个Segment,然后插入操作经过1、HashEntry是否需要扩容,2、将元素加入HashEntry

size操作 先尝试2次不加锁Segment方式来统计各个Segment的大小,如果统计过程中发现modCount改变再通过枷锁的方式来统计

因为put、remove、clean操作都会让modeCount改变

Java线程池

ThreadPoolExecutor
  • 当前运行线程少于corePoolSize,则创建新线程来执行(需要获取全局锁)
  • 若运行线程多于corePoolSize,则将任务加入BlockingQuene
  • 若无法加入BlockingQuene,则创建新的线程来处理任务(需要获取全局锁)
  • 若线程数量超过maximumPoolSize则使用拒绝策略
线程池的创建
  • corePoolSize
  • runnableTaskQueue
  • maimumPoolSize
  • ThreadFactory
  • 拒绝策略

向线程池提交任务:

  • execute() 不需要返回值任务
  • submit() 需要返回值的任务,会返回一个Future对象,通过future.get()获取返回值
Executor框架

java的线程既是工作单元也是执行机制,从JDK1.5开始,将工作单元(Runnable和Callable)、执行机制(Executor框架提供)分开

  • 任务:需要实现Runnable接口或者Callable接口
  • 任务的执行: Executor和继承自Executor的ExecutorService的接口
    • ExecutorService有两个关键类实现了该接口,ThreadPoolExecutor和ScheduledThreadPoolExexutor
  • 异步计算的结果,包括接口Future和实现Future接口的FutureTask类
ThreadPoolExecutor

通常使用工厂类Executors来创建,有3种类型:SingleThreadExecutor、FixedThreadPool和CachedThreadPool

  • SingleThreadExecutor,单个线程
  • FixedThreadPool 固定线程数
  • CachedThreadPool大小无界的线程池
ScheduledThreadPoolExexutor

有两种类型,ScheduledThreadPoolExecutor和SingleThreadScheduledExctutor

  • ScheduledThreadPoolExecutor 包含若干线程的ScheduledThreadPoolExexutor
  • SingleThreadScheduledExctutor 包含一个线程的ScheduledThreadPoolExexutor
FutrueTask

除了实现了Future接口外还实现了Runnable接口

FutureTask的实现基于AQS

你可能感兴趣的:(Java)