同步与异步


异步:

在多个对象对一个数据进行操作时,多个对象的行为都是独立的,它们不会关注该数据是否已经被其他对象操作过,从而导致数据不完整或者不正确的情况
例:多人一起吃蛋糕,只管吃,不管别人会不会咬到自己

同步:

在多个对象对一个数据进行操作时,会一个对象一个对象的进行操作,下一个对象操作的数据一定是上一个对象操作过后的数据,这样可以保证数据的完整性
例:多人排队在饮水机处接热水,后面的人接到的水会越来越冷

异步的例子:

编写买票程序,多个窗口同时卖一班车的票,因为异步,导致了窗口剩余的票数和真正剩余的票数不吻合
由于多根线程同时操作,同一数据,又加上线程之间默认使用的是一种 异步通讯机制,这就导致有可能存在:线程拿到手上的数据,不准确的情况
这就可能危及到线程内部执行的任务,从而体现一种情况:线程不安全

  1. 主线程开启
  2. 窗口1号说:还有200张票!!!
  3. 主线程结束
  4. 窗口3号说:还有199张票!!!
  5. 窗口2号说:还有200张票!!!
  6. 窗口3号说:还有197张票!!!
  7. 窗口1号说:还有196张票!!!
  8. 窗口2号说:还有195张票!!!
  9. 窗口3号说:还有194张票!!!
  10. 窗口2号说:还有193张票!!!
  11. 窗口1号说:还有192张票!!!
  12. 窗口3号说:还有191张票!!!
  13. 窗口2号说:还有190张票!!!
  14. 窗口1号说:还有190张票!!!

同步锁—synchronized关键字


为解决异步给多线程程序带来的数据失真的问题,我们可以采用synchronized关键字实现同步锁的功能,让一个方法或一段代码,每次只能进入一个线程,从而保证了每个线程拿到的数据都是对的

同步块

针对某一块代码,进行同步锁处理

synchronized (取得锁的对象){
//要锁定的代码
}

注意

这里括号中通常可以写:要操作的对象,this,对象.getclass()
常用的方法是:要操作谁,就锁谁,后两个方案是给抢对象提供了一个条件,比如先到跑到终点的人就可以先休息

同步方法

在方法定义时使用synchronized关键字

public synchronized void method(String name){

}

这样方法就被上锁,每次只有一个线程可以进入这个方法,从而实现同步

ReentrantLock 上锁类


一般多线程开发中,如果需要针对相同的数据进行上锁,除了synchronized关键字以外,还有ReentrantLock 类的实例也可以

        /**
     * 定义一把锁
     */
    private ReentrantLock lock = new ReentrantLock();
        // 从这一行开始,下面的代码都被上锁,直到解锁为止
    lock.lock();
    try {
        System.out.println("客户说:当前还有" + goodsBean.getNums() + "件商品!");
        if (goodsBean.getNums() > 0) {
                      goodsBean.setNums(goodsBean.getNums() - 1)
                         System.out.println(Thread.currentThread().getName() + ": 我买了1件!!!");
            } else {
            System.out.println("客户说:商品已售罄,请期待明天上午8:00 抢购活动!");
            }
        }catch (Exception e) {
            e.printStackTrace();
        }finally {
            //解锁代码
            lock.unlock();
        }

synchronized 与 lock 的区别(面试题)


1、synchronized 即可以写同步块,也可以写同步方法,但是lock只能适用用在同步块
2、synchronized 是一种重量级锁,它锁的底层CPU。lock 是相对较轻量级的一种锁,它锁的是对象(它是一种上层锁)
3、synchronized 不需要程序员手动解锁,而lock需要程序员适用unlock()进行手动解锁
4、synchronized用到了悲观锁的概念,lock用到了乐观锁的概念

乐观锁和悲观锁


乐观锁:

每个线程,在操作数据时,都相对乐观的认为,没有其他线程和它在同一时刻,操作同一数据,于是:它加了一个相对比较宽松的锁(锁:任何人都能访问,但是只能由1个人修改,要修改就需要保证自己手上数据是最新的,这是一种公平锁)

简单来说,乐观锁相当于是一种数据更新的验证机制,在线程取得数据并修改之后,需要检验自己的版本是否匹配当前数据的最新版本,如果匹配,则可以更新数据,如果不匹配,需要再次取得最新的数据来进行修改,修改完成后再判断数据是否是最新的版本
这样每个线程都可以访问这个数据,锁起到的只是验证的作用
线程锁-synchronized/lock - 图1

悲观锁:

每个线程,在操作数据时,都悲观的认为,有其他线程和它进行同时操作某一个数据,这时,为了保证线程的安全性,于是他就针对对象结合CPU针对底层进行上锁,而且它上的是一个 非公平锁(排他锁)

悲观锁是底层锁,一旦加上悲观锁,数据每一次只能让一个线程来访问,只要这个线程不放手,其他的线程也进不去,因此效率比较低下
线程锁-synchronized/lock - 图2

两种锁的应用场景


synchronized适合访问人数人数和修改人数都比较少,或者安全性要求高的开发场景
lock适用于访问人数多,修改人数少的场景