synchronized()_靓仔,你会synchronized吗?

悲观锁 & 乐观锁

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

悲观锁分析图:

synchronized()_靓仔,你会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 + "张。");    }}123456789101112

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();        }    }}12345678910

抢票结果:

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

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

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

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

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

对于这个抢票结果,基本可以达到我们预期的效果了。它的实现原理:synchronized给TicketMgt 对象进行加锁了,同一时间只允许一个线程对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    }123456789101112131415
  • 用在方法块上,也分实例方法和类方法:
    /**     *  ①实例方法     *  当在某个线程中执行这段代码块,该线程会获取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          }    }1234567891011121314151617181920212223242526

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

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

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

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

你可能感兴趣的:(synchronized())