欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

靓仔,你会synchronized吗?

程序员文章站 2022-07-03 12:14:17
浅析并发编程之synchronized...

悲观锁 & 乐观锁

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

悲观锁分析图:

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

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

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是如何保证的原子性、顺序性和可见性

本文地址:https://blog.csdn.net/qq_36270361/article/details/107393278