Java 线程同步器:从 synchronized、Lock 到 StampedLock

Java 线程同步器:从 synchronized、Lock 到 StampedLock

 

 

synchronized 和 lock 的对比

synchronized 关键字和 juc 中的 Lock 可以说是 Java 最为常用的线程同步器了,以下我们从多个角度来比较这两者的区别;

首先我们先看一个实际例子中的两者代码的区别:

一个线程共享对象 TimerBean

public class TimerBean {
    private int num = 10;

    //读资源
    public int getNum() {
        //模拟读取等待
        try {
            Thread.sleep(new Random().nextInt(1000) + 800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return num;
    }
    //写资源
    public void setNum(int num) {
        //模拟写入等待
        try {
            Thread.sleep(new Random().nextInt(1000) + 800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.num = num;
    }
}

synchronized 测试类 SynchronziedTest

public class SynchronziedTest {
    final Object monitor = new Object(); //监视器对象
    
    public SynchronziedTest() throws InterruptedException {
        TimerBean bean = new TimerBean();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 1; i <= 10; i++){
            exec.execute(new Task(bean,countDownLatch));
        }
        countDownLatch.await();
        System.out.println(bean.getNum());
        exec.shutdown();
    }
    public static void main(String[] args) throws InterruptedException {
        new SynchronziedTest();
    }

    class Task extends Thread{
        private TimerBean bean;
        private CountDownLatch countDownLatch;

        public Task(TimerBean bean, CountDownLatch countDownLatch) {
            this.bean = bean;
            this.countDownLatch = countDownLatch;
        }
        //通过 synzhronized 对监视器对象加锁
        public void run() {
            synchronized (monitor){
                int initNum = bean.getNum();
                bean.setNum(initNum - 1);
            }
            countDownLatch.countDown();
        }
    }

Lock 测试类 LockTest

public class LockTest {
    ReentrantLock lock = new ReentrantLock();
    
    public LockTest() throws InterruptedException {
        TimerBean bean = new TimerBean();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        for(int i = 1; i <= 10; i++){
            exec.execute(new Task(bean,countDownLatch));
        }
        countDownLatch.await();
        System.out.println(bean.getNum());
        exec.shutdown();
    }
    public static void main(String[] args) throws InterruptedException {
        new LockTest();
    }
    
    class Task extends Thread{
        private TimerBean bean;
        private CountDownLatch countDownLatch;
        public Task(TimerBean bean, CountDownLatch countDownLatch) {
            this.bean = bean;
            this.countDownLatch = countDownLatch;
        }
         //通过 Lock 加线程锁
        public void run() {
            lock.lock();
            int initNum = bean.getNum();
            bean.setNum(initNum - 1);
            lock.unlock();
            countDownLatch.countDown();
        }
    }
}

 

1)存在层面

- synchronized 是 Java 内置的关键字,存在于 JVM 层面上;

- Lock 是 Java1.5  引入的 juc 包中的基础同步器,具体的实现类为 ReentrantLock;

2)锁的获取方式

- synchronized 只能阻塞式获取锁,当线程A获取锁,B线程只能等待,假如此时线程A阻塞,B线程会被一直阻塞;

- Lock 有多种获取锁的方式:

       > Lock # lock 阻塞式锁获取,同 synchronized

       > Lock # tryLock 尝试式锁获取,当当前线程获取不到锁,会立即返回,可以设置一个尝试持续时间;

       >Lock # lock.lockInterruptibly 可中断式锁获取,当当前线程获取到锁后,可以被中断;

3)锁状态是否可判断

- synchronized 无法判断锁的状态;

- Lock 提供了当前线程对于所获取状态的API;

4)锁状态的控制

- synchronzied 通过锁对于线程的控制,是通过对加锁的监视器对象的行为来控制的;

- Lock 通过锁对线程行为的控制,是通过对 Lock 的 Condition 对象来控制的;

可以比较以下2个仿真延时多线程输入开关的例子:

//使用 synchronized 监视器控制线程行为
public class SyncLight {
    private boolean theSwitch = false;
    public void turnOn() throws InterruptedException{
        synchronized(this){
            while(theSwitch){
                this.wait();
            }
            theSwitch = true;
            System.out.println("SyncLight turn on ");
            this.notifyAll();
        }
    }
    public void turnOff() throws InterruptedException{
        synchronized(this){
            while(!theSwitch){
                this.wait();
            }
            theSwitch = false;
            System.out.println("SyncLight turn off ");
            this.notify();
        }
    }
}

//使用 Lock Condition 控制线程行为
public class LockLight {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();
    private boolean theSwitch = false;
    public void turnOn() throws InterruptedException {
        lock.lock();
        while(theSwitch){
            condition.await();
        }
        TimeUnit.SECONDS.sleep(2);
        theSwitch = true;
        condition.signalAll();
        System.out.println("LockLight turn on");
        lock.unlock();

    }
    public void turnOff() throws InterruptedException {
        lock.lock();
        while(!theSwitch){
            condition.await();
        }
        theSwitch = false;
        condition.signalAll();
        System.out.println("LockLight turn off");
        lock.unlock();

    }
}

 

 

从锁的类型对比 synchronized 和 lock 

关于锁的类型参考:https://blog.csdn.net/a314773862/article/details/54095819

1)可重入锁

 synchronized 和 Lock 都是可重入锁,可重入锁的分配是基于线程的分配,如下示例,当一个线程执行到 method1,method1 调用 method2 ,此时线程不需要重新申请监视器锁,可以直接执行方法method2;

class Demo {
    public synchronized void method1() {
        method2();
    }  
    public synchronized void method2() {    
    }
}

2)可中断锁

synchronized 是不可中断锁,Lock 是可中断锁(通过lockInterruptibly获取可中断锁);

如果使用 synshronized 监视器锁,当A获取改监视器锁,并执行锁中的代码,而线程B在等待获取该监视器锁,在这种情况下,是无法让B线程中断自己或者在别的线程中中断B线程;而使用 Lock # lockInterruptibly 获取到的可中断锁可以实现这一点;

3)公平锁

synchronized 是非公平锁,当多个线程在等待同一个锁,无法保证线程获取锁的顺序是按照请求锁的顺序来的;

Lock 的实现类 ReentrantLock 定义了2个静态内部类 NotFairSync,FairSync,分别用于实现非公平锁、公平锁,在默认情况下也是非公平锁,可以直接通过构建函数的参数来设置具体使用为  NotFairSync 还是 FairSync;

/**
 * Creates an instance of {@code ReentrantLock}.
 * This is equivalent to using {@code ReentrantLock(false)}.
 */
public ReentrantLock() {
    sync = new NonfairSync();
}
/**
 * Creates an instance of {@code ReentrantLock} with the
 * given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

4)读写锁

Lock 的具有一个读写锁实现 ReentrantReadWriteLock,获取读锁或写锁,读锁和读锁之间共享,读锁和写锁之间互斥,写锁和写锁之间互斥,通过读写锁的分离,可以很大程度地减少线程等待锁的阻塞时间,同时保证悲观读取;

以下一个简单的例子演示读写锁,多个线程获取读锁的时候,实际上每个线程在自己的工作内存中,对于共享资源都保存了自己的一个工作副本,此时表现在锁上,是线程共享读锁的状态,当其中一个线程获取写锁后,读锁对所有其他线程互斥,等待获取读锁的线程都在等待写锁线程释放写锁;

一个线程共享对象 TimerBean

//示例Bean
public class TimerBean {
    private int num = 10;
    //读资源
    public int getNum() {
        //模拟读取等待
        try {
            Thread.sleep(new Random().nextInt(1000) + 800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return num;
    }
    //写资源
    public void setNum(int num) {
        //莫比写入等待
        try {
            Thread.sleep(new Random().nextInt(1000) + 800);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.num = num;
    }

}

测试 ReadWriteLockTest

public class ReadWriteLockTest {
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public ReadWriteLockTest() throws InterruptedException {
        TimerBean bean = new TimerBean();
        CountDownLatch countDownLatch = new CountDownLatch(10);
        ExecutorService exec = Executors.newCachedThreadPool();
        //执行4个获取读锁线程
        for(int i = 1; i <= 4; i++){
            exec.execute(new ReadTask(bean,countDownLatch));
        }
        //执行1个获取写锁线程
        exec.execute(new WriteTask(bean,countDownLatch));
        //执行5个获取读锁线程
        for(int i = 6; i <= 10; i++){
            exec.execute(new ReadTask(bean,countDownLatch));
        }
        countDownLatch.await();
        exec.shutdown();
    }
    public static void main(String[] args) throws InterruptedException {
        new ReadWriteLockTest();
    }


    //读线程
    class ReadTask extends Thread{
        private TimerBean bean;
        private CountDownLatch countDownLatch;
        public ReadTask(TimerBean bean, CountDownLatch countDownLatch) {
            this.bean = bean;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            lock.readLock().lock();
            int num = bean.getNum();
            System.out.println(String.format("[%s] > %d", Thread.currentThread().getName(), num));
            lock.readLock().unlock();
            countDownLatch.countDown();
        }
}

    //写线程
    class WriteTask extends Thread{
        private TimerBean bean;
        private CountDownLatch countDownLatch;

        public WriteTask(TimerBean bean, CountDownLatch countDownLatch) {
            this.bean = bean;
            this.countDownLatch = countDownLatch;
        }
        @Override
        public void run() {
            lock.writeLock().lock();
            System.out.println("Write Thread in comming! > 5");
            bean.setNum(5);
            lock.writeLock().unlock();
            countDownLatch.countDown();
        }
    }
}

输出结果:

[pool-1-thread-1] > 10      //读锁线程可以共享读锁
[pool-1-thread-7] > 10
[pool-1-thread-6] > 10
[pool-1-thread-2] > 10
[pool-1-thread-3] > 10
[pool-1-thread-4] > 10
Write Thread in comming! > 5   //在写锁释放前,所有等待获取读锁线程都被阻塞
[pool-1-thread-8] > 5
[pool-1-thread-9] > 5
[pool-1-thread-10] > 5

 

 

JDK1.6 以来对 synchronized 的优化

从线程竞争的效率来讲,synchronzied 竞争效率是不如 Lock 的,自从 JDK1.6 以来,JDK 对于 synchronized 提供了多种优化的方案;

1)优化部分阻塞锁为自旋锁

自旋锁本质上就是执行几个空方法,稍微等一等,事项上可能是一段时间的循环、或者几行空的字节码指令;

自旋锁不能代替阻塞锁,自旋锁本身虽然避免了线程切换的开销,但是要占用CPU时间,如果锁被占用的时间很短,自旋等待的效果就就很显著,但是锁被占用的时候很长,反而会带来CPU资源上的浪费;

JDK 1.6 默认开启,可以使用参数-XX:+UseSpinning -XX:PreBlockSpin 控制;

JDK 1.7 之后JVM不允许用户配置自旋锁,自旋锁会在合适的时机执行,自旋锁次数由 JVM 决定;

2)尽可能进行锁消除

JIT 在运行时,通过逃逸分析,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,如果在以下代码中,如果 JIT 观察变量 sb,发现 sb 变量的引用永远不会逃逸到 main 函数之外,其他线程无法访问该变量,那么在即时编译后,该段代码的同步会被忽略掉而直接执行了;

public static void main() {
    StringBuffer sb = new StringBuffer();
    sb.append("a").append("b").append("c");
    System.out.println(sb.toString());
}

3)进行锁粗化

通过锁粗化,使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁,如以下的例子中 StringBuffer 本来每一个 append 都会进行同步操作,但是 JVM 会优化为只在第1个和最后1个 append 操作进行同步; 

StringBuffer sb = new StringBuffer();
sb.append("a").append("b").append("c");

4)提供轻量级锁

轻量级锁能提升程序同步性能的依据是 “对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢;

5)提供偏向锁

引入偏向锁的目的是为了消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能,如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了;

实质就是设置一个变量,判断这个变量是否是当前线程,是就避免再次加锁解锁操作,从而避免了多次的CAS操作;

 

详细可参考:http://www.cnblogs.com/longshiyVip/p/5213771.html

 

 

JDK1.8 引入的新的读写锁 StampedLock 

JDK 1.8 引入了新的读写锁 StampedLock,具有在高并发下更强大的吞吐量;

synchronzied 和 ReentrantLock 都是悲观锁,在 ReentrantLock 的读写锁版本 ReentrantReadWriteLock 中 ,只有在沒有任何读写锁时,才可以取得写入锁,这个特性一般用于实现悲观读取,即如果执行中进行读取时,经常可能有另一执行要写入的需求,在读写频繁的情况下可以保证线程同步;

但是在读多写少的情况下,使用 ReentrantReadWriteLock 经常使写入线程迟迟无法竞争到锁定,而一直处于等待状态;

而 StampedLock 可以很大地改善这个问题;

StampedLock控制锁有三种模式:读锁、写锁、乐观读锁,一个StampedLock状态是由版本和模式两个部分组成,锁获取方法返回一个数字作为 stamp,它用相应的锁状态表示并控制访问,数字0表示没有写锁被授权访问,通过实现乐观读锁可以极大提升程序地吞吐量;

我们来看一下 JDK8 API StampedLock 的例子:

 class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();

   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }

     //乐观读锁示例
   double distanceFromOrigin() { // A read-only method
       //获取乐观读锁
     long stamp = sl.tryOptimisticRead();
     double currentX = x, currentY = y;
      //操作后验证是否有写锁发生
     if (!sl.validate(stamp)) {
         //当发生了写锁,尝试获取悲观读锁,并同步数据
        stamp = sl.readLock();
        try {
          currentX = x;
          currentY = y;
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }

     //悲观读锁示例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) {
         long ws = sl.tryConvertToWriteLock(stamp);
         if (ws != 0L) {
           stamp = ws;
           x = newX;
           y = newY;
           break;
         }
         else {
           sl.unlockRead(stamp);
           stamp = sl.writeLock();
         }
       }
     } finally {
       sl.unlock(stamp);
     }
   }
 }

 

 

推荐阅读

The j.u.c Synchronizer Framework翻译(一)背景与需求 https://yq.aliyun.com/articles/26446

The j.u.c Synchronizer Framework翻译(二)设计与实现 https://yq.aliyun.com/articles/26430

The j.u.c Synchronizer Framework翻译(三)使用、性能与总结  https://yq.aliyun.com/articles/88422

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值