JUC整理

文章概述

本文先介绍了线程的6种状态以及死锁、活锁和线程饥饿,然后讲述线程的三大特性原子性、可见性和有序性。然后介绍volatile关键字,接下来会介绍可重入锁和不可重入锁,乐观锁和悲观锁的区别。紧接着描述ReentrantLock和synchronized,然后描述CAS及其底层实现。

线程状态

线程状态共有6种,分别为NEW、RUNNABLE、BLOCKED、WAITING、TIME_WAITING、TERMINATED

  • NEW 已经创建出来,但还没有调用start方法
  • RUNNABLE 线程此时会有2种运行状态,一种是正在运行,另一种是正在等待CPU资源
  • BLOCKED 阻塞状态,当线程准备进入synchronized同步块或同步方法时,需要申请一个监视器锁而进行的等待,会使线程进入BLOCKED状态
  • WAITING 调用了Object.wait()或者Thread.join()或者LockSupport.park()。处于该状态下的线程在等待另一个线程执行一些其余action来将其唤醒
  • TIME_WAITING 和WAITING状态一样,但是等待时间是明确的
  • TERMINATED 消亡状态,线程执行结束

死锁

产生死锁的四个条件

  • 资源互斥: 一个资源一次只能被一个线程使用
  • 请求与保持条件:一个线程因请求资源而阻塞,且其持有的资源不会主动释放
  • 不剥夺条件:线程已获得的资源,在其执行结束前,不会被剥夺
  • 循环等待:若干线程之间形成一种头尾相接的循环等待资源关系

如何避免死锁

  1. 粗锁法
    增加锁的粒度来消除请求与保持条件(让线程一次性申请完资源,这样不会在执行过程中发生阻塞)
  2. 锁排序法
    在执行时,指定获取锁的顺序,来消除循环等待条件
    锁排序法的实例代码:
public class BankAccount {

    private static final Object tieLock = new Object();//排序相等时用来同步的锁对象
    private String id;
    private volatile double amount;

    public BankAccount(String id, double amount) {
        this.id = id;
        this.amount = amount;
    }

    public void transfer(BankAccount to, double amount) {
        int fromHash = System.identityHashCode(this);
        int toHash = System.identityHashCode(to);
        if (fromHash < toHash) {
            synchronized (this) {
                synchronized (to) {
                    this.amount -= amount;
                    to.amount += amount;
                }
            }
        } else if (fromHash > toHash) {
            synchronized (to) {
                synchronized (this) {
                    this.amount -= amount;
                    to.amount += amount;
                }
            }
        } else {
            synchronized (tieLock) {
                synchronized (this) {
                    synchronized (to) {
                        this.amount -= amount;
                        to.amount += amount;
                    }
                }
            }
        }
    }
}

线程锁死

这个要和死锁做区分,线程锁死是指唤醒该线程的条件永远无法成立或者其他线程无法唤醒这个线程而使其一直处于非运行状态(线程并未终止)导致其任务一直无法进展。

分类

  1. 信号丢失锁死
    没有对应的通知线程来将等待线程唤醒,导致该线程一直处于等待状态。
    例如在使用Object.wait()/Condition.await()时,没有先对synchronized的保护变量进行判断,有可能该变量已经满足条件,不需要wait,但仍然进行了wait操作,此时已满足条件,所以不会再有别的线程执行notify()来唤醒该线程,造成了信号丢失锁死
  2. 嵌套监视器锁死
    嵌套锁导致等待线程永远无法被唤醒
    例如一个线程,只执行了内层的y.wait()操作,但始终持有外层的X锁,而另一个线程需要先获取到x锁,才能执行内层的y.notifyall()操作

活锁

线程始终处于RUNNABLE状态,但却一直无法获取其所需的资源,导致其执行的任务没有进展。例如线程一直在申请内存资源,但却始终申请不到。

线程饥饿

线程一直无法获取到其所需资源而导致任务无法进行。

线程安全三大特性

  • 原子性:对于涉及到共享变量访问的操作,若该操作从执行线程以外的线程来看是不可分割的,则该操作具有原子性。
  • 可见性:是指一个线程更新完一个共享变量后,该次更新其他线程在后续操作中是否可见
  • 有序性:一个处理器上运行的线程所执行的内存访问操作在另一个处理器上运行的线程来看是否有序

volatile关键字

是一个轻量级的锁,可以保证可见性和有序性,但不保证原子性。

轻量级是指在使用时不会从用户态切换到核心态

  • volatile可以保证主内存和工作内存直接产生交互,进行读写操作,保证可见性
  • 它只保证写操作的原子性,但对于复杂的读写操作无法保证其原子性
  • 可以禁止指令重排序

volatile开销较大,因为它需要从高速缓存或者内存中读取而不能从寄存器中读取

可重入锁和不可重入锁

  • 可重入锁是指某个线程想要获取某个共享资源,计数器的值会加1,线程释放共享资源时计数值减1,当计数值为0时释放锁
  • 不可重入锁是指一个线程获取到一个锁之后,除非这个线程释放锁否则其他线程都拿不到这个锁

乐观锁和悲观锁

  • 乐观锁是指在操作数据时非常乐观,认为别的线程不会修改这个值。所以乐观锁是不会真实上锁的,只会在修改数据时判断下该数据是否被别人修改,如果没有修改则执行这次的写操作,否则放弃这次的操作
  • 悲观锁是指在操作数据时比较悲观,认为别的线程会修改这个值,所以在写数据时会直接把这个数据给锁住,直到操作结束别的线程才能拿到这个值

ReentrantLock

ReentrantLock是一种轻量级的可重入锁。它基于CAS实现,是一种显式锁。

首先介绍下AQS。AQS(AbstractQueuedSynchronizer)抽象的队列式同步器。基本思路是被请求的共享资源空闲时将请求该资源的线程设为工作线程并为其分配共享资源,如果共享资源被占用就会将线程加入队列中。

ReentrantLock是通过AQS来实现可重入锁

synchronized

它是一个内部锁,同样也是一个可重入锁

  • 进入时,执行monitorenter,将计数器+1,释放锁monitorexit时,计数器-1
  • 当一个线程判断到计数器为0时,则当前锁空闲,可以占用。反之,当前线程进入等待状态

对于线程的三个特性的保证:

  • 通过互斥来保证原子性,每次只有一个线程可以进入到临界区
  • 通过写线程冲刷处理器缓存和读线程刷新处理器缓存保证可见性
  • 原子性和可见性的保证使得其在临界区的操作都是完全按照代码顺序执行的

CAS

CompareAndSwap,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。

CAS的实现依赖于intel lock前缀。所谓的intel lock前缀是用来保证指令的原子性,确保处理器可以独占使用某些共享内存

原理分析

先来看一个CAS的应用

public class AtomicTest {
    public static void main(String[] args) {
        MyAtomicInteger in = new MyAtomicInteger(1);
        System.out.println(in.getAndIncr());
    }
    static class MyAtomicInteger extends AtomicInteger {
        public MyAtomicInteger(int i) {
            super(i);
        }
        public final int getAndIncr() {
            for (; ; ) {
                int current = get();
                int next = current + 1;
                if (compareAndSet(current, next))
                    return current;
            }
        }
    }
}

CAS中的一个核心实现是Unsafe类。该类中有一个compareAndSwapInt方法,该方法实现了CAS。

public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

compareAndSwapInt()实际上是一个native方法,我们可以看一下它的具体实现。

  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

可以看到该方法的核心实现是cmpxchg方法,我们可以再看一下这个方法的实现。

inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  int mp = os::is_MP();
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    cmp mp, 0
    je L0
    _emit 0xF0
L0:
    cmpxchg dword ptr [edx], ecx
  }
}

大概的逻辑是判断下是不是多核的情况,如果是多核的话,需要在执行修改操作前加上lock前缀避免多核同时访问共享内存出错。修改操作比较简单,就是将eax(compare value)与[edx](expected value)j进行比较,如果两个值相同的话就把[ecx](exchange value)值存入[edx]中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值