Java并发编程知识点总结

2 并发编程

2.1 线程

2.1.1 线程的几种状态

一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态。

Java虚拟机中没有就绪态和运行态的区分,统一称为可运行态。

  • 新建状态:线程创建但未启动
  • 可运行态:正在Java虚拟机中运行,在操作系统层面可能是运行态也可能是就绪态
  • 阻塞态:锁竞争导致线程阻塞
  • 无限期等待态:等待与阻塞的区别在于等待是主动的、阻塞是被动的。无限期等待可能是wait()方法被等待唤醒
  • 限期等待:一定时间后唤醒,如sleep()方法
  • 死亡态:任务结束或因异常而结束线程。

有哪些方法可以保证线程安全?

  • final修饰的一定是线程安全的,因为它不可变,如String类
  • 互斥同步:synchronized或Lock锁保证线程安全
  • 非阻塞同步:CAS保证线程安全、atomic类
  • 无同步也可以保证线程安全
    • 采用局部变量,线程私有当然线程安全
    • 采用ThreadLocal保证线程安全

2.2 创建多线程的几种方式

  • 直接继承Thread类,重写run方法。其实Thread类也是实现了Runable接口但是run方法为空。
  • 实现Runnable接口,重写run方法,实现类作为参数传入Thread的构造方法。其实继承Thread类重写run方法和实现Runnable接口并作为构造函数的参数传入Thread创建对象没有区别。
  • 实现Callable接口,重写call方法,将实现类包装成一个FutureTask对象作为参数传入Thread的构造方法。优点是可以带有返回值,缺点是相对复杂。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。

由于创建线程和销毁线程有一定的代价,所以可以使用线程池来管理线程。线程池创建中有一个ThreadFactory接口作为参数,接口的实现中直接通过new Thread()创建线程。

2.3 并发机制底层实现

2.3.1 synchronized关键字

synchronized关键字的作用

用于为Java对象、方法、代码块提供线程安全的操作,属于排它的悲观锁,也属于可重入锁。

synchronized关键字可作用于代码块、方法、静态方法。

  • 修饰实例方法:作用于当前对象实例加锁。锁的是this
  • 修饰静态方法:给当前类加锁。会作用于当前类的所有实例,因为静态成员不属于任何一个实例,是类成员。锁的是class
  • 修饰代码块:收到传入一个锁对象,锁传入的对象

**注意:**当线程A调用synchronized修饰的非静态方法,线程b调用synchronized修饰的静态方法是被允许的。因为非静态方法是使用实例对象的锁,静态方法是使用类的锁。

synchronized的实现原理

在JVM中,对象在内存的的储存布局为:对象头(markword、类型指针)、实例数据、对齐填充。对象头主要结构是由Mark WordClass Metadata Address组成,其中Mark Word存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Mark Word结构如下:

img

当锁标记为10时为重量级锁,有一个指针指向一个Monitor对象,monitor里面有一些数据结构如锁竞争队列ContentionList、竞争候选列表(EntryList)、等待集合WaitSet分别保存想要获得锁的线程、在锁竞争队列中有资格获得锁的线程、调用wait方法后阻塞的线程(三个集合中的线程都为阻塞状态)。monitor中还有个Owner标识位表示当前哪个线程获得锁,用于互斥。

  1. **修饰代码块时:**synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。
  2. 修饰方法时:对方是否加锁是通过一个标记位来判断的。

synchronized如何保证可见性

  • 线程解锁前,必须把共享变量的最新值刷新到主内存中
  • 线程获得锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新获取最新的值

synchronized锁的执行流程

  1. 首先是偏向锁;如果一个线程获得了锁,那么锁就进入偏向模式,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查锁标记位为偏向锁以及当前线程ID等于对象头中的ThreadID即可,这样就省去了大量有关锁申请的操作。
  2. 轻量级锁;当第二个线程申请锁且没有锁竞争时,就转为轻量级锁,使用CAS方式修改共享变量。
  3. **自旋锁;**若轻量级锁失败,线程不会立即释放cpu资源,而是进行自旋持续的获取锁。(注:这种方式明显造成了不公平现象,最后申请的线程可能获取锁)
  4. **重量级锁;**轻量级锁失败的线程放入锁竞争队列(阻塞态);

虽然synchronized有锁升级的过程,但是这个过程基本不可逆,所以还是推荐使用Lock锁

2.3.2 synchronized与Lock的区别联系

  1. synchronized是java的关键字,JVM实现,通过monitor实现;Lock锁是JUC并发包中的实现,基于AQS模板重写tryAcquire、tryRelease实现;
  2. synchronized可以修饰方法,而Lock只能用于代码块。
  3. synchronized会自动释放锁,而Lock需手动释放。
  4. Lock可以是公平锁也可以是非公平锁,而synchronized只能是非公平锁。
  5. synchronized不可中断,除非抛出异常或者正常执行完毕;ReentrantLock可中断,tryLock可以设置超时时间,lockInterruptibly()放入代码块中,调用interrupt()方法可以中断。
  6. ReentrantLock可以绑定多个Condition条件用于实现分组唤醒需要唤醒的线程,实现精确唤醒。而synchronized要么随机唤醒一个要么全部唤醒。
  7. 两者都是可重入锁。

2.3.3 volatile关键字

轻量级的同步机制,保证可见性和禁止指令重排保证有序性。不保证原子性。

  • volatile作用 :保证可见性和禁止指令重排。Java把处理器的多级缓存抽象为JMM,即线程私有的工作内存和线程公有的主内存,每个线程从主内存拷贝所需数据到自己的工作内存。volatile的作用就是当线程修改被volatile修饰的变量时,要立即写入到主内存,并通知其他线程该变量已经修改,当线程读取被volatile修饰的变量时,要立即到主内存中去读取,保证了可见性。禁止指令重排来保证顺序性。(单例模式的双重校验最好是加上volatile关键字,防止指令重排)
  • 可见性的实现:每个线程从主内存拷贝所需数据到自己的工作内存。volatile的作用就是当线程修改被volatile修饰的变量时,要立即写入到主内存,并通知其他线程该变量已经修改,当线程读取被volatile修饰的变量时,要立即到主内存中去读取,保证了可见性。
  • Volatile实现原理: ①JVM向处理器发送一条LOCK指令,表示将这个变量的缓存行的数据如果修改则写回到内存。②一个处理器的缓存行写回到内存会导致其他处理器的缓存无效(通过缓存一致性协议实现,处理器会嗅探总线上的传播数据来判断自己缓存的数据是否过期)。

总线风暴?

因为缓存一致性原理和CAS循环导致总线无效的交互太多,总线带宽达到峰值。

为什么volatile不保证原子性

javap查看字节码文件,如对一个volatile关键字修饰的变量n执行n++操作的指令是

getfield
iadd
putfield

指令被拆成了三个,那么多线程对一个数据进行修改时,会出现写覆盖的情况。当某个线程执行到getfield指令之后被挂起,那么该线程将获取不到其他线程修改后的最新数据。

如何保证volatile的原子性

如有n++等操作可以使用atomic类如atomicInteger。(atomic原理CAS)

指令重排的原理

指令重排:在保证数据依赖性的情况下,编译器优化可能对指令进行重排,指令重排在单线程情况下不会有任何问题。

在单线程情况下没有依赖性的数据在多线程情况下就可能有依赖性,就会出现问题。所以volatile关键字会禁止指令重排。通过内存屏障来实现禁止指令重排。

在哪里用到volatile

单例模式会用到。双重校验单例模式+volatile禁止指令重排

public class Main {
   
	private volatile Main instance = null;  //volatile关键字禁止指令重排,防止多线程情况下instance不为null但是还未初始化完成的情况出现。
    private Main(){
   
        System.out.println("执行构造函数");
    }
    
    public Main getInstance(){
   
    //双重检验
        if(instance == null){
   
            synchronized (Main.class){
   
                if(instance == null){
   
                    instance = new Main();
                }
            }
        }
        return instance;
    }
}

当new一个对象时instance = new Main();,大体上有三步:分配内存做默认初始化、执行初始化(构造方法)、将instance指向对象。如果不使用volatile关键字禁止指令重排,可能将第二步和第三步颠倒执行,即inst不为空时,还未初始化完成。

所以必须要使用volatile关键字禁止指令重排!

CPU层面如何禁止重排序?

通过内存屏障来禁止重排序。在两个指令中间加上内存屏障,即这两个不可以交换顺序

伪共享

一个缓存行为64字节,不仅仅包含一个数据,而是多个数据。如果一个缓存行中多个变量被volatile变量修饰,数据A在处理器1中修改后,处理器2读写数据B也要去内存中读取,即使数据B并没有失效,因为数据A和数据B在同一个缓存行!!

所以,如果两个volatile变量的定义挨在一起,很可能在一个缓存行里面,应尽量避免!

如果非要定义,使用cache line padding!在volatile变量前后声明7个long类型的变量填充。

2.3.4 atomic包和CAS原理及问题

AtomicInteger中的自增操作详解

AtomicInteger类的定义

public class AtomicInteger extends Number implements java.io.Serializable {
   
    private static final long serialVersionUID = 6214790243416807050L;
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
   
        try {
   
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) {
    throw new Error(ex); }
    }

    private volatile int value;

可见整个value使用volatile关键字修饰,保证可见性和禁止指令重排。

再看自增函数的实现

public final int getAndIncrement() {
   
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //this代表当前对象,valueoffset代表value这个值的内存偏移量,1代表要加1操作。

找到unsafe类的实现。do while循环自旋锁实现

public final int getAndAddInt(Object var1, long var2, int var4) {
   
    int var5;  //声明修改前的值
    do {
   
    	var5 = this.getIntVolatile(var1, var2);  //本地方法,根据对象和内存偏移量获取值。
    } 
    //自旋锁本地方法实现比较并交换。val1:当前对象  val2:位移偏移量  val5:修改前的值  val5+val4:修改后的值
    //根据val1和val2获取当前值,与val5比较,若相等则将该值赋值为val5+val4
    //compareAndSwapInt方法是利用cpu原语实现,不可中断保证原子性。
    while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}
  • 处理器解决原子操作:

    • 总线锁:处理器提供一个LOCK #信号,当一个处理器在总线上输出此信号时,其他处理器请求将被阻塞。(缺点:其他处理器也不能操作其他内存,开销大)
    • 缓存锁:通过缓存锁定实现。
    • 处理器提供一系列指令实现总线锁和缓存锁两个机制,CMPXCHG指令用于实现Java的CAS操作。
  • CAS的三大问题:

    • ABA问题。Atomic包中有一个类可以解决这个问题
    • 循环时间长时CPU开销大(并发太高的情况下不适用)
    • 只能保证一个共享变量的原子操作;
  • ABA问题的解决:时间戳的原子引用

    AtomicReference类

    在atomic包中有基本的原子类实现,如果需要实现自己写的User类的原子操作就需要使用AtomicReference加泛型实现。

    class User{
         
        String username;
        int age;
        User(String name, int age){
         
            this.username = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
         
            return username+"  "+age;
        }
    }
    public class Main {
         
        public static void main(String[] args) {
         
            User u1 = new User("aa", 18);
            User u2 = new User("bb", 20);
            AtomicReference<User> userAtomicReference = new AtomicReference<User>(u1);
            System.out.println(userAtomicReference.compareAndSet(u1, u2));
            System.out.println(userAtomicReference.get());
        }
    }
    

加上修改版本号解决ABA问题

在JUC包中有一个AtomicStampedReference类已经可以实现带版本号的原子引用。

new AtomicStampedReference<User>(u1,1);  //u1为初始值,1为初始版本号。以后每次修改版本号加一

原理:类中是一个Pair类作为数据结构解决ABA问题,修改后版本后不一样。

    private static class Pair<T> {
   
        final T reference;  //我们的数据
        final int stamp;   //版本号
        private Pair(T reference, int stamp) {
   
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
   
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;   //最主要的数据,每次比较这个值




2.4 Java并发容器

2.4.1 List集合的线程安全

    public static void main(String[] args) {
   
        List<String> list = new ArrayList<String>();
        for(int i=0;i<10;++i){
   
            new Thread(()->{
   
                list.add(UUID.randomUUID()
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值