线程安全
-
对于多线程场景下的计数操作,应该使用
AtomicInteger
或AtomicLong
保证线程安全。不能使用i++。 -
volatile
关键字可以保证线程之间的可见性,但是无法保证操作的原子性。对于volatile i = 0; i++
操作,需要加锁或使用CAS操作来保证原子性。 -
Object.wait
和Object.notify
方法必须在synchronized
关键字修饰的代码块中使用。因为wait和notify的语义是:线程在获取到锁之后暂时释放锁(wait)或通知(notify)其他线程可以竞争锁了。所以必须使用synchronized
关键字修饰。 -
对于中断异常
InterruptedException
,不能忽略,需要积极地响应中断,释放资源并准备退出线程。参考# 聊聊JDK推荐的线程关闭方式 -
Thread.interrupted
方法用于返回当前线程的中断状态,并且在此过程中会清除中断状态。如果只想查询中断状态而不清除中断状态,可以使用Thread.isInterrupted
方法。 -
在使用
new Thread()
创建线程对象时,只是在 Java 中创建了一个对象。执行start
方法才真正在操作系统中创建线程并开始执行线程。Java 的线程是基于操作系统内核级线程实现的,而不是虚拟线程。 -
避免使用
Thread.stop 和 Thread.resume
方法停止线程,因为它们已经被标记为不推荐使用的方法。关于如何优雅地终止线程,请参考 # 聊聊JDK推荐的线程关闭方式 -
ThreadPoolExecutor
在达到CoreSize
线程数时,会将请求放入队列中,如果队列已满,则尝试增加线程数到MaxSize。因此,无界队列永远无法达到MaxSize,并且永远不会触发拒绝策略,但存在OOM的风险。 -
使用
Executors.newFixedThreadPool
创建固定线程数的线程池时,使用无界队列。在极端情况下,可能导致OOM。因此,在使用此API创建线程池时要谨慎。 -
ThreadPoolExecutor
线程池的默认拒绝策略是抛出异常,这会导致请求处理失败。可以使用CallerRunsPolicy
策略,让提交线程处理任务,以避免请求处理失败的情况发生。 -
ExecutorService的execute
方法是同步提交任务的,而submit方法是异步提交任务的。 -
synchronized
是非公平模式的锁。如果需要使用公平模式的锁,可以使用ReentrantLock
。公平模式下,先申请锁的先获取锁;非公平模式下,申请锁时会先尝试CAS加锁,如果加锁失败则排队等待获取锁,这样提高了锁切换的速度,但失去了公平性。 -
synchronized
只支持阻塞模式的锁申请。如果需要非阻塞模式,请使用ReentrantLock
。 -
在使用
CountDownLatch
时,即使是在异常场景下,也要确保进行countDown
操作,否则等待线程的 await 方法可能永远无法被唤醒。建议countDown
操作放到 finally 代码块中。 -
Thread.sleep
是一个用于将当前线程置为阻塞状态的方法,它会暂时让出CPU的调度权,直到指定的时间到达或者被Thread.interrupt()
方法中断。相比之下,Thread.yield
方法仅仅是让出CPU的调度权,但操作系统下一次调度时会正常考虑该线程。另一方面,Object.wait()
方法通常用于在synchronized
同步代码块中,它会暂时释放锁,并等待其他持有该锁的线程来唤醒它。在此期间,线程进入阻塞状态。Thread.join
是当前线程等待子线程运行结束才能继续执行 -
一个线程发生OOM ,只会导致这个线程抛出ERRO,进行退出执行。不会影响其他线程。如果想要在OOM后,退出进程,需要添加JVM 启动参数。【
-XX:+HeapDumpOnOutOfMemoryError
参数表示当JVM发生OOM时,自动生成DUMP文件】;【-XX:+ExitOnOutOfMemoryError
在程序发生OOM异常时,强制退出】; 【XX:+CrashOnOutOfMemoryError
在程序发生OOM异常时,强制退出,并生成Crash日志】;
Java 语言基础 易错知识点
Integer
包装器类型若为 null 值转换为 int 会产生空指针异常。应先判断包装器类型是否为非 null,再转换为基本类型。- 在使用
Double 和 Float
时会丢失精度,例如 1.0 - 0.9 期望是0.1,结果是 0.09999999999999998。因此,在涉及金额计算时,最好将金额转换为整型,例如将 1.1 元表示为 110 分。
double result = 1.0 - 0.9;
System.out.println(result);//0.09999999999999998
3. 在 Java 8 中,需要小心使用泛型和重载,以避免类型转换异常。以下代码会抛出异常 String.valueOf(getSimpleString())
。如果指定具体类型,则不会出错,例如 String.valueOf((String)getSimpleString())
。更详细的内容请参考 # 升级Java 8以后,上线就翻车了。这次是泛型的锅
@Test
public void test2() {
System.out.println(String.valueOf(getSimpleString()));
}
public static <T> T getSimpleString() {
return (T) "121";
}
4. break
默认情况下可以跳出当前层的循环。然而,若需要跳出最外层的循环,我们需要为for循环取一个名字,并且在break
语句中指定要跳出哪一层的循环。举个例子,下面的例子中使用了break loop1
,用来指定跳出最外层的循环。
int m = 10;
int n = 4;
loop1:
for (int i = 0; i < m; i++) {
loop2:
for (int j = 0; j < n; j++) {
break loop1;
}
System.out.println("i:" + i);
}
5. 内部类中无法直接修改外部引用的值,此时我们可以采用数组作为容器来传递数值。若在内部类中需要修改suc的值,我们可以将其声明为一个数组类型,并通过修改该数组元素的值来实现目的。
int[] suc = new int[1];
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
i++;
suc[0] = i;
}
}).start();
6. 为了判断字符串是否相等,我们应该使用String.equals或者Objects.equals
方法,而不能简单地使用 == 运算符。
-
在处理包装器类型
Integer、Long
时,不建议使用"=="来判断它们是否相等,最好采用Objects.equals方法来判断相等。因为"=="判断相等的方式是根据对象的内存地址来判断的。(尽管在-127到128的范围内,由于对象池缓存的存在,可以使用"=="进行判等,但为了统一起见,最好还是使用Objects.equals来判断相等) -
为了避免出现乱码,调用
String.getBytes(Charset.forName("utf-8"))
时应明确指定字符集,因为不同的操作系统环境有不同的默认字符集。 -
使用
StringUtils.isEmpty
判断字符串是否为null,使用StringUtils.isBlank
判断字符串是否为空白或者为null。 -
String是不可变对象,调用
substring、replace
等方法不会修改原始的String对象。如果需要修改字符串,应该使用线程安全的StringBuilder。在单线程环境下,也可以使用StringBuffer。 -
枚举类型的相等判断可以使用"=="。例如,
statusEnum == StatusEnum.Success
可以判断枚举类型statusEnum
是否等于StatusEnum.Success
。 -
Java中的基本类型和引用类型都是通过值传递的。因此以下代码尝试交换a、b两个基本类型的值的方法是错误的。
public void testSwap() {
int a = 1;
int b = 2;
swap(a, b); // a、b值不会变化。
}
public void swap(int a, int b) {
int c = a;
a = b;
b = c;
}
13. 不建议在 boolean
类型的命名中使用 is 前缀,因为 getter 方法默认会自动带上 is 前缀。如果使用 is 前缀,可能会导致 JSON 等解析异常。
-
在使用
switch
语句时,只能使用常量,不能使用变量。 -
使用 switch case 语句时,记得和 break 关键字搭配使用。执行完一个 case 后的语句后,流程控制会转移到下一个 case 继续执行。如果你只想执行这一个 case 语句,不想执行其他 case,那么就需要在这个 case 语句后面加上 break,以跳出 switch 语句。
-
如果要使用
Object.clone
方法,需要确保类型实现了Cloneable
接口,否则调用clone
方法会抛出CloneNotSupportedException
异常。 -
Object.clone
方法属于浅层拷贝,即基础数据类型会被复制,而引用类型则是共享的。如果需要实现深层拷贝,可以考虑使用 BeanUtils.copyProperties 方法。 -
在使用 List 进行 for 循环遍历时,不能在遍历期间直接删除元素。如果需要删除元素,可以使用 stream 表达式进行过滤,或者使用
iterator
删除元素。
Iterator<Integer> it = numbers.iterator();
while(it.hasNext()) {
Integer i = it.next();
if(i < 10) {
it.remove(); // 删除小于 10 的元素
}
}
19. 在服务正式运行时,为了记录日志,我们不能使用e.printStackTrace()
和System.out.println
打印日志,而是应该使用log4j Logger
。因为标准输入输出会被重定向,使用日志框架将请求记录到日志文件中更为合适。
-
应该谨慎处理异常捕获,避免直接忽略异常而频繁发生错误。
-
对于
RuntimeException
等异常,我们也应该记录异常的根因。我们应该使用new RuntimeException("构建xxx出现异常", e)
的方式记录根异常,而不是仅仅使用new RuntimeException("构建xxx出现异常")
。这样的做法可以保留异常堆栈信息,有助于问题的排查。
try{
//业务逻辑
}catch(Exception e){
throw new RuntimeException("构建xxx出现异常", e);
}
22. Error
和Exception
都属于throwable
类型。如果在捕获异常时使用catch Exception
,那么遇到Error是无法被捕获的。常见的Error有OutOfMemoryError
、NoSuchMethodError
等。如果想要捕获Error类型的异常,需要使用catch Throwable
,这样就可以捕获到Exception
和Error
。
- 当我们使用finally代码块中的return语句时,try代码块中的return语句也会同时执行。最终,返回的值将会是finally代码块中return的值。
@Test
public void testTry() {
System.out.println("getValue:" + getValue());
}
public int getValue() {
int i = 0;
try {
System.out.println("try");
i = 1;
return i;
} finally {
i = 2;
return i;
}
}
执行结果getValue
返回 2
24. 当try代码块中使用return语句返回i时,无论finally块中如何修改i,都不会影响返回值。
@Test
public void testTry() {
System.out.println("getValue:" + getValue());
}
public int getValue() {
int i = 0;
try {
System.out.println("try");
i = 1;
return i;
} finally {
i = 2;
}
}
getValue()函数的执行结果为1。尽管 finllay
修改了 i
的值,但并不会对 try
模块的返回值产生影响。这是因为返回值在 return
语句执行后会被暂存在栈中,并且后续对其再次的修改也不会对该返回值产生影响,除非在 finllay
中再次使用 return
语句来覆盖返回值。
Jdk和其他类库易错知识点
-
在使用
HashMap
时,我们可能会遇到哈希冲突的问题。因为两个元素可以拥有相同的hashCode
,但是它们的equals方法却不能相等。为了解决这个问题,我们需要重写键值的hashCode
和equals
方法,确保当两个元素相等时,它们的hashCode
和equals
都应该相等。 -
HashMap是非线程安全的。如果我们需要在多线程环境下使用
HashMap
,可以考虑使用ConcurrentHashMap
这个线程安全类。 -
HashMap
的values和keySet方法返回的是无序集合。如果我们希望按照插入顺序排序,可以使用LinkedHashMap
。 -
对于Set集合,其中的对象也是无序的,遍历Set集合的结果与插入Set集合的顺序并不相同。如果我们希望有序,可以使用
LinkedHashSet
。 -
HashMap可以插入key、value都是null的元素,并且
containsKey(null)
会返回true。这是需要我们在使用HashMap时要注意的一点。
Map<String, String> map = new HashMap();
map.put(null, null);
System.out.println(map.containsKey(null));
containsKey 返回 true
-
ConcurrentHashMap
的 key 和 value 都不能为 null,否则会出现 NPE 错误。 -
当我们使用
Arrays.asList
方法创建一个List对象时,这个List对象是不允许添加add、清理clear元素的。 -
使用ArrayList的
subList
方法创建子List,这个子List会与父List共享相同的底层存储空间。因此,在子List中添加元素会对父级List产生影响,即父级List也会被修改。 -
在使用Java中的
List.toArray
方法将List转为数组时,我们需要指定转换后的数组类型。这个方法可以将一个List对象转换为其对应的数组形式。
List<String> list = Lists.newArrayList("1", "2", "3");
String[] array = (String[]) list.toArray();
System.out.println(array);
此时强转为 String[] 会出现异常。
需要指定类型,将其转换为具体的数组,因为List在运行时会进行类型擦除,所以需要重新指定入参的类型。可以使用
list.toArray(new String[0])
进行转换。在这种情况下,不需要给String数组指定长度,指定0即可,toArray
方法会自动处理。
List<String> list = Lists.newArrayList("1", "2", "3");
String[] array = list.toArray(new String[0]);
System.out.println(array);
34. ArrayList
是一个非线程安全的集合类,可以使用Collections.synchronizedList
方法将其转换为线程安全的List。但是,由于get方法也使用synchronize
关键字修饰,这会严重影响并发读取。可以考虑使用CopyOnWriteArrayList
来代替。
-
使用
ArrayList.subList
方法并不会修改原始对象,需要通过返回值获取subList。 -
在使用
Optional.of
方法时,如果参数为null,会抛出异常。如果允许参数为null,请使用Optional.ofNullable
方法。 -
SimpleDateFormat
是一个非线程安全的类,可以使用线程局部变量避免多线程访问的问题。或者可以使用JDK 8中引入的线程安全的Formatter类DateTimeFormatter
来代替,例如DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss")
。 -
慎用
Files.readAllBytes
方法一次性读取磁盘文件,这可能导致内存溢出的问题。建议使用 File 类和BufferedReader
类的缓冲区循环读取文件,以避免一次性加载过多数据。 -
在比较大小时,可以使用
Comparable
接口。该接口定义了compareTo
方法,当当前对象大于目标对象时,返回正数,即按升序排序;当当前对象小于目标对象时,返回负数,即按降序排序。 -
在使用
Class.isAssignableFrom
方法时,如果子类在后面,父类在前面,则返回 true;否则返回 false。
if (Object.class.isAssignableFrom(String.class)) {
System.out.println("true");
}
41. Redis 客户端 Jedis
不具备线程安全性,因此需要通过使用 JedisPool
来进行管理。
-
对于使用
Spring Async、Transactional
等注解的方法,需要注意它们应该被调用于不同的类中。如果在本类中调用这些注解,其效果将无法生效。 -
涉及到IO请求的连接一定能够要及时关闭。
-
Java 泛型的实际类型在编译时会被擦除,因此无法在运行时获取其类型参数的具体类型。在运行时,泛型类型会统一被擦除为Object类型。(如果类型参数被定义为 T extends Fruit,则默认为Fruit类型)。泛型的好处是在编译的时候检查类型安全
List<String> list = new ArrayList<String>();
System.out.println(list instanceof List<String>)
以上代码无法编译通过,JVM中并不存在List<String>.class
或是List<Integer>.class
,而只有List.class。
我的开源项目
最后夹带一点私货,五阳最近花了3个月的时间完成一个开源项目。
开源3周以来,已有近 230 多个关注和Fork
Gitee:gitee.com/juejinwuyan…
GitHub github.com/juejin-wuya…
开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是
- 功能堆砌,大部分功能用不上,需要大量裁剪;
- 逻辑差异点较多,需要大量修改;
- 功能之间耦合,难以独立替换某个功能。
由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显
所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?
- 他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
- 他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
- 他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响
开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?
memberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!
具体介绍可参见
Gitee开源地址:gitee.com/juejinwuyan…
GitHub开源地址 : github.com/juejin-wuya…
在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。
- Mybatis、Mybatis-plus 集成多数据源
- Sharding-jdbc 多数据源分库分表
- redis/redisson 缓存
- Apollo 分布式配置中心
- Spring Cloud 微服务全家桶
- RabbitMq 消息队列
- H2 内存数据库
- Swagger + Lombok + MapStruct
同时你也可以学习到以下组件的实现原理
- 流程引擎的实现原理
- 扩展点引擎实现原理
- 分布式重试组件实现原理
- 通用日志组件实现原理 参考:juejin.cn/post/740727…
- 商品库存实现原理: 参考:juejin.cn/post/731377…
- 分布式锁组件: 参考:
- Redis Lua的使用
- Spring 上下文工具类 参考: juejin.cn/post/746927…