Java多线程系列(二)synchronized关键字

前言

当我们讨论多线程的时候,有一个事情是怎么也绕不开的——线程安全性,或者说,数据一致性。

而在java程序中,synchronized关键字,就是解决线程安全问题的最方便也是最基础的手段。

这篇文章,我们就从线程竞争开始讨论synchronized出现的原因,然后讨论该关键字底层的实现逻辑,以及与底层实现相关的锁升级的概念。最后还会说一些面试题中跟synchronized相关的几个问题。

synchronized的出现原因和用法

线程不安全的隐患

在多线程的环境下,如果没有特殊处理,多个线程对同一份数据进行读写操作几乎是必定会发生的情况。

比如下面这段代码

public class ThreadDemo1 implements Runnable {
    private int totalCount = 100;

    public static void main(String[] args) throws IOException {
        Runnable thread1 = new ThreadDemo1();
        for(int i = 0; i < 5; i++) {
            new Thread(thread1).start();
        }
    }

    @Override
    public void run() {
        for(int i = 0; i < 20; i++) {
            totalCount--;
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " -- " + totalCount);
        }
    }
}

上面的代码有一个初始值为100的变量totalCount,并且起了5个线程,每个线程通过for循环从这个变量中减去20,在每次减操作之后,把totalCount输出出来。

问题在于,输出语句输出的totalCount与计算得到的totalCount的值可能是不一样的。

比如,在thread1得出结果99并准备输出的时候,thread2把结果该变量改成了98,这个时候,thread1就输出了98。

为了让结果更明显,我们在计算语句和输出语句之间让线程睡眠5毫秒,这样最后的输出结果如下图:

在这里插入图片描述

如果我们仔细分析一下这种情况,就知道这个问题的根源在于多个线程同时访问了一个数据,导致任何一个线程,都不能百分百掌控某个变量。为了解决这个问题,synchronized就应运而生了。

如何使用synchronized

synchronized关键字,通过给某个对象加锁的方式,保证了同一时间,只能由一个线程访问某一段代码。

下面这段demo代码显示了synchronized关键字的使用方法

Object obj = new Object();

synchronized(obj) {
	// code block...
}

不难发现,要使用synchronized关键字,后面的括号是要传入一个对象的。这个对象,就是一个用来被加锁的对象

这个被加锁的对象,有没有什么要求呢?

答案是:该对象不能是String类,也不能是基础类型的数据。其他的对象都可以放在synchronized关键字后面的括号里。

我们也见过synchronized的另外一种用法,是直接把这个关键字加在方法声明行,就像下面代码所示的一样,这种做法,就相当于给该方法所在的对象加了一把锁。

public synchronized void method() {
	// code block
}

// 相当于下面的代码

public void method() {
	synchronized(this) {
		// code block
	}
}

那就会让人很好奇啊,仅仅一个关键字synchronized和一个被加锁的对象,就能保证线程的安全性了吗?

下面通过synchronized的底层实现原理,来研究一下是怎么保证线程安全性的。

synchronized的实现原理

synchronized,就是保证同一时间,只有一个线程能够访问代码块中的内容。

是怎么保证的呢?

在java中,任意一个对象同一时刻只能被加一把锁,成功给这个对象加上锁的线程,得到执行同步代码块的权利。

那么是怎么给一个对象加锁的呢?

其实就是在这个对象字节码的头两位做标记,具体的实现超出了本文的范围,这里暂时不做讨论。

这个给对象加锁的过程,就涉及到了一个锁升级的概念。

锁升级

在JDK的早期版本(1.5之前),当线程尝试给某个对象加锁的时候,会直接向操作系统申请锁,我们都知道任何程序涉及到操作系统的操作时,运行效率会不可避免的下降。

为了提高加锁的效率,JDK用锁升级的概念来代替无脑申请系统锁。锁的级别从低到高分别是:偏向锁、自旋锁、系统锁。

偏向锁

偏向锁是最轻量的锁,本质是压根儿就没上锁。

只有一个线程尝试给某个对象上锁的时候,直接在这个Object上记录下这个线程的线程ID。

当又有一个线程来尝试给这个对象加锁的时候,先对比这个线程的线程ID和记录下来的线程ID,如果相同,会让这个线程继续运行。

而如果不同,会将锁升级成自旋锁。

自旋锁

什么是自旋锁呢?

简单的理解就是不断去检查这个对象上的锁有没有释放,相当于有个while(true)的循环不断去检查。

默认的自旋次数(检查次数)是10次,如果10次之内,该对象的锁被释放了,当前这个自旋的线程会拿到这把锁。

如果自旋次数达到上限,该锁就升级成系统锁。

所以我们知道,自旋锁是一直存在于用户态的锁,不涉及跟操作系统的交互。一直在调用CPU。

系统锁

如果升级到系统锁,那么该线程不在耗费CPU去检查锁有没有被释放。而是进入操作系统的等待队列,由操作系统去调度线程,决定什么时候获得该对象的锁。

可以想象,这种锁是特别重量级的锁,运行效率不会太高。

几道跟Synchronized相关的面试题

面试题1:同步方法和非同步方法能否被同时调用?

这是肯定的可以的啊。既然说起了是同时调用,那么肯定是在两个线程里。调用非同步方法(即普通方法),是不需要抢锁的,因此是可以同时执行的。

那么就是说,非同步方法,是可以读到同步方法的中间状态的。

来看下面这个代码:

public class ThreadDemo1 {

    private String accountName;
    private int balance;

    public synchronized void setAccountAndBalance() {
        this.accountName = "testAccount";

        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        this.balance = 100;
    }

    public void readAccoutAndBalance() {
        System.out.println("account: " + this.accountName + "--- balance " + this.balance);
    }

    public static void main(String[] args) {
        ThreadDemo1 demo = new ThreadDemo1();
        // thread1 is going to set account and balance
        (new Thread(new Runnable() {
            @Override
            public void run() {
                demo.setAccountAndBalance();
            }
        })).start();

        // thread2 is going to get account and its balance
        (new Thread(new Runnable() {
            @Override
            public void run() {
                demo.readAccoutAndBalance();
            }
        })).start();
    }
}

这段代码模拟了银行开户的流程,首先有个方法setAccountAndBalance 来初始化一个账户和其中的余额。并且用sleep方法在设置账户和设置余额中间模拟了费时的操作。

同时有个方法readAccoutAndBalance去读取这个账户和余额,这样就会读到中间的状态。

面试题2: synchronized是不是可重入锁?

首先要明确,什么是可重入锁?

顾名思义,就是可以被重新进入的锁。

在实际应用中,相当于在一个同步方法中调用另一个同步方法。如果一个线程已经得到了这个对象的锁,那么同一个线程仍然能得到另一个同步方法的锁。

如果同一个对象的锁不让同一个线程重复进入,上面这种一个同步方法调用另一个同步方法的情况就是一种死结。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值