靓仔,你会synchronized吗?

悲观锁 & 乐观锁

在介绍synchronized之前,需要知道悲观锁&乐观锁
悲观锁与乐观锁是一种广义上的概念,体现了看待线程同步的不同角度。对于同一个数据的并发操作,悲观锁认为自己在使用数据的时候一定会有其他的线程来修改数据,因此在获取数据的时候会先加锁,以此确保数据不会被其他线程修改。JAVA中,synchronized关键字和Lock实现类都是悲观锁。

悲观锁分析图:

靓仔,你会synchronized吗?_第1张图片
乐观锁恰恰与之相反,乐观锁认为自己在使用数据时不会有其他线程修改数据,所以不会加锁,只是在更新数据的时候去判断之前有没有其他线程更新了这个数据。如果这个数据没有被更新,则当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新了,则根据不同实现方式执行不同操作,比如报错或者自动重试。

其实,悲观锁&乐观锁说白了,就是判断线程要不要锁住同步资源,如需要锁住,则是悲观锁,不需要锁住,就是乐观锁。

synchronized

synchronized关键字主要有两种用法,分别是同步方法和同步代码块,被synchronized修饰的方法或者代码块,在同一时间,只能被一个线程访问。
举个栗子:
售票处现在只剩下2张票,但是有10个人来抢票,到底谁可以买到票呢?
传统写法:

public class TicketMgt {

    private int ticket = 2; //还剩最后2张票

    public void booking() {
        if (ticket > 0) {
            ticket--;
            System.out.print("这张票我买了!");
        }
        System.out.println(" 还剩:" + ticket + "张。");
    }
}

10个壮汉来抢票:

public class Main {
    
    public static void main(String[] args) {
        TicketMgt ticketMgt = new TicketMgt();
        for (int i = 0; i < 10; i++) {
            new Thread(ticketMgt::booking).start();
        }
    }
}

抢票结果:

这张票我买了! 还剩:0张。
 还剩:0张。
这张票我买了! 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。

What? 弄啥嘞,为什么会出现这个结果呢?这不乱套了吗?
嗯哼。是时候体现在出synchronized的重要性了,只需要用synchronized修饰booking()方法就可以啦:

    public synchronized void booking() {
       ......
    }

10个壮汉再抢一次,抢票结果:

这张票我买了! 还剩:1张。
这张票我买了! 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。
 还剩:0张。

对于这个抢票结果,基本可以达到我们预期的效果了。它的实现原理:synchronizedTicketMgt 对象进行加锁了,同一时间只允许一个线程对ticket进行修改。就好比壮汉去售票处买票,synchronized相当于一把门锁,1#壮汉进去就把门锁上,2#、3#、4#等其他壮汉就在门外等着,1#壮汉买完开锁出来,其他壮汉才能接着进去买,保证每次只能进去一个买票的壮汉。

针对上面的小栗子,稍微讲讲synchronized的两种用法吧:

  • 用在方法上,又分实例方法和类方法:
    /**
     *  ①synchronized修饰实例方法
     *  锁的是对象,同一时间只能有一个线程访问方法。
     */
    public synchronized void doStd() {
        // TODO: 2020/7/28  
    }

    /**
     *  ②synchronized修饰类方法
     *  锁住的是类,同一时间只能有一个线程访问这个类
     */
    public synchronized static void doStaticStd() {
        // TODO: 2020/7/28
    }
  • 用在方法块上,也分实例方法和类方法:
    /**
     *  ①实例方法
     *  当在某个线程中执行这段代码块,该线程会获取this对象的锁,从而使得其他线程无法同时访问该代码块
     */
    public void doSynchronizedStd() {
        synchronized (this) {
            // TODO: 2020/7/28  
        }

        /**
         *  当一个线程访问对象的一个synchronized(this)同步代码块时
         *  另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。
         */
        // TODO: 2020/7/28  可被其他线程访问到
    }
    

    /**
     *  ②类方法
     *  锁住的是类,这个类的所有对象是同一把锁,效果与synchronized修饰类方法一样
     */
    public static void doSynchronizedStaticStd() {
        synchronized (MultiThreads.class) {
            // TODO: 2020/7/28  
        }
    }

两者区别:synchronized代码块锁粒度要比 synchronized方法小一些,因为 synchronized代码块所在的方法里还可以有其他代码。

synchronized 是非公平锁,也是可重入锁。

  • 公平锁:获取不到锁的时候,会自动加入队列,等待线程释放后,队列的第一个线程获取锁
  • 非公平锁:获取不到锁的时候,会自动加入队列,等待线程释放锁后所有等待的线程同时去竞争
  • 可重入锁:也叫递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞
    代码示例:
    public synchronized void booking() {
        System.out.println("提交订单");
        WeChatPay();
    }

    public synchronized void WeChatPay() {
        System.out.println("微信支付");
    }

在上面的代码中,类中的两个方法都是被内置锁synchronized修饰的,booking()方法中调用WeChatPay()方法。因为内置锁是可重入的,所以同一个线程在调用booking()时可以直接获得当前对象的锁,进入WeChatPay()进行操作。
看到这里,是不是对synchronized并没有那么陌生了呢。非常感谢你能看到最后,如果能帮助到你,是我的荣幸。后期可能还会写一篇关于synchronized实现原理的文章,还有synchronized是如何保证的原子性、顺序性和可见性

你可能感兴趣的:(Java,并发编程,多线程,java,synchronized)