被逼无奈的程序员,怒而整理多线程面试必问源码知识点


本文是多线程系列之一,主要介绍多线程中比较基本的synchronized和volatile。

起因

很简单,别逼无奈,天知道这群大佬怎么想的,用什么思考的面试题,你面试阿里这一类编程航母也就罢了,问题是一些中型企业,在面试的时候也问的相当底层,刚开始我没在意,后来面试了几家公司这一块回答的模模糊糊,然后面完了就没有下文了,太#@###...&&&***,你有什么办法?没办法,整呗,幸好还有点时间

这里就面试中我最常见到的两个问题给大家解答一下

一、synchronized

1.1 synchronized使用

synchronized是java的关键字,用来对资源加锁,在多线程的环境下,不可避免的要用到共享资源,此时可以使用synchronized关键字对共享资源解锁,防止多个线程同时对共享资源操作。

synchronized的使用相对简单,以下面的代码为例,想要访问synchronized修饰的代码块,必须先获得对象o的锁。这里锁定的就是对象o。

public class ThreadTest {

    private int count = 10;
    Object o = new Object();

    private void m1(){
        synchronized (o){
            count --;
            System.out.println(Thread.currentThread().getId()+":count="+count);
        }

    }

    public static void main(String[] args){
        ThreadTest tt = new ThreadTest();
        new Thread(()->tt.m1(), "t1").start();
        new Thread(()->tt.m1(), "t2").start();
    }
}

实际上并不用每次都要new一个无用的对象,我们可以使用this对象,锁定当前对象就可以。

synchronized (this){            
 count --;             
System.out.println(Thread.currentThread().getId()+":count="+count);        
 }

synchronized还可修饰方法,在修饰方法的时候与该方法的一开始锁定this对象是一样的,也就是说下面两种写法是一样的。

private void m1(){     
synchronized (this){        
 count --;         
System.out.println(Thread.currentThread().getId()+":count="+count);     
} 
}  
private synchronized  void m1(){        
 count --;        
 System.out.println(Thread.currentThread().getId()+":count="+count); 
}

如果是静态方法,synchronized (this)锁定的就是类本身。

synchronized的使用相对简单,我们在使用synchronized进行加锁时,如果一个方法只有很小的一部分需要解锁,那就不要给整个方法加锁,但是如果一段代码中有好几个部分需要加锁,那就不要加多个锁,可以在整个方法上加锁,这样避免了所得频繁创建。

1.2 synchronized原理

使用synchronized加锁并不会刚一开始就向cpu申请锁定资源,而是经历了一个锁升级的过程。假如第一个线程来访问共享资源,刚开始的时候只是在对象的头记录了线程id,现在只是一个偏向锁,如果此时又来也一个线程需要申请锁,锁就会升级成自旋锁,就是线程会原地等待,通过一个while true的循环,循环十次,直到锁释放,如果循环十次还没有被释放,才会向操作系统申请资源,这种情况就变成了重量级锁。

synchronized是可重入的,如果m1和m2都用synchronized修饰,而且锁定的都是同一个对象,此时如果m1调用m2,同一个线程已经获取了这个对象的锁,那么m1调用m2是不需要再申请锁的。

二、Volatile

Volatile是java的关键字,用来修饰变量,它有两个作用:

1. 禁止指令重新排序

我们的代码在被jvm编译成字节码时,jvm会对我们的代码的执行顺序进行优化,当然这个优化保证不会改变执行的结果,在多线程的情况下,指令重排序有可能会导致线程安全问题,而且这个问题很难发现,所以我们用Volatile,禁止指令重新排序。

2. 使一个变量在多个线程中可见。

大家都知道java是有堆内存的,堆内存是所有线程的共享内存,但是每个线程又有自己的私有内存空间,假如有线程A 和线程B,两个线程都要访问变量t,线程A 和 B都会将tcopy一份到自己的线程空间中,这样他们对t所做的修改彼此是不知道的,因为线程并不会将t立刻写回堆内存,同样,线程也不会每次都从堆内存将t重新copy。如果加了Volatile修饰,如果线程对t做了修改,就会强制将t立刻写回堆内存,同样的,当线程使用t时,也会强制将t从堆内存重新copy。这样就保证了线程对变量的修改对其他线程是可见的。

但是Volatile并不能代替synchronized,如果多个线程同时修改t,只加了Volatile限制,同样会出问题。例如下面这段代码:

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ThreadTest1{

    private volatile int count = 10000;
    private static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();

    public void run(){
        count --;
        System.out.println(Thread.currentThread().getName()+":count="+count);

    }

    public static void main(String[] args){
        ThreadTest1 tt1 = new ThreadTest1();
        Integer limit = 10000;
        for(int i=0; i<limit; i++){
            new Thread(tt1::run, "Thread"+i).start();
        }

    }
}

运行这段代码就会发现,count并不会按顺序减少,而是有重复出现的情况。所以Volatile并不能代替Synchronized。


关于Synchronized和Volatile就介绍到这里,关于多线程,其实需要了解的东西还是很多的,不信?往下看

我把遇到的问题整理形成了一张知识导图

面试题

这是我在面试的过程中遇到的面试题,有回答上来的,也有没回答上来的,面试完了后再网上查答案进行了相应的总结

参考文档

这是我在整理面试答案的过程中,无意发现的一份文档,整理的很详细,都是从源码对于多线程进行讲解,并且在每一章节的后面,都会有相应的知识图谱进行知识点总结

相应的文章已经整理形成文档,git扫码获取资料看这里

https://gitee.com/biwangsheng/personal.git


原文链接:https://www.cnblogs.com/bwscode/p/13532296.html