Java多线程初级学习总结
- 一.Java创建线程的几种方法
- 二.Thread类常用方法
- 三.sleep方法与wait方法的区别
- 四.start方法与run方法的区别
- 五.Synchronized与Lock的区别
- 补充.并发与并行
- 六.线程的生命周期(码出高效)
- 七.多线程的使用
- 八.守护线程
- 九.线程4种终止方法
- 十.Volatile总结
- 十一.AQS
- 十二. 三大方法、七大参数、四种拒绝策略
- 十三. CAS
- 十四. 线程池
- 十五. 解决线程安全的办法
- 十六. synchronized的使用
- 十七. 悲观锁、乐观锁、互斥锁、共享锁、读写锁、分段锁概念解析
- 十八.Java提供常见的锁
- 十九.ReentrantLock实战案例
- 二十. ReentrantLock 与synchronized的区别
- 二十一.ReadWriteLock实战案例
- 二十二.ReentrantLock是怎样提供公平锁、非公平锁、可中断锁、可超时锁的
- 二十三.StampedLock 锁实战案例
- 二十四.Semaphore实战
- 二十五.CountDownLatch实战
- 二十六. Java解决线程安全问题的办法有以下几种:
- 二十七.符合阿里巴巴Java代码规范的线程池实战
- 附录
一.Java创建线程的几种方法
- 继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println("你好");
}
}
------------------------------------------------
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
- 实现Runnable接口
public class MyThread1 implements Runnable {
@Override
public void run() {
System.out.println("你好");
}
}
--------------------------------------------------
public class MyThread1Test {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
Thread thread = new Thread(myThread1);
thread.start();
}
}
- 实现Callable接口
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
return "hello";
}
}
--------------------------------------------------
public class Thread3Test {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread3 myThread3 = new MyThread3();
FutureTask futureTask = new FutureTask(myThread3);
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
- 使用线程池
public class TestCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "通过线程池实现多线程";
}
}
--------------------------------------------------
public class NewTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable testCallable = new TestCallable();
ExecutorService executorService = Executors.newFixedThreadPool(3);
// 提交任务
Future<String> future = executorService.submit(testCallable);
String result = future.get();
System.out.println(result);
}
}
二.Thread类常用方法
- join方法
A线程调用B线程的Join方法,将会使A等待B执行直到等待B线程终止,如果传入time参数将会使A等待B执行time时间,如果time时间到达,将会切换进A线程继续执行A线程。
在Java中,Thread类的join()方法可以用来等待一个线程的执行完成,确保当前线程在继续运行前,必须等待被加入线程执行完成后,才能继续执行。
下面是一个示例代码,演示了join()方法的具体使用:
public class JoinThreadExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "Thread 1");
Thread t2 = new Thread(new MyRunnable(), "Thread 2");
Thread t3 = new Thread(new MyRunnable(), "Thread 3");
t1.start();
t2.start();
t3.start();
try {
//等待t1线程执行完毕
t1.join();
//等待t2线程执行完毕
t2.join();
//等待t3线程执行完毕
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("All threads are finished.");
}
static class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " started.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " finished.");
}
}
}
在该代码中,创建了三个线程t1、t2和t3并启动它们。接着在主线程中调用t1.join()、t2.join()和t3.join()方法,表示主线程必须等待t1、t2和t3三个线程都执行完毕后,才能继续执行。
三个线程的执行过程是一样的,即线程首先打印出线程名称,然后休眠5秒钟再打印出线程名称。最终,主线程会在三个线程都执行完毕后输出"All threads are finished."。
2. yield方法
yield方法:当前线程放弃CPU的使用切换其他线程使用,执行此方法会向系统线程调度器(scheduler)发出一个暗示,告诉当前Java线程打算放弃对CPU的使用,但该暗示有可能被调度器忽略。
在Java中,Thread类的yield()方法可以让当前线程让出CPU资源,让其他线程有更多的机会获得CPU执行时间。该方法只是一个提示,它不会强制线程让出CPU,只是让线程具有更高的可运行性。
下面是一个示例代码,演示了yield()方法的具体使用:
public class YieldThreadExample {
public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), "Thread 1");
Thread t2 = new Thread(new MyRunnable(), "Thread 2");
t1.start();
t2.start();
}
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " is running.");
Thread.yield();
}
}
}
}
在该代码中,创建了两个线程t1和t2并启动它们。两个线程的执行过程是一样的,即在线程的run()方法中使用for循环输出当前线程的名称,然后调用Thread.yield()方法让出CPU执行权。
在运行该程序时,我们可以看到,当一个线程调用yield()方法时,它让出了CPU执行权,另一个线程得到了执行的机会。然后两个线程交替执行,直到完成所有循环。
需要注意的是,yield()方法不应该被滥用,因为它只是一个提示,不能保证其他线程一定会获得CPU执行时间。如果线程需要长时间的休眠或者计算,那么使用yield()方法可能会导致其他线程无法获得足够的CPU执行时间,从而影响整个应用程序的性能。
3. interrupt方法
该方法终端当前线程的执行,允许当前线程对自身进行终端,否则将会校验调用方法线程是否有该线程的权限。
在Java中,Thread类的interrupt()方法用于中断线程。当一个线程被interrupt()方法中断时,会设置线程的中断状态,如果线程正在等待某些资源或者处于阻塞状态,那么会抛出InterruptedException异常。
下面是一个示例代码,演示了interrupt()方法的具体使用:
public class InterruptThreadExample {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(new MyRunnable(), "Thread 1");
t1.start();
Thread.sleep(5000); //主线程休眠5秒钟
t1.interrupt();
}
static class MyRunnable implements Runnable {
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " is running.");
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + " is interrupted.");
}
}
}
}
在该代码中,创建了一个线程t1并启动它。线程t1的执行过程是在run()方法中使用for循环输出当前线程的名称并休眠1秒钟。主线程休眠5秒钟后,调用t1.interrupt()方法中断t1线程。
当线程t1被中断时,由于线程正在sleep()方法中阻塞,那么会抛出InterruptedException异常。在run()方法中捕获该异常,并输出线程被中断的信息。最后,线程t1终止运行。
需要注意的是,中断一个线程并不会强制终止线程的执行,只是设置线程的中断状态,线程需要根据自己的中断状态来决定是否退出线程。可以使用Thread.currentThread().isInterrupted()方法检查线程的中断状态。同时,需要避免在synchronized块或者某些阻塞方法中使用interrupt()方法,否则可能会导致死锁或者不可预期的行为。
4. interrupted
查看当前线程是否处于中断状态,该方法特殊之处在于如果调用成功,当前线程的interrupt status清除。所以如果连续两次调用该方法第二次返回false。
public class MyThread extends Thread {
public void run() {
while (!Thread.interrupted()) {
System.out.println("线程未被中断");
}
System.out.println("线程已被中断");
}
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
// 让主线程休眠3秒钟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 中断线程
thread.interrupt();
}
}
在上面的代码中,我们创建了一个MyThread类,并在其run()方法中使用while循环来检查线程是否被中断。如果线程未被中断,就会一直输出"线程未被中断"的信息。如果线程被中断,就会输出"线程已被中断"的信息。
在main()方法中,我们启动线程并让主线程休眠3秒钟,然后中断线程。当线程被中断时,while循环中的条件就会不成立,线程将退出循环并执行后面的语句。
需要注意的是,调用Thread.interrupted()方法后,中断状态标志会被清除。因此,在while循环中使用Thread.interrupted()方法可以检查线程是否被中断,并且在执行跳出循环后清除中断状态标志。
总之,Thread类的interrupted()方法可以帮助我们检查线程是否被中断,并且在需要时清除中断状态标志。在多线程编程中,中断机制是一种非常有用的手段,可以使线程在遇到异常或者需要停止时立即退出,并且避免出现死锁等问题。
5. stop(已过时)
由于stop方法可以让一个线程A终止掉另一个线程B,被终止的线程B会立即释放锁,这可能会使对象处于不一致状态。
三.sleep方法与wait方法的区别
- sleep方法属于Thread类,而wait方法属于Object类中的。
- sleep方法导致程序暂停给了暂停时间,让出CPU给其他线程,但他的监控状态依旧保持着,到了时间又会恢复运行状态。
- 在调用sleep方法的过程中线程不会释放掉锁对象。
- 当调用wait方法时,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后,该线程才进入对象锁定池,准备获取对象锁进入运行状态。
四.start方法与run方法的区别
- start方法用来启动线程真正实现了多线程运行。这时无需等待run方法执行完毕可以继续执行下面的代码。
- 通过调用Thread类的start方法来启动一个线程,此时线程是处于就绪状态并没有运行。
- 方法run()称为线程体,包含了要执行的这个线程的内容,线程进入了运行状态,开始运行run函数中的代码,run()方法运行结束,此时线程终止,然后CPU再调度其他线程。
五.Synchronized与Lock的区别
- Synchronized是Java内置的关键字,Lock是Java的一个类。
- Synchronized无法获取锁的状态,Lock可以判断是否获取到了锁。
- Synchronized会自动释放锁,Lock必须手动释放锁!如果不释放会造成死锁。
- Synchronized 线程1(获取锁,阻塞),线程2(等待,傻傻的等待),Lock锁就不一定会等下去。
- Synchronized可重入锁,不可中断,非公平的,Lock锁是可重入的锁,公平不公平可以设置。
- Synchronized适合少量的代码同步问题,Lock适合大量的同步代码。
补充.并发与并行
六.线程的生命周期(码出高效)
-
NEW 初始状态/新建状态,是线程被创建且未启动的状态。创建线程的方式有三种:第一种是继承自Thread类,第二种是实现Runnable接口,第三种是实现Callable接口。相比第一种,推荐第二种,因为继承自Thread类往往不符合里氏代换原则,而实现Runnable接口可以使编程更加灵活,对外暴露的细节比较少,让使用者专注于实现线程的run()方法上。第三种Callable接口的call()声明如下:
由此可知,Callable与Runnable有两点不同:第一,可以通过call()方法获得返回值。前两种方式都有一个共同的缺陷,即在任务执行完成后,无法直接获取执行结果,需要截取共享变量获取,而Callable和Future则很好地解决了这个问题;第二,call()可以抛出异常。而Runnable只有通过setDefaultUncaughtExceptionHandler()
的方式才能在主线程中捕获到子线程异常。 -
RUNNABLE,即就绪状态,是调用start()之后运行之前的状态。线程的start()不能多次调用,否则会抛出IllegalStateException异常。
-
RUNNING即运行状态,是
run()
正在执行时的线程的状态。现成可能会由于某些因素而退出RUNNING,如时间、异常、锁、调度等。 -
BLOCKED即阻塞状态,进入此状态有以下几种情况。
a.同步阻塞:锁被其他线程占用。
b.主动阻塞:调用Thread的某些方法,主动让出CPU执行权,比如sleep()、join()等。
c.等待阻塞:执行了wait() -
DEAD,即终止状态,是run()执行结束,或因异常退出后的状态,此状态不可逆转。
再用医生坐诊的例子说明 医生并发地处理多个病人的询问、开化验单、查看化验结果、开药等工作,任何一个环节一旦出现数据混淆,都可能引发严重的医疗事故。延伸到计算机的结程处理过程中,因为各个线程轮流占用 CP 的计算资源,可能会出现某个线程尚未执行完就不得不中断的情况,容易导致线程不安全。例如,在服务端某个高并发业务共享某用户数据,首先 线程执行用户数据的查询任务 但数据尚未返回就退出 CPU 时间片,然后 线程抢占了 PU 资源执行并覆盖了该用户数据最后线程返回到执行现场,直接将 线程处理过后的用户数据返回给前端,导致页面显示数据错误。为保证线程安全,在多个线程并发地竞争共享资源时,通常采用同步机制协调各个线程的执行,以确保得到正确的结果。
七.多线程的使用
package com.ruoyi.framework.config;
import com.ruoyi.common.utils.Threads;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author ruoyi
**/
@Configuration
public class ThreadPoolConfig {
// 核心线程池大小
private int corePoolSize = 50;
// 最大可创建的线程数
private int maxPoolSize = 200;
// 队列最大长度
private int queueCapacity = 1000;
// 线程池维护线程所允许的空闲时间
private int keepAliveSeconds = 300;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(maxPoolSize);
executor.setCorePoolSize(corePoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
// 线程池对拒绝任务(无线程可用)的处理策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
/**
* 执行周期性或定时任务
*/
@Bean(name = "scheduledExecutorService")
protected ScheduledExecutorService scheduledExecutorService() {
return new ScheduledThreadPoolExecutor(corePoolSize,
new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
new ThreadPoolExecutor.CallerRunsPolicy()) {
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
Threads.printException(r, t);
}
};
}
}
以下是一个符合阿里巴巴Java代码规范的线程池示例:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 获取CPU核心数
int processors = Runtime.getRuntime().availableProcessors();
// 任务队列数组
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1024);
// 线程工厂,用于创建新线程
ThreadFactory threadFactory = Executors.defaultThreadFactory();
// 拒绝策略,用于处理任务队列已满的情况
RejectedExecutionHandler rejectedExecutionHandler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
// 核心线程数等于CPU核心数
processors,
// 最大线程数为核心数的两倍
processors * 2,
// 线程空闲时间为60秒
60L,
// 空闲时间单位为秒
TimeUnit.SECONDS,
// 任务队列
workQueue,
// 线程工厂
threadFactory,
// 拒绝策略
rejectedExecutionHandler
);
// 提交任务
executor.submit(() -> System.out.println("任务执行"));
// 关闭线程池
executor.shutdown();
}
}
在此示例中,我们使用了以下阿里巴巴Java代码规范建议:
- 使用
ThreadPoolExecutor
来创建线程池。 - 限制线程池大小。我们设置核心线程数等于 CPU 核心数,最大线程数是核心数的两倍。
- 使用有界任务队列,这避免了任务过多的情况。
- 在创建线程池时指定拒绝策略。在这个例子中,我们使用了默认的
AbortPolicy
策略,当任务队列已满时,新任务将被立即拒绝并抛出异常。 - 关闭线程池。我们使用了
shutdown()
方法来关闭线程池。
八.守护线程
守护线程也称为服务线程,他是后台线程,它有一个特性即为用户提供公共服务,在没有用户线程可服务的时候会自动离开。
优先级:守护线程的优先级比较低。
设置:通过setDaemon(true)
来设置成为守护线程。
example:垃圾回收就是一个守护线程。
九.线程4种终止方法
- 正常运行结束
- 使用退出标志
一般run()方法执行完毕,线程就会正常结束,然而常常有些线程是伺服线程。他们需要长时间运行,只有在外部某些条件满足的情况下,才能关闭这些线程。例如最直接的方法就是设一个boolean类型的标志通过这个标志为true或者false来控制while循环。
public volatile boolean exit = false;
public void run(){
while(!exit){
}
}
- 使用Interrupt()方法结束
使用Interrupt()方法不会真正使这个线程停下来,仅仅是给线程发一个信号告诉它,他应该结束了。 - stop()方法但是不安全。
十.Volatile总结
Volatile是JVM提供最轻量级的一个关键字,说到Volatile首先说到我们计算的模型,CPU和内存之间的线程效率是差了好几个数量级但是为了保证他们之间的计算,不影响CPU的计算,然后中间有好多LLV那种缓存,我们线程在这个缓存中去工作,首先取数据会从主内存取到工作内存中,在工作内存中计算完之后再传回去,这个就有一个问题多线程之间的可见性,如何保证。在计算机层面有很多协议,在JVM它为了解决这些比较复杂的东西,它提供了像JMM这种模型,像被Volatile修饰的一个变量他就可以保证这个变量在所有线程间的可用性,在这个线程修改这个变量后,他可以立刻刷回到主内存中,它在使用时会立刻从主内存中取出刷新那个值,Volatile它是不保证原子性,像自增自减操作它是不能保证的。
volatile
是 Java 中的一个关键字,用于修饰变量。被 volatile
修饰的变量具有可见性和禁止指令重排两个特性。
- 可见性
被 volatile
修饰的变量,当一个线程对它进行了修改,对其他线程是立即可见的。换句话说,就是写入一个 volatile
变量会立即能被其他线程看到。
通常情况下,Java 中的变量都是存储在主内存中,而每个线程有自己的工作内存。当一个线程执行时,它会将主内存中的变量值读取到自己的工作内存中,在工作内存中进行操作,然后将结果写回主内存。如果一个变量没有使用 volatile
关键字进行修饰,那么它在一个线程中的修改操作可能会被延迟到写回主内存的时候才会对其他线程可见。但是如果使用了 volatile
关键字,那么它的修改操作就会立即被其他线程看到,因为每次读取 volatile
变量时,都会从主内存中重新获取最新的值。
- 禁止指令重排
Java 中的指令执行顺序有可能会被 JVM 进行重排,这个重排是为了优化程序的执行效率。但是,在多线程环境下,这会影响到程序的正确性。如果一个变量被 volatile
修饰,那么 JVM 就会保证对它的操作不会被重排,从而保证了程序的正确性。
总之,volatile
关键字主要用于保证变量在并发环境下的正确性,可以保证线程之间的可见性,以及禁止指令重排。
Volatile代码实战
以下是一个使用 volatile
的实战示例:
public class MyRunnable implements Runnable {
private volatile boolean isRunning = true;
public void run() {
while (isRunning) {
// 执行任务的代码
}
// 线程结束的清理工作
}
public void stopRunning() {
isRunning = false;
}
}
上面的代码定义了一个 MyRunnable
类,这个类实现了 Runnable
接口,用于执行某些任务。MyRunnable
类中有一个被 volatile
修饰的布尔型变量 isRunning
,用来控制任务的执行状态。
run()
方法中的 while(isRunning)
循环会一直执行任务,直到 isRunning
被其他线程设为 false
。通过 stopRunning()
方法可以停止任务的执行,因为 isRunning
是 volatile
变量,所以其他线程调用 stopRunning()
方法之后,对它的修改操作会被立即可见,从而及时停止任务的执行。
在多线程环境中,如果不使用 volatile
修饰 isRunning
变量,那么由于各个线程的工作内存中的变量值可能会不同,导致一个线程修改了 isRunning
变量的值,但是其他线程还是读取的旧值,这样就无法及时停止任务的执行。
因此,在需要保证变量在多线程环境下的可见性时,就可以使用 volatile
关键字,从而避免出现线程安全问题。
十一.AQS
AQS(AbstractQueuedSynchronizer)是一个用于构建同步器的框架,是Java并发包中实现锁与同步器的核心,ReentrantLock 和 CountDownLatch 等工具类都是通过 AQS 实现的。
AQS 是一种基于 FIFO 队列的同步队列实现,它为 FIFO 等待队列、线程的阻塞和唤醒(park和unpark)机制、同步状态管理等相关操作提供了底层的操作支持,使得构建复杂的同步器变得容易。
AQS 的基本思想是,如果请求资源的线程获取到了同步状态(资源),则可以直接进入执行,否则就进入等待队列,并且在访问共享资源时,必须先获取同步状态,成功获取同步状态的线程才可以进入对应的临界区,当线程执行完临界区代码后,会释放同步状态,这样等待队列中的其他线程才有机会获取同步状态(资源)。
AQS 主要使用一个 volatile 类型的整型变量 state 来表示同步状态,同时通过内部的同步器 state (Unsafe + CAS + volatile) 方法来进行特定操作的原子性操作,例如 acquire 和 release 等方法。具体来讲,AQS 的使用方式有两种:
-
继承AQS实现一个同步器:重写 AQS 中的 tryAcquire、tryRelease、tryReleaseShared、tryAcquireShared 等方法,可以构建出各种同步器,例如 Semaphore、ReentrantLock、ReentrantReadWriteLock 等。
-
使用AQS构造同步器:使用AQS提供的基础构建块,例如等待队列、同步状态等底层特性,来构建自己的同步器。
总之,AQS 可以方便地帮助开发人员构建复杂的同步器,从而实现更加高效的多线程编程。
十二. 三大方法、七大参数、四种拒绝策略
线程池的四种拒绝策略什么情况下怎么使用!!!
线程池是 Java 中一种用于管理和复用线程的机制,可以有效地控制系统中的并发线程数量,提高系统的资源利用率和性能。当线程池中的工作队列已满且线程池中的线程数达到最大值时,线程池需要采取一些策略来处理这些新提交的任务。这就是线程池的拒绝策略。
Java提供了四种不同的线程池拒绝策略:
-
AbortPolicy:抛出一个 RejectedExecutionException 异常,拒绝新任务的提交。这是默认的拒绝策略。
-
CallerRunsPolicy:直接在提交任务的线程中执行任务(也就是主线程),不会新建线程。这样可以减慢任务的速度,但可以保证不会丢失任务。
-
DiscardOldestPolicy:当线程池的工作队列已满时,丢弃队列中最老的未处理任务,并将新任务加入队列中。
-
DiscardPolicy:当线程池的工作队列已满时,直接丢弃新的任务。
这四种拒绝策略的使用场景如下:
-
AbortPolicy:对于需要立即处理的任务,由于异步处理方式,如果无法立即处理,可能会影响系统性能,此时应该选择 AbortPolicy。
-
CallerRunsPolicy:线程池队列满载导致无法处理新任务时,需要保证已提交任务不丢失,并且需要保证有过程通知主线程,那么可以选择 CallerRunsPolicy。
-
DiscardOldestPolicy:当队列中的任务已经达到最大值,无法添加新任务时,如果有一些任务的重要性比较低,可以使用这种策略,删除队列中最老的任务,腾出位置来处理更加重要的任务。
-
DiscardPolicy:当队列中的任务已经达到最大值,无法再添加新任务时,如果新任务的可处理性比较低,不会造成系统的影响,可以选择 DiscardPolicy 丢弃这些任务。
在使用线程池时,我们应该根据实际情况选择合适的拒绝策略。需要注意的是,线程池的拒绝策略并不是处理任务失败的唯一方式,它只是一种保护机制,应该在合适的时候配合其他的处理方式,例如线程池执行任务之前进行预检查、手动扩容线程池或者通过消息队列等方式缓冲任务。
十三. CAS
十四. 线程池
十五. 解决线程安全的办法
十六. synchronized的使用
在Java中,实现锁的常见方式是使用synchronized关键字。synchronized关键字可以用于方法、代码块以及静态方法等多个场景,使得在同一时刻只有一个线程可以访问被锁定的资源。
下面是一些使用synchronized关键字实现锁的示例:
- 在方法上加锁
使用synchronized关键字修饰方法,使得在同一时刻只有一个线程可以执行该方法:
public synchronized void method() {
// 该方法中的代码同步执行
}
- 在代码块上加锁
使用synchronized关键字修饰代码块,使得在同一时刻只有一个线程可以访问该代码块中的代码:
public void method() {
synchronized (this) {
// 该代码块中的代码同步执行
}
}
- 在静态方法上加锁
使用synchronized关键字修饰静态方法,使得在同一时刻只有一个线程可以执行该静态方法:
public synchronized static void method() {
// 该静态方法中的代码同步执行
}
需要注意的是,使用synchronized关键字加锁的对象是当前类的实例对象或该类的Class对象,因此在使用时需要注意锁的粒度。另外,synchronized关键字对锁的细节实现是由JVM自动实现的,开发者无需关心具体实现。
十七. 悲观锁、乐观锁、互斥锁、共享锁、读写锁、分段锁概念解析
- 悲观锁
悲观锁是一种保守的锁策略,它假定每次访问数据的时候都会发生冲突,因此在每次访问时都会加锁,直到完成数据操作,才会释放锁。这种锁的特点是对共享资源的使用效率较低,但能够有效确保数据的一致性和安全性。
- 乐观锁
乐观锁是一种乐观的锁策略,它假定每次访问数据的时候都不会发生冲突,因此在每次访问时都不会加锁,而是在更新数据的时候再检查是否有其他的修改。如果没有,则更新数据并解锁,否则抛出异常或者进行重试。这种锁的特点是对共享资源的使用效率较高,但需要更多的计算和检查操作。
- 互斥锁
互斥锁是一种基本的锁机制,它使得只有一个线程可以访问被保护的共享资源,其他线程需要等待该线程释放锁才能访问。互斥锁可以保证数据的一致性和安全性,但是对共享资源的使用效率相对较低。
- 共享锁
共享锁是一种允许多个线程同时访问共享资源的锁机制,但是这些线程只能读取共享资源,不能修改。共享锁可以提高对共享资源的并发读取性能,但不能保证数据的一致性和安全性。
- 读写锁
读写锁是一种特殊的共享锁,使得多个线程可以同时读取共享资源,但只有一个线程可以进行写操作。读写锁可以提高对共享资源的并发读取性能,同时保证写操作的原子性和一致性。
- 分段锁
分段锁是一种将共享资源划分为多个片段,并对每个片段加锁的机制。这种锁可以减小锁的粒度,从而提高对共享资源的并发访问性能。分段锁常用于高并发的数据结构中,例如ConcurrentHashMap等。
需要注意的是,以上这些锁并不是互相独立的,常常需要多种锁的组合使用来保证对共享资源的并发访问性能和安全性。在使用锁的时候,需要根据具体的场景和需求来选取适合的锁策略。
十八.Java提供常见的锁
-
synchronized 关键字锁:synchronized 是 Java 的内置锁机制,用于同步访问指定对象或方法。在 synchronized 块的开始和结束时,Java 自动获得和释放锁。
-
ReentrantLock 锁:是一个可重入锁,它允许一个线程多次获得同一个锁,避免死锁。它提供了与 synchronized 相同的功能,但提供了更多灵活性和控制。
-
ReadWriteLock 读写锁:该锁允许多个线程同时访问共享资源,但同时限制写操作只能由一个线程进行。在读取操作时,可以并发执行,但在写入操作时,必须要等待所有读取操作完成。
-
StampedLock 锁:是 Java 8 中引入的一种可重入的读写锁。它使用乐观锁来实现非阻塞的读取操作,并且提供了更高的并发性能。
-
Semaphore 信号量:与锁不同,信号量可以控制多个线程同时访问共享资源的数量。Semaphore 可以维护一个逻辑许可证,每次访问共享资源前,线程需要获得许可证。
-
CountDownLatch 门闩锁:是一种同步工具,它允许一个或多个线程等待其他线程完成操作后再继续执行。它的工作方式类似于门闩,当门闩打开时,线程可以通过。
这些锁提供了不同的用途和灵活性,可以选择适合特定场景的锁。
十九.ReentrantLock实战案例
ReentrantLock 是 Java 提供的一种可重入的独占锁,可以在多线程并发操作中保证数据的正确性和一致性。下面是一个 ReentrantLock 实战案例,该案例演示了如何使用 ReentrantLock 来保证线程安全。
假设有一个账户类 Account,该类包含了账户的余额和提现的方法 withdraw(),我们需要使用 ReentrantLock 来保证在多线程环境下,账户余额的正确计算。
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private final ReentrantLock lock = new ReentrantLock();
private double balance;
public Account(double balance) {
this.balance = balance;
}
public void withdraw(double amount) {
lock.lock(); // 加锁
try {
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + " withdraw " + amount + ", balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " withdraw failed, balance not enough.");
}
} finally {
lock.unlock(); // 解锁
}
}
}
在上面的代码中,我们使用了 ReentrantLock 来保证在 withdraw() 方法中的多线程并发操作的同步和互斥。在方法的开始和结束位置,分别使用 lock() 和 unlock() 方法来获取和释放锁。
下面是一个使用 Account 类的示例代码:
public class Demo {
public static void main(String[] args) {
Account account = new Account(1000);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.submit(() -> account.withdraw(50));
}
executorService.shutdown();
}
}
在上面的示例中,我们创建了一个线程池,向里面提交 20 个线程,每个线程都调用了 withdraw() 方法来提取 50 块钱。由于 ReentrantLock 的加锁和解锁机制,多个线程可以安全地并发地访问共享变量,确保账户余额的正确性。
总之,使用 ReentrantLock 可以有效地避免线程安全问题,并提高多线程并发操作的执行效率和可靠性。
二十. ReentrantLock 与synchronized的区别
ReentrantLock 和 synchronized 都是 Java 中用于实现线程同步的机制,但二者在实现上存在一些不同点,主要有以下几个方面:
-
可重入性:ReentrantLock 是一个可重入锁,可以允许线程多次获取同一个锁,而 synchronized 是不可重入的,如果一个线程已经获取了锁,再次获取锁会导致死锁。
-
锁的获取方式:ReentrantLock 提供了多种锁获取方式,如公平锁、非公平锁、可中断锁、可超时锁等,而 synchronized 只提供了一种获取锁的方式。
-
锁的粒度:ReentrantLock 可以通过 lock() 和 unlock() 方法来手动控制锁的粒度,而 synchronized 的锁粒度是代码块或方法,无法手动控制。
-
性能:在多线程并发操作的情况下,ReentrantLock 的性能相对于 synchronized 来说更好一些,由于 ReentrantLock 提供了更细粒度的控制和多种获取锁的方式,可以更好地避免死锁和竞争条件,提高并发操作的效率。
-
可见性:synchronized 在获取锁的同时,会自动将修改的变量刷新到主内存中,保证了锁的可见性,而 ReentrantLock 则需要手动实现变量的可见性。
总之,ReentrantLock 和 synchronized 在保证线程同步方面的作用是相同的,但是 ReentrantLock 提供了更丰富的功能和更好的性能优化,因此在某些情况下使用 ReentrantLock 可能会更加适合。但是在大多数情况下,synchronized 已经可以满足需求,而且使用 synchronized 更加方便简洁。
二十一.ReadWriteLock实战案例
ReadWriteLock 是 Java 提供的一种读写锁,用于解决多线程并发读写操作的问题。下面是一个 ReadWriteLock 实战案例,该案例演示了如何使用 ReadWriteLock 来保证线程安全。
假设有一个缓存类 Cache,该类包含了一个 Map 对象和两个方法 get() 和 put(),在多线程并发下,需要使用读写锁来保证数据的正确性和一致性。
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Cache {
private final Map<String, String> cache = new HashMap<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
public String get(String key) {
lock.readLock().lock(); // 获取读锁
try {
return cache.get(key);
} finally {
lock.readLock().unlock(); // 释放读锁
}
}
public void put(String key, String value) {
lock.writeLock().lock(); // 获取写锁
try {
cache.put(key, value);
} finally {
lock.writeLock().unlock(); // 释放写锁
}
}
}
在上面的代码中,我们使用了 ReadWriteLock 来保证在 get() 和 put() 方法中的多线程并发操作的同步和互斥。在 get() 和 put() 方法中,分别使用了 readLock() 和 writeLock() 方法来获取读锁和写锁,从而保证了数据的正确性。
下面是一个使用 Cache 类的示例代码:
public class Demo {
public static void main(String[] args) {
Cache cache = new Cache();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
int index = i;
executorService.submit(() -> {
cache.put(String.valueOf(index), String.valueOf(index));
System.out.println(Thread.currentThread().getName() + " put " + index);
});
executorService.submit(() -> {
String value = cache.get(String.valueOf(index));
System.out.println(Thread.currentThread().getName() + " get " + value);
});
}
executorService.shutdown();
}
}
在上面的示例中,我们创建了一个线程池,向里面提交 20 个线程,每个线程都调用了 put() 方法来添加缓存数据,并且调用了 get() 方法来获取缓存数据。由于 ReadWriteLock 的读写锁机制,多个读操作可以同时访问共享变量,但是写操作需要互斥地访问共享变量,确保了缓存数据的正确性和一致性。
总之,使用 ReadWriteLock 可以有效地避免线程安全问题,并提高多线程并发操作的执行效率和可靠性。
二十二.ReentrantLock是怎样提供公平锁、非公平锁、可中断锁、可超时锁的
ReentrantLock 是 Java 提供的一种可重入锁,支持多种锁获取方式,如公平锁、非公平锁、可中断锁、可超时锁等。
- 公平锁和非公平锁
ReentrantLock 提供了两种锁的获取机制,公平锁和非公平锁。公平锁获取锁的顺序与线程请求的顺序有关,即按照先进先出的规则获取锁,而非公平锁则是让抢到锁的线程直接获得锁,而不考虑等待的线程。
可以在创建 ReentrantLock 对象时使用参数来指定锁的类型,默认为非公平锁,如果需要使用公平锁,可以通过将参数设置为 true 来实现:
ReentrantLock fairLock = new ReentrantLock(true); // 公平锁
ReentrantLock unfairLock = new ReentrantLock(false); // 非公平锁
- 可中断锁
ReentrantLock 还支持可中断锁,即允许线程在等待获取锁的过程中可以响应中断。可以通过 interrupt() 方法来中断线程,同时在获取锁的方法中使用 tryLock(long time, TimeUnit unit) 方法来设置等待锁的超时时间,如果等待超时,则返回 false。
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
// 获取锁成功
} else {
// 等待超时,获取锁失败
}
} catch (InterruptedException e) {
// 等待过程中发生中断
} finally {
lock.unlock();
}
- 可超时锁
ReentrantLock 还支持可超时锁,即允许线程尝试获取锁时限制等待时间。与可中断锁类似,也是使用 tryLock(long time, TimeUnit unit) 方法来实现,在指定的时间内尝试获取锁,如果获取成功则返回 true,否则返回 false。
ReentrantLock lock = new ReentrantLock();
try {
if (lock.tryLock(1000, TimeUnit.MILLISECONDS)) {
// 获取锁成功
} else {
// 等待超时,获取锁失败
}
} finally {
lock.unlock();
}
总之,ReentrantLock 提供了多种锁获取方式,可以根据实际需求灵活选择,从而实现更加高效和可靠的多线程并发操作。
二十三.StampedLock 锁实战案例
StampedLock 是 Java 提供的一种乐观读写锁(Optimistic Read Lock),可以有效地提高多线程并发操作的性能。下面是一个 StampedLock 实战案例,该案例演示了如何使用 StampedLock 来保证线程安全。
假设有一个 Point 类,该类包含了两个变量 x 和 y,需要在多线程并发下修改 x 和 y 的值,并且需要使用 StampedLock 来保证数据的正确性和一致性。
import java.util.concurrent.locks.StampedLock;
public class Point {
private double x, y;
private final StampedLock stampedLock = new StampedLock();
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获取一个乐观读锁
double currentX = x, currentY = y;
if (!stampedLock.validate(stamp)) {
// 检查锁是否被其他写线程获得
stamp = stampedLock.readLock(); // 获取悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
在上面的代码中,我们使用了 StampedLock 来保证在 move() 和 distanceFromOrigin() 方法中的多线程并发操作的同步和互斥。在 move() 方法中,使用了 writeLock() 方法来获取写锁,从而实现对 x 和 y 的修改。在 distanceFromOrigin() 方法中,使用了 tryOptimisticRead() 方法来获取乐观读锁,如果读操作完成后确定没有写操作发生,则直接返回计算结果,否则再获取悲观读锁,从而保证数据的正确性和一致性。
下面是一个使用 Point 类的示例代码:
public class Demo {
public static void main(String[] args) {
Point point = new Point();
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executorService.submit(() -> {
point.move(1, 1);
System.out.println(Thread.currentThread().getName() + " move to (" + point.getX() + ", " + point.getY() + ")");
});
executorService.submit(() -> {
double distance = point.distanceFromOrigin();
System.out.println(Thread.currentThread().getName() + " distance from origin: " + distance);
});
}
executorService.shutdown();
}
}
在上面的示例中,我们创建了一个线程池,向里面提交 20 个线程,每个线程都调用了 move() 方法来修改 Point 的坐标,并且调用了 distanceFromOrigin() 方法来计算 Point 到原点的距离。由于 StampedLock 的乐观读写锁机制,对于读操作可以直接返回结果,而不需要阻塞等待,有效提高了多线程并发操作的执行效率和可靠性。
总之,使用 StampedLock 可以提高多线程并发操作的性能,并保证数据的正确性和一致性,从而实现更加高效和可靠的多线程并发编程。
二十四.Semaphore实战
Semaphore是Java中的一个线程同步工具,用于控制同时访问资源的线程数量。在以下的实战中,我将演示如何使用Semaphore来控制对共享资源的访问。
假设我们有一个共享资源,它是一个整数数组,我们要允许最多5个线程同时访问该资源,而其余的线程必须等待,直到有可用的访问权限。下面是Java Semaphore的实现:
import java.util.concurrent.Semaphore;
public class SharedResource {
private int[] resource = new int[10];
private Semaphore semaphore = new Semaphore(5);
public void accessResource(int id) throws InterruptedException {
semaphore.acquire();
try {
//访问共享资源
System.out.println("Thread " + id + " is accessing the resource.");
Thread.sleep(2000);
} finally {
//释放资源
System.out.println("Thread " + id + " has released the resource.");
semaphore.release();
}
}
}
在上述代码中,Semaphore的初始许可数为5,即最多允许5个线程同时访问资源。通过调用semaphore.acquire()方法获取Semaphore的许可,如果当前没有可用的许可,线程将被阻塞。一旦线程成功获取了许可,则可以访问共享资源。在访问结束后,线程调用semaphore.release()方法释放Semaphore的许可,以便让其他线程获取访问权限。
下面是一个测试类,它创建了10个线程来访问共享资源:
public class SemaphoreTest {
public static void main(String[] args) {
SharedResource resource = new SharedResource();
for (int i = 0; i < 10; i++) {
final int id = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
resource.accessResource(id);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
在运行上述代码时,你会看到类似如下的输出,其中线程0-4先获取了许可,然后5-9线程开始等待:
Thread 0 is accessing the resource.
Thread 3 is accessing the resource.
Thread 1 is accessing the resource.
Thread 2 is accessing the resource.
Thread 4 is accessing the resource.
Thread 1 has released the resource.
Thread 0 has released the resource.
Thread 2 has released the resource.
Thread 4 has released the resource.
Thread 3 has released the resource.
Thread 5 is accessing the resource.
...
以上就是Java Semaphore的实战,它使得我们可以轻松地控制对共享资源的访问,有助于避免由于线程竞争而导致的访问冲突和性能问题。
二十五.CountDownLatch实战
CountDownLatch是Java并发包中的一个工具,用于协调多个线程之间的同步。CountDownLatch的作用是使一个或多个线程等待其他线程完成它们的工作后才能继续执行。
以下是一个使用CountDownLatch的实战示例,假设我们需要处理一批数据,需要等待所有数据处理完成后才能继续执行其他任务,代码如下:
import java.util.concurrent.CountDownLatch;
public class DataProcessor {
public static void main(String[] args) throws InterruptedException {
int numberOfThreads = 5;
CountDownLatch latch = new CountDownLatch(numberOfThreads);
for (int i = 0; i < numberOfThreads; i++) {
Thread thread = new Thread(new DataProcessorThread(latch));
thread.start();
}
// 等待所有线程完成工作
latch.await();
System.out.println("所有数据处理完成,继续执行其他任务");
}
private static class DataProcessorThread implements Runnable {
private final CountDownLatch latch;
public DataProcessorThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
// 模拟数据处理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据处理完成");
// 通知CountDownLatch,当前线程已经完成工作
latch.countDown();
}
}
}
在这个示例中,我们创建了5个线程来处理数据,每个线程在运行前都会调用CountDownLatch的countDown()方法,表示当前线程已经完成工作。在主线程中,通过调用CountDownLatch的await()方法来等待所有数据处理线程完成工作。当所有线程都完成工作后,程序会输出“所有数据处理完成,继续执行其他任务”的提示。
需要注意的是,CountDownLatch的计数器只能被减少,不能被增加,这意味着一旦计数器的值减少到0以后,就无法重新使用。如果需要重新使用计数器,则需要创建一个新的CountDownLatch对象。
二十六. Java解决线程安全问题的办法有以下几种:
- 同步方法:在方法前面加上synchronized关键字,确保同一时间只有一个线程可以执行该方法。
public synchronized void increment() {
count++;
}
- 同步代码块:在需要同步的代码块前后分别加上synchronized关键字,确保同一时间只有一个线程可以执行该代码块。
synchronized (lock) {
count++;
}
- 使用锁机制:使用Java中的ReentrantLock或者ReentrantReadWriteLock类,这些类提供了更灵活的锁定机制,允许线程进行更高级别的互斥访问,同时还允许实现公平的锁定机制。
private final Lock lock = new ReentrantLock();
public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
- 使用原子类:Java中的AtomicInteger等原子类提供了一种线程安全的计数器,可以避免出现竞态条件。
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.getAndIncrement();
}
- 使用线程安全的集合类:Java中的ConcurrentHashMap、ConcurrentLinkedQueue等集合类是线程安全的,可以避免在多线程环境下出现并发访问问题。
private final Map<String, Integer> map = new ConcurrentHashMap<>();
public void increment(String key) {
map.compute(key, (k, v) -> v == null ? 1 : v + 1);
}
总之,在Java中,为了保证线程安全,需要避免多个线程同时访问共享资源,可以通过以上方法来实现线程安全。
二十七.符合阿里巴巴Java代码规范的线程池实战
下面是一个符合阿里巴巴 Java 代码规范的线程池实战:
import java.util.concurrent.*;
public class ThreadPoolExample {
public static void main(String[] args) {
// 定义线程池工厂
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("ThreadPool-%d")
.build();
// 定义拒绝策略
RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.AbortPolicy();
// 创建线程池,使用 2 倍 CPU 核数作为基础线程数,最大线程数为 10,超过核心线程数的线程存活时间为 1 分钟
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2,
10,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
threadFactory,
rejectedHandler);
// 提交 1000 个任务给线程池
for (int i = 1; i <= 1000; i++) {
final int taskId = i;
threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + " - Task " + taskId + " is running.");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
// 关闭线程池
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
threadPool.shutdownNow();
}
}
}
这个线程池实现了以下符合阿里巴巴 Java 代码规范的要求:
-
使用了线程池工厂来给线程命名,可以方便调试和排查问题。
-
使用了拒绝策略,对于线程池无法处理的任务做出了处理。
-
使用了 Builder 模式创建线程池,创建参数清晰易读。
-
在创建线程池时,基础线程数为 CPU 核数的两倍,可以充分利用 CPU。
-
使用了 shutdown() 来关闭线程池,并等待一分钟让线程池中的任务处理完毕。如果等待超时,则强制关闭线程池。