一、synchronized与Lock

1.1 区别

image.png

1.2 lock对象方法

lock/unlock
public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
        lock.lock();
        try {
            System.out.println("线程名"+thread.getName() + "获得了锁");
        }catch(Exception e){
            e.printStackTrace();
        } finally {
            System.out.println("线程名"+thread.getName() + "释放了锁");
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        final LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

/*
线程名t1获得了锁
线程名t1释放了锁
线程名t2获得了锁
线程名t2释放了锁

*/
tryLock
public class LockTest {
    private Lock lock = new ReentrantLock();

    //需要参与同步的方法
    private void method(Thread thread){
        if(lock.tryLock()){
            try {
                System.out.println("线程名"+thread.getName() + "获得了锁");
            }catch(Exception e){
                e.printStackTrace();
            } finally {
                System.out.println("线程名"+thread.getName() + "释放了锁");
                lock.unlock();
            }
        }else{
            System.out.println("我是"+Thread.currentThread().getName()+"有人占着锁,我就不要啦");
        }
    }

    public static void main(String[] args) {
        final LockTest lockTest = new LockTest();

        //线程1
        Thread t1 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {

            @Override
            public void run() {
                lockTest.method(Thread.currentThread());
            }
        }, "t2");

        t1.start();
        t2.start();
    }
}

1.3 公平锁

按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利。
ReentrantLock


	public ReentrantLock() {
        sync = new NonfairSync();
    }

	public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
	
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }

   
    static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
        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;
        }
    }

从以上源码可以看出在Lock中可以自己控制锁是否公平,而且,默认的是非公平锁

1.4 锁的底层实现

synchronized

synchronized映射成字节码指令就是增加来两个指令:monitorenter和monitorexit。
image.png
image.png

  1. 当一条线程进行执行的遇到monitorenter指令的时候,它会去尝试获得锁,如果获得锁那么锁计数+1(为什么会加一呢,因为它是一个可重入锁,所以需要用这个锁计数判断锁的情况),如果没有获得锁,那么阻塞。
  2. 当它遇到monitorexit的时候,锁计数器-1,当计数器为0,那么就释放锁。
  3. synchronized锁释放有两种机制,一种就是执行完释放;另外一种就是发送异常,虚拟机释放。图中第二个monitorexit就是发生异常时执行的流程,而且第13行有一个goto指令,也就是说如果正常运行结束会跳转到19行执行。
Lock
  1. synchronized的实现是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。
  2. Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。底层主要靠volatile和CAS操作实现的。

1.5 synchronized的优化

自旋锁
  1. java线程其实是映射在内核之上的,线程的挂起和恢复会极大的影响开销。
  2. 很多线程在等待很短的一段时间就获得了锁,所以并不需要把线程挂起,而是让他无目的的循环一定次数(10次)。
  3. 适应性自旋可以根据它前面线程的自旋情况,从而调整它的自旋,甚至是不经过自旋而直接挂起。
  4. 自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。
轻量级锁
  1. 轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。
  2. 使用轻量级锁时,不需要申请互斥量,仅仅将对象头的Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。
  3. 如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁,那么维持轻量级锁的过程就成了浪费。
  4. 如果不仅仅没有实际竞争,自始至终,使用锁的线程都只有一个,那么,维护轻量级锁都是浪费的。
  5. 由于轻量级锁天然瞄准不存在锁竞争的场景,如果存在锁竞争但不激烈,仍然可以用自旋锁优化,自旋失败后再膨胀为重量级锁。
偏向锁
  1. 偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。
  2. “偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),因此,只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁;否则,说明有其他线程竞争,膨胀为轻量级锁。
  3. 偏向锁无法使用自旋锁优化,因为一旦有其他线程申请锁,就破坏了偏向锁的假定。
  4. 如果明显存在其他线程申请锁,那么偏向锁将很快膨胀为轻量级锁
锁消除

锁消除是把不必要的同步在编译阶段进行移除。
锁消除是由于逃逸分析带来的优化,它消除了多余的同步。当内部同步代码没有逃逸到外部时,runtime就可以完全消除同步了。

锁粗化

在遇到一连串地对同一锁不断进行请求和释放的操作时,把所有的锁操作整合成锁的一次请求,从而减少对锁的请求同步次数,这个操作叫做锁的粗化。

image.png

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议