线程安全
-
对于多线程场景下的计数操作,应该使用
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 分。
ini
double result = 1.0 - 0.9;
System.out.println(result);//0.09999999999999998
- 在 Java 8 中,需要小心使用泛型和重载,以避免类型转换异常。以下代码会抛出异常
String.valueOf(getSimpleString())
。如果指定具体类型,则不会出错,例如String.valueOf((String)getSimpleString())
。更详细的内容请参考 # 升级Java 8以后,上线就翻车了。这次是泛型的锅
typescript
@Test
public void test2() {
System.out.println(String.valueOf(getSimpleString()));
}
public static <T> T getSimpleString() {
return (T) "121";
}
break
默认情况下可以跳出当前层的循环。然而,若需要跳出最外层的循环,我们需要为for循环取一个名字,并且在break
语句中指定要跳出哪一层的循环。举个例子,下面的例子中使用了break loop1
,用来指定跳出最外层的循环。
ini
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);
}
- 内部类中无法直接修改外部引用的值,此时我们可以采用数组作为容器来传递数值。若在内部类中需要修改suc的值,我们可以将其声明为一个数组类型,并通过修改该数组元素的值来实现目的。
ini
int[] suc = new int[1];
new Thread(new Runnable() {
@Override
public void run() {
int i = 0;
i++;
suc[0] = i;
}
}).start();
-
为了判断字符串是否相等,我们应该使用
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两个基本类型的值的方法是错误的。
ini
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;
}
-
不建议在
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
删除元素。
scss
Iterator<Integer> it = numbers.iterator();
while(it.hasNext()) {
Integer i = it.next();
if(i < 10) {
it.remove(); // 删除小于 10 的元素
}
}
-
在服务正式运行时,为了记录日志,我们不能使用
e.printStackTrace()
和System.out.println
打印日志,而是应该使用log4j Logger
。因为标准输入输出会被重定向,使用日志框架将请求记录到日志文件中更为合适。 -
应该谨慎处理异常捕获,避免直接忽略异常而频繁发生错误。
-
对于
RuntimeException
等异常,我们也应该记录异常的根因。我们应该使用new RuntimeException("构建xxx出现异常", e)
的方式记录根异常,而不是仅仅使用new RuntimeException("构建xxx出现异常")
。这样的做法可以保留异常堆栈信息,有助于问题的排查。
php
try{
//业务逻辑
}catch(Exception e){
throw new RuntimeException("构建xxx出现异常", e);
}
-
Error
和Exception
都属于throwable
类型。如果在捕获异常时使用catch Exception
,那么遇到Error是无法被捕获的。常见的Error有OutOfMemoryError
、NoSuchMethodError
等。如果想要捕获Error类型的异常,需要使用catch Throwable
,这样就可以捕获到Exception
和Error
。 -
当我们使用finally代码块中的return语句时,try代码块中的return语句也会同时执行。最终,返回的值将会是finally代码块中return的值。
csharp
@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,都不会影响返回值。
csharp
@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时要注意的一点。
javascript
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对象转换为其对应的数组形式。
ini
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
方法会自动处理。
ini
List<String> list = Lists.newArrayList("1", "2", "3");
String[] array = list.toArray(new String[0]);
System.out.println(array);
-
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。
vbnet
if (Object.class.isAssignableFrom(String.class)) {
System.out.println("true");
}
-
Redis 客户端
Jedis
不具备线程安全性,因此需要通过使用JedisPool
来进行管理。 -
对于使用
Spring Async、Transactional
等注解的方法,需要注意它们应该被调用于不同的类中。如果在本类中调用这些注解,其效果将无法生效。 -
涉及到IO请求的连接一定能够要及时关闭。
-
Java 泛型的实际类型在编译时会被擦除,因此无法在运行时获取其类型参数的具体类型。在运行时,泛型类型会统一被擦除为Object类型。(如果类型参数被定义为 T extends Fruit,则默认为Fruit类型)。泛型的好处是在编译的时候检查类型安全
javascript
List<String> list = new ArrayList<String>();
System.out.println(list instanceof List<String>)
以上代码无法编译通过,JVM中并不存在List<String>.class
或是List<Integer>.class
,而只有List.class。