美文网首页程序员每天写500字每天写1000字
Java面试之volatile和synchronized及Loc

Java面试之volatile和synchronized及Loc

作者: 追梦人_奋斗青年 | 来源:发表于2019-04-10 14:38 被阅读6次

面试中经常会遇到以下的问题:
Java中的volatile和synchronized的区别?
Java中的Lock和synchronized的区别?

今天我们来深入聊聊volatile、synchronized、Lock这三者之间的区别。
希望通过这篇文章的总结,让大家在面试中能回答的更加完美,给面试官留下耳目一新的感觉。

面试官的意图是通过这个问题想了解你对并发过程中如何处理可见性、原子性、有序性的问题的理解和解决。因此我们在回答的时候要往这里去靠,体现我们对多线程并发的理解和解决思路。

一、了解多线程的三个特性:可见性,原子性和有序性

1、什么是可见性

Java内存模型(Java Memory Model)导致可见性问题,在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。

下面是Java内存模型的抽象示意图:


Java内存模型的抽象示意图

从上图可以看出:

共享变量存储在主内存,每个线程都有自己私有的本地内存,存储共享变量的副本,线程执行时,先把共享变量从主内存读取到线程自己的本地内存,然后再对该变量进行操作,对该变量操作完成后,再把变量刷新回主内存中。

虽然Java线程通信是通过共享内存的方式进行通信的,实际上JMM为了加快执行的速度,线程一般是不会直接操作主内存的,而是操作本地内存。因此,线程1修改的变量,线程2是不会立刻看到的,所以在两个线程之间产生了变量内容不一致的问题,这就是线程间的可见性问题。

可见性是指:如果一个线程对于某个共享变量的进行更新之后,后续访问该变量的线程可以读取到该更改的结果,那么我们就说这个线程对于共享变量的的更新是可见的。

一段程序演示可见性问题:

public class ThreadApp {
private volatile static boolean flag = true;

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int count = 0;
while (flag){
                    count++;
                }
                System.out.println("循环结束,count="+count);
            }
        });
        thread.start();
        Thread.sleep(1000);
        flag = false;
    }
}

以上程序我们通过对共享变量flag来控制while循环的结束,有人会认为程序在休眠1秒后,flag变量值为false,那么while循环就会结束,最终输出count变量的值。

实际上,运行一下这段程序会让我们发现,循环不会停止,会一直运行下去,最终不会输出count变量的值。这就说明了flag共享变量存在不可见的问题。

怎么解决这个问题呢,我们可以使用volatile关键字对共享变量进行修饰,volatile修饰的变量,线程则不会从本地内存获取,而是直接从主内存获取变量。

以上的程序只需要对flag变量用volatile修饰,就可以达到解决可见性的问题,使程序可以结算while循环,并输出count变量的值。

通过volatile解决可见性的问题的原理:

对声明了volatile的变量进行写操作的时候,JMM会向处理器发送一条lock的指令,会把这个变量缓存的数据写回到主内存,在多处理器的情况下,为了保证各个处理器缓存一致性的特点,对本地内存失效的数据会重新从主内存获取。

2、什么是原子性

原子操作是不可分割的,指访问某个共享变量的操作从其执行线程之外的线程来看,该操作要么已经执行完毕,要么尚未发生,其他线程不会看到执行操作的中间结果。

非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。

一段程序说明原子性问题:

public class AtomicApp {
private static int count = 0;

public static int increase(){
try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//休眠,为了更好的体现原子性测试结果
return count++;
    }

public static void main(String[] args) throws InterruptedException {
for (int i=0;i<1000;i++){
new Thread(new Runnable() {
@Override
public void run() {
                    AtomicApp.increase();
                }
            }).start();
        }
        Thread.sleep(4000); //休眠,为使多线程执行完毕,输出count的最终值
        System.out.println("1000个线程操作后的结果,count="+count);
    }
}

程序运行的结果如下:

1000个线程操作后的结果,count=997

由于count++操作具备非原子性的特点,所以最后的结果count的值不是1000,而是小于1000,就是说有的子线程读取count的时候,上一个线程还没把更新的count值写入内存,这就是因无法保证操作的原子性而导致的线程安全问题。

通过synchronized关键字对increase()方法加锁后就能保证count++操作的原子性,使count的值为1000。

public synchronized static int increase(){
try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
//休眠,为了更好的体现原子性测试结果
return count++;
    }

输出结果为:

1000个线程操作后的结果,count=1000

3、什么是有序性

有序性是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。

在Java内存模型中,JVM为了加快程序的运行速度允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

可以通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

二、volatile、synchronized、Lock的作用

1、volatile的作用

volatile关键字修饰的变量可以保证可见性;
volatile关键字修饰的变量不会被指令重排序优化,可以实现有序性;
volatile不能保证线程的原子性,比如复合操作的原子性(i++);

Volatile实现内存可见性是通过store和load指令完成的;对volatile变量执行写操作时,会在写操作后加入一条store指令,强迫线程将最新的值刷新到主内存中;而在读操作时,会加入一条load指令,即强迫从主内存中读入变量的值。

2、synchronized的作用

synchronized关键字能够实现原子性和可见性;
synchronized关键字能够给对象和方法或者代码块加锁从而实现线程同步;
synchronized关键字可实现线程的安全性;

synchronized底层是通过使用对象的监视器锁(monitor)来确保同一时刻只有一个线程执行被修饰的方法或者代码块。

synchronized关键字加锁过程:

1、首先获得互斥锁
2、获得锁后,清空工作内存
3、在主内存中拷贝共享变量的副本到工作内存
4、执行加锁的代码
5、将修改后的共享变量的值刷新到主内存中
6、最后释放互斥锁

3、Lock的作用

java5以后出现的juc包(java.util.concurent)中有很多Lock的实现类,扩展了很多实用的功能,更佳灵活的实现线程间的同步互斥。

Lock丰富了锁的种类:

1、读写锁(ReadLock、WriteLock、ReadWriteLock)
读写锁对一个资源的访问可分成了2个锁,一个读锁和一个写锁,可以使得多个线程之间的读写操作不会发生冲突。ReadWriteLock就是读写锁,它是一个接口,ReentrantReadWriteLock实现了这个接口,可以通过readLock获取读锁,通过writeLock获取写锁。

2、可重入锁(ReentrantLock)
如果锁具备可重入性,则称为可重入锁,也就是说一个线程执行完毕后,由CPU调度,下一次任然有可能获得锁,继续执行代码。像synchronized和ReentrantLock都是可重入锁,可重入锁是基于线程的分配机制。

3、可中断锁
如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,如果等待时间过长,线程B不想等待了,想处理其他事情,就可以通过抛出interruptedexception异常中断线程执行,这种就是可中断锁。
在Java中,synchronized就不是可中断锁,而Lock是可中断锁。

4、公平锁和非公平锁
公平锁以请求锁的顺序来获取锁,非公平锁则是无法保证按照请求的顺序执行,在ReentrantLock中定义了2个静态内部类,一个是NotFairSync,一个是FairSync,分别用来实现非公平锁和公平锁。
synchronized就是非公平锁,它无法保证等待的线程获取锁的顺序。
对于ReentrantLock和ReentrantReadWriteLock,它默认情况下是非公平锁,但是可以通过构造方法设置为公平锁。

ReentrantLock lock = new ReentrantLock(true);

5、可重入读写锁(ReentrantReadWriteLock)
ReentrantReadWriteLock被大量使用在缓存中,因为缓存中的对象总是被共享大量读操作,偶尔修改这个对象或者其中的子对象,比如状态,那么只要通过ReentrantReadWriteLock来更新对象就可以了,这就实现了并发中对原子性的要求,而大大提高并发的性能。

三、volatile、synchronized、Lock的区别

1、volatile和synchronized是Java的关键字,而Lock是jdk5之后juc包下的一个接口;
2、volatile关键字修饰的变量可以保证可见性、有序性,但是不能保证线程的原子性,而synchronized对可见性、原子性与有序性都能保证;
3、volatile仅能用于修饰变量,而synchronized可用于修饰变量、方法、代码块等;
4、volatile不会造成线程阻塞,synchronized可能会造成线程阻塞;
5、synchronized和Lock都能通过加锁来实现线程同步;
6、synchronized锁在获取锁的线程执行完了该代码块或者线程执行出现异常后释放锁,而Lock可以主动去释放锁;
7、对于不同场景使用不同的锁,Lock实现的锁种类丰富;
8、Lock的性能比synchronized强。

希望大家通过这篇文章了解多线程volatile、synchronized、Lock的区别。

相关文章

网友评论

    本文标题:Java面试之volatile和synchronized及Loc

    本文链接:https://www.haomeiwen.com/subject/wcpamqtx.html