synchronized关键字介绍(秋招篇)——上

一、心路历程

在Java并发编程当中,synchronized关键字无疑是被问到频率较高的一个问题,在面试当中,很多面试官对你对synchronized关键字及对它底层的了解程度都是比较重视的。如果你能回答上来,无疑是比较加分的。以下是个人通过自己了解到的,已经书中的知识结合总结的内容,欢迎大家指正!

二、什么是synchronized关键字

在多线程的环境下,多个线程同时访问共享资源会出现一些问题,而synchronized关键字则是用来保证线程同步的。

三、JMM及Java内存的可见性

那我们在了解synchronized关键字的底层实现原理之前,需要先首先了解一下Java内存模型(JMM),看看synchronized关键字是如何起作用的。

当然,这里的本地内存并不是真实存在的,只是Java内存模型的一个抽象概念,(JMM也更偏向于Java当中的一个规约)它包含了控制器、运算器、缓 存等。

同时Java内存模型规定,线程对共享变量的操作必须在自己的本地内存中进行,不能直接在主内存中操作共享变量。这种内存模型会出现什么问题呢?

  1. 线程A获取到共享变量X的值,此时本地内存A中没有X的值,所以加载主内存中的X值并缓存到本地内存A中,线程A修改X的值为1,并将X的值刷新到主内存中,这时主内存及本地内存中的X的值都为1。

  2. 线程B需要获取共享变量X的值,此时本地内存B中没有X的值,加载主内存中的X值并缓存到本地内存B中,此时X的值为1。线程B修改X的值为2,并刷新到主内存中,此时主内存及本地内存B中的X值为2,本地内存A中的X值为1。

  3. 线程A再次获取共享变量X的值,此时本地内存中存在X的值,所以直接从本地内存中A获取到了X为1的值,但此时主内存中X的值为2,到此出现了所谓内存不可见的问题。

该问题Java内存模型是通过synchronized关键字或volatile关键字就可以解决,那么synchronized关键字是如何解决的呢,其实进入synchronized块就是把在synchronized块内使用到的变量从线程的本地内存中擦除,这样在synchronized块中再次使用到该变量就不能从本地内存中获取了,需要从主内存中获取,解决了内存不可见问题。

四、synchronized和volatile的区别

上午我们说了synchronized都可以解决JMM中的问题,那么他们有什么区别呢?

  • volatile 解决的是内存可见性问题,会使得所有对 volatile 变量的读写都直接写入主存,即 保证了变量的可见性。

  • synchronized 解决的事执行控制的问题,它会阻止其他线程获取当前对象的监控锁,这样一来就让当前对象中被 synchronized 关键字保护的代码块无法被其他线程访问,也就是无法并发执行。而且,synchronized 还会创建一个 内存屏障,内存屏障指令保证了所有 CPU 操作结果都会直接刷到主存中,从而保证操作的内存可见性,同时也使得这个锁的线程的所有操作(基于happens-before规范)于随后获得这个锁的线程的操作。

主要区别:

  1. volatile 本质是在告诉 JVM 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

  2. volatile 仅能使用在变量级别;synchronized 则可以使用在 变量. 方法. 和类级别的

  3. volatile 仅能实现变量的修改可见性,不能保证原子性;而synchronized 则可以 保证变量的修改可见性和原子性

  4. volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。

  5. volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化(锁升级机制)。

五、Synchronized 和Lock的区别

  • Synchronized 内置的Java关键字,Lock 是一个Java类

  • Synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁

  • Synchronized 会自动释放锁,Lock 必须要手动释放锁,如果不释放锁,会造成死锁

  • Synchronized 假设A线程获得锁,B线程等待,如果A线程阻塞,B线程会一直等待,Lock 锁 可以通过 tryLock 判断有没有锁

  • Synchronized 可重入锁、不可中断、非公平 ,Lock 可重入,可判断,可公平

  • Synchronized 适合锁少量的代码同步问题,Lock 适合锁大量的同步代码

六、synchronized使用方式

  • 修饰普通同步方法

public class synTest implements Runnable {
    private volatile static int i = 0;   //共享资源
    private  synchronized void add() {
        i++;
    }
​
    public void run() {
        for (int j = 0; j < 10000; j++) {
            add();
        }
    }
    public static void main(String[] args) throws Exception {
        synTest syncTest = new synTest();
​
        Thread t1 = new Thread(syncTest);
        Thread t2 = new Thread(syncTest);
        t1.start();
        t2.start();
​
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

结果:

20000

我们更改一下代码结构,再次查看结果:

public class synTest implements Runnable {
    private volatile static int i = 0;   //共享资源
    private  synchronized void add() {
        i++;
    }
​
    public void run() {
        for (int j = 0; j < 10000; j++) {
            add();
        }
    }
    public static void main(String[] args) throws Exception {
//        synTest syncTest = new synTest();
​
        Thread t1 = new Thread(new synTest());
        Thread t2 = new Thread(new synTest());
        t1.start();
        t2.start();
​
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

结果:

< 20000

第二个示例中的add() 方法虽然也使用synchronized关键字修饰了,但是因为两次new synTest() 操作建立的是两个不同的对象,也就是说存在两个不同的对象锁,线程t1和t2使用的是不同的对象锁,所以不能保证线程安全。那这种情况应该如何解决呢?因为每次创建的实例对象都是不同的,而类对象却只有一个,如果synchronized关键字作用于类对象,即用synchronized修饰静态方法,问题则迎刃而解了。

  • 修饰静态方法

只需要在add()方法前使用static修饰即可,即当synchronized作用于静态方法,锁的就是当前的class对象。

private static synchronized void add() {
        i++;
}

结果:

2000

  • 修饰同步代码狂

如果某些情况下,整个方法体比较大,需要同步的代码只是一小部分,如果直接对整个方法体进行同步,会使得代码性能变差,这时只需要对一小部分代码进行同步即可。代码如下:

public class synTest implements Runnable {
    private  static int i = 0;   //共享资源
​
    public void run() {
        synchronized (this){ // this表示当前对象实例,这里还可以使用synTest.class;表示class对象锁
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }
    }
    public static void main(String[] args) throws Exception {
        synTest syncTest = new synTest();
​
        Thread t1 = new Thread(syncTest);
        Thread t2 = new Thread(syncTest);
        t1.start();
        t2.start();
​
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

输出结果:

20000

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值