面试回顾笔记

2023.7.26

1.问题1:多线程使用?

之前的问题,现在多个任务需要处理【如查询订单、库存。。。】,如何多线程优化?:

我说开启异步线程去做这些工作,主线程继续执行。面试官又问,那主线程需要这些线程的数据呢?蒙了一下答不上来,现在个人感觉是使用Callable接口,通过提交后的future对象get获得数据吧。

1.1.手脚架代码搭建

  • 引入mybatis依赖
<dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.13.2</version>
          <scope>test</scope>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-jdbc</artifactId>
          <version>5.2.10.RELEASE</version>
      </dependency>

      <dependency>
          <groupId>org.springframework</groupId>
          <artifactId>spring-test</artifactId>
          <version>5.2.10.RELEASE</version>
      </dependency>

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis</artifactId>
          <version>3.5.6</version>
      </dependency>

      <dependency>
          <groupId>org.mybatis</groupId>
          <artifactId>mybatis-spring</artifactId>
          <version>1.3.0</version>
      </dependency>

      <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <version>5.1.47</version>
      </dependency>

      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>druid</artifactId>
          <version>1.1.16</version>
      </dependency>
  • 配置类
@Configuration
public class MybatisConfig {
    @Bean
    public DataSource getDataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName("com.mysql.jdbc.Driver"); //驱动
        dataSource.setUrl("jdbc:mysql://localhost:3306/communcity?characterEncoding=utf-8&useSSL=false&serverTimeZone=Hongkong");//连接数据库URL
        dataSource.setUsername("root");//mysql登陆用户名
        dataSource.setPassword("!Wzh2352186607");//mysql登陆密码
        dataSource.setMaxActive(100);//连接池最大连接数
        dataSource.setMinIdle(20);//连接池最小连接数
        dataSource.setMaxWait(1000);//连接池超时时间
        return dataSource;
    }
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        factoryBean.setTypeAliasesPackage("org.example.entity");
        return factoryBean;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer(){
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage("org.example.dao");
        return msc;
    }
}
@EnableAspectJAutoProxy
@Configuration
@Import({MybatisConfig.class})
@PropertySource("classpath:jdbc.properties")
@EnableTransactionManagement
@ComponentScan({"org.example"})
public class SpringConfig {
    
}
  • mapper层
public interface CommentMapper {

    List<Comment> getCommentByUserId(@Param("userId") int userId);
}
public interface MessageMapper {
    List<Message> getMessageByUserId(int userId);
}
public interface UserMapper {
    List<User> selectById(@Param("userId") int userId);
}

<mapper namespace="org.example.dao.CommentMapper">
    <sql id="selectFields">
        id,user_id,entity_type,entity_id,target_id,content,status,create_time
    </sql>
    <select id="getCommentByUserId" resultType="Comment">
        select <include refid="selectFields"></include>
        from comment
        where user_id=#{userId}
    </select>
</mapper>


<mapper namespace="org.example.dao.MessageMapper">
    <sql id="selectFields">
        id,from_id,to_id,conversation_id,content,status,create_time
    </sql>
    <select id="getMessageByUserId" resultType="Message">
        select <include refid="selectFields"></include>
        from `message`
        where to_id = #{userId}
    </select>
</mapper>


<mapper namespace="org.example.dao.UserMapper">
    <sql id="selectFileds">
        id,username,password,salt,email,`type`,status,activation_code,header_url,create_time
    </sql>
    <select id="selectById" resultType="User">
        select <include refid="selectFileds"></include>
        from `user`
        where id = #{userId}
    </select>
</mapper>
  • 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class UserDaoTest {
    @Resource
    private UserMapper userMapper;
    @Autowired
    private CommentMapper commentMapper;
    @Autowired
    private MessageMapper messageMapper;

    @Test
    public void testA(){
        //查询user信息
        List<User> list = userMapper.selectById(111);
        list.stream().forEach(System.out::println);
        //查询mapper信息
        List<Comment> comments = commentMapper.getCommentByUserId(111);
        comments.stream().forEach(System.out::println);
        //查询私信信息
        List<Message> messages = messageMapper.getMessageByUserId(111);
        messages.stream().forEach(System.out::println);
    }
}

可以查询到

1.2.测试查询耗时

@Test
    public void testA(){
        long begin1 = System.currentTimeMillis();
        for (int j = 0; j < 160; j++) {
            //查询user信息
            List<User> list = userMapper.selectById(j);
            //查询mapper信息
            List<Comment> comments = commentMapper.getCommentByUserId(j);
            //查询私信信息
            List<Message> messages = messageMapper.getMessageByUserId(j);
        }
        System.out.println("单线程查询耗时:"+(System.currentTimeMillis()-begin1)+"ms");
        long begin2 = System.currentTimeMillis();
        for(;i<160;i++){
            new Thread(()->{
                //查询user信息
                List<User> list = userMapper.selectById(i);
                //查询mapper信息
                List<Comment> comments = commentMapper.getCommentByUserId(i);
                //查询私信信息
                List<Message> messages = messageMapper.getMessageByUserId(i);
            },"t").start();
        }
        System.out.println("多线程查询耗时"+(System.currentTimeMillis()-begin2)+"ms");
    }

可以看到确实查询耗时减少了,我也是这样想的,嘴贱多说了一句主线程继续往下执行,当然这样也确实是。
在这里插入图片描述
面试官又问主线程如果需要异步线程执行的结果呢?当时脑袋抽了,应该是想让我介绍用Callbable和FutureTask来解决吧:

1.3.主线程需要异步线程的数据

	@Test
    public void testB() throws InterruptedException, ExecutionException {
        long begin2 = System.currentTimeMillis();
        List<FutureTask<Map<String, List>>> ftList = new ArrayList<>();
        for(;i<160;i++){
            final int userId = i;
            FutureTask<Map<String, List>> ft = new FutureTask<>(() -> {
                //查询user信息
                List<User> list = userMapper.selectById(userId);
                //查询mapper信息
                List<Comment> comments = commentMapper.getCommentByUserId(userId);
                //查询私信信息
                List<Message> messages = messageMapper.getMessageByUserId(userId);
                HashMap<String, List> map = new HashMap<>();
                map.put("user", list);
                map.put("comments", comments);
                map.put("messages", messages);
                return map;
            });
            ftList.add(ft);
            Thread t = new Thread(ft,"t"+i);
            t.start();
        }
        for(FutureTask<Map<String, List>> ft: ftList){
            System.out.println(ft.get().get("user"));
        }
        System.out.println("多线程查询耗时"+(System.currentTimeMillis()-begin2)+"ms");
    }

循环中每次循环会创建一个新的栈帧,内置局部变量表、操作数栈
在这里插入图片描述

2.问题2:线程模型

较差的回答

面试官:Java的线程模型
我:是在说JMM吗?
面试官:不是,是用户级线程这些。
我:哦,Java的内存模型是内核级线程:用户级线程=1:1的模型,这种模型线程间的切换需要涉及系统内核态和用户态的切换,是一种较为重量级的实现,而比如go语言支持的协程,是1:n或者m:n的实现,较少涉及用户态/内核态的切换,是一种较为轻量的实现。
面试官:为什么用户态->内核态需要开销
我:需要保存寄存器的数据,恢复寄存器的数据这些。。。

现在的优化

感觉回答的不是很好,这里再次修改一下:
Java的线程模型是1:1的一种实现,较多的涉及用户态/内核态的切换,开销较大。而向go语言提供协程的实现,相当于1:n/m:n模型,这样的实现线程的切换开销更小,不涉及用户态->内核态的切换。Java早期也支持像绿色线程这样的用户级线程实现,但是由于一方面对于程序员来说难以管理,另一方面在特殊情况下难以发挥多核处理的优势,所以取消了它。改为通过线程池、NIO等技术优化内核级线程的重量开销。

3.问题3

错误的回答

下面是错误回答:

面试官:如何自定义类加载器
我:继承ClassLoader,实现findClass/loadClass
面试官:说下findClass和loadClass的区别
我:find遵循双亲委派,load不遵循【错误的说法】

正确的回答

1.如何自定义类加载器?

  • 继承ClassLoader
  • 重写findClass方法
    • 根据是否调用loadClass决定是否遵循双亲委派。
    • 流的方式读取class文件,输出为byte[]数组。
    • 根据defineClass返回类实例。
  • 使用时,调用自定义类加载器的实例的loadClass方法加载文件

当然也可以直接重写loadClass方法,不推荐这样使用,因为loadClass内置了默认的双亲委派机制,重写之后会破坏双亲委派机制。
还有一种更简单的方式直接继承URLClassLoader,构造器中传入路径、父级加载器、工厂即可。

2.loadClass和findClass的区别?
loadClass方法是ClassLoader类的一个重要方法,他的作用是遵循双亲委派来加载类。
findClass方法是ClassLoader类的一个抽象方法,他的作用是查找类路径,并生成class对象。

3.自定义类加载器的作用?
加载项目路径外的类,类的加解密等场景。

代码实现:

  • 使用双亲委派机制
public class MyClassLoader extends ClassLoader{
    //默认ApplicationClassLoader为父类加载器
    public MyClassLoader(){
        super();
    }

    //加载类的路径
    private String path = "D:\\MyJava";//在这个位置新建一个java文件并编译

    //重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] dataByte = new byte[0];
        try {
            dataByte = ClassDataByByte(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return this.defineClass(name, dataByte, 0, dataByte.length);
    }

    //读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组
    private byte[] ClassDataByByte(String name) throws IOException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
        name = name.replace(".", "/"); // 为了定位class文件的位置,将包名的.替换为/
        is = new FileInputStream(new File(path+"\\" + name + ".class"));
        int c = 0;
        while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流
            arrayOutputStream.write(c);
        }
        data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组
        is.close();
        arrayOutputStream.close();
        return data;
    }

    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader();
        Class<?> clazz = myClassLoader.loadClass("hello");
        System.out.println(clazz.getClassLoader());
    }
}

因为这个文件不再项目路径中,所以使用自定义的类加载器加载。
如果直接加载本项目中的类加载器,会使用上级Application加载。

在这里插入图片描述

类加载源码

Launcher.class 该类是java的入口:

在这里插入图片描述

Laucher加载器初始化
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //初始化扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
 
        try {
             // 初始化应用类加载器,并将 扩展类加载器作为参数
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        // 将应用类加载器设置为当前线程的加载器
        Thread.currentThread().setContextClassLoader(this.loader);
        。。。。
        }
 
    }

从这里可以看出:
这三个类加载器的关系是关联【has a】,而非继承【is a】,初始化低级类加载器需要传入上级类加载器。

可以通过直接设置上下文加载器为自定义类加载器破坏双亲委派

  • ExtClassLoader
    发现它加载的路径就是lib/ext下

在这里插入图片描述
在这里插入图片描述

  • AppClassLoader

观察它的加载路径就是项目路径下的
在这里插入图片描述
容易现他们都继承URLClassLoader

URLClassLoader源码【集成好了】
  • findClass源码
    在这里插入图片描述

  • loadClass源码
    在这里插入图片描述

ClassLoader源码【未集成findClass】

URLClassLoader继承SecureClassLoader
而SecureClassLoader继承了ClassLoader

观察源码中的loadClass实现了双亲委派机制。
而源码中的findClass没有实现功能。
在这里插入图片描述

4.问题:yield和sleep的区别

  • 优先级:sleep()方法暂停当前线程后,会给其他线程执行机会,不区分优先级;但yield()方法只会给优先级相同,或优先级更高的线程执行机会。
  • 抛出异常:sleep()方法声明抛出了InterruptedException异常;而yield()方法不抛出。
  • 运行状态区分:sleep()方法会将线程转入阻塞状态,直到经过阻塞时间才会转入就绪状态;而yield()不会将线程转入阻塞状态,它只是强制当前线程进入就绪状态。因此完全有可能某个线程被yield()方法暂停之后,立即再次获得处理器资源被执行。

5.问题:怎么尽量减少线程切换

在 Linux 系统下,可以使用 Linux 内核提供的 **vmstat **命令,来监视 Java 程序运行过程中系统的上下文切换频率

1.设置合适的线程数量,使用线程池管理线程的数量。计算密集型通常是核心数2;IO密集型是核心数2。
2.设置合理的线程优先级:根据任务的重要程度控制线程优先级,避免优先级反转等问题。
2.使用线程安全的容器,ConcurrentHashMap,CopyOnWriteList等减少竞争和锁的使用。
3.控制锁的粒度,避免不必要的锁竞争;可以考虑使用读写锁。
4.减小竞争,通过wait/notify或者阻塞队列,来减少竞争时间片的线程的个数。

在这里插入图片描述

5.问题5:设计模式的原则

单一职责原则
一个类应该只有一个引起它变化的原因。这意味着一个类应该只有一个责任。这有助于保持类的简单性和可维护性。
开放/封闭原则:这意味着应该通过添加新的代码来扩展功能,而不是修改现有的代码。
**里氏替换原则:**子类型必须能够替换其基类型,而不影响程序的正确性。这保证了继承关系的一致性和可靠性。
依赖倒置原则: 依赖于抽象而非具体实现
接口隔离原则:这意味着接口应该小而专注,而不是大而臃肿。
合成/聚合复用原则:首选使用对象组合(合成)或聚合,而不是继承来实现代码复用。这有助于减少类之间的紧耦合关系。
**最少知识原则:**一个对象应该对其他对象有尽可能少的了解,它只应该与它的直接朋友通信。这有助于减少类之间的依赖关系。
迪米特法则:一个软件实体应该尽可能少地与其他实体发生相互作用,它应该仅与其直接的朋友通信。这有助于降低耦合度。

6.Redis的大key排查

排查策略:

  • 使用自带的bigkeys参数查找。
  • scan+memory use。
  • 开源RDB分析工具。
    解决策略:
  • 业务层分割为多个小key
  • 主动删除【scan+del/unlink】
    • scan分批取得少量元素+删除命令
    • unlink命令异步删除
  • 开启惰性删除【被动删除】,异步释放内存,避免阻塞主线程。

7.Redis内存满了怎么处理

  • 设置内存淘汰策略:
  • 删除不需要的键:

8.线程池的大小

对于CPU密集型任务:核心数+1。
对于IO密集型任务: 2*核心数。

9.分页查询

要在MySQL中进行分页查询,每页显示30条数据,并查询第五页的数据,你可以使用LIMIT子句来实现。LIMIT允许你指定查询结果的偏移量(起始行号)和行数(每页显示的数据量)。在这个场景下,你可以设置偏移量为120(因为前四页共有120条数据)以及行数为30,以获取第五页的数据。

10.动态链接静态链接

其他语言
如C大概的原代码到可执行文件过程分为:
预处理、编译为汇编文件、链接成可执行文件。链接发生在第三个阶段。
**静态链接:**编译器编译时直接将代码和库连接成一个单独的可执行文件。
动态链接将代码按照模块拆分为各个独立的模块,程序运行时动态的连接起来雄城一个完整的程序。

Java的话:
源代码先编译成class文件,第二步通过JVM转为可执行文件。
首先类加载分为:加载、链接【验证、准备、解析】、初始化、使用、卸载
静态链接:类加载的链接的解析阶段,将符号引用转换为直接引用。通常是静态、私有、构造、父类、final这类方法
动态链接:程序运行时动态的将符号引用转化为直接引用。通常是除了上述的方法以外的其他的方法。
与那些在编译时进行链接的语言不同,Java类型的加载和链接过程都是在运行的时候进行的,这样虽然在类加载的时候稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值