Java 多线程学习笔记—— volatile、ThreadLocal、synchornized、Lock、ReentrantLock 加锁源码分析等

1. 什么时候需要加锁?

Java进程运行时,为每一个线程开辟一个线程栈,分配一块共享的堆内存。在方法中定义普通类型变量时,它是在栈中分配内存,是线程私有的。而new一个对象时,会在堆中分配内存。当有两个线程或者多个线程持有该对象的引用,然后去访问该对象的实例变量时,便会引来资源竞争问题,此时需要使用加锁手段来保证同一时刻只有一个线程在访问该资源。

2. 如果不加锁会怎样?

当线程在没有同步的情况下读取变量时,可能会得到一个失效值,但至少这个值是由之前某个线程设置的值,而不是一个随机值。这种安全性保证也被称为最低安全性保证。最低安全性保证适用于绝大数变量,但是存在一个例外:非 volatile 类型的64位数值变量,double 和 long(这个跟操作系统底层取数指令有关系、64位的数据可能要经过两次取数操作)。

3. Java的内存模型JMM以及共享变量的可见性

JMM(Java Memoy Model)决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程有着自己独有的工作内存,工作内存中保存了被该线程使用到的变量,这些变量来自主内存变量的副本拷贝。线程对变量的所有读写操作都必须在工作内存中进行,不能直接读写主内存中的变量。而不同线程间的工作内存也是独立的,一个线程无法访问其他线程的工作内存中的变量。

 对于普通的共享变量来讲,线程A将其修改为某个值发生在线程A的本地内存中,此时还未同步到主内存中去;而线程B已经缓存了该变量的旧值,所以就导致了共享变量值的不一致。

4. volatile 作用

对变量加锁有两种,一种是轻量级锁,在该变量前面加volatile关键字。volatile有个非常关键的作用是可以保证其变量的内存可见性。这个特性是指在在一个线程中对该变量做了操作后,会及时的将数据刷会主内存,另一个线程始终会读到最新的变量值。另一个特性就是禁止指令重排序。

指令重排序是指计算机在执行程序时,为了提高性能,编译器和处理器常常会重新调整指令执行顺序。在单线程环境下,指令重排序没有问题。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象

首先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:

  • 保证特定操作的顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。

5. ThreadLocal的实现原理

ThreadLocal的实现原理:ThreadLocal 内部有一个 ThreadLocalMap 静态类,Thread 类里面有个属性 ThreadLocal.ThreadLocalMap threadLocals,当调用ThreadLocal类的set()方法时,该方法先获取到调用线程,然后获得该thread的threadLocals实例对象,如果没有则创建,然后调用put方法,Thread.ThreadLocalMap类的 key 为 ThreadLocal 类实例,value 为用户设置的值。 所以只有在一个主线程中new了多个 ThreadLocal 实例时,该key才可能存在多个。

public class ThreadLocal<T> {
   public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
   }
   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
   }
   void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
   }
}

6. synchronized 原理

synchronized 是一种重量级加锁手段,有如下特性:

  • 内存可见性:同步快的可见性是由“如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值”、“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这两条规则获得的。
  • 操作原子性:持有同一个锁的两个同步块只能串行地进入

synchronized 加锁有两种手段:1 是对方法加锁;2 是对代码块加锁。

public class SynchronizedTest {  
    public synchronized void syncFunc() {
        // 执行同步方法
    }
    public void syncBlock() {
        synchronized (obj) {
            // 执行同步代码块
        }
    }
}

synchornized 对方法加锁时,如果是静态方法,则加锁对象是当前类.class 实例,如果是实例方法,则加锁对象是当前类实例,在编译成 class 文件时,会在该方法内容首行加上 ACC_SYNCHRONIZED 标记;对代码块加锁时,加锁对象是括号里面指定的对象,在编译成 class 文件时,会在同步代码块前后加入 monitorenter 和 monitorexit 指令。

网上讲 synchornized 原理时,都会先从什么对象头,偏向锁讲起,讲的头头是道,但是偏偏对关键的 mutex 和 monitor 避而不谈,小弟就以自己的理解来谈谈这两个东西。

Mutex 和 Monitor

首先 synchornized 需要锁住一个对象,并不是直接在该对象头部设置互斥量,而是在该对象头部的 markword 设置一个指向底层 Mutex 变量的地址(在 java 1.5 版本未对 synchornized 做优化的情况下)。c 语言中 mutex 数据结构的关键部分定义如下:

struct mutex {
	atomic_long_t		owner;           //原子计数,用于指向锁持有者的task struct结构
	spinlock_t		wait_lock;              //自旋锁,用于wait_list链表的保护操作
	struct list_head	wait_list;          //链表,用于管理所有在该互斥锁上睡眠的进程
};

mutext 工作原理大致如下:

  • 1) 申请mutex
  • 2) 如果成功,则持有该mutex
  • 3) 如果失败,则进行spin自旋. spin的过程就是在线等待mutex, 不断发起mutex gets, 直到获得mutex或者达到spin_count限制为止
  • 4) 依据工作模式的不同选择yiled还是sleep
  • 5) 若达到sleep限制或者被主动唤醒或者完成yield, 则重复1)~4)步,直到获得为止

简单地说就是一个 spinlock(自旋锁,在获取锁失败时,会先尝试一段时间的自旋等待) 搭配一个代表等待队列的双向链表"wait_list",外加一个记录mutex生命周期的状态变化的"owner"。没有线程持有mutex时,"owner"的值为0,有线程持有时,需将"owner"转换为指向该线程的"task_struct"的指针。 

如果直接基于 mutex 编程容易出错, 因为我们需要去亲自操作变量以及对 进程/线程 进行阻塞和唤醒。所以我们需要一个更高级的抽象 api:monitor ,它可以提供线程(进程)被阻塞和被唤醒的管理机制。

那么 java 中的 monitor 究竟指的是什么呢? 答案是就是被加锁的 java 对象,但是任意 java Object 都可以充当锁对象,为什么该对象可以变成 monitor 呢?答案在 java.lang.Object 类 wait(),notify(),notifyAll() 方法的底层实现上。

java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖于一个叫 ObjectMonitor 模式的实现,这是 JVM 内部基于 C++ 实现的一套机制,基本原理如下所示:在这里插入图片描述

 当一个线程需要获取 Object 的锁时,会被放入 EntrySet 中进行等待,如果该线程获取到了锁,成为当前锁的 owner。如果根据程序逻辑,一个已经获得了锁的线程缺少某些外部条件,而无法继续进行下去(例如生产者发现队列已满或者消费者发现队列为空),那么该线程可以通过调用 wait 方法将锁释放,进入 wait set 中阻塞进行等待,其它线程在这个时候有机会获得锁,去干其它的事情,从而使得之前不成立的外部条件成立,这样先前被阻塞的线程就可以重新进入 EntrySet 去竞争锁。这个**外部条件**在 monitor 机制中称为**条件变量**。

7. Lock 接口

从Java 5之后,在 java.util.concurrent.locks 包下提供了另外一种方式来实现同步访问,那就是Lock。使用 synchornized 加锁的好处是在进入同步代码块时自动加锁,在离开同步块时自动释放锁,不用担心未释放锁造成死锁的问题。坏处是它一定要获取到锁,如果该锁被别的线程一直占用,它便会进入长时间的等待(不给不行,赖着不走了)。而 Lock 提供了 tryLock 机制,它可以在未获取到锁时方法直接返回 false。

Lock 是一个接口,一共定义了 7 个方法:

public interface Lock {
    void lock();
    void lockInterruptibly() throws InterruptedException;
    boolean tryLock();
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
    void unlock();
    Condition newCondition();
}

 lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()是用来获取锁的。unLock()方法是用来释放锁的。newCondition() 方法是基于条件等待。

tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待。

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

lockInterruptibly()方法比较特殊,当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。也就使说,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()方法能够中断线程B的等待过程。由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出InterruptedException。

8. ReentrantLock 加锁过程源码分析

ReetrantLock 是 Lock 接口的直接子类,ReetrantReadWriteLock 是 ReadWriteLock 的直接子类,它有两个内部类 WriteLock 和 ReadLock,这两个类也实现了 Lock 接口。ReetrantLock 和ReetrantReadWriteLock 实现加锁的主要逻辑都是通过一个叫 Sync 的内部类来实现的,该类继承自 AbstractQueuedSynchronizer 类,它是实现加锁过程的核心类,类图关系如下图所示:

 要研究 ReetrantLock 类的源码,先来看一下我个人实现的简易版本的加锁版本类:

public class MyMutex implements Lock {
    private Sync sync = new Sync();

    private class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected boolean tryAcquire(int acquires) {
            assert acquires == 1; // Otherwise unused
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
            return false;
        }

        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1; // Otherwise unused
        if (getState() == 0) throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }
    
    @Override
    public void lock() {
        sync.acquire(1);
    }

    @Override
    public void unlock() {
        sync.release(1);
    }
}

MyMutex 类的 lock 方法去调用了 Sync 类的 acquire 方法,该方法在其父类中定义。AbstractQueuedSynchronizer.acquire() 方法先调用 tryAcquire 方法给资源加锁,该方法默认抛出异常,必须在子类中重新该方法,Sync 类重新了该方法。如果 tryAcquire 方法返回 true,则表示加锁成功,acquire 方法直接返回,否则当前线程要进入排队等待获取锁的节奏。

OK,明白了上述流程后,接下来我们就来分析源码的实现过程:

1) acquire 方法

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{

    private volatile int state;

    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
}

acquire 方法的参数 arg 表示资源,它先调用Sync类的 tryAcquire 方法对资源加锁,该方法实现很简单,调用其父类的 compareAndSetState(0, 1) 方法,该方法想把类实例变量 state 的值修改为1, 注意该方法传入的第一个参数为0,表示期望 state 的当前值为0,第二个参数为设置 state 的新值。如果资源已经被其他线程加锁,即 state 值不为0,则整个方法返回 false。否则会将 state 的值成功设置为1,然后调用 setExclusiveOwnerThread(Thread.currentThread()) 方法声明当前锁的持有者,方法返回 true。

tryAcquire 方法如果返回 true,则会直接执行 selfInterrupt 方法,该方法跟线程中断有关,这里先不做讨论。考虑 tryAcquire 方法返回 false,则表示现在存在锁竞争且竞争失败了,那失败了肯定要开始排队了,即调用 acquiredQueued 方法。

2) addWaiter 方法

在调用 acquiredQueued 方法之前,先调用 addWaiter 方法添加一个新的排队者,addWaiter 方法要求传入一个 Node 对象。一看名字 Node,我们就可以知道这个队列是基于 Node 的链表结构。Node 类是 AbstractQueuedSynchronizer 的内部类,先来看下它的数据结构:

static final class Node {
    // 等待状态:-3: propagate, -2: condition, -1: signal, 1: canceled, 0: other staus
    volatile int waitStatus;
    // 前驱节点
    volatile Node prev;
    // 后继节点
    volatile Node next;
    // 关联的当前线程
    volatile Thread thread;
    // 与条件等待相关的下一个等待者
    Node nextWaiter;
    // 获取前驱节点的方法
    final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
   }
}

AbstractQueuedSynchronizer 类里面有 head 和 tail 指针。 再来看看 addWaiter 方法:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
    
    private transient volatile Node head; // 队列头节点

    private transient volatile Node tail; // 队列尾节点

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
}

该方法先创建了一个 Node 对象:Node node = new Node(Thread.currentThread(), mode); 我们先不关心第二个参数 mode,它跟共享锁有关。该 node 的 thread 实例指向当前线程,waitStatus = 0,prev 和 next 实例都为 null。 

接着就是将该 node 入队的过程了,addWaiter 方法先判断队列是否为空(判断 tail 指针是否为 null),如果不为空,则直接使用 CAS 操作尝试将 tail 指针指向 node 节点。如果队列为空或者 CAS 操作失败(CAS会失败是因为同一时刻有其他线程在入队),则需要调用复杂的 enq 方法将元素入队,enq 方法代码如下:

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

enq 方法使用自旋(无限for循环)+ CAS 完成入队,但是需要注意的是,队列为空时,它是先创建了一个空节点,将 head 指向该空节点,然后再插入新加的 node。假如是第一次插入的话,结果如下图所示:

 ok,上面我们清楚了 node 的入队过程,也明白了 node 里面的 thread 实例跟当前线程相关联。那入了队就完了吗?并不是,因为 AbstractQueuedSynchronizer 并没有开启另外一个调度线程来调度这些队列中的元素。那能怎么办呢?那只能一直排着,等待别人走后,自己上前顶替(这跟我们在食堂打饭排队的情况类似)。这一过程是在 acquiredQueued 方法中实现的

3) acquiredQueued 方法

acquiredQueued 方法的源码如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
}

该方法的核心逻辑如下:

  1. 如果 node 已经排到第一位(node 的 前驱节点为 head,而 head 指向的是一个空节点),则再一次调用子类实现的 tryAcquire 方法对资源加锁,那么此刻该方法会返回 false 么?答案是会(这没天理啊,你可以想象作为一个干饭人,好不容易排到第一名,结果没打成饭)。使用非公平锁就会出现这种情况,非公平锁第一次调用 tryAcquire 方法时,不管有没有其他人排队,都会去尝试获取锁。如果 tryAcquire 方法返回 false,那没办法,只能进入下一次循环。
  2. 那如果当前 node 还在队列后面,要排长时间的队,一直调用 for 循环让 cpu 空转也不是好办法。node 觉得太累了,想休息会(即让当前方法阻塞)。但也不能乱休息,它必须寻找一个安全点才能休息。所谓的安全点就是排在某个节点之后,而这个节点出队之前一定会叫醒它,这样它才可以安全休息。具体逻辑在 shouldParkAfterFailedAcquire 方法中。

4) shouldParkAfterFailedAcquire 方法

shouldParkAfterFailedAcquire 方法我把代码贴出来,就不做具体分析了,网上的分析教程也很多。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值