大家好,我是田螺。最近看了一下阿里巴巴Java开发手册,整理了并发处理的12条规范,并且都给出对应代码的例子,大家看完一定会有收获的。
我们在获取单例对象的时候,要确保线性安全哈。
比如双重检查锁定(Double-Checked Locking)的单例模式,就是一个经典案例,你在获取单实例对象的时候,就需要保证线性安全,比如加synchronized
确保现象安全,代码如下:
public class Singleton {
private volatile static Singleton instance;
private Singleton() { }
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
大家在写资源驱动类、工具类、单例工厂类的时候,都需要注意获取单例对象需要保证线程安全哈。
使用线程池时,如果没有给线程池一个有意义的名称,将不好排查回溯问题。
反例:
public class TianLuoBoyThreadTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20));
executorOne.execute(()->{
System.out.println("关注公众号:捡田螺的小男孩");
throw new NullPointerException();
});
}
}
运行结果:
关注公众号:捡田螺的小男孩
Exception in thread "pool-1-thread-1" java.lang.NullPointerException
at com.example.dto.TianLuoBoyThreadTest.lambda$main$0(ThreadTest.java:17)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
可以发现,默认打印的线程池名字是pool-1-thread-1
,如果排查问题起来,并不友好。因此建议大家给自己线程池自定义个容易识别的名字。其实用CustomizableThreadFactory
即可,正例如下:
public class ThreadTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,
TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),
new CustomizableThreadFactory("TianluoBoy-Thread-pool"));
executorOne.execute(()->{
System.out.println("关注公众号:捡田螺的小男孩");
throw new NullPointerException();
});
}
}
日常开发中,我们经常需要使用到多线程。线程资源要求通过线程池提供,而不允许显式创建线程。
因为如果显示创建线程,可能造成系统创建大量同类线程而导致消耗完内存。使用线程池主要有这些好处:
GC
垃圾回收流程,都是需要资源开销的。反例(显式创建线程):
public class DirectThreadCreation {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new WorkerThread("Task " + i));
thread.start();
}
}
}
class WorkerThread implements Runnable {
private String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " executing " + taskName);
// 执行任务的具体逻辑
}
}
正例(线程池):
public class ThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);
// 提交任务给线程池执行
for (int i = 0; i < 10; i++) {
Runnable task = new WorkerThread("Task " + i);
executor.execute(task);
}
// 关闭线程池
executor.shutdown();
}
}
class WorkerThread implements Runnable {
private String taskName;
public WorkerThread(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " executing " + taskName);
// 执行任务的具体逻辑
}
}
SimpleDateFormat 是线程不安全的类,因为它内部维护了一个 Calendar 实例,而 Calendar 不是线程安全的。因此,在多线程环境下,如果多个线程共享一个 SimpleDateFormat 实例,可能会导致并发问题。
如果需要在多线程环境下使用SimpleDateFormat
,可以通过加锁的方式来确保线程安全。
public class SafeDateFormatExample {
private static final Object lock = new Object();
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
Runnable task = () -> {
try {
parseAndPrintDate("2022-01-01 12:30:45");
} catch (ParseException e) {
e.printStackTrace();
}
};
// 启动多个线程来同时解析日期
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
private static void parseAndPrintDate(String dateString) throws ParseException {
synchronized (lock) {
Date date = sdf.parse(dateString);
System.out.println(Thread.currentThread().getName() + ": Parsed date: " + date);
}
}
}
这是因为Executors 返回的线程池:
FixedThreadPool
允许的请求队列长度为 Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致 OOM
CachedThreadPool
:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM
。反例:
/**
* 公众号:捡田螺的小男孩
*/
public class NewFixedTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
使用 Executors的newFixedThreadPool
创建的线程池,是会有坑的,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。运行一下以上代码,出现了OOM。
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
at com.example.dto.NewFixedTest.main(NewFixedTest.java:14)
这是因为Executors
的newFixedThreadPool
使用了无界的阻塞队列的LinkedBlockingQueue
,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo代码设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终出现OOM。
而ThreadPoolExecutor
创建的时候,需要明确配置线程池参数,可以避免资源耗尽风险。
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
通俗易懂讲就是,在保证数据安全的情况下,尽可能使加锁的代码块工作量尽可能的小。因为在高并发场景,为了防止超卖等情况,我们经常需要加锁来保护共享资源。但是,如果加锁的粒度过粗,是很影响接口性能的。 再比如,我们不推荐在加锁的代码块中,再调用RPC
方法。
对于锁的粒度,我给大家个代码例子哈:
比如,在业务代码中,有一个ArrayList
因为涉及到多线程操作,所以需要加锁操作,假设刚好又有一段比较耗时的操作(代码中的slowNotShare
方法)不涉及线程安全问题。反例加锁,就是一锅端,全锁住:
//不涉及共享资源的慢方法
private void slowNotShare() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
}
}
//错误的加锁方法
public int wrong() {
long beginTime = System.currentTimeMillis();
IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {
//加锁粒度太粗了,slowNotShare其实不涉及共享资源
synchronized (this) {
slowNotShare();
data.add(i);
}
});
log.info("cosume time:{}", System.currentTimeMillis() - beginTime);
return data.size();
}
正例:
public int right() {
long beginTime = System.currentTimeMillis();
IntStream.rangeClosed(1, 10000).parallel().forEach(i -> {
slowNotShare();//可以不加锁
//只对List这部分加锁
synchronized (data) {
data.add(i);
}
});
log.info("cosume time:{}", System.currentTimeMillis() - beginTime);
return data.size();
}
HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升。在开发过程中可以使用其它数据结构或加锁来规避此风险。
在普通的 HashMap
中,可能出现死锁的场景通常与多线程并发修改 HashMap 的结构有关。这种情况下,多个线程同时对 HashMap 进行插入、删除等操作,可能导致链表形成环,进而导致死锁。
比如这个例子,演示了多线程同时对 HashMap 进行修改可能导致死锁的情况:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
public class HashMapDeadlockExample {
public static void main(String[] args) throws InterruptedException {
final Map<String, String> hashMap = new HashMap<>();
final CountDownLatch latch = new CountDownLatch(2);
// 线程1向HashMap中插入元素
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
hashMap.put(String.valueOf(i), String.valueOf(i));
}
latch.countDown();
});
// 线程2删除HashMap中的元素
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 100000; i++) {
hashMap.remove(String.valueOf(i));
}
latch.countDown();
});
thread1.start();
thread2.start();
// 等待两个线程执行完成
latch.await();
// 打印HashMap的大小
System.out.println("HashMap size: " + hashMap.size());
}
}
解决或规避这个问题的方式可以使用使用ConcurrentHashMap
:ConcurrentHashMap
是 HashMap
的线程安全版本,它使用了分段锁(Segment)来提高并发性能,减小锁的粒度,降低了并发冲突的可能性。
使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown
方法,线程执行代码注意 catch
异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
CountDownLatch
是一个多线程同步工具,它的作用是允许一个或多个线程等待其他线程完成操作。在这里,你想要使用 CountDownLatch
实现异步转同步操作,确保每个线程退出前都调用countDown
方法。给个代码示例,演示了如何使用 CountDownLatch 实现这种同步:
import java.util.concurrent.CountDownLatch;
public class AsyncToSyncExample {
public static void main(String[] args) throws InterruptedException {
int numThreads = 3; // 假设有3个线程
// 创建一个 CountDownLatch,计数器初始化为线程数量
CountDownLatch latch = new CountDownLatch(numThreads);
// 启动多个线程
for (int i = 0; i < numThreads; i++) {
Thread thread = new Thread(() -> {
try {
// 线程执行的业务逻辑
doSomeWork();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 无论如何,都需要调用 countDown 方法
latch.countDown();
}
});
thread.start();
}
// 等待所有线程完成,最多等待5秒(超时时间可以根据实际情况调整)
if (!latch.await(5000, java.util.concurrent.TimeUnit.MILLISECONDS)) {
// 超时处理逻辑
System.out.println("Timeout while waiting for threads to finish.");
} else {
// 所有线程执行完成后的逻辑
System.out.println("All threads have finished their work.");
}
}
private static void doSomeWork() {
// 模拟线程执行的业务逻辑
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " has finished its work.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
在 Timer 运行多个 TimerTask 时,如果其中一个 TimerTask 抛出了未捕获的异常,将导致整个 Timer 终止,而未抛出异常的任务也将停止执行。这是因为 Timer 的设计导致一个任务的异常会影响到整个 Timer 的执行。代码如下:
import java.util.Timer;
import java.util.TimerTask;
public class TimerTaskExample {
public static void main(String[] args) {
Timer timer = new Timer();
// 任务1,抛出异常
TimerTask task1 = new TimerTask() {
@Override
public void run() {
System.out.println("Task 1 is running...");
throw new RuntimeException("Exception in Task 1");
}
};
// 任务2
TimerTask task2 = new TimerTask() {
@Override
public void run() {
System.out.println("Task 2 is running...");
}
};
// 安排任务1和任务2执行
timer.schedule(task1, 0, 1000);
timer.schedule(task2, 0, 1000);
}
}
使用 ScheduledExecutorService
则没有这个问题:
public class ScheduledExecutorExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// 任务1,每隔2秒执行一次,可能抛出异常
scheduler.scheduleAtFixedRate(() -> {
try {
System.out.println("Task 1 is running...");
throw new RuntimeException("Exception in Task 1");
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 2, TimeUnit.SECONDS);
// 任务2,每隔3秒执行一次
scheduler.scheduleAtFixedRate(() -> {
try {
System.out.println("Task 2 is running...");
} catch (Exception e) {
e.printStackTrace();
}
}, 0, 3, TimeUnit.SECONDS);
}
}
虽然 Random
实例的方法是线程安全的,但是当多个线程共享相同的Random
实例并竞争相同的 seed
时,可能会因为竞争而导致性能下降。这是因为 Random 使用一个原子变量来维护其内部状态,当多个线程同时调用 nextInt
等方法时,可能会发生竞争,从而影响性能。
大家可以看下这个例子哈:
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class SharedRandomPerformanceExample {
public static void main(String[] args) throws InterruptedException {
int numThreads = 10;
int iterations = 1000000;
// 共享一个 Random 实例
Random sharedRandom = new Random();
// 使用多线程执行任务
ExecutorService executorService = Executors.newFixedThreadPool(numThreads);
for (int i = 0; i < numThreads; i++) {
executorService.execute(() -> {
for (int j = 0; j < iterations; j++) {
int randomNumber = sharedRandom.nextInt();
// 模拟使用随机数的业务逻辑
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
}
}
在这个例子中,多个线程共享相同的 Random
实例 sharedRandom
,并且在循环中调用 nextInt
方法。由于 Random
内部使用CAS
操作来维护其状态,多个线程可能会竞争同一 seed
导致性能下降。
如果你希望避免这种竞争,可以考虑为每个线程创建独立的 Random 实例,以确保每个线程都有自己的状态。在 JDK7
之后,可以直接使用 API ThreadLocalRandom
,而在 JDK7 之前,需要编码保证每个线程持有一个实例。
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version
作为更新依据。
如果每次访问冲突概率小于20%
,推荐使用乐观锁,因为证明并发不是很高。否则使用悲观锁。乐观锁的重试次数不得小于3 次。
线程一需要对表 A、B、C
依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C
,否则可能出现死锁。在多线程环境中,当需要对多个资源、数据库表或对象同时加锁时,为了避免死锁,所有线程必须保持一致的加锁顺序。这就是所谓的"锁顺序规范"。
大家有兴趣可以看下这个例子哈,两个线程按照相同的顺序加锁以避免死锁:
public class DeadlockExample {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread 1 acquired lockA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("Thread 1 acquired lockB");
}
}
});
Thread thread2 = new Thread(() -> {
// 保持一致的加锁顺序,先尝试获取 lockA,再获取 lockB
synchronized (lockA) {
System.out.println("Thread 2 acquired lockA");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockB) {
System.out.println("Thread 2 acquired lockB");
}
}
});
thread1.start();
thread2.start();
}
}
本文由微信公众号捡田螺的小男孩原创,哈喽比特收录。
文章来源:https://mp.weixin.qq.com/s/crWQn2bT3AqtfmnK9PUKMQ
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。