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应用程序提供高度的灵活性。