面试笔试题总结

2024年1月9日

1、Java Synchronized关键字是如何使用的?

在多线程环境中,难免会出现多个线程对一个对象的实例变量进行同时访问和操作,如果编程处理不当,会产生脏读现象。

线程安全问题介绍

我们先来看一个简单的线程安全问题的例子!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class DataEntity {
 
    private int count = 0;
 
    public void addCount(){
        count++;
    }
 
    public int getCount(){
        return count;
    }
}
 
public class MyThread extends Thread {
 
    private DataEntity entity;
 
    public MyThread(DataEntity entity) {
        this.entity = entity;
    }
 
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            entity.addCount();
        }
    }
}
 
public class MyThreadTest {
 
    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
        //使用多线程编程对数据进行计算
        for (int i = 0; i < 10; i++) {
            MyThread thread = new MyThread(entity);
            thread.start();
        }
 
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

上面的代码中,总共开启了 10 个线程,每个线程都累加了 1000000 次,如果结果正确的话,自然而然总数就应该是 10 * 1000000 = 10000000。

但是多次运行结果都不是这个数,而且每次运行结果都不一样,为什么会出现这个结果呢?

简单的说,这是主内存和线程的工作内存数据不一致,以及多线程执行时无序,共同造成的结果!

 

 

如上图所示,线程 A 和线程 B 之间,如果要完成数据通信的话,需要经历以下几个步骤:

  • 1.线程 A 从主内存中将共享变量读入线程 A 的工作内存后并进行操作,之后将数据重新写回到主内存中;
  • 2.线程 B 从主存中读取最新的共享变量,然后存入自己的工作内存中,再进行操作,数据操作完之后再重新写入到主内存中;

如果线程 A 更新后数据并没有及时写回到主存,而此时线程 B 从主内存中读到的数据,可能就是过期的数据,于是就会出现“脏读”现象。

因此在多线程环境下,如果不进行一定干预处理,可能就会出现像上文介绍的那样,采用多线程编程时,程序的实际运行结果与预期会不一致,就会产生非常严重的问题。

针对多线程编程中,程序运行不安全的问题,Java 提供了synchronized关键字来解决这个问题,当多个线程同时访问共享资源时,会保证线程依次排队操作共享变量,从而保证程序的实际运行结果与预期一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DataEntity {
 
    private int count = 0;
 
    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        count++;
    }
 
    public int getCount(){
        return count;
    }
}

多次运行结果如下: 

1
2
3
4
第一次运行:result: 10000000
第二次运行:result: 10000000
第三次运行:result: 10000000
...

运行结果与预期一致!  

synchronized 使用详解

synchronized作为 Java 中的关键字,在多线程编程中,有着非常重要的地位,也是新手了解并发编程的基础,从功能角度看,它有以下几个比较重要的特性:

  • 原子性:即一个或多个操作要么全部执行成功,要么全部执行失败。synchronized关键字可以保证只有一个线程拿到锁,访问共享资源
  • 可见性:即一个线程对共享变量进行修改后,其他线程可以立刻看到。执行synchronized时,线程获取锁之后,一定从主内存中读取数据,释放锁之前,一定会将数据写回主内存,从而保证内存数据可见性
  • 有序性:即保证程序的执行顺序会按照代码的先后顺序执行。synchronized关键字,可以保证每个线程依次排队操作共享变量

synchronized也被称为同步锁,它可以把任意一个非 NULL 的对象当成锁,只有拿到锁的线程能进入方法体,并且只有一个线程能进入,其他的线程必须等待锁释放了才能进入,它属于独占式的悲观锁,同时也属于可重入锁。

关于锁的知识,我们后面在介绍,大家先了解一下就行。

从实际的使用角度来看,synchronized修饰的对象有以下几种:

  • 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
  • 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
  • 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象,使用上比较灵活

 

修饰一个方法

synchronized修饰一个方法时,多个线程访问同一个对象,哪个线程持有该方法所属对象的锁,就拥有执行权限,否则就只能等待。

如果多线程访问的不是同一个对象,不会起到保证线程同步的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class DataEntity {
 
    private int count;
 
    /**
     * 在方法上加上 synchronized 关键字
     */
    public synchronized void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public int getCount() {
        return count;
    }
}
 
public class MyThreadA extends Thread {
 
    private DataEntity entity;
 
    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }
 
    @Override
    public void run() {
        entity.addCount();
    }
}
 
public class MyThreadB extends Thread {
 
    private DataEntity entity;
 
    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }
 
    @Override
    public void run() {
        entity.addCount();
    }
}
 
public class MyThreadTest {
 
    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
 
        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();
 
        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

 运行结果如下:

1
2
3
4
5
6
7
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

当两个线程共同操作一个对象时,此时每个线程都会依次排队执行。

假如两个线程操作的不是一个对象,此时没有任何效果,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MyThreadTest {
 
    public static void main(String[] args) {
        DataEntity entity1 = new DataEntity();
        MyThreadA threadA = new MyThreadA(entity1);
        threadA.start();
 
        DataEntity entity2 = new DataEntity();
        MyThreadA threadB = new MyThreadA(entity2);
        threadB.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity1.getCount());
        System.out.println("result: " + entity2.getCount());
    }
}

 运行结果如下: 

1
2
3
4
5
6
7
8
Thread-0:0
Thread-1:0
Thread-0:1
Thread-1:1
Thread-0:2
Thread-1:2
result: 3
result: 3

 从结果上可以看出,当synchronized修饰一个方法,当多个线程访问同一个对象的方法,每个线程会依次排队;如果访问的不是一个对象,线程不会进行排队,像正常执行一样。 

修饰一个静态的方法

synchronized修改一个静态的方法时,代表的是对当前.java文件对应的 Class 类加锁,不区分对象实例。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class DataEntity {
 
    private static int count;
 
    /**
     * 在静态方法上加上 synchronized 关键字
     */
    public synchronized static void addCount(){
        for (int i = 0; i < 3; i++) {
            try {
                System.out.println(Thread.currentThread().getName() + ":" + (count++));
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
    public static int getCount() {
        return count;
    }
}
 
public class MyThreadA extends Thread {
 
    @Override
    public void run() {
        DataEntity.addCount();
    }
}
 
public class MyThreadB extends Thread {
 
    @Override
    public void run() {
        DataEntity.addCount();
    }
}
 
public class MyThreadTest {
 
    public static void main(String[] args) {
 
        MyThreadA threadA = new MyThreadA();
        threadA.start();
 
        MyThreadB threadB = new MyThreadB();
        threadB.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + DataEntity.getCount());
    }
}

 运行结果如下:

1
2
3
4
5
6
7
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6 

修饰一个代码块

synchronized用于修饰一个代码块时,只会控制代码块内的执行顺序,其他试图访问该对象的线程将被阻塞,编程比较灵活,在实际开发中用的应用比较广泛。

示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class DataEntity {
 
    private int count;
 
    /**
     * 在方法上加上 synchronized 关键字
     */
    public void addCount(){
        synchronized (this){
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
 
    public int getCount() {
        return count;
    }
}
 
public class MyThreadTest {
 
    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
 
        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();
 
        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

  运行结果如下:

1
2
3
4
5
6
7
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

 其中synchronized (this)中的this,表示的是当前类实例的对象,效果等同于public synchronized void addCount()

除此之外,synchronized()还可以修饰任意实例对象,作用的范围就是具体的实例对象。

比如,修饰个自定义的类实例对象,作用的范围是拥有lock对象,其实也等价于synchronized (this)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DataEntity {
 
    private Object lock = new Object();
 
    /**
     * synchronized 可以修饰任意实例对象
     */
    public void addCount(){
        synchronized (lock){
            // todo...
        }
    }
}

 当然也可以用于修饰类,表示类锁,效果等同于public synchronized static void addCount() 

1
2
3
4
5
6
7
8
9
10
11
public class DataEntity {
     
    /**
     * synchronized 可以修饰类,表示类锁
     */
    public void addCount(){
        synchronized (DataEntity.class){
            // todo...
        }
    }
}

 synchronized修饰代码块,比较经典的应用案例,就是单例设计模式中的双重校验锁实现。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Singleton { 
 
    private volatile static Singleton singleton; 
     
    private Singleton (){} 
     
    public static Singleton getSingleton() { 
        if (singleton == null) { 
            synchronized (Singleton.class) { 
                if (singleton == null) { 
                    singleton = new Singleton(); 
                
            
        
        return singleton; 
    
}

 采用代码块的实现方式,编程会更加灵活,可以显著的提升并发查询的效率。 

synchronized 锁重入介绍

synchronized关键字拥有锁重入的功能,所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁,而无需等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class DataEntity {
 
    private int count = 0;
 
     
    public synchronized void addCount1(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount2();
    }
 
    public synchronized void addCount2(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
        addCount3();
    }
 
    public synchronized void addCount3(){
        System.out.println(Thread.currentThread().getName() + ":" + (count++));
 
    }
 
    public int getCount() {
        return count;
    }
}
 
public class MyThreadA extends Thread {
 
    private DataEntity entity;
 
    public MyThreadA(DataEntity entity) {
        this.entity = entity;
    }
 
    @Override
    public void run() {
        entity.addCount1();
    }
}
 
public class MyThreadB extends Thread {
 
    private DataEntity entity;
 
    public MyThreadB(DataEntity entity) {
        this.entity = entity;
    }
 
    @Override
    public void run() {
        entity.addCount1();
    }
}
 
public class MyThreadTest {
 
    public static void main(String[] args) {
        // 初始化数据实体
        DataEntity entity = new DataEntity();
 
        MyThreadA threadA = new MyThreadA(entity);
        threadA.start();
 
        MyThreadB threadB = new MyThreadB(entity);
        threadB.start();
 
 
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("result: " + entity.getCount());
    }
}

  运行结果如下:

1
2
3
4
5
6
7
Thread-0:0
Thread-0:1
Thread-0:2
Thread-1:3
Thread-1:4
Thread-1:5
result: 6

 从结果上看线程没有交替执行,线程Thread-0获取到锁之后,再次调用其它带有synchronized关键字的方法时,可以快速进入,而Thread-1线程需等待对象锁完全释放之后再获取,这就是锁重入。

小结

从上文中我们可以得知,在多线程环境下,恰当的使用synchronized关键字可以保证线程同步,使程序的运行结果与预期一致。

  • 1.当synchronized修饰一个方法时,作用的范围是整个方法,作用的对象是调用这个方法的对象;
  • 2..当synchronized修饰一个静态方法时,作用的范围是整个静态方法,作用的对象是这个类的所有对象;
  • 3.当synchronized修饰一个代码块时,作用的范围是代码块,作用的对象是修饰的内容,如果是类,则这个类的所有对象都会受到控制;如果是任意对象实例子,则控制的是具体的对象实例,谁拥有这个对象锁,就能进入方法体

synchronized是一种同步锁,属于独占式,使用它进行线程同步,JVM 性能开销很大,大量的使用未必会带来好处。

 2、BeanFactory和ApplicationContext的区别总结

BeanFactory:

是Spring里面最底层的接口,提供了最简单的容器的功能,只提供了实例化对象和拿对象的功能;

ApplicationContext:

应用上下文,继承BeanFactory接口,它是Spring的一各更高级的容器,提供了更多的有用的功能;

  • 1) 国际化(MessageSource)
  • 2) 访问资源,如URL和文件(ResourceLoader)
  • 3) 载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层
  • 4) 消息发送、响应机制(ApplicationEventPublisher)
  • 5) AOP(拦截器)

两者装载bean的区别

BeanFactory:BeanFactory在启动的时候不会去实例化Bean,中有从容器中拿Bean的时候才会去实例化;

ApplicationContext:ApplicationContext在启动的时候就把所有的Bean全部实例化了。它还可以为Bean配置lazy-init=true来让Bean延迟实例化;

我们该用BeanFactory还是ApplicationContent
延迟实例化的优点:(BeanFactory)

  • 应用启动的时候占用资源很少;对资源要求较高的应用,比较有优势;

不延迟实例化的优点: (ApplicationContext)

  • 1. 所有的Bean在启动的时候都加载,系统运行的速度快;
  • 2. 在启动的时候所有的Bean都加载了,我们就能在系统启动的时候,尽早的发现系统中的配置问题
  • 3. 建议web应用,在启动的时候就把所有的Bean都加载了。(把费时的操作放到系统启动中完成)

BeanFactory类关系继承图

 1. BeanFactory类结构体系:

BeanFactory接口及其子类定义了Spring IoC容器体系结构,由于BeanFactory体系非常的庞大和复杂,因此要理解Spring IoC,需要先理清BeanFactory的继承机构。

2. ApplicationContext的结构体系:
ApplicationContext接口是一个BeanFactory基础上封装了更多功能的,Spring中最为常用的IoC容器,其包含两个子接口:ConfigurableApplicationContext、WebApplicationContext。
ConfigurableApplicationContext其结构体系如下:

 

 详细的结构体系如下:
a.AbstractApplicationContext结构体系如下:

 b.ConfigurablePortletApplicationContext体系结构如下:

 c.ConfigurableWebApplicationContext结构体系如下:

 2).WebApplicationContext体系结构如下:

1、容器是spring的核心,使IoC管理所有和组件
2、spring的两种容器:

  • a、BeanFactoy
  • b、ApplicationContext应用上下文

3、BeanFactory:BeanhFactory使用延迟加载所有的Bean,为了从BeanhFactory得到一个Bean,只要调用getBean()方法,就能获得Bean

4、ApplicationContext:

  • a、提供文本信息解析,支持I18N
  • b、提供载入文件资源的通用方法
  • c、向注册为监听器的Bean发送事件
  • d、ApplicationContext接口扩展BeanFactory接口
  • e、ApplicationContext提供附加功能

5、ApplicationContext的三个实现类:

  • a、ClassPathXmlApplication:把上下文文件当成类路径资源
  • b、FileSystemXmlApplication:从文件系统中的XML文件载入上下文定义信息
  • c、XmlWebApplicationContext:从Web系统中的XML文件载入上下文定义信息

6、在默认情况下,Bean全都是单态,在<bean>中的singleton为false

7、<bean>中的id属性必须遵循Java规范,而name属性可以不遵循

8、Bean的实例化的时候需要一些初始化的动作,同样当不再使用的时候,需要从容器中将其销毁

9、对象的初始化:<beaninit-method="方法名">

10、对象的销毁:<beandestroy-method="方法名">,销毁对象的过程:

  • a、主线程先被中断
  • b、Java虚拟机使用垃圾回收机制回收Bean对象

Spring的IoC容器就是一个实现了BeanFactory接口的可实例化类。事实上,Spring提供了两种不同的容器:一种是最基本的BeanFactory,另一种是扩展的ApplicationContext。BeanFactory 仅提供了最基本的依赖注入支持,而 ApplicationContext 则扩展了BeanFactory ,提供了更多的额外功能。二者对Bean的初始化也有很大区别。BeanFactory当需要调用时读取配置信息,生成某个类的实例。如果读入的Bean配置正确,则其他的配置中有错误也不会影响程序的运行。而ApplicationContext 在初始化时就把 xml 的配置信息读入内存,对 XML 文件进行检验,如果配置文件没有错误,就创建所有的Bean ,直接为应用程序服务。相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。

ApplicationContext会利用Java反射机制自动识别出配置文件中定义的BeanPostProcessor、InstantiationAwareBeanPostProcessor和BeanFactoryPostProcessor,并自动将它们注册到应用上下文中;而BeanFactory需要在代码中通过手工调用addBeanPostProcessor()方法进行注册。
Bean装配实际上就是让容器知道程序中都有哪些Bean,可以通过以下两种方式实现:

配置文件(最为常用,体现了依赖注入DI的思想)
编程方式(写的过程中向BeanFactory去注册)

作用:

1. BeanFactory负责读取bean配置文档,管理bean的加载,实例化,维护bean之间的依赖关系,负责bean的声明周期。

2. ApplicationContext除了提供上述BeanFactory所能提供的功能之外,还提供了更完整的框架功能:

a. 国际化支持

b. 资源访问:Resource rs = ctx. getResource(“classpath:config.properties”), “file:c:/config.properties”

c. 事件传递:通过实现ApplicationContextAware接口

3. 常用的获取ApplicationContext的方法:

FileSystemXmlApplicationContext:从文件系统或者url指定的xml配置文件创建,参数为配置文件名或文件名数组
ClassPathXmlApplicationContext:从classpath的xml配置文件创建,可以从jar包中读取配置文件
WebApplicationContextUtils:从web应用的根目录读取配置文件,需要先在web.xml中配置,可以配置监听器或者servlet来实现

1
2
3
4
5
6
7
8
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>context</servlet-name>
<servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

这两种方式都默认配置文件为web-inf/applicationContext.xml,也可使用context-param指定配置文件  

1
2
3
4
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/myApplicationContext.xml</param-value>
</context-param>

  

 

 

posted @   leagueandlegends  阅读(22)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
历史上的今天:
2021-01-09 微服务架构及其概念
点击右上角即可分享
微信分享提示