如何做好集团网站建设,口碑营销的产品,wordpress 作者 链接,天水网络推广1. 获取单例对象需要保证线程安全
我们在获取单例对象的时候#xff0c;要确保线性安全哈。
比如双重检查锁定#xff08;Double-Checked Locking#xff09;的单例模式#xff0c;就是一个经典案例#xff0c;你在获取单实例对象的时候#xff0c;就需要保证线性安全要确保线性安全哈。
比如双重检查锁定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;}
}大家在写资源驱动类、工具类、单例工厂类的时候都需要注意获取单例对象需要保证线程安全。 2. 创建线程或线程池时请指定有意义的线程名称方便出错时回溯。
使用线程池时如果没有给线程池一个有意义的名称将不好排查回溯问题。
反例
public class TianLuoBoyThreadTest {public static void main(String[] args) throws Exception {ThreadPoolExecutor executorOne new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new ArrayBlockingQueueRunnable(20));executorOne.execute(()-{System.out.println(谢谢谢谢谢谢);throw new NullPointerException();});}
}运行结果
Exception in thread pool-1-thread-1 java.lang.NullPointerExceptionat 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 ArrayBlockingQueueRunnable(20),new CustomizableThreadFactory(TianluoBoy-Thread-pool));executorOne.execute(()-{System.out.println(谢谢谢谢谢谢);throw new NullPointerException();});}
}3. 线程资源必须通过线程池提供不允许在应用中自行显式创建线程。
日常开发中,我们经常需要使用到多线程。线程资源要求通过线程池提供而不允许显式创建线程。
因为如果显示创建线程可能造成系统创建大量同类线程而导致消耗完内存。使用线程池主要有这些好处 帮我们管理线程避免增加创建线程和销毁线程的资源损耗。因为线程其实也是一个对象创建一个对象需要经过类加载过程销毁一个对象需要走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;}Overridepublic 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;}Overridepublic void run() {System.out.println(Thread.currentThread().getName() executing taskName);// 执行任务的具体逻辑}
}4. SimpleDateFormat 是线程不安全的类一般不要定义为 static 变量如果定义为static必须加锁
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);}}
}5. 线程池不允许使用 Executors 去创建而是通过 ThreadPoolExecutor 的方式
这是因为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 exceededat 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 创建的时候需要明确配置线程池参数可以避免资源耗尽风险。 6. 高并发的时候同步调用要考虑锁的粒度。
高并发时同步调用应该去考量锁的性能损耗。能用无锁数据结构就不要用锁能锁区块就不要锁整个方法体能用对象锁就不要用类锁。
通俗易懂讲就是在保证数据安全的情况下尽可能使加锁的代码块工作量尽可能的小。因为在高并发场景为了防止超卖等情况我们经常需要加锁来保护共享资源。但是如果加锁的粒度过粗是很影响接口性能的。 再比如我们不推荐在加锁的代码块中再调用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();
}7. HashMap 在容量不够进行 resize 时由于高并发可能出现死链导致 CPU 飙升。
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 MapString, 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来提高并发性能减小锁的粒度降低了并发冲突的可能性。 8.使用 CountDownLatch 进行异步转同步操作每个线程退出前必须调用 countDown方法。
使用 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();}}
} 9. 多线程并行处理定时任务时Timer 运行多个 TimeTask 时只要其中之一没有捕获抛出的异常其它任务便会自动终止运行。
在 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() {Overridepublic void run() {System.out.println(Task 1 is running...);throw new RuntimeException(Exception in Task 1);}};// 任务2TimerTask task2 new TimerTask() {Overridepublic 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);}
}10. 避免 Random 实例被多线程使用虽然共享该实例是线程安全的但会因竞争同一seed 导致的性能下降。
虽然 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 之前需要编码保证每个线程持有一个实例。 11.并发修改同一记录时避免更新丢失需要加锁。
并发修改同一记录时避免更新丢失需要加锁。要么在应用层加锁要么在缓存加锁要么在数据库层使用乐观锁使用 version作为更新依据。
如果每次访问冲突概率小于20%推荐使用乐观锁因为证明并发不是很高。否则使用悲观锁。乐观锁的重试次数不得小于3 次。 12. 对多个资源、数据库表、对象同时加锁时需要保持一致的加锁顺序否则可能会造成死锁。
线程一需要对表 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再获取 lockBsynchronized (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();}
}