Java并发编程的艺术,学习笔记(二)锁

synchronized

synchronized 是根据 Monitor Object 模式来实现的,在这个模式下有4个角色:

  1. 监视对象(Monitor Object)
  2. 同步方法(临界区)
  3. 监视锁
  4. 监视条件 (什么时候决定 wait 和 notify)
    在这里插入图片描述
    当线程需要进入临界区,首先线程需要获取临界区的锁。
    再具体点,对象设置markword里面锁的地址(线程地址)
    每个对象只能被一个线程获取,一个线程可以获取多个锁(类似嵌套synchronized)
    监视条件则提供了线程可以恰当的时候进行阻塞/唤醒,阻塞的线程会被放入阻塞队列中,等待唤醒(对应的就是 监视对象的 wait() 和 notify() 方法)
    所以这正好验证了别人说的:java从最初设计出来就适合多线程编程。

Lock接口

java 中提供了两种方式来给代码加锁,一种是隐式加锁synchronized,当然他的解锁也是隐式的。
另一种就是显示加锁 Lock ,他的解锁需要放到finally 里面去解锁,以保证可以在异常的时候正常解除锁。

 Lock lock = new ReentrantLock();
 lock.lock();
 try {
     
 }finally {
     lock.unlock();
 }

lock 比 synchronized 的好处

  1. 获取锁可以被中断
  2. 公平或者非公平锁(ReentrantLock)
  3. 多条件
  4. 超时获取锁

队列同步器

队列同步器(AbstractQueuedSynchronizer)(AQS)
他的内部维护了一给双向列表,并且符合FIFO的规则。
在这里插入图片描述
在尝试获取锁的时候,失败了就会以节点的方式被添加进同步器里面的列表内。
源码介绍:

	// 获取锁
    public final void acquire(int arg) {
    	// 进行一次尝试获取
        if (!tryAcquire(arg) &&
        	// 如果获取失败的话,就添加到队列中 addWaiter() 
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

以上是获取锁的代码,方法套方法,接下来一个个解释。

    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

尝试获取锁,此方法需要被重写,尝试获取锁,可以是 循环cas的方法,可以参考ReentrantLock 里面的方法。

// 做为新节点添加进 队列中
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 进行一次快速的尝试,如果失败了则进入 enq()方法 进行 循环+CAS的方式添加
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 如果快速尝试失败了 就进入 循环CAS
        enq(node);
        return node;
    }    
    
    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;
                // jvm的时候说过cas是硬件支持的只有一条指令方法,native方法由unsafe类提供
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

如果获取锁失败的话,创建节点,并进行自旋
在这里插入图片描述


	// 节点被添加进入后就进入自旋
    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;
                }
                // 如果不是头节点,或者 获取锁失败类
                // 现判断前节点的status,如果不是-1,则设置成-1 (SIGNAL),如果大于0则将节点取消
                // parkAndCheckInterrupt,使用LockSupport.part()将线程挂起。
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

节点创建成功后,会进行第一次自旋,失败后会挂起。

在这里插入图片描述

这是独占锁获取的流程。
在这里插入图片描述
图为共享锁的访问。当有一个获取共享锁的时候,其他需要共享锁的都可以进入,独占的不行。
如果独占锁占着资源,共享和其他独占的都需要等待。代码和独占的大同小异,就不贴了。

重入锁

  1. 相同的线程可以重复得到锁。
  2. 获得几次,退出几次,锁才会被最终释放

获取锁过程(code)

下面给ReentrantLock的代码注释下。

    public boolean tryLock() {
    	// 获取锁的请求 +1
        return sync.nonfairTryAcquire(1);
    }
    // 非公平获取锁
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        // 获取现在的状态,重入的个数
        int c = getState();
        // 第一给抢到的锁
        if (c == 0) {
        	// cas设置 state值 = 1
            if (compareAndSetState(0, acquires)) {
            	// 成功,设置锁的主人
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 如果重入个数>=1,且是锁的主人
        else if (current == getExclusiveOwnerThread()) {
        	// 设置 重入个数++
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            // 设置state,返回    
            setState(nextc);
            return true;
        }
        return false;
    }

以上是请求锁的过程,包括了重入的情况。

    protected final boolean tryRelease(int releases) {
    	// 得到状态--
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        // 如果重入的个数是0了,那就是没有人占着锁了
        if (c == 0) {
            free = true;
            // 锁主人为 null
            setExclusiveOwnerThread(null);
        }
        // 设置state
        setState(c);
        return free;
    }

上面是释放锁的时候,会对重入数做减法。

公平和非公平的区别

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
   		// 和非公平有出入的地方就是这里。
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

他会去判断队列同步器是否存在节点,如果有存在同步队列则会从同步队列的头开始取。
开始可能会疑惑,非公平的好像也是从同步队列头开始取,然后头结束了再去取下一个节点。

在竞争情况下,所有线程可能同时在竞争锁,失败的会进入队列。然后在下一刻,又有许多线程开始竞争,这是可能是外部的线程和队列中的线程同时在抢,如果外部的抢锁成功,那么同步队列的头会等待下次的竞争。所以不公平就不公平在同步队列会跟外部的其他线程竞争,而公平就是有竞争就进队列,一定让头开始先获得锁

读写锁(ReentrantReadWriteLock)

特点:读锁可以被共享,但是写锁只能是一个线程在写。
ReentrantReadWriteLock 对 读和写读状态 是另外设计的。
int 32位
高16位给 读锁使用,低16位给 写锁使用。
取读锁的state的时候,state>>>16 无符号右移16位
取写锁的state的时候,state & 0x0000FFFF 与操作

final boolean tryReadLock() {
    Thread current = Thread.currentThread();
    for (;;) {
    	// 得到状态
        int c = getState();
        // exclusiveCount 是否存在写锁,1则是
        // getExclusiveOwnerThread 独占的线程owner
        if (exclusiveCount(c) != 0 &&
            getExclusiveOwnerThread() != current)
            return false;
        // 得到共享的count
        int r = sharedCount(c);
        if (r == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // cas 添加读锁数
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (r == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
            	// cachedHoldCounter 每个线程可能会有再次重入读的情况
            	// 记录的是 每个线程获取锁的次数
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    cachedHoldCounter = rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
            }
            return true;
        }
    }
}

读请求锁过程,没有写锁,那就是会获取锁成功。

LockSupport工具

public static void main(String[] args) {
    Thread t = new Thread(() -> {
        try {
            System.out.println(System.currentTimeMillis()+ " park start!!!");
            // 挂起线程
            LockSupport.park();
            System.out.println(System.currentTimeMillis() + " end!!!");
        } finally {
        }
    });
    Thread t2 = new Thread(() -> {
        try {
            System.out.println(System.currentTimeMillis()+ " start");
            Thread.sleep(3000);
            // 唤醒指定线程
            LockSupport.unpark(t);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
        }
    });
    t.start();
    t2.start();
}

输出结果:

1547304943153 park start!!!
1547304943153 start
1547304946158 end!!!

Condition接口

之前说着比较 lock 和 synchronized 的有点就是有个是:可以多条件

Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
private void conditionAwait() throws InterruptedException {
    lock.lock();
    try {
        condition.await();
    } finally {
        lock.unlock();
    }
}
private void conditionSignal() {
    lock.lock();
    try {
        condition.signal();
    } finally {
        lock.unlock();
    }
}

这只是单条件的实现方式。
多条件可以参考生产者和消费者来编写。

condition的实现

在这里插入图片描述
单方向链表的等待队列。
如果是object的监视者模型的话,一个对象有一个同步队列和一个等待队列。
但是lock的话是一个同步队列和多个等待队列。
在这里插入图片描述
如果有新的线程需要等待,如果被通知的话就会从等待队列进入同步队列。
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值