【Java】synchronized和ReentrantLock分析

synchronized和ReentrantLock分析

参考:
子路老师博客:https://blog.csdn.net/java_lyvee/article/details/110996764
并发编程网:http://ifeve.com/monitors-java-synchronization-mechanism/

代码需求:

有一个猫窝
有猫长老、猫爸、 猫妈妈,还有6只小猫
猫爸爸需要进猫窝拿钱才捕猎,不然就睡觉
猫妈妈需要进猫窝拿扫把才做家务,不然也是睡觉
6只小猫就也想等着进猫窝玩
但是猫窝的进出权限是猫长老控制的

思路:

所以需要有4类线程,分别竞争一个猫窝,用syhchronized来实现。

代码:

⚠️注意:wait条件的时候要用while去判断,不能用if。

@Slf4j(topic = "console")
public class LockTest {
    static boolean isMoney = false;
    static boolean isClean = false;
    public static void main(String[] args) throws InterruptedException {
        // 需要while(条件) + wait()
        Object home = new Object();
        new Thread(() -> {
            synchronized (home) {
                while (!isMoney) {
                    log.debug("sleeping");
                    try {
                        home.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("hunting");
            }
        }, "catDad").start();
        new Thread(() -> {
            synchronized (home) {
                while (!isClean) {
                    log.debug("sleeping");
                    try {
                        home.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("cleaning");
            }
        }, "catMom").start();
      //为了能让dad和mon先调度
        TimeUnit.MILLISECONDS.sleep(10);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                synchronized (home) {
                    log.debug("playing!!!");
                }
            }, "littleKitty"+ i).start();
        }
        new Thread(() -> {
            synchronized (home){
                isMoney = true;
                isClean = true;
                /*
                notifyAll只能唤醒所有线程,可能会导致唤醒不应该的线程。
                ReentrantLock 的 Condition可以解决这个问题。
                 */
                home.notifyAll();
            }
        },"catElder").start();
    }
}

缺点:

使用synchronized的线程唤醒notify只能叫醒一个线程,而notifyAll能叫醒该锁的所有线程,假如这时有3个条件的,但我们只要叫醒2个,就会有一个虚假唤醒了。java的AQS提供了ReentrantLockCondition,能很好解决。

ReentrantLock、Condition的await和signal

思路

Q:为啥使用ReentrantLockCondition呢?

A:synchronized的唤醒和唤醒条件的问题,能通过为一个ReentrantLock对象创建多个Condition对象,而各个Condition对象可以分开唤醒各自的waiting线程;

ReentrantLock用法

ReentrantLock可以替换synhcronized关键字,一个是jvm提供的用C++实现的,一个java的实现的。

//创建重入锁对象
ReentrantLock lock = new ReentrantLock();
//创建Condition对象
Condition moneyCondition = lock.newCondition();
 new Thread(() -> {
   //上锁的方式。通常要和try搭配,然后把解锁写在finally里
     lock.lock();
     try {
         while (!isMoney) {
             log.debug("sleeping");
           //各自的条件对象调用await方法
             moneyCondition.await();
         }
     } catch (Exception e) {
         e.printStackTrace();
     } finally {
       //解锁通常写在finnaly,不然抛异常后锁会不释放。
       //synchronized是由jvm管理的,只要抛异常了,锁就自动释放了。
         lock.unlock();
     }
     log.debug("hunting");
 }, "catDad").start();

代码:

@Slf4j(topic = "console")
public class ReentrantLockTest {
    static boolean isMoney = false;
    static boolean isClean = false;
    public static void main(String[] args) throws InterruptedException {
        //创建一个重入锁
        ReentrantLock lock = new ReentrantLock();
      //为重入锁创建2个条件对象
        Condition moneyCondition = lock.newCondition();
        Condition cleanCondition = lock.newCondition();
        new Thread(() -> {
            lock.lock();
            try {
                while (!isMoney) {
                    log.debug("sleeping");
                  //各自的条件对象调用await方法
                    moneyCondition.await();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            log.debug("hunting");
        }, "catDad").start();

        new Thread(() -> {
            lock.lock();
            try {
                while (!isClean) {
                    log.debug("sleeping");
                    cleanCondition.await();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            log.debug("cleaning");
        }, "catMom").start();
        TimeUnit.MILLISECONDS.sleep(10);
        for (int i = 0; i < 6; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    log.debug("playing!!!");
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }

            }, "littleKitty" + i).start();
        }
        new Thread(() -> {
            lock.lock();
            try {
              //可以为各自的的条件进行唤醒,
              //signal对应notify唤醒一个线程
 						  //signal对应notifyAll唤醒所有线程
                isMoney = true;
                moneyCondition.signalAll();
                isClean = true;
                cleanCondition.signalAll();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "catElder").start();
    }
}

synchronized分析

线程的6个状态

参考这2篇,国内那篇透彻讲解最好别看了

https://blog.csdn.net/Baisitao_/article/details/99766322

https://www.cnblogs.com/aspirant/p/8900276.html

然后我总结了一个图:

总体来说在java并发编程中wait和time-wait有2种,

  1. 一种是在进行了锁操作(synchroized):

    它从waiting/timed-waiting状态不会直接到runnable。会先进行blocked等待获取锁。

  2. 另外一种是没有进行锁操作的:

    它从waiting/timed-waiting状态直接到runnable。不用等待获取锁。

image-20210529215808159

当线程竞争不到锁时阻塞时

通过上面介绍,这是线程是进入blocked阻塞状态。当线程进入blocked状态,会有什么操作呢?

对于synchronized,是jvm来管理的,在jvm中有一个EntryList(双向链表)管理了这些获取不到锁的线程,我们在从jvm的监视器(monitor)相关知识了解到了。[java监视器概念]

进入EntryList的线程的顺序和获得锁启动的顺序是相反的即:先入后出

image-20210530145537271

可以通过下面代码验证。

代码:

public static void main(String[] args) throws InterruptedException {
    Object lock = new Object();
    ArrayList<Thread> threads = new ArrayList<>();
  //用一个list装5个线程,并都进行锁竞争
    for (int i = 0; i < 5; i++) {
        threads.add(
                new Thread(() -> {
                    synchronized (lock) {
                        log.debug("{} running", Thread.currentThread().getName());
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }, "t" + (i + 1))
        );
    }
//mian线程上锁时启动依次启动调度5个线程
    synchronized (lock) {
        log.debug("线程启动顺序: ");
        for (Thread thread : threads) {
            log.debug("{}", thread.getName());
            thread.start();
            //为了能按顺序被调度,因为拿不到锁,所以进入EntryList的顺序是t1 - t5
            TimeUnit.MILLISECONDS.sleep(100);
        }
        log.debug("拿到锁的顺序:");
    }
}

输出:

14:59:28.372 [main] DEBUG console - 线程启动顺序: 
14:59:28.374 [main] DEBUG console - t1
14:59:28.477 [main] DEBUG console - t2
14:59:28.583 [main] DEBUG console - t3
14:59:28.688 [main] DEBUG console - t4
14:59:28.793 [main] DEBUG console - t5
14:59:28.897 [main] DEBUG console - 拿到锁的顺序:
14:59:28.898 [t5] DEBUG console - t5 running
14:59:28.898 [t4] DEBUG console - t4 running
14:59:28.899 [t3] DEBUG console - t3 running
14:59:28.899 [t2] DEBUG console - t2 running
14:59:28.899 [t1] DEBUG console - t1 running

结果入我们说的那样,先阻塞的线程最后才获得锁。

当线程调用wait时

线程调用wait方法进入waiting状态,并释放了锁。再当调用了notify方法来唤醒时,会发生什么?

对于synchronized,线程会把waiting状态的线程放进WaitSet中;

当线程被唤醒时,线程这是肯定获取不到锁,会从WaitSet调度进到EntryList中等待获得锁。

image-20210530153440076

代码:

@Slf4j(topic = "console")
public class WaitAnalyze {
    static boolean isMoney = false;
    public static void main(String[] args) throws InterruptedException {
        Object home = new Object();
        new Thread(() -> {
            synchronized (home) {
                while (!isMoney) {
                    log.debug("sleeping");
                    try {
                        home.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("hunting");
            }
        }, "CatDad").start();
        //为了dad先调度
        TimeUnit.MILLISECONDS.sleep(10);
        synchronized (home) {
          //直接唤醒dad再去启动kitty们
            isMoney = true;
            home.notifyAll();
            for (int i = 0; i < 6; i++) {
                log.debug("kitty的启动顺序:{}",i+1);
                new Thread(() -> {
                    synchronized (home) {
                        log.debug("playing!!!");
                    }
                }, "Kitty" + (i + 1)).start();
                //为了kitty的调度顺序和启动顺序一致
                TimeUnit.MILLISECONDS.sleep(100);
            }
            /*
            把dad唤醒。
            dad的状态:
            waiting -> blocking,
            从WaitSet转移到EntryList
             */
        }
    }

输出:

15:01:09.187 [CatDad] DEBUG console - sleeping
15:01:09.199 [main] DEBUG console - kitty的启动顺序:1
15:01:09.305 [main] DEBUG console - kitty的启动顺序:2
15:01:09.410 [main] DEBUG console - kitty的启动顺序:3
15:01:09.516 [main] DEBUG console - kitty的启动顺序:4
15:01:09.621 [main] DEBUG console - kitty的启动顺序:5
15:01:09.726 [main] DEBUG console - kitty的启动顺序:6
15:01:09.830 [Kitty6] DEBUG console - playing!!!
15:01:09.830 [Kitty5] DEBUG console - playing!!!
15:01:09.830 [Kitty4] DEBUG console - playing!!!
15:01:09.830 [Kitty3] DEBUG console - playing!!!
15:01:09.830 [Kitty2] DEBUG console - playing!!!
15:01:09.830 [Kitty1] DEBUG console - playing!!!
15:01:09.831 [CatDad] DEBUG console - hunting

虽然CatDad是在Kitty们在启动调度之前被唤醒,但是CatDad却是最后一个获得锁的。

这就证明了CatDad不是立即唤醒并先获得锁的,而是需要进入EntryList等待获得锁,然后因为EntryList是FILO(先入后出)的,

CatDad就是最后获得锁的。

ReentrantLock分析

ReentrantLock是java中的API,JUC包里的。锁的使用和jvm提供synchronized相似。所以可以通过学习其源码去学习java中锁机制的原理。

这里我们主要阐述的ReentrantLock和synchronized区别是在线程获取不到锁,进入blocked状态,然后到获得锁这个线程的启动顺序的差别。

上面我们知道阻塞状态的线程在synchronized中是把放入过ObjectMonitor的EntryList里,然后顺序是FILO的。

但是ReentrantLock的都是的阻塞队列是由基于AQS实现公平锁(FairSync)和非公平锁(NoFairSync)的所维护的,他的顺序是FIFO的,AQS的release方法可以看到:

把线程节点取出队列:

    @ReservedStackAccess
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
          //先取的是头部节点
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

当线程竞争不到锁时阻塞时

代码:

 */
@Slf4j(topic = "console")
@SuppressWarnings({"all"})
public class SyncAnalyze {
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition = reentrantLock.newCondition();
        threads = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            threads.add(
                    new Thread(() -> {
                        reentrantLock.lock();
                        try {
                            log.debug("{} running", Thread.currentThread().getName());
                            TimeUnit.MILLISECONDS.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } finally {
                            reentrantLock.unlock();
                        }
                    }, "t" + (i + 1))
            );
        }
        TimeUnit.MILLISECONDS.sleep(100);
        {
            reentrantLock.lock();
            log.debug("线程启动顺序: ");
            for (Thread thread : threads) {
                log.debug("{}", thread.getName());
                thread.start();
                //为了能按顺序被调度,因为拿不到锁,所以进入EntryList的顺序是t1 - t5
                TimeUnit.MILLISECONDS.sleep(100);
            }
            log.debug("拿到锁的顺序:");
            reentrantLock.unlock();
        }
    }
}

输出:

18:05:16.279 [main] DEBUG console - 线程启动顺序: 
18:05:16.279 [main] DEBUG console - t1
18:05:16.382 [main] DEBUG console - t2
18:05:16.484 [main] DEBUG console - t3
18:05:16.589 [main] DEBUG console - t4
18:05:16.690 [main] DEBUG console - t5
18:05:16.795 [main] DEBUG console - 拿到锁的顺序:
18:05:16.796 [t1] DEBUG console - t1 running
18:05:16.808 [t2] DEBUG console - t2 running
18:05:16.821 [t3] DEBUG console - t3 running
18:05:16.834 [t4] DEBUG console - t4 running
18:05:16.846 [t5] DEBUG console - t5 running

可以看到是FIFO的。

当线程调用await时

ReentrantLock当中Condition可以实现和synchronized的wait一样的功能,并且可以支持多条件。

当线程调用await方法,对应synchronized的WaitSet,在这里是ConditionObjet的AQS队列,而ConditionObject是AQS的内部类。

对应源码:

/**
 * Implements interruptible condition wait.
 * <ol>
 * <li> If current thread is interrupted, throw InterruptedException.
 * <li> Save lock state returned by {@link #getState}.
 * <li> Invoke {@link #release} with saved state as argument,
 *      throwing IllegalMonitorStateException if it fails.
 * <li> Block until signalled or interrupted.
 * <li> Reacquire by invoking specialized version of
 *      {@link #acquire} with saved state as argument.
 * <li> If interrupted while blocked in step 4, throw InterruptedException.
 * </ol>
 */
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

代码:

@Slf4j(topic = "console")
@SuppressWarnings({"all"})
public class WaitAnalyze {
    static boolean isMoney = false;
    public static void main(String[] args) throws InterruptedException {
        ReentrantLock rHome = new ReentrantLock();
        Condition condition = rHome.newCondition();
        log.debug("使用ReentrantLock方式");
        new Thread(() -> {
            rHome.lock();
            while (!isMoney) {
                log.debug("sleeping");
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            rHome.unlock();
            log.debug("hunting");
        }, "CatDad").start();

        //为了dad先调度
        TimeUnit.MILLISECONDS.sleep(10);
        {
            rHome.lock();
            isMoney = true;
            condition.signalAll();
            for (int i = 0; i < 6; i++) {
                log.debug("kitty的启动顺序:{}", i + 1);
                new Thread(() -> {
                    rHome.lock();
                        log.debug("playing!!!");
                    rHome.unlock();
                }, "Kitty" + (i + 1)).start();
                //为了kitty的调度顺序和启动顺序一致
                TimeUnit.MILLISECONDS.sleep(100);
            }
            rHome.unlock();
        }
    }

}

输出:

19:20:03.275 [main] DEBUG console - 使用ReentrantLock方式
19:20:03.277 [CatDad] DEBUG console - sleeping
19:20:03.289 [main] DEBUG console - kitty的启动顺序:1
19:20:03.393 [main] DEBUG console - kitty的启动顺序:2
19:20:03.498 [main] DEBUG console - kitty的启动顺序:3
19:20:03.604 [main] DEBUG console - kitty的启动顺序:4
19:20:03.708 [main] DEBUG console - kitty的启动顺序:5
19:20:03.813 [main] DEBUG console - kitty的启动顺序:6
19:20:03.918 [CatDad] DEBUG console - hunting
19:20:03.918 [Kitty1] DEBUG console - playing!!!
19:20:03.919 [Kitty2] DEBUG console - playing!!!
19:20:03.919 [Kitty3] DEBUG console - playing!!!
19:20:03.919 [Kitty4] DEBUG console - playing!!!
19:20:03.919 [Kitty5] DEBUG console - playing!!!
19:20:03.919 [Kitty6] DEBUG console - playing!!!

和synchronized结果方式类似,只是因为阻塞队列的顺序原因,获得锁的顺序是FIFO的。

也可以证明在锁同步中,线程从wait到runnable状态会会先进入blocked状态等待获取锁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java中的`synchronized`和`ReentrantLock`都是用来实现线程同步的机制,它们的作用是让多个线程按照一定的顺序执行,避免出现竞态条件(race condition)。 下面是`synchronized`和`ReentrantLock`的区别: 1. 语法:`synchronized`是Java语言内置的关键字,`ReentrantLock`是Java提供的一个类。 2. 锁的获取方式:`synchronized`是隐式获取锁,当线程进入同步块或同步方法时会自动获取锁并在执行完毕后释放锁;`ReentrantLock`则需要显式获取和释放锁,即在代码中通过调用`lock()`方法获取锁,在执行完毕后再通过调用`unlock()`方法释放锁。 3. 锁的可重入性:`synchronized`是可重入的,即同一个线程在已经获取到锁的情况下,可以重复获取该锁而不会发生死锁;`ReentrantLock`也是可重入的,但需要注意的是,每次获取锁都会增加锁的计数器,需要在释放锁时把计数器减一,否则将导致其他线程无法获取到锁。 4. 锁的可中断性:`synchronized`是不可中断的,即当一个线程获取到锁之后,其他线程只能等待锁被释放;`ReentrantLock`可以通过调用`lockInterruptibly()`方法实现可中断锁,即当其他线程在等待锁的时候,可以通过调用该方法中断当前线程的等待。 5. 条件变量:`ReentrantLock`提供了`Condition`接口来实现线程之间的协调与通信,而`synchronized`则没有。 总的来说,`ReentrantLock`比`synchronized`更加灵活和可控,但需要注意的是,使用`ReentrantLock`时需要手动管理锁的获取和释放,否则容易导致死锁;而`synchronized`则可以简化代码,但对于一些复杂的同步场景可能会有限制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值