多线程学习[转]
1.Java 并发基础
Thread 类的每一个实例都表示一个线程, 进程是操作系统级别的多任务,JVM 就是运行在一个进程中的。所以在Java 中我我们只考虑线程。进程有独立的内存,一个进程间的多个线程共享进程的内存进程中至少要有一个线程。
1.1 线程状态
- New:当我们创建一个线程时,该线程并没有纳入线程调度,其处于一个new状态。
- Runnable:当调用线程的start方法后,该线程纳入线程调度的控制,其处于一个可运行状态,等待分配时间片段以并发运行。
- Running:当该线程被分配到了时间片段后其被CPU运行,这是该线程处于running状态。
- Blocked:当线程在运行过程中可能会出现阻塞现象,比如等待用户输入信息等。但阻塞状态不是百分百出现的,具体要看代码中是否有相关需求。
- Dead:当线程的任务全部运行完毕,或在运行过程中抛出了一个未捕获的异常,那么线程结束,等待GC回收
1.2 创建线程
线程有几个不可控因素:
- cpu分配时间片给哪个线程我们说了不算。
- 时间片长短也不可控。
- 线程调度会尽可能均匀的将时间片分配给多个线程。
方式1:定义一个类并继承Thread,然后重写run方法,在其中书写线程任务逻辑:
启动线程要调用start方法,不能直接调用run方法。start方法会将当前线程纳入到线程调度中,使其具有并发运行的能力。start方法很快会执行完毕。当start方法执行完毕后,当前线程的run方法会很快的被执行起来(只要获取到了cpu时间片)。但不能理解为调用start方法时run方法就执行了!
不足:
- 由于java是单继承的,这就导致我们若继承了Thread类就无法再继承其他类,这在写项目时会遇到很大问题;
- 由于我们定义线程的同时重写run方法来定义线程要执行的任务,这就导致线程与任务有一个强耦合关系,线程的重用性变得非常局限。
方式2:定义一个类并实现Runnable接口然后在创建线程的同时将任务指定。因为是实现Runnable接口,所以不影响其继承其他类:
1.3 线程相关API
获取当前线程: Thread current = Thread.currentThread();
获取当前线程ID: Thread.currentThread().getId();//1
获取当前线程名字: Thread.currentThread().getName();//main
获取当前线程优先级: Thread.currentThread().getPriority();//1
判断当前线程是否还活着: Thread.currentThread().isAlive();//true
判断当前线程是否为守护线程:Thread.currentThread().isDaemon();//false
判断当前线程是否被中断: Thread.currentThread().isInterrupted();//false
1.4 线程的优先级
线程优先级分为10个等级,1最低,5默认,10最高。线程提供了3个常量:
- MIN_PRIORITY:1 对应最低优先级;
- MAX_PRIORITY: 10 对应最高优先级;
- NORM_PRIORITY:5 默认优先级。
1.5 线程阻塞
Thread提供了一个静态方法: sleep,该方法会阻塞运行当前方法的线程指定毫秒。当超时后,线程会自动回到Runnable状态,等待再次分配时间片运行:
1.6 守护线程
后台线程,又叫做守护线程,当一个进程中的所有前台线程都结束了,进程就会结束,无论进程中的其他后台线程是否还在运行,都要被强制中断:new Thread().setDaemon(true).start();
1.7 yield
该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,等待分配时间片。
1.8 join
允许当前线程在另一个线程上等待,直到另一个线程结束工作。通常是用来协调两个线程工作使用:
private static boolean isFinish = false;
public static void main(String[] args) {
Thread download = new Thread(() -> {
System.out.println("开始下载图片...");
for (int i = 1; i <= 100; i++) {
System.out.println("down:" + i + "%");
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("图片下载完毕!");
isFinish = true;
});
Thread show = new Thread(() -> {
System.out.println("开始显示图片...");
try {
//等待download线程执行完毕才继续向下执行本线程
download.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!isFinish) {
throw new RuntimeException("图片加载失败!");
}
System.out.println("图片显示完毕!");
});
download.start();
show.start();
}
1.9 synchroinzed
多个线程并发读写同一个临界资源时候会发生“线程并发安全问题”
常见的临界资源:
- 多线程共享实例变量;
- 多线程共享静态公共变量。
若想解决线程安全问题,需要将异步的操作变为同步操作。 何为同步?那么我们来对比看一下什么是同步什么异步:
- 所谓异步操作是指多线程并发的操作,相当于各干各的。
- 所谓同步操作是指有先后顺序的操作,相当于你干完我再干。
而java中有一个关键字名为:synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。使用锁需要注意两个方面:
- 选择合适的锁对象:使用synchroinzed需要对一个锁对象上锁以保证线程同步。那么这个锁对象应当注意多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象引用。否则达不到同步效果。 通常我们会使用this来作为锁对象。
- 选择合适的锁范围:在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。
synchronized关键字有两个用法:
- 修饰方法,这样的话,该方法就称为”同步方法”,多个线程就不能同时进入到方法内部去执行。可以避免由于线程切换不确定,导致的逻辑错误。
- synchronized块,可以将某段代码片段括起来,多个线程不能同时执行里面的代码。
public static void main(String[] args) {
final Table table = new Table();
Thread t0 = new Thread() {
public void run() {
while (true) {
int bean = table.getBean();
//t0线程执行1次就让出时间片一次,相当于t0和t1交替执行
Thread.yield();
System.out.println(getName() + ":" + bean);
}
}
};
Thread t1 = new Thread() {
public void run() {
while (true) {
int bean = table.getBean();
Thread.yield();
System.out.println(getName() + ":" + bean);
}
}
};
t0.start();
t1.start();
}
static class Table {
private int beans = 20;
synchronized int getBean() {
if (beans == 0) {
throw new RuntimeException("没有豆子了!");
}
Thread.yield();
return beans--;
}
}
如果不加synchronized关键字,可能beans为负数了,线程还在执行,成了死循环。
synchronized块的例子:
public static void main(String[] args) {
final Shop shop = new Shop();
Thread t1 = new Thread(shop::buy);
Thread t2 = new Thread(shop::buy);
t1.start();
t2.start();
}
static class Shop {
void buy() {
try {
Thread t = Thread.currentThread();
System.out.println(t + "正在挑选衣服.");
Thread.sleep(1000);
synchronized (this) {
System.out.println(t + "正在试衣服.");
Thread.sleep(1000);
}
System.out.println(t + "结账离开.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时,“正在试衣服”这个代码块一次只能一个线程执行。
1.10 wait¬ify
Object类中定义了两个方法wait()和notify()。它们也可以实现协调线程之间同步工作的方法。当一个线程调用了某个对象的wait方法时,这个线程就进入阻塞状态,直到这个对象的notify方法被调用,这个线程才会解除wait阻塞,继续向下执行代码。
若多个线程在同一个对象上调用wait方法进入阻塞状态后,那么当该对象的notify方法被调用时,会随机解除一个线程的wait阻塞,这个不可控。若希望一次性将所有线程的wait阻塞解除,可以调用notifyAll方法。
private static boolean isFinish;
private static final Object obj = new Object();
public static void main(String[] args) {
final Thread download = new Thread(() -> {
System.out.println("down:开始下载图片...");
for (int i = 1; i <= 100; i++) {
System.out.println("down:" + i + "%");
try {
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("down:图片下载完毕!");
isFinish = true;
//通知 show线程开始工作
synchronized (obj) {
obj.notify();
}
});
Thread show = new Thread(() -> {
System.out.println("show:开始显示图片...");
synchronized (obj) {
try {
//show线程一启动就进入等待状态,直到被唤醒才继续执行
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (!isFinish) {
throw new RuntimeException("图片加载失败!");
}
System.out.println("show:图片显示完毕!");
});
download.start();
show.start();
}
1.11 线程池
当我们的逻辑中出现了会频繁创建线程的情况时,就要考虑使用线程池来管理线程。这可以解决创建过多线程导致的系统威胁。
线程池主要解决两个问题:
- 控制线程数量;
- 重用线程。
2.集合线程安全问题
常用的集合类型如ArrayList,HashMap,HashSet等,在并发环境下修改操作都是线程不安全的,会抛出java.util.ConcurrentModificationException
异常,如何在并发环境下安全地修改集合数据:
2.1 List
举个ArrayList线程不安全的例子:
public class CollectionTest {
public static void main(String[] args) {
List<String> data = new ArrayList<>();
IntStream.range(0, 30).forEach(
i -> new Thread(() -> data.add(String.valueOf(i)), String.valueOf(i)).start()
);
System.out.println(data);
}
}
上面例子中,有30个线程对data进行修改操纵,多运行几次上面的程序,控制台便会抛出如下异常:
之所以会抛出java.util.ConcurrentModificationException
异常,是因为多个线程并发争抢修改data导致,当一个线程正在写入但还未写完时,另一个线程抢夺写入,这便导致了数据异常。
要解决这个问题,主要有以下三种方法:
1. 使用Vector
:
public class CollectionTest {
public static void main(String[] args) {
List<String> data = new Vector<>();
IntStream.range(0, 30).forEach(
i -> new Thread(() -> data.add(String.valueOf(i)), String.valueOf(i)).start()
);
System.out.println(data);
}
}
Vector
类的add
方法是通过加同步锁来实现线程安全的,查看源码:
2. 使用Collections
工具类的synchronizedList
方法:
public class CollectionTest {
public static void main(String[] args) {
List<String> data = Collections.synchronizedList(new ArrayList<>());
IntStream.range(0, 30).forEach(
i -> new Thread(() -> data.add(String.valueOf(i)), String.valueOf(i)).start()
);
System.out.println(data);
}
}
其本质还是通过同步锁来解决线程安全问题:
3. 使用CopyOnWriteArrayList
:
public class CollectionTest {
public static void main(String[] args) {
List<String> data = new CopyOnWriteArrayList<>();
IntStream.range(0, 30).forEach(
i -> new Thread(() -> data.add(String.valueOf(i)), String.valueOf(i)).start()
);
System.out.println(data);
}
}
查看CopyOnWriteArrayList
类的add
方法源码,会发现它是通过可重入锁来解决线程安全问题的:
上面方法的源码使用了写时复制的思想(CopyOnWrite
):往一个容器添加元素的时候,不直接往当前容器Object[]
添加,而是先将当前容器Object[]
进行复制,复制出一个新的容器Object[] newElements
,然后往新的容器Object[] newElements
里添加新的元素。添加完后,再将原容器的引用指向新的容器(即源码中的setArray(newElements)
)。
这种做法的好处是可以对CopyOnWrite
的容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以这也是一种读写分离的思想,即读和写的容器是不同的容器。
上面这三种解决办法中,更推荐使用CopyOnWriteArrayList
。
2.2 Set
HashSet在并发下修改也会出现线程安全问题。解决办法同样可以通过Collections#synchronizedSet
和CopyOnWriteArraySet
解决,过程和上述类似,所以不再赘述。
2.3 Map
HashMap在并发环境下修改时的线程安全问题同样可以通过Collections.synchronizedMap
解决。此外并没有所谓的CopyOnWriteHashMap
,线程安全的HashMap可以使用ConcurrentHashMap
:
public class CollectionTest {
public static void main(String[] args) {
Map<String, String> data = new ConcurrentHashMap<>();
IntStream.range(0, 30).forEach(
i -> new Thread(() -> data.put(String.valueOf(i), String.valueOf(i)), String.valueOf(i)).start()
);
System.out.println(data);
}
}
3.深入学习Java线程池
通过new Thread
来创建一个线程,由于线程的创建和销毁都需要消耗一定的CPU资源,所以在高并发下这种创建线程的方式将严重影响代码执行效率。而线程池的作用就是让一个线程执行结束后不马上销毁,继续执行新的任务,这样就节省了不断创建线程和销毁线程的开销。
3.1 ThreadPoolExecutor
创建Java线程池最为核心的类为ThreadPoolExecutor
它提供了四种构造函数来创建线程池,其中最为核心的构造函数如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
这7个参数的含义如下:
- corePoolSize 线程池核心线程数。即线程池中保留的线程个数,即使这些线程是空闲的,也不会被销毁,除非通过ThreadPoolExecutor的
allowCoreThreadTimeOut(true)
方法开启了核心线程的超时策略; - maximumPoolSize 线程池中允许的最大线程个数;
- keepAliveTime 用于设置那些超出核心线程数量的线程的最大等待时间,超过这个时间还没有新任务的话,超出的线程将被销毁;
- unit 超时时间单位;
- workQueue 线程队列。用于保存通过execute方法提交的,等待被执行的任务;
- threadFactory 线程创建工程,即指定怎样创建线程;
- handler 拒绝策略。即指定当线程提交的数量超出了maximumPoolSize后,该使用什么策略处理超出的线程。
在通过这个构造方法创建线程池的时候,这几个参数必须满足以下条件,否则将抛出IllegalArgumentException
异常:
- corePoolSize不能小于0;
- keepAliveTime不能小于0;
- maximumPoolSize 不能小于等于0;
- maximumPoolSize不能小于corePoolSize;
此外,workQueue、threadFactory和handler不能为null,否则将抛出空指针异常。
下面举些例子来深入理解这几个参数的含义。
使用上面的构造方法创建一个线程池:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, 2, 10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy());
System.out.println("线程池创建完毕");
int activeCount = -1;
int queueSize = -1;
while (true) {
if (activeCount != threadPoolExecutor.getActiveCount()
|| queueSize != threadPoolExecutor.getQueue().size()) {
System.out.println("活跃线程个数 " + threadPoolExecutor.getActiveCount());
System.out.println("核心线程个数 " + threadPoolExecutor.getCorePoolSize());
System.out.println("队列线程个数 " + threadPoolExecutor.getQueue().size());
System.out.println("最大线程数 " + threadPoolExecutor.getMaximumPoolSize());
System.out.println("------------------------------------");
activeCount = threadPoolExecutor.getActiveCount();
queueSize = threadPoolExecutor.getQueue().size();
}
}
上面的代码创建了一个核心线程数量为1,允许最大线程数量为2,最大活跃时间为10秒,线程队列长度为1的线程池。
假如我们通过execute方法向线程池提交1个任务,看看结果如何:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, 2, 10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy());
System.out.println("线程池创建完毕");
threadPoolExecutor.execute(() -> sleep(100));
int activeCount = -1;
int queueSize = -1;
while (true) {
if (activeCount != threadPoolExecutor.getActiveCount()
|| queueSize != threadPoolExecutor.getQueue().size()) {
System.out.println("活跃线程个数 " + threadPoolExecutor.getActiveCount());
System.out.println("核心线程个数 " + threadPoolExecutor.getCorePoolSize());
System.out.println("队列线程个数 " + threadPoolExecutor.getQueue().size());
System.out.println("最大线程数 " + threadPoolExecutor.getMaximumPoolSize());
System.out.println("------------------------------------");
activeCount = threadPoolExecutor.getActiveCount();
queueSize = threadPoolExecutor.getQueue().size();
}
}
ThreadPoolExecutor的execute和submit方法都可以向线程池提交任务,区别是,submit方法能够返回执行结果,返回值类型为Future
sleep方法代码:
private static void sleep(long value) {
try {
System.out.println(Thread.currentThread().getName() + "线程执行sleep方法");
TimeUnit.SECONDS.sleep(value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
启动程序,控制台输出如下:
线程池核心线程数量为1,通过execute提交了一个任务后,由于核心线程是空闲的,所以任务被执行了。由于这个任务的逻辑是休眠100秒,所以在这100秒内,线程池的活跃线程数量为1。此外,因为提交的任务被核心线程执行了,所以并没有线程需要被放到线程队列里等待,线程队列长度为0。
假如我们通过execute方法向线程池提交2个任务,看看结果如何:
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
线程池核心线程数量为1,通过execute提交了2个任务后,一开始核心线程是空闲的,Thread-0被执行。由于这个任务的逻辑是休眠100秒,所以在这100秒内,线程池的活跃线程数量为1。因为核心线程数量为1,所以另外一个任务在这100秒内不能被执行,于是被放到线程队列里等待,线程队列长度为1。
假如我们通过execute方法向线程池提交3个任务,看看结果如何:
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
这三个任务都是休眠100秒,所以核心线程池中第一个任务正在被执行,第二个任务被放入到了线程队列。而当第三个任务被提交进来时,线程队列满了(我们定义的长度为1),由于该线程池允许的最大线程数量为2,所以线程池还可以再创建一个线程来执行另外一个任务,于是乎之前在线程队列里的线程被取出执行(FIFO),第三个任务被放入到了线程队列。所以,只有当队列满时才会创建大于核心线程数而小于最大线程数的线程来执行新任务.
改变第二个和第三个任务的睡眠时间,观察输出:
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(5));
threadPoolExecutor.execute(() -> sleep(5));
第二个任务提交5秒后,任务执行完毕,所以线程队列里的任务被执行,于是队列线程个数为0,活跃线程数量为2(第一个和第三个任务)。再过5秒后,第三个任务执行完毕,于是活跃线程数量为1(第一个100秒还没执行完毕)。
在第三个任务结束的瞬间,我们观察线程快照:
可以看到,线程池中有两个线程,Thread-0在执行第一个任务(休眠100秒,还没结束),Thread-1执行完第三个任务后并没有马上被销毁。过段时间后(10秒钟后)再观察线程快照:
可以看到,Thread-1这个线程被销毁了,因为我们在创建线程池的时候,指定keepAliveTime 为10秒,10秒后,超出核心线程池线程外的那些线程将被销毁。
假如一次性提交4个任务,看看会怎样:
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
threadPoolExecutor.execute(() -> sleep(100));
因为我们设置的拒绝策略为AbortPolicy,所以最后提交的那个任务直接被拒绝了。
3.2 关闭线程池
线程池包含以下几个状态:
当线程池中所有任务都处理完毕后,线程并不会自己关闭。我们可以通过调用shutdown
和shutdownNow
方法来关闭线程池。两者的区别在于:
shutdown
方法将线程池置为shutdown状态,拒绝新的任务提交,但线程池并不会马上关闭,而是等待所有正在执行的和线程队列里的任务都执行完毕后,线程池才会被关闭。所以这个方法是平滑的关闭线程池。shutdownNow
方法将线程池置为stop状态,拒绝新的任务提交,中断正在执行的那些任务,并且清除线程队列里的任务并返回。所以这个方法是比较“暴力”的。
shutdown
和shutdownNow
方法都不是阻塞的。常与shutdown
搭配的方法有awaitTermination
。
awaitTermination
方法接收timeout和TimeUnit两个参数,用于设定超时时间及单位。当等待超过设定时间时,会监测ExecutorService是否已经关闭,若关闭则返回true,否则返回false。该方法是阻塞的:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, 4, 10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy());
threadPoolExecutor.execute(new shortTask());
threadPoolExecutor.execute(new longTask());
threadPoolExecutor.execute(new longTask());
threadPoolExecutor.execute(new shortTask());
threadPoolExecutor.shutdown();
boolean isShutdown = threadPoolExecutor.awaitTermination(3, TimeUnit.SECONDS);
if (isShutdown) {
System.out.println("线程池在3秒内成功关闭");
} else {
System.out.println("等了3秒还没关闭,不等了╰(‵□′)╯");
}
System.out.println("------------");
}
static class shortTask implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName() + "执行shortTask完毕");
} catch (InterruptedException e) {
System.err.println("shortTask执行过程中被打断" + e.getMessage());
}
}
}
static class longTask implements Runnable {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread().getName() + "执行longTask完毕");
} catch (InterruptedException e) {
System.err.println("longTask执行过程中被打断" + e.getMessage());
}
}
}
启动程序输出如下:
3.3 四大拒绝策略
CallerRunsPolicy:由调用线程处理该任务:
AbortPolicy:丢弃任务,并抛出RejectedExecutionException
异常。
DiscardOldestPolicy:丢弃最早被放入到线程队列的任务,将新提交的任务放入到线程队列末端:
DiscardPolicy:直接丢弃新的任务,不抛异常:
3.4 线程池工厂方法 Executors
除了使用ThreadPoolExecutor的构造方法创建线程池外,我们也可以使用Executors
提供的工厂方法来创建不同类型的线程池:
3.4.1 newFixedThreadPool
查看newFixedThreadPool
方法源码:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到,通过newFixedThreadPool
创建的是一个固定大小的线程池,大小由nThreads
参数指定,它具有如下几个特点:
- 因为corePoolSize和maximumPoolSize的值都为nThreads,所以线程池中线程数量永远等于nThreads,不可能新建除了核心线程数的线程来处理任务,即keepAliveTime实际上在这里是无效的。
- LinkedBlockingQueue是一个无界队列(最大长度为Integer.MAX_VALUE),所以这个线程池理论是可以无限的接收新的任务,这就是为什么上面没有指定拒绝策略的原因。
3.4.2 newCachedThreadPool
查看newCachedThreadPool
方法源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这是一个理论上无限大小的线程池:
- 核心线程数为0,SynchronousQueue队列是没有长度的队列,所以当有新的任务提交,如果有空闲的还未超时的(最大空闲时间60秒)线程则执行该任务,否则新增一个线程来处理该任务。
- 因为线程数量没有限制,理论上可以接收无限个新任务,所以这里也没有指定拒绝策略。
3.4.3 newSingleThreadExecutor
查看newSingleThreadExecutor
源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- 核心线程数和最大线程数都为1,每次只能有一个线程处理任务。
- LinkedBlockingQueue队列可以接收无限个新任务。
3.4.4 newScheduledThreadPool
查看newScheduledThreadPool
源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
......
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
所以newScheduledThreadPool
理论是也是可以接收无限个任务,DelayedWorkQueue也是一个无界队列。
使用newScheduledThreadPool创建的线程池除了可以处理普通的Runnable任务外,它还具有调度的功能:
1.延迟指定时间后执行:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 延迟5秒执行
executorService.schedule(() -> System.out.println("hello"), 5, TimeUnit.SECONDS);
2.按指定的速率执行:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
// 延迟1秒执行,然后每5秒执行一次
executorService.scheduleAtFixedRate(
() -> System.out.println(LocalTime.now()), 1, 5, TimeUnit.SECONDS
);
3.按指定的时延执行:
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.scheduleWithFixedDelay(
() -> System.out.println(LocalTime.now()), 1, 5, TimeUnit.SECONDS
);
乍一看,scheduleAtFixedRate和scheduleWithFixedDelay没啥区别,实际它们还是有区别的:
- scheduleAtFixedRate按照固定速率执行任务,比如每5秒执行一个任务,即使上一个任务没有结束,5秒后也会开始处理新的任务;
- scheduleWithFixedDelay按照固定的时延处理任务,比如每延迟5秒执行一个任务,无论上一个任务处理了1秒,1分钟还是1小时,下一个任务总是在上一个任务执行完毕后5秒钟后开始执行。
对于这些线程池工厂方法的使用,阿里巴巴编程规程指出:
因为这几个线程池理论是都可以接收无限个任务,所以这就有内存溢出的风险。实际上只要我们掌握了ThreadPoolExecutor构造函数7个参数的含义,我们就可以根据不同的业务来创建出符合需求的线程池。一般线程池的创建可以参考如下规则:
- IO密集型任务:IO密集型任务线程并不是一直在执行任务,应该配置尽可能多的线程,线程池线程数量推荐设置为2 * CPU核心数;对于IO密集型任务,网络上也有另一种线程池数量计算公式:CPU核心数/(1 - 阻塞系数),阻塞系数取值0.8~0.9,至于这两种公式使用哪一个,可以根据实际环境测试比较得出;
- 计算密集型任务:此类型需要CPU的大量运算,所以尽可能的去压榨CPU资源,线程池线程数量推荐设置为CPU核心数 + 1。
CPU核心数可以使用Runtime
获得:Runtime.getRuntime().availableProcessors()
3.5 一些API的用法
几个判断线程池状态的方法:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, 2, 5, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy()
);
//threadPoolExecutor.allowCoreThreadTimeOut(true);
threadPoolExecutor.execute(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadPoolExecutor.shutdown();
System.out.println("线程池为shutdown状态:" + threadPoolExecutor.isShutdown());
System.out.println("线程池正在关闭:" + threadPoolExecutor.isTerminating());
System.out.println("线程池已经关闭:" + threadPoolExecutor.isTerminated());
threadPoolExecutor.awaitTermination(6, TimeUnit.SECONDS);
System.out.println("线程池已经关闭" + threadPoolExecutor.isTerminated());
}
程序输出如下:
前面我们提到,线程池核心线程即使是空闲状态也不会被销毁,除非使用allowCoreThreadTimeOut
设置了允许核心线程超时:值得注意的是,如果一个线程池调用了allowCoreThreadTimeOut(true)
方法,那么它的keepAliveTime
不能为0。
remove
方法,查看其源码:
public boolean remove(Runnable task) {
boolean removed = workQueue.remove(task);
tryTerminate(); // In case SHUTDOWN and now empty
return removed;
}
它删除的是线程队列中的任务,而非正在被执行的任务。
核心线程保持活跃的方法:
默认情况下,只有当往线程池里提交了任务后,线程池才会启动核心线程处理任务。我们可以通过调用prestartCoreThread
方法,让核心线程即使没有任务提交,也处于等待执行任务的活跃状态:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, 2, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy()
);
System.out.println("活跃线程数: " + threadPoolExecutor.getActiveCount());
threadPoolExecutor.prestartCoreThread();
System.out.println("活跃线程数: " + threadPoolExecutor.getActiveCount());
threadPoolExecutor.prestartCoreThread();
System.out.println("活跃线程数: " + threadPoolExecutor.getActiveCount());
threadPoolExecutor.prestartCoreThread();
System.out.println("活跃线程数: " + threadPoolExecutor.getActiveCount());
}
程序输出如下所示:
该方法返回boolean类型值,如果所以核心线程都启动了,返回false,反之返回true。
还有一个和它类似的prestartAllCoreThreads
方法,它的作用是一次性启动所有核心线程,让其处于活跃地等待执行任务的状态。
ThreadPoolExecutor的invokeAny方法用于随机执行任务集合中的某个任务,并返回执行结果,该方法是同步方法:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, 5, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy()
);
// 任务集合
List<Callable<Integer>> tasks = IntStream.range(0, 4).boxed().map(i -> (Callable<Integer>) () -> {
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(5));
return i;
}).collect(Collectors.toList());
// 随机执行结果
Integer result = threadPoolExecutor.invokeAny(tasks);
System.out.println("-------------------");
System.out.println(result);
threadPoolExecutor.shutdownNow();
}
启动程序,输出如下:
ThreadPoolExecutor的invokeAll则是执行任务集合中的所有任务,返回Future集合:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2, 5, 3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), (ThreadFactory) Thread::new,
new ThreadPoolExecutor.AbortPolicy()
);
List<Callable<Integer>> tasks = IntStream.range(0, 4).boxed().map(i -> (Callable<Integer>) () -> {
TimeUnit.SECONDS.sleep(ThreadLocalRandom.current().nextInt(5));
return i;
}).collect(Collectors.toList());
List<Future<Integer>> futureList = threadPoolExecutor.invokeAll(tasks);
futureList.stream().map(f->{
try {
return f.get();
} catch (InterruptedException | ExecutionException e) {
return null;
}
}).forEach(System.out::println);
threadPoolExecutor.shutdownNow();
}
输出如下:
总结下这些方法:
方法 | 描述 |
---|---|
allowCoreThreadTimeOut(boolean value) | 是否允许核心线程空闲后超时,是的话超时后核心线程将销毁,线程池自动关闭 |
awaitTermination(long timeout, TimeUnit unit) | 阻塞当前线程,等待线程池关闭,timeout用于指定等待时间。 |
execute(Runnable command) | 向线程池提交任务,没有返回值 |
submit(Runnable task) | 向线程池提交任务,返回Future |
isShutdown() | 判断线程池是否为shutdown状态 |
isTerminating() | 判断线程池是否正在关闭 |
isTerminated() | 判断线程池是否已经关闭 |
remove(Runnable task) | 移除线程队列中的指定任务 |
prestartCoreThread() | 提前让一个核心线程处于活跃状态,等待执行任务 |
prestartAllCoreThreads() | 提前让所有核心线程处于活跃状态,等待执行任务 |
getActiveCount() | 获取线程池活跃线程数 |
getCorePoolSize() | 获取线程池核心线程数 |
threadPoolExecutor.getQueue() | 获取线程池线程队列 |
getMaximumPoolSize() | 获取线程池最大线程数 |
shutdown() | 让线程池处于shutdown状态,不再接收任务,等待所有正在运行中的任务结束后,关闭线程池。 |
shutdownNow() | 让线程池处于stop状态,不再接受任务,尝试打断正在运行中的任务,并关闭线程池,返回线程队列中的任务。 |
4. BlockingQueue && BlockingDeque
BlockingQueue即阻塞队列,一个阻塞队列在数据结构中起到的作用大致如下图所示:
上图中,线程1往阻塞队列中添加元素,线程2从阻塞队列中移出元素。当阻塞队列是空的时候,从队列中获取元素的操作将会被阻塞;当阻塞队列是满的时候,往队列中添加元素的操作将会被阻塞。
使用BlockingQueue的好处是,我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这些都由BlockingQueue自动完成。
4.1 7️⃣大实现类
BlockingQueue是一个接口类,主要有7种实现类型,UML类图如下所示:
- ArrayBlockingQueue:由数组机构组成的有界阻塞队列;
- LinkedBlockingQueue:由链表结构组成的有界(默认大小非常大,为Integer.MAX_VALUE)阻塞队列;
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列;
- DelayQueue:一个使用优先级队列实现的无界阻塞队列;
- SynchronousQueue:一个不存储元素的阻塞队列;
- LinkedTransferQueue:一个由链表结构组成的无界阻塞队列;
- LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
前六种都是单向队列,实现的是BlockingQueue
接口,LinkedBlockingDeque
是一个双向队列,实现的是BlockDeque
接口,该接口继承了BlockingQueue
接口。
4.2 常用方法
BlockingQueue的相关方法大致可以分为以下四种类型:
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
获取并移除队列的头 | remove() | poll() | take() | poll(time,unit) |
获取但不移除队列的头 | element() | peek() | 不可用 | 不可用 |
- 抛出异常:当阻塞队列满时,再往队列里add插入元素就会抛出IllegalStateException: Queue full;当阻塞队列空时,再往队列里remove移出元素就会抛出NoSuchElementException;
- 返回特殊值:插入方法,成功时返回true,失败时返回false;移出方法,成功时候返回移出队列的元素,没有元素就返回null;
- 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到消费者取出数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用;
- 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。当阻塞队列为空时,队列会阻塞消费者线程一段时间,如果超过一定时间,消费者线程就会退出。
BlockDeque也提供了这四种类型对应的方法,不过由于是双向队列,所以这些方法可以分为头部操作和尾部操作:
头部操作:
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | addFirst(e) | offerFirst(e) | putFirst(e) | offerFirst(e, time, unit) |
获取并移除队列的头 | removeFirst() | pollFirst() | takeFirst() | pollFirst(time, unit) |
获取但不移除队列的头 | getFirst() | peekFirst() | 不适用 | 不适用 |
尾部操作:
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | addLast(e) | offerLast(e) | putLast(e) | offerLast(e, time, unit) |
获取并移除队列的头 | removeLast() | pollLast() | takeLast() | pollLast(time, unit) |
获取但不移除队列的头 | getLast() | peekLast() | 不适用 | 不适用 |
4.3 简单介绍
4.3.1 ArrayBlockingQueue
ArrayBlockingQueue
是一个用数组实现的有界阻塞队列。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问队列,所谓公平访问队列是指阻塞的所有生产者线程或消费者线程,当队列可用时,可以按照阻塞的先后顺序访问队列,即先阻塞的生产者线程,可以先往队列里插入元素,先阻塞的消费者线程,可以先从队列里获取元素。通常情况下为了保证公平性会降低吞吐量。我们可以使用以下代码创建一个公平的阻塞队列:
ArrayBlockingQueue<String> fairArrayBlockingQueue = new ArrayBlockingQueue<>(5, true);
上面代码指定了一个初始大小为5的公平的ArrayBlockingQueue
。访问者的公平性是使用可重入锁实现的,构造器源码如下:
4.3.2 LinkedBlockingQueue
LinkedBlockingQueue
是一个用链表实现的有界阻塞队列。此队列按照先进先出的原则对元素进行排序。此队列的默认和最大长度为Integer.MAX_VALUE
:
所以推荐的做法是不要使用无参构造器,而是通过有参构造器指定容器的初始大小。
4.3.3 PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界队列。默认情况下元素采取自然顺序排列,也可以通过比较器comparator来指定元素的排序规则。
不指定排序时:
public class PriorityBlockingQueueTest {
public static void main(String[] args) {
PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(3);
IntStream.range(0, 3).forEach(i -> queue.add(i + "hello"));
queue.forEach(s -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
输出:
0hello
1hello
2hello
指定排序规则时:
public class PriorityBlockingQueueTest {
public static void main(String[] args) {
PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(3,
Comparator.comparingInt(String::hashCode).reversed());
IntStream.range(0, 3).forEach(i -> queue.add(i + "hello"));
queue.forEach(s -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
输出:
2hello
1hello
0hello
此外,因为PriorityBlockingQueue
是一个无界队列,所以可以添加无数个元素:
public class PriorityBlockingQueueTest {
public static void main(String[] args) {
PriorityBlockingQueue<String> queue = new PriorityBlockingQueue<>(1);
IntStream.range(0, 5).forEach(i -> queue.add(i + "hello"));
queue.forEach(s -> {
try {
System.out.println(queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
输出:
0hello
1hello
2hello
3hello
4hello
因为是无界队列,所以上述例子并没有抛出java.lang.IllegalStateException: Queue full
异常。
4.3.4 DelayQueue
DelayQueue
也是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。DelayQueue
的所有方法只能操作“到期的元素“,例如,poll()
、remove()
、size()
等方法,都会忽略掉未到期的元素。 我们可以将DelayQueue
运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询
DelayQueue
,一旦能从DelayQueue
中获取元素时,表示缓存有效期到了; - 定时任务调度。使用
DelayQueue
保存当天将会执行的任务和执行时间,一旦从DelayQueue
中获取到任务就开始执行。
DelayQueue
的实现是基于PriorityQueue
,是一个优先级队列,是以延时时间的长短进行排序的。所以,DelayQueue
需要知道每个元素的延时时间,而这个延时时间是由Delayed
接口的getDelay()
方法获取的。所以,DelayQueue
的元素必须实现Delayed
接口。
举个例子,先创建一个队列元素类Item,实现Delayed
接口:
public class Item implements Delayed {
private final String value;
private final long expireTime;
public Item(String value, long delayTime) {
this.value = value;
this.expireTime = System.currentTimeMillis() + delayTime;
}
@Override
public long getDelay(TimeUnit unit) {
// 获取延迟时间
long diff = expireTime - System.currentTimeMillis();
return unit.convert(diff, TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(@Nonnull Delayed o) {
Item item = (Item) o;
// 按延迟时间从小到大排序
return Long.compare(this.expireTime, item.getExpireTime());
}
public String getValue() {
return value;
}
public long getExpireTime() {
return expireTime;
}
}
getDelay
方法用于获取延迟时间,compareTo
方法用于指定排序规则。DelayQueue
内部使用PriorityBlockingQueue
实现排序。
创建一个测试用例:
public class DelayQueueTest {
private static Logger log = LoggerFactory.getLogger(DelayQueueTest.class);
public static void main(String[] args) throws InterruptedException {
DelayQueue<Item> delayQueue = new DelayQueue<>();
Item item1 = new Item("item1 - 延迟1秒", 1000);
Item item2 = new Item("item2 - 延迟4秒", 4000);
Item item3 = new Item("item3 - 延迟2秒", 2000);
delayQueue.add(item1);
delayQueue.add(item2);
delayQueue.add(item3);
log.info("start");
while (delayQueue.size() > 0) {
log.info(delayQueue.take().getValue());
}
}
}
程序运行结果如下:
14:11:32.488 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - start
14:11:33.490 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item1 - 延迟1秒
14:11:34.488 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item3 - 延迟2秒
14:11:36.490 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item2 - 延迟4秒
可以看到,队列内元素是按延迟时间从小到大排序的,延迟1秒后take
方法从队列里获取到了item1,延迟2秒后获取到了item3,延迟4秒后获取到了item2。
如果我们将排序规则改为按延迟时间从大到小排列,会发生什么呢?
public class Item implements Delayed {
......
@Override
public int compareTo(@Nonnull Delayed o) {
Item item = (Item) o;
// 按延迟时间从大到小排序
return Long.compare(item.getExpireTime(), this.expireTime);
}
}
程序输出如下:
14:15:01.721 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - start
14:15:05.722 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item2 - 延迟4秒
14:15:05.723 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item3 - 延迟2秒
14:15:05.723 [main] INFO cc.mrbird.wwj.blocking.DelayQueueTest - item1 - 延迟1秒
可以看到,程序延迟4秒后,take
方法从队列中获取到了item2,因为item3和item1的延迟时间小于4秒,所以4秒后,可以直接取出。
4.3.4 SynchronousQueue
SynchronousQueue
是一个不存储元素的阻塞队列。每一个put操作必须等待一个take操作,否则不能继续添加元素。SynchronousQueue
可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身并不存储任何元素,非常适合于传递性场景,比如在一个线程中使用的数据,传递给另外一个线程使用。
4.3.5 LinkedTransferQueue
LinkedTransferQueue
是一个由链表结构组成的无界阻塞TransferQueue
队列。相对于其他阻塞队列LinkedTransferQueue
多了tryTransfer
和transfer
方法。
transfer
方法:如果当前有消费者正在等待接收元素(消费者使用take()
方法或带时间限制的poll()
方法时),transfer
方法可以把生产者传入的元素立刻transfer
(传输)给消费者。如果没有消费者在等待接收元素,transfer
方法会将元素存放在队列的tail
节点,并等到该元素被消费者消费了才返回。tryTransfer
方法。则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false
。和transfer
方法的区别是tryTransfer
方法无论消费者是否接收,方法立即返回。而transfer
方法是必须等到消费者消费了才返回。我们也可以使用重载方法tryTransfer(E e, long timeout, TimeUnit unit)
指定超时时间。
4.3.6 LinkedBlockingDeque
LinkedBlockingDeque
是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。相比其他的阻塞队列,LinkedBlockingDeque
多了一些首尾元素的操作方法,具体可以参考上面的表格。
参考文章
ThreadLocal使用学习
2019-03-15 | Visit count
ThreadLocal字面上的意思是局部线程变量,每个线程通过ThreadLocal的get
和set
方法来访问和修改线程自己独有的变量。简单地说,ThreadLocal的作用就是为每一个线程提供了一个独立的变量副本,每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
5. ThreadLocal的基本使用
ThreadLocal是一个泛型类,在创建的时候需要指定变量的类型:
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal提供了set
方法来设置变量的值,get
方法获取变量的值,remove
方法移除变量:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
threadLocal.set("mrbird");
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
程序输出如下:
我们也可以给ThreadLocal设置初始值,设置初始值有两种方式:
1.重写initialValue
方法:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "初始值";
}
};
public static void main(String[] args) throws InterruptedException {
System.out.println(threadLocal.get()); // 初始值
}
}
2.使用ThreadLocal的withInitial
方法:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "初始值");
public static void main(String[] args) throws InterruptedException {
System.out.println(threadLocal.get()); // 初始值
}
}
值得注意的是remove
无法移除初始值:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "初始值");
public static void main(String[] args) throws InterruptedException {
threadLocal.remove();
System.out.println(threadLocal.get()); // 初始值
}
}
5.1 演示多线程间独立
在多个线程中使用ThreadLocal:
public class ThreadLocalTest2 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
threadLocal.set("thread t1");
try {
TimeUnit.MICROSECONDS.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1");
Thread thread2 = new Thread(() -> {
threadLocal.set("thread t2");
try {
TimeUnit.MICROSECONDS.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}
}
程序输出如下:
结果证明了ThreadLocal在每个线程间是相互独立的,threadLocal在thread1、thread2和main线程间都有一份独立拷贝。
5.2 ThreadLocal基本原理
在ThreadLocal类中有一个静态内部类ThreadLocalMap(概念上类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本。
我们使用Map来代替ThreadLocalMap,创建一个简易的类ThreadLocal实现:
public class MyThreadLocal<T> {
private final Map<Thread, T> threadLocalMap = new HashMap<>();
public void set(T t) {
synchronized (this) {
Thread key = Thread.currentThread();
threadLocalMap.put(key, t);
}
}
public T get() {
synchronized (this) {
Thread key = Thread.currentThread();
T t = threadLocalMap.get(key);
if (t == null) {
return initalValue();
} else {
return t;
}
}
}
public T initalValue() {
return null;
}
}
使用方式和之前的例子一致:
public class ThreadLocalTest3 {
private static MyThreadLocal<String> threadLocal = new MyThreadLocal<String>() {
@Override
public String initalValue() {
return "initalValue";
}
};
private static Random random = new Random(System.currentTimeMillis());
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
threadLocal.set("thread t1");
try {
TimeUnit.MICROSECONDS.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread1");
Thread thread2 = new Thread(() -> {
threadLocal.set("thread t2");
try {
TimeUnit.MICROSECONDS.sleep(random.nextInt(1000));
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
}
}
程序输出如下:
5.3 使用建议
- 将ThreadLocal变量指定为
private static
; - 使用完毕后显式地调用
remove
方法移除。
6. Java Concurrency Lock
Lock锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源(但是有些锁可以允许多个线程并发的访问共享资源,比如读写锁)。在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的,而Java SE 5之后,并发包中新增了Lock接口(以及相关实现类)用来实现锁功能,它提供了与synchronized关键字类似的同步功能,只是在使用时需要显式地获取和释放锁。虽然它缺少了(通过synchronized块或者方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。
6.1 ReentrantLock
ReentrantLock字面上意思就是可重入锁(又称为递归锁),表示该锁能够支持一个线程对资源的重复加锁。定义一个ReentrantLock:
ReentrantLock lock = new ReentrantLock();
默认无参构造函数创建的是非公平锁,构造函数重载方法ReentrantLock(boolean fair)
支持传入true
创建公平锁。公平锁的意思是多线程在获取锁的时候是公平的,也就是等待时间最长的线程最优先获取锁,类似FIFO。
使用ReentrantLock可以实现和synchronized一样的功能:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
IntStream.range(0, 2).forEach(i -> new Thread(ReentrantLockTest::needLock).start());
}
public static void needLock() {
try {
lock.lock();
System.out.println(Thread.currentThread().getName() + "开始工作");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
确保在finally里释放锁,否则容易造成死锁。
上面例子同一时刻只能有一个线程可以获得锁lock
,别的线程必须等待锁被释放(unlock
)才能开始竞争获取锁。程序运行结果如下所示:
needLock
方法和下面通过synchronized关键字实现锁方法效果是一样的:
public static void needLockBySync() {
synchronized (ReentrantLockTest.class) {
try {
System.out.println(Thread.currentThread().getName() + "开始工作");
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
为什么ReentrantLock
又称为递归锁呢?这是因为:一个线程如果获取了某个方法的锁,这个方法内部即使调用了别的需要获取锁的方法,那么这个线程不需要再次等待获取锁,可以直接进去。说着可能有点抽象,下面举个例子:
public class Test {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
IntStream.rangeClosed(0,1).forEach(i-> new Thread(Test::method1, String.valueOf(i)).start());
}
public static void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " invoked method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " invoked method2");
} finally {
lock.unlock();
}
}
}
程序运行结果:
0 invoked method1
0 invoked method2
1 invoked method1
1 invoked method2
上面例子中,method1和method2都加了锁,线程0获取到了method1的锁后,内部可以直接调用method2,无需重新获取锁对象。synchronized
也具有相同的特性。
ReentrantLock
可以对一个方法不限次的重复加锁,但解锁次数必须和加锁次数一致,否则锁永远不会被释放,别的线程将无法获取该方法的锁,比如:
public class Test {
private static Lock lock = new ReentrantLock();
public static void main(String[] args) {
IntStream.rangeClosed(0,1).forEach(i-> new Thread(Test::method1, String.valueOf(i)).start());
}
public static void method1() {
// 加锁4次
lock.lock();
lock.lock();
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " invoked method1");
} finally {
// 解锁3次
lock.unlock();
lock.unlock();
lock.unlock();
}
}
}
程序运行结果:
线程1一直处于WAITING状态,因为线程0加锁了4次,但只释放了3次锁,所以线程1一直无法获取到锁。
lock
方法是不可被打断的,即调用线程的interrupt
方法不起作用:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(ReentrantLockTest::testLockUnInterruptibly);
thread1.start();
TimeUnit.SECONDS.sleep(1);
Thread thread2 = new Thread(ReentrantLockTest::testLockUnInterruptibly);
thread2.start();
TimeUnit.SECONDS.sleep(1);
thread2.interrupt();
}
public static void testLockUnInterruptibly() {
try {
lock.lock(); // 不可以被打断
System.out.println(Thread.currentThread().getName() + "开始工作");
while (true) { }
} finally {
lock.unlock();
}
}
}
运行结果:
thread2(Thread-1)依旧在继续等待获取锁,没有被打断。
ReentrantLock提供了可打断获取锁的方法lockInterruptibly
:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(ReentrantLockTest::testLockInterruptibly);
thread1.start();
TimeUnit.SECONDS.sleep(1);
Thread thread2 = new Thread(ReentrantLockTest::testLockInterruptibly);
thread2.start();
TimeUnit.SECONDS.sleep(1);
thread2.interrupt();
}
public static void testLockInterruptibly() {
try {
lock.lockInterruptibly(); // 可以被打断
System.out.println(Thread.currentThread().getName() + "开始工作");
while (true) { }
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
thread2在等待获取锁时被打断,抛出InterruptedException
异常。
ReentrantLock的tryLock
方法用于尝试获取锁,返回boolean类型,表示获取锁成功与否:
public class ReentrantLockTest {
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(ReentrantLockTest::testTryLock, "thread1");
thread1.start();
TimeUnit.SECONDS.sleep(1);
Thread thread2 = new Thread(ReentrantLockTest::testTryLock, "thread2");
thread2.start();
}
public static void testTryLock() {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + "开始工作");
while (true) { }
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + "没有获取到锁");
}
}
}
thread1抢到锁后进入死循环,一直不释放锁。thread2尝试获取锁失败后直接放弃。
tryLock
的重载方法tryLock(long timeout, TimeUnit unit)
可以设置尝试获取锁的时间范围,超过这个时间没有获取到锁则返回false。
ReentrantLock一些别的方法:
方法 | 含义 |
---|---|
getQueueLength() |
等待获取锁线程数量 |
hasQueuedThreads() |
是否有在等待获取锁的线程 |
hasQueuedThread(Thread thread) |
等待获取锁的线程队列里是包含指定的线程 |
isLocked |
当前锁是否被任意一个线程获取到了 |
6.2 Spin Lock
JUC中并没有自旋锁对应的类,而所谓的自旋锁就是:尝试获取锁的线程不会马上阻塞,而是采用循环的方式去尝试获取锁。这种方式的好处是可以减少线程上下文切换的消耗,缺点是循环会消耗CPU资源。一个经典的自旋锁例子就是unsafe类里的CAS思想:
我们可以利用CAS实现一个自旋锁:
public class SpinLock {
AtomicReference<Thread> reference = new AtomicReference<>();
public void lock() {
Thread thread = Thread.currentThread();
System.out.println(thread.getName() + "尝试获取锁");
while (!reference.compareAndSet(null, thread)) {
// 自旋锁就是利用CAS思想制造循环,block住代码
}
System.out.println(thread.getName() + "获取到了锁");
}
public void unlock() {
Thread thread = Thread.currentThread();
reference.compareAndSet(thread, null);
System.out.println(thread.getName() + "释放锁");
}
public static void main(String[] args) {
SpinLock lock = new SpinLock();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "做某事...");
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "线程1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "做某事...");
TimeUnit.SECONDS.sleep(5);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "线程2").start();
}
}
程序输出如下:
线程1尝试获取锁
线程1获取到了锁
线程1做某事...
线程2尝试获取锁
线程1释放锁
线程2获取到了锁
线程2做某事...
线程2释放锁
6.3 ReadWriteLock
ReadWriteLock为读写锁。ReentrantLock为排他锁,同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
简而言之,ReadWriteLock包含读写锁,遵循以下规则:
- 写的时候不能读
- 写的时候不能写
- 读的时候不能写
- 读的时候可以读
ReadWriteLock为接口,我们使用它的实现类ReentrantReadWriteLock创建读写锁实例:
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
使用读写锁创建一个读写的例子:
public class ReadWriteLockTest {
private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
// 读锁
private static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
// 写锁
private static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
// 存放数据
private static List<Long> data = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (true) {
write();
}
}, "writer").start();
new Thread(() -> {
while (true) {
read();
}
}, "reader").start();
}
public static void write() {
try {
writeLock.lock(); // 写锁
TimeUnit.SECONDS.sleep(1);
long value = System.currentTimeMillis();
data.add(value);
System.out.println(Thread.currentThread().getName() + " 写入value: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock(); // 释放写锁
}
}
public static void read() {
try {
readLock.lock(); // 获取读锁
TimeUnit.SECONDS.sleep(1);
String value = data.stream().map(String::valueOf).collect(Collectors.joining(","));
System.out.println(Thread.currentThread().getName() + "读取data: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock(); // 释放读锁
}
}
}
ReentrantReadWriteLock还包含了一些别的实用方法:
功能和上面介绍的ReentrantLock差不多,方法名见名知其意,不再演示了。
6.4 JDK8 StampedLock
JDK8 新增了一个锁StampedLock,它是对ReadWriteLock的改进。
使用ReadWriteLock的时候,当读线程数量远大于写线程数量的时候就会出现“写饥饿”现象。因为锁大概率都被读线程抢走了,写线程很难抢到锁,这将使得读写效率非常低下。
JDK8的StampedLock就是为了解决这个问题而设计的,StampedLock包含乐观锁和悲观锁:
- 乐观锁:每次去拿数据的时候,并不获取锁对象,而是判断标记位(stamp)是否有被修改,如果有修改就再去读一次。
- 悲观锁:每次拿数据的时候都去获取锁。
通过乐观锁,当写线程没有写数据的时候,标志位stamp并没有改变,所以即使有再多的读线程在读取数据,它们都可以直接去读数据,而无需获取锁,这就不会使得写线程抢不到锁了。
stamp类似一个时间戳的作用,每次写的时候对其+1来改变被操作对象的stamp值。
下面我们通过一个例子来模拟写饥饿的情况:创建20个线程,其中19个线程用于读数据,1个线程用于写数据:
public class StampedLockTest {
private static StampedLock lock = new StampedLock();
private static List<Long> data = new ArrayList<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
Runnable read = StampedLockTest::read;
Runnable write = StampedLockTest::write;
IntStream.range(0, 19).forEach(i -> executorService.submit(read));
executorService.submit(write);
executorService.shutdown();
}
private static void read() {
long stamped = -1;
try {
stamped = lock.readLock(); // 获取悲观锁,阻塞写线程
TimeUnit.SECONDS.sleep(1);
String collect = data.stream().map(String::valueOf).collect(Collectors.joining(","));
System.out.println(Thread.currentThread().getName() + " read value: " + collect);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlockRead(stamped);
}
}
private static void write() {
long stamped = -1;
try {
stamped = lock.writeLock();
TimeUnit.SECONDS.sleep(1);
long value = System.currentTimeMillis();
data.add(value);
System.out.println(Thread.currentThread().getName() + " write value: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlockWrite(stamped);
}
}
}
上面例子通过StampedLock调用writeLock
、unlockWrite
、readLock
和unlockRead
的时候都会导致StampedLock的stamp值的变化,即每次+1,直到加到最大值,然后从0重新开始。
上面程序运行结果如下:
可以看到写线程最后才抢到锁并写入数据。
我们通过乐观锁来改善这个例子:
public class StampedLockTest2 {
private static StampedLock lock = new StampedLock();
private static List<Long> data = new ArrayList<>();
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(20);
Runnable read = StampedLockTest2::read;
Runnable write = StampedLockTest2::write;
IntStream.range(0, 19).forEach(i -> executorService.submit(read));
executorService.submit(write);
executorService.shutdown();
}
private static void read() {
long stamped = lock.tryOptimisticRead(); // 获取乐观锁
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 直接读取值
String collect = data.stream().map(String::valueOf).collect(Collectors.joining(","));
// 如果戳被改变,方法返回false,说明stamped被修改过了(被write方法修改过了,有新的数据写入),
// 那么重新获取锁并去读取值,否则直接使用上面读取的值。
if (!lock.validate(stamped)) {
try {
stamped = lock.readLock();
TimeUnit.SECONDS.sleep(1);
collect = data.stream().map(String::valueOf).collect(Collectors.joining(","));
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlockRead(stamped);
}
}
System.out.println(Thread.currentThread().getName() + " read value: " + collect);
}
private static void write() {
long stamped = -1;
try {
stamped = lock.writeLock();
TimeUnit.SECONDS.sleep(1);
long value = System.currentTimeMillis();
data.add(value);
System.out.println(Thread.currentThread().getName() + " write value: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlockWrite(stamped);
}
}
}
我们重点关注read
方法。read
方法一开始通过调用StampedLock的tryOptimisticRead
方法来获取标志位stamp,获取乐观锁并不会真正的去获取锁(所以不会阻塞写操作),然后直接去读数据。接着通过StampedLock的validate
方法判断标志位stamp是否被修改了(write
方法里会修改标志位的值),如果方法返回true,则说明数据没有被修改过,直接使用前面读取的数据即可;否则需要去获取锁重新去读数据,阻止写操作。
上面例子运行结果如下:
可以看到,写操作一开始就抢到了锁,并写入了数据。
简而言之,StampedLock解决了在没有新数据写入时,由于过多读操作抢夺锁而使得写操作一直获取不到锁无法写入新数据的问题。
6.5 Condition
Condition接口提供了类似Object的wait
、notify
和notifyAll
方法,与Lock配合可以实现生产/消费模式,但是这两者在使用方式以及功能特性上还是有差别的。
使用Codition实现一个生产消费的例子:
public class ConditionTest {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
// 初始数据
private static int data = 0;
// 是否被消费
private static volatile boolean consumed = false;
public static void main(String[] args) {
new Thread(() -> {
while (true) {
produceData();
}
},"producer").start();
new Thread(() -> {
while (true) {
consumeData();
}
},"consumer").start();
}
public static void produceData() {
try {
lock.lock(); // 获取锁
while (!consumed) { // 判断数据是否被消费
condition.await(); // 如果没有被消费则进入等待
}
TimeUnit.SECONDS.sleep(1);
data++;
System.out.println(Thread.currentThread().getName() + " produce data = " + data);
consumed = false; // 生产完数据将消费标识置为false
condition.signal(); // 解除await,用于通知消费者可以开始消费了
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
public static void consumeData() {
try {
lock.lock(); // 获取锁
while (consumed) { // 判断数据是否被消费
condition.await(); // 如果被消费了则进入等待
}
TimeUnit.MICROSECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + " consume data = " + data);
consumed = true; // 消费完将消费标识置为true
condition.signal(); // 解除await,用于通知生产者可以开始生产了
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock(); // 释放锁
}
}
}
上面例子中,通过consumed
判断数据是否被消费。produceData
方法在获取锁后,判断数据是否被消费,如果没有被消费,则调用Condition的await
方法进入等待,直到Condition对象的signal
方法被调用;consumeData
方法逻辑和produceData
一致。
Condition核心用法就是通过await
方法让线程进入阻塞等待状态,通过signal
解除阻塞状态。上面的例子运行效果如下所示:
对应上面的例子我们可以思考下面三个问题:
- 是否可以只使用Lock而不使用Condition?
- 生产者抢到了锁进入await,并没有释放锁,为什么消费者可以获得锁?
- 是否可以只使用Condition不使用Lock?
对于第一个问题:是否可以只使用Lock而不使用Condition?
虽然我们可以定义公平的ReentrantLock,但是实际上并不能确保100%公平,只是尽可能的公平。生产消费模型必须为生产者生成完了数据通知消费者消费,消费者消费完了通知生产者生产,这是环环相扣的,不允许出现别的情况。
对于第二个问题:生产者抢到了锁进入await,并没有释放锁,为什么消费者可以获得锁?
假如一开始produceData
方法先通过lock.lock()
获取到了锁,consumed初始值为false,所以接着方法会调用condition.await()
进入阻塞等待。await
方法会使得当前线程释放锁对象,然后进入休眠状态,直到发生下面三种情况之一才会被解除休眠:
- Condition的
signal
方法被调用; - Condition的
signalAll
方法被调用; - 其他线程调用了当前线程的
interrupt
方法。
对于第三个问题:是否可以只使用Condition不使用Lock?
既然await
会使得线程进入阻塞等待状态,那么是否可以直接使用await
,而不使用Lock呢?我们改造上面的例子,去掉获取和释放锁的相关代码:
public class ConditionTest {
// 公平锁
private static ReentrantLock lock = new ReentrantLock(true);
private static Condition condition = lock.newCondition();
// 初始数据
private static int data = 0;
// 是否被消费
private static volatile boolean consumed = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (true) {
produceData();
}
}).start();
new Thread(() -> {
while (true) {
consumeData();
}
}).start();
}
public static void produceData() {
try {
while (!consumed) {
condition.await();
}
TimeUnit.SECONDS.sleep(1);
data++;
System.out.println(Thread.currentThread().getName() + " produce data = " + data);
consumed = false;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void consumeData() {
try {
while (consumed) {
condition.await();
}
TimeUnit.MICROSECONDS.sleep(500);
System.out.println(Thread.currentThread().getName() + " consume data = " + data);
consumed = true;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
可以看到,程序抛出IllegalMonitorStateException
异常,所以Condition必须配合Lock使用。
正如前面说的,Condition的功能类似于Object对象的wait
和notify
方法,下面我们使用Object对象的wait
和notify
方法实现一个类似上面生产消费的功能:
public class WaitNotifyExample {
private static int data = 0;
private static volatile boolean used = false;
private final static Object MONITOR = new Object();
public static void main(String[] args) {
new Thread(() -> {
while (true) {
produceData();
}
}, "thread1").start();
new Thread(() -> {
while (true) {
consumeData();
}
}, "thread2").start();
}
public static void produceData() {
synchronized (MONITOR) {
while (!used) {
try {
MONITOR.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
data++;
System.out.println(Thread.currentThread().getName() + " 生产data = " + data);
used = false;
MONITOR.notifyAll();
}
}
public static void consumeData() {
synchronized (MONITOR) {
while (used) {
try {
MONITOR.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 消费data = " + data);
used = true;
MONITOR.notifyAll();
}
}
}
效果如下所示:
Condition
还可以绑定多个条件,并唤醒指定的线程,举个三个线程循环干活的例子:
public class Loop {
private volatile String value = "a";
private Lock lock = new ReentrantLock();
// 条件1
private Condition condition1 = lock.newCondition();
// 条件2
private Condition condition2 = lock.newCondition();
// 条件3
private Condition condition3 = lock.newCondition();
public void printA() {
try {
lock.lock();
while (!value.equals("a")) {
condition1.await();
}
System.out.println(Thread.currentThread().getName() + " print a");
value = "b";
condition2.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printB() {
try {
lock.lock();
while (!value.equals("b")) {
condition2.await();
}
System.out.println(Thread.currentThread().getName() + " print b");
value = "c";
condition3.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void printC() {
try {
lock.lock();
while (!value.equals("c")) {
condition3.await();
}
System.out.println(Thread.currentThread().getName() + " print c");
value = "a";
condition1.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Loop loop = new Loop();
IntStream.rangeClosed(0, 2).forEach(i -> new Thread(loop::printA, "线程A").start());
IntStream.rangeClosed(0, 2).forEach(i -> new Thread(loop::printB, "线程B").start());
IntStream.rangeClosed(0, 2).forEach(i -> new Thread(loop::printC, "线程C").start());
}
}
程序输出如下:
线程A print a
线程B print b
线程C print c
线程A print a
线程B print b
线程C print c
线程A print a
线程B print b
线程C print c
6.6 synchronized和Lock区别
synchronized和Lock都是用于控制多个线程访问共享资源的方式,但他们还是有区别的,主要可以从下面几个方面比较:
1.构成不一样
synchronized
是Java关键字,属于JVM层面,底层是由monitorenter和monitorexit指令完成(查看字节码证实):
而Lock是JUC下的Java接口类,是API层面的🔐。
2.使用方式不同
synchronized
不需要我们手动释放锁,当synchronized
代码执行完后,当前线程会自动释放锁;
ReentrantLock
需要手动释放锁,不然会造成死锁。
3.可中断性
synchronized
是不可中断的,除非同步方法内抛出异常或者程序正常运行完成; ReentrantLock
是可以中断的,比如lockInterruptibly()
方法。
4.公平否
synchronized
是非公平锁;
ReentrantLock
可以通过构造方法ReentrantLock(boolean fair)
设置公平与否。
5.灵活性
synchronized
不可以设置条件;
ReentrantLock
可以通过condition绑定多条件,精确唤醒指定线程。
7. Fork/Join使用学习
2019-03-21 | Visit count 689315
JDK7提供了一个将任务“分而治之”的框架 — Fork/Join。它把一个大的任务分割成足够小的子任务,如果子任务比较大的话还要对子任务进行继续分割。分割的子任务分别放到双端队列里,然后启动线程分别从双端队列里获取任务执行。子任务执行完的结果都放在另外一个队列里,启动一个线程从队列里取数据,然后合并这些数据。
Fork/Join的思想如下所示:
7.1 RecursiveTask
RecursiveTask适用于将任务分而治之,并且有返回值的情况,举个计算1到100和的例子:
public class RecursiveTest {
// 定义最小区间为10
private final static int MAX_THRESHOLD = 10;
public static void main(String[] args) {
final ForkJoinPool forkJoinPool = new ForkJoinPool();
ForkJoinTask<Integer> future = forkJoinPool.submit(new CalculateRecursiveTask(1, 100));
try {
Integer result = future.get();
System.out.println(result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
private static class CalculateRecursiveTask extends RecursiveTask<Integer> {
// 起始
private int start;
// 结束
private int end;
public CalculateRecursiveTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
// 如果起始和结束范围小于我们定义的区间范围,则直接计算
if ((end - start) <= MAX_THRESHOLD) {
return IntStream.rangeClosed(start, end).sum();
} else {
// 否则,将范围一分为二,分成两个子任务
int middle = (start + end) / 2;
CalculateRecursiveTask leftTask = new CalculateRecursiveTask(start, middle);
CalculateRecursiveTask rightTask = new CalculateRecursiveTask(middle + 1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 汇总子任务
return leftTask.join() + rightTask.join();
}
}
}
}
ForkJoinPool使用submit或invoke提交的区别:invoke是同步执行,调用之后需要等待任务完成,才能执行后面的代码;submit是异步执行,只有在Future调用get的时候会阻塞。
启动程序输出如下:
其实这里执行子任务调用fork方法并不是最佳的选择,最佳的选择是invokeAll方法:
// 执行子任务
// leftTask.fork();
// rightTask.fork();
invokeAll(leftTask,rightTask);
// 汇总子任务
return leftTask.join() + rightTask.join();
7.2 RecursiveAction
使用方式和RecursiveTask类似,只不过没有返回值:
public class RecursiveActionTest {
// 定义最小区间为10
private final static int MAX_THRESHOLD = 10;
private final static AtomicInteger SUM = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(new CalculateRecursiveAction(0, 100));
forkJoinPool.awaitTermination(2, TimeUnit.SECONDS);
System.out.println(SUM);
}
private static class CalculateRecursiveAction extends RecursiveAction {
// 起始
private final int start;
// 结束
private final int end;
private CalculateRecursiveAction(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected void compute() {
// 如果起始和结束范围小于我们定义的区间范围,则直接计算
if ((end - start) <= MAX_THRESHOLD) {
SUM.addAndGet(IntStream.rangeClosed(start, end).sum());
} else {
// 否则,将范围一分为二,分成两个子任务
int middle = (end + start) / 2;
CalculateRecursiveAction leftAction = new CalculateRecursiveAction(start, middle);
CalculateRecursiveAction rightAction = new CalculateRecursiveAction(middle + 1, end);
// 执行子任务
invokeAll(leftAction, rightAction);
// 没有汇总子任务结果过程,因为没有返回值。
}
}
}
}
输出结果也是5050。
7.3 什么时候用
上面只是为了演示Fork/Join的用法,实际是采用这种方式计算反而更加费时,因为切割任务,分配线程需要额外的开销。其实什么时候用不必太纠结,一个足够大的任务,如果采用Fork/Join来处理比传统处理方式快的话,那就毫不犹豫的选择它吧!
参考文章:https://www.imooc.com/article/24822
8. JUC之CountDownLatch
CountDownLatch允许一个或多个线程等待其他线程完成操作。定义CountDownLatch的时候,需要传入一个正数来初始化计数器(虽然传入0也可以,但这样的话CountDownLatch没什么实际意义)。其countDown
方法用于递减计数器,await
方法会使当前线程阻塞,直到计数器递减为0。所以CountDownLatch常用于多个线程之间的协调工作。
8.1 CountDownLatch示例
假设我们现在有这样一个需求:
- 从数据库获取数据
- 对这批数据进行处理
- 保存这批数据
为了让程序执行效率更高,第2步中我们可以使用多线程来并行处理这批数据,大致过程如下所示:
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
// 1. 模拟从数据库获取数据
int[] data = query();
System.out.println("获取数据完毕");
// 2. 数据处理
IntStream.range(0, data.length).forEach(i -> {
ExecutorService.execute(() -> {
System.out.println(Thread.currentThread() + "处理第" + (i + 1) + "条数据");
int value = data[i];
if (value % 2 == 0) {
data[i] = value * 2;
} else {
data[i] = value * 10;
}
});
});
System.out.println("所有数据都处理完了");
// 关闭线程池
ExecutorService.shutdown();
// 3. 保存数据
save(data);
}
private static int[] query() {
return new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
}
private static void save(int[] data) {
System.out.println("保存数据 - " + Arrays.toString(data));
}
}
由于线程获取CPU时间片的不确定性,所以有可能数据还没有处理完毕,第3步就执行完了:
获取数据完毕
所有数据都处理完了
保存数据 - [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Thread[pool-1-thread-2,5,main]处理第2条数据
Thread[pool-1-thread-1,5,main]处理第1条数据
Thread[pool-1-thread-2,5,main]处理第3条数据
Thread[pool-1-thread-1,5,main]处理第4条数据
Thread[pool-1-thread-1,5,main]处理第6条数据
Thread[pool-1-thread-2,5,main]处理第5条数据
Thread[pool-1-thread-1,5,main]处理第7条数据
Thread[pool-1-thread-1,5,main]处理第9条数据
Thread[pool-1-thread-2,5,main]处理第8条数据
Thread[pool-1-thread-1,5,main]处理第10条数据
我们可以借助CountDownLatch解决这个问题:
public class CountDownLatchTest {
private static ExecutorService ExecutorService = Executors.newFixedThreadPool(2);
private static CountDownLatch latch = new CountDownLatch(10);
public static void main(String[] args) throws InterruptedException {
// 1. 模拟从数据库获取数据
int[] data = query();
System.out.println("获取数据完毕");
// 2. 数据处理
IntStream.range(0, data.length).forEach(i -> {
ExecutorService.execute(() -> {
System.out.println(Thread.currentThread() + "处理第" + (i + 1) + "条数据");
int value = data[i];
if (value % 2 == 0) {
data[i] = value * 2;
} else {
data[i] = value * 10;
}
latch.countDown();
});
});
latch.await();
System.out.println("所有数据都处理完了");
// 关闭线程池
ExecutorService.shutdown();
// 3. 保存数据
save(data);
}
private static int[] query() {
return new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
}
private static void save(int[] data) {
System.out.println("保存数据 - " + Arrays.toString(data));
}
}
我们定义了一个CountDownLatch,计数器值为10,和数据量一致。然后在第2步中,当每个线程执行完毕的时候调用countDown
方法,让计数器减1。在第3步前调用await
方法让main线程阻塞等待,直到计数器被减为0。所以这就保证了只有当所有数据加工完毕才执行保存数据操作。
执行方法,程序输出如下所示:
获取数据完毕
Thread[pool-1-thread-1,5,main]处理第1条数据
Thread[pool-1-thread-1,5,main]处理第3条数据
Thread[pool-1-thread-1,5,main]处理第4条数据
Thread[pool-1-thread-1,5,main]处理第5条数据
Thread[pool-1-thread-1,5,main]处理第6条数据
Thread[pool-1-thread-1,5,main]处理第7条数据
Thread[pool-1-thread-1,5,main]处理第8条数据
Thread[pool-1-thread-1,5,main]处理第9条数据
Thread[pool-1-thread-1,5,main]处理第10条数据
Thread[pool-1-thread-2,5,main]处理第2条数据
所有数据都处理完了
保存数据 - [10, 4, 30, 8, 50, 12, 70, 16, 90, 20]
await
有重载方法:await(long timeout, TimeUnit unit)
,设置最大等待时间,超过这个时间程序将继续执行不再被阻塞:
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(4);
System.out.println(Thread.currentThread() + "线程执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
}, "thread1").start();
latch.await(3, TimeUnit.SECONDS); // 最多等待 3秒
System.out.println("main线程执行完毕");
}
}
输出如下:
main线程执行完毕
Thread[thread1,5,main]线程执行完毕
9. JUC之CyclicBarrier
CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。CyclicBarrier默认的构造方法是CyclicBarrier(int parties)
,其参数表示屏障拦截的线程数量,每个线程调用await
方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
9.1 CyclicBarrier示例
使用“人满发车”的例子来演示CyclicBarrier:
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(2);
System.out.println("快上车来不及解释了");
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread() + "已上车");
barrier.await();
System.out.println("所有人已上车,发车");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Jane").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread() + "已上车");
barrier.await();
System.out.println("所有人已上车,发车");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Mike").start();
}
}
上面例子中我们定义了一个等待2个线程完成的CyclicBarrier,在两个线程内部调用了await
方法,让其阻塞等待,并告知CyclicBarrier我已经到达屏障了。只有当两个线程都执行到barrier.await()
这一行时,屏障开启,线程才会继续往下执行。程序输出如下所示:
快上车来不及解释了
Thread[Mike,5,main]已上车
Thread[Jane,5,main]已上车
所有人已上车,发车
所有人已上车,发车
CyclicBarrier的构造函数支持传入一个回调方法:
CyclicBarrier barrier = new CyclicBarrier(n, () -> {
System.out.println("当所有线程到达屏障时,执行该回调");
});
改造上面的例子:
public class CyclicBarrierTest {
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(2, new Thread(() -> {
System.out.println("发车,嘟嘟嘟");
}));
System.out.println("快上车来不及解释了");
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
System.out.println(Thread.currentThread() + "已上车");
barrier.await();
System.out.println("所有人已上车");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Jane").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread() + "已上车");
barrier.await();
System.out.println("所有人已上车");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "Mike").start();
}
}
输出如下所示:
快上车来不及解释了
Thread[Mike,5,main]已上车
Thread[Jane,5,main]已上车
发车,嘟嘟嘟
所有人已上车,发车
所有人已上车,发车
9.2 设置超时时间
await
的重载方法:await(long timeout, TimeUnit unit)
可以设置最大等待时长,超出这个时间屏障还没有开启的话则抛出TimeoutException
:
public class CyclicBarrierTest2 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(3);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread1").start();
new Thread(() -> {
try {
barrier.await(1, TimeUnit.SECONDS);
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}, "thread2").start();
}
}
9.3 BrokenBarrierException
抛出BrokenBarrierException
异常时表示屏障破损,此时标志位broken=true。抛出BrokenBarrierException
异常的情况主要有:
- 其他等待的线程被中断,则当前线程抛出
BrokenBarrierException
异常; - 其他等待的线程超时,则当前线程抛出
BrokenBarrierException
异常; - 当前线程在等待时,其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常。
模拟第1种情况,其他等待的线程被中断,则当前线程抛出BrokenBarrierException
异常:
public class CyclicBarrierTest2 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(3);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread1").start();
Thread thread2 = new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(1);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread2");
thread2.start();
TimeUnit.SECONDS.sleep(2);
thread2.interrupt();
}
}
输出:
上面例子中thread2线程睡眠1秒后先到达屏障点,然后进入等待状态。2秒后main线程执行thread2.interrupt()
中断等待中的thread2线程,所以程序抛出BrokenBarrierException
异常。3秒后thread1线程到达屏障点,此时屏障已经被破坏了,所以也抛出BrokenBarrierException
异常。
模拟第2种情况:其他等待的线程超时,则当前线程抛出BrokenBarrierException
异常:
public class CyclicBarrierTest2 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(3);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread1").start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(1);
barrier.await(1, TimeUnit.SECONDS);
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException | TimeoutException e) {
e.printStackTrace();
}
}, "thread2").start();
}
}
输出:
上面例子中thread2睡眠1秒后到达屏障点,然后进入等待状态(最多等待1秒),然而因为thread1要3秒后才能到达屏障点,所以thread2将抛出TimeoutException
。3秒后,thread1到达屏障点,但这时候由于thread2的await
方法抛出的异常破坏了屏障,所以thread1将抛出BrokenBarrierException
异常。
模拟第3中情况:当前线程在等待时,其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常:
public class CyclicBarrierTest2 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(2);
new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(3);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread1").start();
new Thread(() -> {
try {
System.out.println(Thread.currentThread() + "开始执行");
TimeUnit.SECONDS.sleep(1);
barrier.await();
System.out.println(Thread.currentThread() + "继续执行");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}, "thread2").start();
TimeUnit.SECONDS.sleep(2);
System.out.println(barrier.getNumberWaiting());
barrier.reset();
}
}
输出:
上面例子中,thread2睡眠1秒后到达屏障点,然后进入等待状态。2秒后main线程调用reset
方法重置了屏障,所以在等待状态中的thread2抛出BrokenBarrierException
异常。3秒后,thread1到达屏障点,由于reset
方法重置了屏障,所以thread1并不会抛出BrokenBarrierException
异常,而是一直在屏障点进行等待别的线程到达屏障点。
从上面的三个例子中可以看到,无论是哪种情况导致屏障破坏,屏障点后面的代码都没有被执行,main方法也没有退出。
9.4 和CountDownLatch区别
- CountDownLatch:一个线程(或者多个),等待另外N个线程完成某个事情之后才能执行;CyclicBarrier:N个线程相互等待,任何一个线程完成之前,所有的线程都必须等待。
- CountDownLatch:一次性的;CyclicBarrier:可以重复使用。
10. JUC之Exchanger
JUC中的Exchanger允许成对的线程在指定的同步点上通过exchange
方法来交换数据。如果第一个线程先执行exchange
方法,它会一直等待第二个线程也 执行exchange
方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将当前线程生产 出来的数据传递给对方。
10.1 Exchanger示例
两个线程通过Exchanger交换数据的简单示例:
public class ExchangerTest {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
System.out.println("thread1开始");
try {
String exchange = exchanger.exchange("来自thread1的数据");
System.out.println("接收thread2发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}, "thread1").start();
new Thread(() -> {
System.out.println("thread2开始");
try {
String exchange = exchanger.exchange("来自thread2的数据");
System.out.println("接收thread1发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2结束");
}, "thread2").start();
}
}
在定义Exchanger的时候需要指定交换的数据类型,这里为String类型。exchange
方法用于向另一个线程发送数据,方法的返回值为另一个线程发送过来的数据。上面例子输出如下:
thread1开始
thread2开始
接收thread2发送的数据:来自thread2的数据
thread1结束
接收thread1发送的数据:来自thread1的数据
thread2结束
上面说过,只有当成对的线程都到达同步点的时候,才会执行数据交换操作。现在我们让thread2休眠一会儿,看看thread1是否会进入等待:
public class ExchangerTest {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
System.out.println("thread1开始");
try {
String exchange = exchanger.exchange("来自thread1的数据");
System.out.println("接收thread2发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}, "thread1").start();
new Thread(() -> {
System.out.println("thread2开始");
try {
TimeUnit.SECONDS.sleep(3); // thread1也会进入等待,直到双方都准备好交换数据。
String exchange = exchanger.exchange("来自thread2的数据");
System.out.println("接收thread1发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2结束");
}, "thread2").start();
}
}
程序输出如下所示:
那么如果线程不成对会出现什么情况呢?我们添加thread3线程:
public class ExchangerTest {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
System.out.println("thread1开始");
try {
String exchange = exchanger.exchange("发送数据-thread1");
System.out.println("接收数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}, "thread1").start();
new Thread(() -> {
System.out.println("thread2开始");
try {
String exchange = exchanger.exchange("发送数据-thread2");
System.out.println("接收数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2结束");
}, "thread2").start();
new Thread(() -> {
System.out.println("thread3开始");
try {
String exchange = exchanger.exchange("发送数据-thread3");
System.out.println("接收数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread3结束");
}, "thread3").start();
}
}
程序输出如下所示:
thread1开始
thread3开始
接收数据:发送数据-thread1
thread3结束
thread2开始
接收数据:发送数据-thread3
thread1结束
可看到thread1和thread3交换了数据然后正常停止了,而thread2由于没有线程和它交换数据而苦苦等待,线程永远不会停止。查看线程快照可以证明这点:
线程匹配是随机的,所以也有可能thread1和thread2匹配,thread3进入无休止的等待,这就类似于…
另一个值得一提的点就是通过Exchanger交换的是同一个对象,而不是对象的拷贝:
public class ExchangerTest {
public static void main(String[] args) {
final Exchanger<Object> exchanger = new Exchanger<>();
new Thread(() -> {
System.out.println("thread1开始");
Object object = new Object();
System.out.println("thread1发送数据:" + object);
try {
Object exchange = exchanger.exchange(object);
System.out.println("接收thread2发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}, "thread1").start();
new Thread(() -> {
System.out.println("thread2开始");
Object object = new Object();
System.out.println("thread2发送数据:" + object);
try {
Object exchange = exchanger.exchange(object);
System.out.println("接收thread1发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2结束");
}, "thread2").start();
}
}
程序输出如下:
thread1开始
thread2开始
thread2发送数据:java.lang.Object@6d559005
thread1发送数据:java.lang.Object@7702c19
接收thread2发送的数据:java.lang.Object@6d559005
接收thread1发送的数据:java.lang.Object@7702c19
thread2结束
thread1结束
可以看到thread1发送的对象和thread2接收的对象句柄是一致的。
10.2 设置超时时间
如果不想线程在交换数据的时候等待过长的时间,我们可以使用exchanger
的重载方法exchange(V x, long timeout, TimeUnit unit)
来指定超时时间:
public class ExchangerTest {
public static void main(String[] args) {
final Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
System.out.println("thread1开始");
try {
String exchange = exchanger.exchange("来自thread1的数据", 5, TimeUnit.SECONDS);
System.out.println("接收thread2发送的数据:" + exchange);
} catch (InterruptedException | TimeoutException e) {
e.printStackTrace();
}
System.out.println("thread1结束");
}, "thread1").start();
new Thread(() -> {
System.out.println("thread2开始");
try {
TimeUnit.SECONDS.sleep(10);
String exchange = exchanger.exchange("来自thread2的数据");
System.out.println("接收thread1发送的数据:" + exchange);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2结束");
}, "thread2").start();
}
}
上面例子中,thread2休眠10秒后才开始交换数据,而thread1在等待5秒后没能成功交换数据就抛出TimeoutException
异常了。10秒后由于没有线程再和thread2交换数据,所以thread2会一直等待:
11. 深入理解volatile关键字
volatile关键字修饰的成员变量具有两大特性:保证了该成员变量在不同线程之间的可见性;禁止对该成员变量进行重排序,也就保证了其有序性。但是volatile修饰的成员变量并不具有原子性,在并发下对它的修改是线程不安全的。下面分别举例来演示这两个特性,并且分析为什么volatile不是线程安全的。
11.1 可见性
通过对JMM的学习,我们都知道线程对主内存中共享变量的修改首先会从主内存获取值的拷贝,然后保存到线程的工作内存中。接着在工作内存中对值进行修改,最终刷回主内存。由于不同线程拥有各自的工作内存,所以它们对某个共享变量值的修改在没有刷回主内存的时候只对自己可见。
举个例子,假如有两个线程,其中一个线程用于修改共享变量value,另一个线程用于获取修改后的value:
public class VolatileTest {
private static int INIT_VALUE = 0;
private final static int LIMIT = 5;
public static void main(String[] args) {
new Thread(() -> {
int value = INIT_VALUE;
while (value < LIMIT) {
if (value != INIT_VALUE) {
System.out.println("获取更新后的值:" + INIT_VALUE);
value = INIT_VALUE;
}
}
}, "reader").start();
new Thread(() -> {
int value = INIT_VALUE;
while (INIT_VALUE < LIMIT) {
System.out.println("将值更新为:" + ++value);
INIT_VALUE = value;
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "writer").start();
}
}
writer线程每隔0.5秒将INIT_VALUE值递增,直到INIT_VALUE大于等于5。而reader线程则是不停的去获取INIT_VALUE的值,直到INIT_VALUE的值大于等于5。程序执行结果如下:
多执行几次可能每次结果都不一样,但是可以确定的是,writer对值的修改reader并不能感知到(如果能感知到的话,reader线程就不会停不下来了)。
为什么会出现上面的结果呢?因为writer线程在工作内存中修改了INIT_VALUE的值,即使它刷回主内存了,但是reader线程在此之前已经从主内存获取了INIT_VALUE的值(因为线程获取CPU时间片不确定性,这个值可能是0,也可能是被writer修改后的值,但writer线程是每隔0.5毫秒才会去修改值,所以reader获取到的INIT_VALUE的值一般不会是writer修改的最终值5),并保存到了reader线程的工作内存中。reader线程通过while不断的轮询判断value和INIT_VALUE的值是否相等,但是由于reader线程工作内存中已经有INIT_VALUE的值的拷贝了,所以reader并不会重新从主内存中获取被writer修改后的INIT_VALUE的值,reader线程里while条件一直成立,这就是为什么reader线程不会正常停止并且没有输出修改后的值的原因。
修改上面的例子,将INIT_VALUE成员变量使用volatile关键字修饰:
public class VolatileTest {
private volatile static int INIT_VALUE = 0;
private final static int LIMIT = 5;
public static void main(String[] args) {
new Thread(() -> {
int value = INIT_VALUE;
while (value < LIMIT) {
if (value != INIT_VALUE) {
System.out.println("获取更新后的值:" + INIT_VALUE);
value = INIT_VALUE;
}
}
}, "reader").start();
new Thread(() -> {
int value = INIT_VALUE;
while (INIT_VALUE < LIMIT) {
System.out.println("将值更新为:" + ++value);
INIT_VALUE = value;
try {
TimeUnit.MICROSECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "writer").start();
}
}
可以看到,reader线程已经可以正常停止了,因为最终INIT_VALUE的值肯定是5,并且reader可以感知到这个值被修改为5了。
为什么volatile修饰的成员变量在线程间具有可见性呢?因为通过volatile修饰,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,加了这个指令后,会引发两件事情:
- 将当前处理器缓存行的内容写回到系统内存,也就是强制将工作内存中的值刷回主内存;
- 这个写回到内存的操作会使得在其他CPU里缓存了该内存地址的数据失效。其他CPU缓存数据失效,则会重新去内存中读取值,也就是被修改的数据。
通过上面这两个特性,我们可以确定的是,writer对值进行修改并刷回主内存后,reader里INIT_VALUE值的拷贝就失效了,所以reader线程会再次从主内存中获取INIT_VALUE的值,这时候这个值已经是被writer线程修改刷新后的值了。
11.2 有序性
来看一个线程不安全的单例实现(双重同步锁单例模式,更多关于单例的介绍可以参考单例的几种写法和对比):
public class SingletonTest {
// 私有化构造函数,让外部没办法直接通过new来创建
private SingletonTest() {
}
// 单例对象
private static SingletonTest instance = null;
// 静态工厂方法
public static SingletonTest getInstance() {
if (instance == null) { // 双重检测
synchronized (SingletonTest.class) { // 同步锁
if (instance == null) {
instance = new SingletonTest();
}
}
}
return instance;
}
}
上面的例子虽然加了同步锁,但是在多线程下并不是线程安全的。第12行instance = new SingletonTest()
在实际执行的时候会被拆分为以下三个步骤:
- 分配存储SingletonTest对象的内存空间;
- 初始化SingletonTest对象;
- 将instance指向刚刚分配的内存空间。
通过JMM的学习我们都知道,在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,因为第2步和第3步并没有依赖关系,所以可能发生重排序,排序后的步骤为:
- 分配存储SingletonTest对象的内存空间;
- 将instance指向刚刚分配的内存空间;
- 初始化SingletonTest对象。
经过重排序后,上面的例子在多线程下就会出现问题。假如现在有两个线程A和B同时调用SingletonTest#getInstance,线程A执行到了代码的第12行instance = new SingletonTest()
,已经完成了对象内存空间的分配并将instance指向了该内存空间,线程B执行到了第9行,发现instance并不是null(因为已经指向了内存空间),所以就直接返回instance了。但是线程A并还没有执行初始化SingletonTest操作,所以实际线程B拿到的SingletonTest实例是空的,那么线程B后续对SingletonTest操控将抛出空指针异常。
要让上面的例子是线程安全的,只需要用volatile修饰单例对象即可:
public class SingletonTest {
// 私有化构造函数,让外部没办法直接通过new来创建
private SingletonTest() {
}
// 单例对象
private volatile static SingletonTest instance = null;
// 静态工厂方法
public static SingletonTest getInstance() {
if (instance == null) { // 双重检测
synchronized (SingletonTest.class) { // 同步锁
if (instance == null) {
instance = new SingletonTest();
}
}
}
return instance;
}
}
因为通过volatile修饰的成员变量会添加内存屏障来阻止JVM进行指令重排优化。
11.3 线程不安全
举个递增的例子:
public class VolatileTest2 {
private volatile static int value;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value += 1));
Thread thread2 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value += 1));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
}
多次运行上面的例子:
可以看到最终的值有可能小于1000。
volatile可以保证修改的值能够马上更新到主内存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?
因为在Java中,只有对基本类型的赋值和修改才是原子性的,而对共享变量的修改并不是原子性的。通过JMM内存交互协议我们可以知道,一个线程修改共享变量的值需要经过下面这些步骤:
- 线程从主内存中读取(read)共享变量的值,然后载入(load)到线程的工作内存中的变量;
- 使用(use)工作内存变量的值,执行加减操作,然后将修改后的值赋值(assign)给工作内存中的变量;
- 将工作内存中修改后的变量的值存储(store)到主内存中,并执行写入(write)操作。
所以上面的例子中,可能出现下面这种情况:
thread1和thread2同时获取了value的值,比如为100。thread1执行了+1操作,然后写回主内存,这个时候thread2刚好执行完use操作(+1),准备执行assign(将+1后的值写回工作内存对应的变量中)操作。虽然这时候thread2工作内存中value值的拷贝无效了(因为volatile的特性),但是thread2已经执行完+1操作了,它并不需要再从主内存中获取value的值,所以thread2可以顺利地将+1后的值赋值给工作内存中的变量,然后刷回主存。这就是为什么上面的累加结果可能会小于1000的原因。
要让上面的例子是线程安全的话可以加同步锁,或者使用atomic类,后续会介绍到。
12.JUC之Semaphore
JUC的Semaphore俗称信号量,可用来控制同时访问特定资源的线程数量。通过它的构造函数我们可以指定信号量(称为许可证permits可能更为明确)的数量,线程可以调用Semaphore对象的acquire
方法获取一个许可证,调用release
来归还一个许可证。
下面举个Semaphore的基本使用示例。
12.1 Semaphore示例
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
// 定义许可证数量
final Semaphore semaphore = new Semaphore(2);
IntStream.range(0, 4).forEach(i -> {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始");
try {
semaphore.acquire(); // 一次拿一个许可证
System.out.println(Thread.currentThread().getName() + "获取许可证");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放许可证");
semaphore.release();
}
System.out.println(Thread.currentThread().getName() + "结束");
}, "thread" + (i + 1)).start();
});
}
}
上面的例子中,我们定义许可证的数量为2个,然后4个线程通过acquire
方法去获取许可证,结束行通过release
方法释放许可证。acquire
方法默认一次只拿一个许可证,所以上面的例子中,同一时刻最多只有两个线程同时执行。
程序输出如下所示:
acquire
的重载方法acquire(int permits)
允许线程一次性获取N个许可证;同样的release
的重载方法release(int permits)
允许线程一次性释放N个许可证。
Semaphore还有一个tryAcquire
,它允许线程尝试去获取1个许可证,如果许可证不足没有获取到的话,线程也会继续执行,而非阻塞等待。tryAcquire
方法的重载方法tryAcquire(long timeout, TimeUnit unit)
可以指定尝试获取许可证的超时时间。
12.2 acquireUninterruptibly
从上面的例子我们会发现acquire
方法会抛出InterruptedException
异常,说明这个方法是可以被打断的:
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(1);
Thread thread1 = new Thread(() -> {
try {
semaphore.acquire(2);
} catch (InterruptedException e) {
System.err.println("semaphore InterruptedException");
e.printStackTrace();
}
});
thread1.start();
TimeUnit.MICROSECONDS.sleep(500);
thread1.interrupt();
}
}
上面例子thread1线程获取2个许可证,但许可证总数只有1个,所以会阻塞等待。main线程通过调用thread1的interrupt
方法去打断thread1线程,结果如下:
而通过acquireUninterruptibly
方法去获取许可证是不可被打断的:
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(1);
Thread thread1 = new Thread(() -> {
semaphore.acquireUninterruptibly(2);
});
thread1.start();
TimeUnit.MICROSECONDS.sleep(500);
thread1.interrupt();
}
}
上面程序并不会抛出InterruptedException
,thread1会一直处于阻塞状态。
12.3 drainPermits
drainPermits
方法一次性获取所有许可证(drain抽干榨干😮):
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
final Semaphore semaphore = new Semaphore(5);
new Thread(() -> {
System.out.println("availablePermits: " + semaphore.availablePermits());
semaphore.drainPermits(); // 获取所有许可证,抽干
System.out.println("availablePermits: " + semaphore.availablePermits());
try {
TimeUnit.SECONDS.sleep(4);
} catch (InterruptedException e) {
e.printStackTrace();
}
semaphore.release(5);
System.out.println(Thread.currentThread().getName() + "结束");
}, "thread1").start();
}
}
availablePermits
方法用于获取当前可用许可证数量的预估值。程序输出如下:
12.4 别的API
hasQueuedThreads
方法用于判断是否有处于等待获取许可证状态的线程;getQueueLength
用于获取处于等待获取许可证状态的线程的数量;getQueuedThreads
用于获取处于等待获取许可证状态的线程集合。
getQueuedThreads
是protected
的,所以要使用它,我们得自定义一个Semaphore的子类:
public class SemaphoreTest {
public static void main(String[] args) {
// 定义许可证数量
final MySemaphore semaphore = new MySemaphore(1);
IntStream.range(0, 4).forEach(i -> {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "开始");
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "获取许可证");
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName() + "释放许可证");
semaphore.release();
}
System.out.println(Thread.currentThread().getName() + "结束");
}, "thread" + (i + 1)).start();
});
while (true) {
if (semaphore.hasQueuedThreads()) {
System.out.println("等待线程数量:" + semaphore.getQueueLength());
Collection<Thread> queuedThreads = semaphore.getQueuedThreads();
System.out.println("等待线程:" + queuedThreads.stream().map(Thread::getName).collect(Collectors.joining(",")));
}
}
}
static class MySemaphore extends Semaphore {
private static final long serialVersionUID = -2595494765642942297L;
public MySemaphore(int permits) {
super(permits);
}
public MySemaphore(int permits, boolean fair) {
super(permits, fair);
}
public Collection<Thread> getQueuedThreads() {
return super.getQueuedThreads();
}
}
}
程序输出如下所示(截取一部分):
12.5 Api总结
总结下Semaphore常用的方法:
方法 | 描述 |
---|---|
acquire() |
获取一个许可证,可以被打断,没有足够的许可证时阻塞等待 |
acquire(int permits) |
获取指定数量的许可证,可以被打断,没有足够的许可证时阻塞等待 |
acquireUninterruptibly() |
获取一个许可证,不可被打断,没有足够的许可证时阻塞等待 |
acquireUninterruptibly(int permits) |
获取指定数量的许可证,不可被打断,没有足够的许可证时阻塞等待 |
tryAcquire() |
尝试获取一个许可证,没有足够的许可证时程序继续执行,不会被阻塞 |
tryAcquire(int permits) |
尝试获取指定数量的许可证,没有足够的许可证时程序继续执行,不会被阻塞 |
tryAcquire(long timeout, TimeUnit unit) |
在指定的时间范围内尝试获取1个许可证,没有足够的许可证时程序继续执行, 不会被阻塞,在该时间方位内可以被打断 |
tryAcquire(int permits, long timeout, TimeUnit unit) |
在指定的时间范围内尝试获取指定数量的许可证,没有足够的许可证时程序 继续执行,不会被阻塞,在该时间方位内可以被打断 |
release() |
释放一个许可证 |
drainPermits() |
一次性获取所有可用的许可证 |
availablePermits() |
获取当前可用许可证数量的预估值 |
hasQueuedThreads() |
判断是否有处于等待获取许可证状态的线程 |
getQueueLength() |
获取处于等待获取许可证状态的线程的数量的预估值 |
getQueuedThreads() |
获取处于等待获取许可证状态的线程集合 |
13. 深入学习CAS
在深入理解volatile关键字一节中,我们提到volatile并不能确保线程安全性,要解决文章中提到的累加例子线程安全问题的话,可以使用同步锁(synchronized)和Atomic类型。但就那个例子来说,使用synchronized同步锁有点小题大作,我们可以选择更为轻量的AtomicInteger来解决。
将前面的例子改写为:
public class AtomticIntegerTest {
private static AtomicInteger value = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value.getAndIncrement()));
Thread thread2 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value.getAndIncrement()));
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
}
我们将value类型从volatile修饰的int类型改为AtomicInteger类型,这个例子无论运行多少次,结果都是我们期望的1000:
Atomic类型内部并没有通过锁来保证线程安全,那么它们是如何实现的呢?这就是本节要讨论的CAS。
13.1 什么是CAS
CAS是Compare-And-Swap的缩写,意思为比较并交换。以AtomicInteger为例,其提供了compareAndSet(int expect, int update)
方法,expect
为期望值(被修改的值在主内存中的期望值),update
为修改后的值。compareAndSet
方法返回值类型为布尔类型,修改成功则返回true,修改失败返回false。
举个compareAndSet
方法的例子:
public class AtomticIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
boolean result = atomicInteger.compareAndSet(0, 1);
System.out.println(result);
System.out.println(atomicInteger.get());
}
}
上面例子中,通过AtomicInteger(int initialValue)
构造方法指定了AtomicInteger
类成员变量value
的初始值为0:
public class AtomicInteger extends Number implements java.io.Serializable {
......
private volatile int value;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
......
}
接着执行compareAndSet
方法,main线程从主内存中拷贝了value
的副本到工作线程,值为0,并将这个值修改为1。如果此时主内存中value的值还是为0的话(言外之意就是没有被其他线程修改过),则将修改后的副本值刷回主内存更新value的值。所以上面的例子运行结果应该是true和1:
将上面的例子修改为:
public class AtomticIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(0);
boolean firstResult = atomicInteger.compareAndSet(0, 1);
boolean secondResult = atomicInteger.compareAndSet(0, 1);
System.out.println(firstResult);
System.out.println(secondResult);
System.out.println(atomicInteger.get());
}
}
上面例子中,main线程第二次调用compareAndSet
方法的时候,value的值已经被修改为1了,不符合其expect的值,所以修改将失败。上面例子输出如下:
13.2 CAS底层原理
查看compareAndSet
方法源码:
该方法通过调用unsafe
类的compareAndSwapInt
方法实现相关功能。compareAndSwapInt
方法包含四个参数:
-
this
,当前对象; -
valueOffset
,value
成员变量的内存偏移量(也就是内存地址):private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
-
expect
,期待值; -
update
,更新值。
所以这个方法的含义为:获取当前对象value
成员变量在主内存中的值,和传入的期待值相比,如果相等则说明这个值没有被别的线程修改过,然后将其修改为更新值。
那么unsafe
又是什么?它的compareAndSwapInt
方法是原子性的么?查看该方法的源码:
该方法并没有具体Java代码实现,方法通过native
关键字修饰。由于Java方法无法直接访问底层系统,Unsafe
类相当于一个后门,可以通过该类的方法直接操作特定内存的数据。Unsafe
类存在于sun.msic
包中,JVM会帮我们实现出相应的汇编指令。Unsafe
类中的CAS方法是一条CPU并发原语,由若干条指令组成,用于完成某个功能的一个过程。原语的执行必须是连续的,在执行过程中不允许被中断,不会存在数据不一致的问题。
13.3 getAndIncrement方法剖析
了解了CAS原理后,我们回头看下AtomicInteger
的getAndIncrement
方法源码:
该方法通过调用unsafe
类的getAndAddInt
方法实现相关功能。继续查看getAndAddInt
方法的源码:
结合这两张图,我们便可以很直观地看出为什么AtomicInteger
的getAndIncrement
方法是线程安全的了:
上图中,var1
是AtomicInteger
对象本身;var2
是AtomicInteger
对象的成员变量value
的内存地址;var4
是需要变更的数量;var5
是通过unsafe
的getIntVolatile
方法获得AtomicInteger
对象的成员变量value
在主内存中的值。do while循环中的逻辑为:用当前对象的值和var5
比较,如果相同,说明该值没有被别的线程修改过,更新为var5+var4
,并返回true(CAS);否则继续获取值并比较,直到更新完成。
13.4 CAS的缺点
CAS并不是完美的,其存在以下这些缺点:
- 如果刚好while里的CAS操作一直不成功,那么对CPU的开销大;
- 只能确保一个共享变量的原子操作;
- 存在ABA问题。
CAS实现的一个重要前提是需要取出某一时刻的数据并在当下时刻比较交换,这之间的时间差会导致数据的变化。比如:thread1线程从主内存中取出了变量a的值为A,thread2页从主内存中取出了变量a的值为A。由于线程调度的不确定性,这时候thread1可能被短暂挂起了,thread2进行了一些操作将值修改为了B,然后又进行了一些操作将值修改回了A,这时候当thread1重新获取CPU时间片重新执行CAS操作时,会发现变量a在主内存中的值仍然是A,所以CAS操作成功。
13.5 解决ABA问题
那么如何解决CAS的ABA问题呢?由上面的阐述课件,光通过判断值是否相等并不能确保在一定时间差内值没有变更过,所以我们需要一个额外的指标来辅助判断,类似于时间戳,版本号等。
JUC为我们提供了一个AtomicStampedReference
类,通过查看它的构造方法就可以看出,除了指定初始值外,还需指定一个版本号(戳):
我们就用这个类来解决ABA问题,首先模拟一个ABA问题场景:
public class AtomticIntegerTest {
public static void main(String[] args) {
AtomicReference<String> atomicReference = new AtomicReference<>("A");
new Thread(() -> {
// 模拟一次ABA操作
atomicReference.compareAndSet("A", "B");
atomicReference.compareAndSet("B", "A");
System.out.println(Thread.currentThread().getName() + "线程完成了一次ABA操作");
}, "thread1").start();
new Thread(() -> {
// 让thread2先睡眠2秒钟,确保thread1的ABA操作完成
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = atomicReference.compareAndSet("A", "B");
if (result) {
System.out.println(Thread.currentThread().getName() + "线程修改值成功,当前值为:" + atomicReference.get());
}
}, "thread2").start();
}
}
运行程序,输出如下:
使用AtomicStampedReference
解决ABA问题:
public class AtomticIntegerTest {
public static void main(String[] args) {
// 初始值为A,版本号为1
AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + "当前版本号为:" + stamp);
// 休眠1秒,让thread2也拿到初始的版本号
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 模拟一次ABA操作
reference.compareAndSet("A", "B", reference.getStamp(), reference.getStamp() + 1);
reference.compareAndSet("B", "A", reference.getStamp(), reference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "线程完成了一次ABA操作");
}, "thread1").start();
new Thread(() -> {
int stamp = reference.getStamp();
System.out.println(Thread.currentThread().getName() + "当前版本号为:" + stamp);
// 让thread2先睡眠2秒钟,确保thread1的ABA操作完成
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = reference.compareAndSet("A", "B", stamp, stamp + 1);
if (result) {
System.out.println(Thread.currentThread().getName() + "线程修改值成功,当前值为:" + reference.getReference());
} else {
System.out.println(Thread.currentThread().getName() + "线程修改值失败,当前值为:" + reference.getReference() + ",版本号为:" + reference.getStamp());
}
}, "thread2").start();
}
}
程序输出如下:
本文来自博客园,作者:bgtong,转载请注明原文链接:https://www.cnblogs.com/bgtong/p/16260252.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)