线程的几种状态
- 创建
- 运行
- 阻塞:往往是因为线程拿不到同步代码块或者同步方法的锁而造成的线程阻塞,(总体就是说当前线程没有拿到CPU的运行时间片)
- 等待: 线程执行无参的构造方法 , 死等,直到有线程执行notify方法唤醒此线程
- 定时等待:线程执行有参的sleep方法或者有参的wait方法
- 运行完成: 线程执行完成run方法或者调用stop方法
什么是可重入锁
概念
可重入锁(Reentrant Lock)是一种锁机制,允许同一个线程在持有锁的情况下再次获得该锁,而不会被阻塞或导致死锁。这种锁的主要特点是支持锁的递归获取,即如果一个线程已经持有了锁,那么它可以在持有锁的过程中再次获得该锁。
可重入锁的关键特点
- 递归性:当一个线程获得锁之后,它可以再次进入同步代码块或方法而不会被阻塞。这对于递归方法或需要多次进入相同同步代码的复杂场景非常有用。
- 锁计数:可重入锁通常会维护一个计数器来记录锁的持有次数。每当同一线程获取锁时,计数器增加;当线程释放锁时,计数器减少。只有当计数器减到0时,锁才会真正被释放。
- 避免死锁:由于同一个线程可以多次获取锁,因此在递归调用或复杂的调用链中不会因为尝试重新获取锁而导致死锁。
可重入锁的实现
在Java中,synchronized
和 ReentrantLock
都是可重入锁的实现:
synchronized
关键字:Java的内置锁是可重入的。如果一个线程进入一个被synchronized
修饰的方法或代码块,并试图再次进入该锁定的同步方法或代码块,它不会被阻塞。ReentrantLock
类:这是Java并发包中提供的一种显式锁,它也支持可重入性。与synchronized
相比,ReentrantLock
提供了更多的功能,如可中断锁请求、定时锁请求和条件变量支持。
示例代码
synchronized
的可重入性
public class ReentrantExample {
public synchronized void method1() {
System.out.println("Inside method1");
method2(); // 同一线程可以再次获取锁并进入 method2
}
public synchronized void method2() {
System.out.println("Inside method2");
}
public static void main(String[] args) {
ReentrantExample example = new ReentrantExample();
example.method1(); // 将同时调用 method1 和 method2
}
}
ReentrantLock
的可重入性
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private final ReentrantLock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println("Inside method1");
method2(); // 同一线程可以再次获取锁并进入 method2
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println("Inside method2");
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockExample example = new ReentrantLockExample();
example.method1(); // 将同时调用 method1 和 method2
}
}
ReentrantLock
和 synchronized
的对比
ReentrantLock
和 synchronized
都是用于实现线程同步的锁机制,但它们有一些重要的区别。以下是它们的主要区别:
1. 锁的获取和释放方式
-
synchronized
- 是Java中的一种隐式锁。
- 锁的获取和释放是隐式的,开发者不需要手动操作。当一个线程进入一个
synchronized
方法或代码块时,锁会自动被获取;当线程退出方法或代码块时,锁会自动释放。
-
ReentrantLock
- 是一种显式锁。
- 锁的获取和释放需要显式地调用
lock()
和unlock()
方法。开发者必须确保每次获取锁后都要在finally
块中释放锁,以避免死锁。
2. 功能和灵活性
synchronized
:- 功能简单,适用于基本的同步需求。
- 不支持可中断锁定、尝试锁定(即在超时时间内获取锁)或公平锁等高级功能。
ReentrantLock
:- 提供了更多高级功能和更大的灵活性。
- 支持以下高级功能:
- 可中断锁定:可以使用
lockInterruptibly()
方法尝试获取锁,支持响应中断。 - 尝试锁定:可以使用
tryLock()
方法尝试获取锁,失败后不阻塞。 - 定时锁定:可以使用
tryLock(long timeout, TimeUnit unit)
方法在指定时间内尝试获取锁。 - 公平锁:可以通过构造函数
ReentrantLock(boolean fair)
来创建一个公平锁,确保线程按照先后顺序获取锁。 - 条件变量:通过
newCondition()
方法获取Condition
对象,可以实现比synchronized
更复杂的线程间通信。
- 可中断锁定:可以使用
3. 性能
synchronized
- 在Java早期版本中,
synchronized
的性能较差。 - 从Java 6开始,JVM对
synchronized
进行了很多优化,如偏向锁、自旋锁等,使得它在许多情况下性能已经非常接近ReentrantLock
。
- 在Java早期版本中,
ReentrantLock
- 在高竞争环境下通常表现得更好,特别是在需要频繁尝试锁定或使用可中断锁的场景中。
4. 可中断性
synchronized
: 不支持中断。当一个线程在等待锁时,不能被中断。ReentrantLock
: 支持中断,使用lockInterruptibly()
方法可以在等待锁时响应中断。
5. 条件变量(Condition)
synchronized
- 只能通过对象的
wait()
、notify()
和notifyAll()
方法来实现线程间的等待/通知机制,且只能有一个等待队列。
- 只能通过对象的
ReentrantLock
- 提供了更强大的条件变量支持,可以通过
newCondition()
方法创建多个Condition
对象,形成多个等待队列,实现更加灵活的线程间通信。
- 提供了更强大的条件变量支持,可以通过
6. 可重入性
- 两者都支持:
ReentrantLock
和synchronized
都是可重入的,意味着一个线程可以多次获取同一把锁而不会被阻塞。
7. 公平性
synchronized
:- 默认是非公平的,锁的分配可能会造成某些线程的“饥饿”状态。
ReentrantLock
:- 可以通过构造方法指定是否使用公平锁。
选择建议
- 简单场景:如果只是需要简单的同步机制,
synchronized
通常是首选,代码更简洁,且无需担心锁的释放问题。 - 复杂场景:如果需要更多控制和高级功能,比如可中断锁、尝试锁、多个条件变量或公平锁,
ReentrantLock
是更合适的选择。
公平锁和非公平锁
1、听故事把知识掌握了
在一个村子里面,有一口井水,水质非常的好,村民们都想打井里的水。这井只有一口,村里的人那么多,所以得出个打水的规则才行。村长绞尽脑汁,最终想出了一个比较合理的方案,咱们来仔细的看看聪明的村长大人的智慧。
井边安排一个看井人,维护打水的秩序。
打水时,以家庭为单位,哪个家庭任何人先到井边,就可以先打水,而且如果一个家庭占到了打水权,其家人这时候过来打水不用排队。而那些没有抢占到打水权的人,一个一个挨着在井边排成一队,先到的排在前面。打水示意图如下 :
是不是感觉很和谐,如果打水的人打完了,他会跟看井人报告,看井人会让第二个人接着打水。这样大家总都能够打到水。是不是看起来挺公平的,先到的人先打水,当然不是绝对公平的,自己看看下面这个场景 :
看着,一个有娃的父亲正在打水,他的娃也到井边了,所以女凭父贵直接排到最前面打水,羡煞旁人了。
以上这个故事模型就是所谓的公平锁模型,当一个人想到井边打水,而现在打水的人又不是自家人,这时候就得乖乖在队列后面排队。
事情总不是那么一帆风顺的,总会有些人想走捷径,话说看井人年纪大了,有时候,眼力不是很好,这时候,人们开始打起了新主意。新来打水的人,他们看到有人排队打水的时候,他们不会那么乖巧的就排到最后面去排队,反之,他们会看看现在有没有人正在打水,如果有人在打水,没辄了,只好排到队列最后面,但如果这时候前面打水的人刚刚打完水,正在交接中,排在队头的人还没有完成交接工作,这时候,新来的人可以尝试抢打水权,如果抢到了,呵呵,其他人也只能睁一只眼闭一只眼,因为大家都默认这个规则了。这就是所谓的非公平锁模型。新来的人不一定总得乖乖排队,这也就造成了原来队列中排队的人可能要等很久很久。
java可重入锁-ReentrantLock实现细节
ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。
- 公平锁模型:
初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:
线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:
初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:
此处可能有人会问 在代码里边怎么理解这种可重入锁的形态呢?
public static int i = 0;
public void run()
{
for (int j = 0;j<100000;j++)
{
lock.lock();
lock.lock();
try {
i++;
}finally {
lock.unlock();
lock.unlock();
}
}
}
为什么需要使用可重入锁 在故事描述完后进行具体说明;
到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:
仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。
- 非公平锁
如果你已经明白了前面讲的公平锁模型,那么非公平锁模型也就非常容易理解了。当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。
生产者和消费者问题(syn)
代码
public class LockTest3 {
public static void main(String[] args) {
Data lockTest3 = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.incr();
}
}, "线程1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.decr();
}
}, "线程2").start();
}
}
class Data {
private int num = 0;
public synchronized void incr() {
if(num != 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println(Thread.currentThread().getName() + "---->" + num);
this.notifyAll();
}
public synchronized void decr() {
if(num == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num--;
System.out.println(Thread.currentThread().getName() + "---->" + num);
this.notifyAll();
}
}
但是以上代码会发生虚假唤醒的情况;
虚假唤醒
我们可以看一下逻辑,当有不止两个线程操作数据的时候,就有可能是,其中有B线程刚刚把数据更新完成,但是还没有来得及唤醒其他的线程, 但是此时有个别A线程发现这个目前这个数据已经符合运行条件了,所以就会提前进入逻辑.
解决方法:
将 if 判断改为 while 判断,也就是说当监视的值只要有变化,就去判断其是否满足运行逻辑,如果满足,就去运行,如果不满足,就继续等待;
public class LockTest3 {
public static void main(String[] args) {
Data lockTest3 = new Data();
public static void main(String[] args) {
Data lockTest3 = new Data();
for (int j = 0; j < 3; j++) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.incr();
}
}, "线程A"+j).start();
}
for (int j = 0; j < 3; j++) {
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.decr();
}
}, "线程B"+j).start();
}
}
}
class Data {
private int num = 0;
Lock lock = new ReentrantLock();
Condition conditionIncr = lock.newCondition();
Condition conditionDecr = lock.newCondition();
public void incr() {
try {
lock.lock();
if(num != 0) {
conditionIncr.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "---->" + num);
conditionDecr.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
try {
lock.lock();
if(num == 0) {
conditionDecr.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "---->" + num);
conditionIncr.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
线程A0---->1
线程B1---->0
线程B0---->-1
线程B0---->-2
线程B0---->-3
线程B0---->-4
线程B0---->-5
线程B0---->-6
线程B0---->-7
线程A1---->-6
线程A2---->-5
线程A0---->-4
线程B0---->-5
Condition是什么
Condition因素出Object监视器方法( wait , notify和notifyAll )成不同的对象,以得到具有多个等待集的每个对象,通过将它们与使用任意的组合的效果Lock个实现。 Lock替换synchronized方法和语句的使用, Condition取代了对象监视器方法的使用。
条件(也称为条件队列或条件变量 )为一个线程暂停执行(“等待”)提供了一种方法,直到另一个线程通知某些状态现在可能为真。 因为访问此共享状态信息发生在不同的线程中,所以它必须被保护,因此某种形式的锁与该条件相关联。 等待条件的关键属性是它原子地释放相关的锁并挂起当前线程,就像Object.wait 。
一个Condition实例本质上绑定到一个锁。 要获得特定Condition实例的Condition实例,请使用Lock的newCondition() 方法;
以下是官方的API给出的实例用法
例如,假设我们有一个有限的缓冲区,它支持put和take方法。 如果在一个空的缓冲区尝试一个take ,则线程将阻塞直到一个项目可用; 如果put试图在一个完整的缓冲区,那么线程将阻塞,直到空间变得可用。 我们希望在单独的等待集中等待put线程和take线程,以便我们可以在缓冲区中的项目或空间可用的时候使用仅通知单个线程的优化。 这可以使用两个Condition实例来实现。
class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
public void put(Object x) throws InterruptedException {
lock.lock(); try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally { lock.unlock(); }
}
public Object take() throws InterruptedException {
lock.lock(); try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally { lock.unlock(); }
}
}
生产者和消费者问题(Lock)
public class LockTest3 {
public static void main(String[] args) {
Data lockTest3 = new Data();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.incr();
}
}, "线程1").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
lockTest3.decr();
}
}, "线程2").start();
}
}
class Data {
private int num = 0;
Lock lock = new ReentrantLock();
Condition conditionIncr = lock.newCondition();
Condition conditionDecr = lock.newCondition();
public void incr() {
try {
lock.lock();
while (num != 0) {
conditionIncr.await();
}
num++;
System.out.println(Thread.currentThread().getName() + "---->" + num);
conditionDecr.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decr() {
try {
lock.lock();
while (num == 0) {
conditionDecr.await();
}
num--;
System.out.println(Thread.currentThread().getName() + "---->" + num);
conditionIncr.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
线程之间通讯以及顺序打印
/**
* 顺序打印线程
*/
public class LockTest4 {
private static int num = 1;
private static Lock lock = new ReentrantLock();
private static Condition conditionA = lock.newCondition();
private static Condition conditionB = lock.newCondition();
private static Condition conditionC = lock.newCondition();
public static void main(String[] args) {
Runnable runA = () -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
while (num != 1) {
conditionA.await();
}
System.out.println(Thread.currentThread().getName() + "---->>>" + num);
num = 2;
conditionB.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
Runnable runB = () -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
while (num != 2) {
conditionB.await();
}
System.out.println(Thread.currentThread().getName() + "---->>>" + num);
num = 3;
conditionC.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
Runnable runC = () -> {
lock.lock();
try {
TimeUnit.SECONDS.sleep(1);
while (num != 3) {
conditionC.await();
}
System.out.println(Thread.currentThread().getName() + "---->>>" + num);
num = 1;
conditionA.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
};
new Thread(runA, "xianchengA").start();
new Thread(runB, "xianchengB").start();
new Thread(runC, "xianchengC").start();
}
}
Callable的使用
发现以下变量a只加了一次,所以说结果会被缓存,效率高
public class LockTest5 {
private static Integer a = 0;
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable1 = () -> {
System.out.println(Thread.currentThread().getName() + "---->" + a++);
return a;
};
FutureTask<Integer> futureTask = new FutureTask<>(callable1);
new Thread(futureTask, "线程A").start();
new Thread(futureTask, "线程B").start();
Integer integer = futureTask.get();
System.out.println("integer = " + integer);
}
}
线程安全集合框架的使用
/**
* 不安全map
*/
public class LockTest7 {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
final int temp = i;
new Thread(() -> {
map.put(Thread.currentThread().getName(), temp);
}).start();
}
System.out.println(map);
}
}
/**
* 安全
*/
public class LockTest7 {
public static void main(String[] args) {
Map<String, Integer> map = new ConcurrentHashMap<>();
for (int i = 0; i < 100; i++) {
final int temp = i;
new Thread(() -> {
map.put(Thread.currentThread().getName(), temp);
}).start();
}
System.out.println(map);
}
}
Set
// 不安全set
public class LockTest7 {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
for (int i = 0; i < 1000; i++) {
final int temp = i;
new Thread(() -> {
set.add(temp);
}).start();
}
System.out.println(set);
}
}
//安全的set
public class LockTest7 {
public static void main(String[] args) {
Set<Integer> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 1000; i++) {
final int temp = i;
new Thread(() -> {
set.add(temp);
}).start();
}
System.out.println(set);
}
}
安全的set的新增元素的源码
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) {
// Optimize for lost race to another addXXX operation
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
if (indexOf(e, current, common, len) >= 0)
return false;
}
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
安全的List
List<Integer> set = new CopyOnWriteArrayList<>();
底层代码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
线程计数器 CountDownLatch(减),CyclicBarrier(加)
CountDownLatch
A CountDownLatch用给定的计数初始化。 await方法阻塞,直到由于countDown()方法的调用而导致当前计数达到零,之后所有等待线程被释放,并且任何后续的await 调用立即返回。 这是一个一次性的现象 - 计数无法重置。 如果您需要重置计数的版本,请考虑使用CyclicBarrier 。
public class LockTest8 {
public static void main(String[] args) throws InterruptedException {
// 要求 有六个线程执行完毕,才能执行主线程
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 10; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+" Go out");
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println("Close Door");
}
}
CyclicBarrier
允许一组线程全部等待彼此达到共同屏障点的同步辅助。 循环阻塞在涉及固定大小的线程方的程序中很有用,这些线程必须偶尔等待彼此。 屏障被称为循环 ,因为它可以在等待的线程被释放之后重新使用。
A CyclicBarrier支持一个可选的Runnable命令,每个屏障点运行一次,在派对中的最后一个线程到达之后,但在任何线程释放之前。 在任何一方继续进行之前,此屏障操作对更新共享状态很有用。
例如 集齐7颗龙珠才能召唤神龙
public class LockTest9 {
public static void main(String[] args) throws InterruptedException {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,
// 集齐7颗龙珠之后要做的事情
() -> {
System.out.println("出来吧,神龙");
});
for (int i = 1; i <= 6; i++) {
final int tempI = i;
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "找到了" + tempI + "星球");
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
Semaphore
一个计数信号量。 在概念上,信号量维持一组许可证。 如果有必要,每个acquire()都会阻塞,直到许可证可用,然后才能使用它。 每个release()添加许可证,潜在地释放阻塞获取方。 但是,没有使用实际的许可证对象; Semaphore只保留可用数量的计数,并相应地执行。
信号量通常用于限制线程数,而不是访问某些(物理或逻辑)资源。
代码
// 抢车位的例子
public class LockTest10 {
public static void main(String[] args) {
// 例如停车场只有十个车位
Semaphore semaphore = new Semaphore(10);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
// 从该信号量获取许可证,阻止直到可用 (也就是占用车位)
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "抢到车位了");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName() + "离开车位了");
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放许可证,将其返回到信号量 (也就是离开停车位)
semaphore.release();
}
}).start();
}
}
}
ReadWriteLock 读写锁
A ReadWriteLock维护一对关联的locks ,一个用于只读操作,一个用于写入。 read lock可以由多个阅读器线程同时进行,只要没有作者。 write lock是独家的。
所有ReadWriteLock实现必须保证的存储器同步效应writeLock操作(如在指定Lock接口)也保持相对于所述相关联的readLock 。 也就是说,一个线程成功获取读锁定将会看到在之前发布的写锁定所做的所有更新。
读写锁允许访问共享数据时的并发性高于互斥锁所允许的并发性。 它利用了这样一个事实:一次只有一个线程( 写入线程)可以修改共享数据,在许多情况下,任何数量的线程都可以同时读取数据(因此读取器线程)。 从理论上讲,通过使用读写锁允许的并发性增加将导致性能改进超过使用互斥锁。 实际上,并发性的增加只能在多处理器上完全实现,然后只有在共享数据的访问模式是合适的时才可以。
读写锁是否会提高使用互斥锁的性能取决于数据被读取的频率与被修改的频率相比,读取和写入操作的持续时间以及数据的争用 - 即是,将尝试同时读取或写入数据的线程数。 例如,最初填充数据的集合,然后经常被修改的频繁搜索(例如某种目录)是使用读写锁的理想候选。 然而,如果更新变得频繁,那么数据的大部分时间将被专门锁定,并且并发性增加很少。 此外,如果读取操作太短,则读写锁定实现(其本身比互斥锁更复杂)的开销可以支配执行成本,特别是因为许多读写锁定实现仍将序列化所有线程通过小部分代码。 最终,只有剖析和测量将确定使用读写锁是否适合您的应用程序。
以下是没有加读写锁的实例
/**
* 独占锁(写锁) 一次只能被一个线程占有
* 共享锁(读锁) 多个线程可以同时占有
* ReadWriteLock
* 读-读 可以共存! *
* 读-写 不能共存!
* 写-写 不能共存!
*/
public class LockTest11 {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
static Lock writeLock = lock.writeLock();
static Lock readLock = lock.readLock();
private static Map<String, String> map = new HashMap<>();
public static void main(String[] args) {
// 模拟一千个用户同时插入数据
for (int i = 0; i < 1000; i++) {
final String temStr = String.valueOf(i);
new Thread(() -> putToMap(temStr), "用户" + temStr).start();
}
// 模拟一千个用户同时读取数据
for (int i = 0; i < 1000; i++) {
final String temStr = String.valueOf(i);
new Thread(() -> getFromMap(temStr), "用户" + temStr).start();
}
}
private static void putToMap(String temStr) {
try {
System.out.println("write " + Thread.currentThread().getName() + "\t" + "在map中插入的元素是" + "\t" + temStr);
sleepSeconds(1);
map.put(temStr, temStr);
System.out.println("write " + Thread.currentThread().getName() + "\t" + "在map中插入的元素" + temStr + "\t" + "成功");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void getFromMap(String tempI) {
try {
System.out.println("读 " + Thread.currentThread().getName() + "\t" + "开始读取数据" + "\t" + tempI);
sleepSeconds(1);
String res = map.get(String.valueOf(tempI));
System.out.println("读 " + Thread.currentThread().getName() + "\t" + "读取map中的数据" + "\t" + tempI + "\t" + "成功" + "\t" + "结果是" + "\t" + res);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sleepSeconds(Integer time) {
try {
TimeUnit.MILLISECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//
//write 用户0 在map中插入的元素是 0
//write 用户1 在map中插入的元素是 1
//write 用户1 在map中插入的元素1 成功
//write 用户0 在map中插入的元素0 成功
// 根据以上结果我们不难发现在没有加锁的情况下 用户0在写入数据的时候 其他线程插入进来 这样明显是线程不安全的
// 正常情况下应该是一个用户将数据写入成功之后才能让其他用户写入数据
以下是加读写锁的实例
public class LockTest11 {
private static ReadWriteLock lock = new ReentrantReadWriteLock();
static Lock writeLock = lock.writeLock();
static Lock readLock = lock.readLock();
private static Map<String, String> map = new HashMap<>();
public static void main(String[] args) {
// 模拟一千个用户同时插入数据
for (int i = 0; i < 1000; i++) {
final String temStr = String.valueOf(i);
new Thread(() -> {
writeLock.lock();
putToMap(temStr);
writeLock.unlock();
}, "用户" + temStr).start();
}
// 模拟一千个用户同时读取数据
for (int i = 0; i < 1000; i++) {
final String temStr = String.valueOf(i);
new Thread(() -> {
readLock.lock();
getFromMap(temStr);
readLock.unlock();
}, "用户" + temStr).start();
}
}
private static void putToMap(String temStr) {
try {
System.out.println("write " + Thread.currentThread().getName() + "\t" + "在map中插入的元素是" + "\t" + temStr);
sleepSeconds(1);
map.put(temStr, temStr);
System.out.println("write " + Thread.currentThread().getName() + "\t" + "在map中插入的元素" + temStr + "\t" + "成功");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void getFromMap(String tempI) {
try {
System.out.println("读 " + Thread.currentThread().getName() + "\t" + "开始读取数据" + "\t" + tempI);
sleepSeconds(1);
String res = map.get(String.valueOf(tempI));
System.out.println("读 " + Thread.currentThread().getName() + "\t" + "读取map中的数据" + "\t" + tempI + "\t" + "成功" + "\t" + "结果是" + "\t" + res);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sleepSeconds(Integer time) {
try {
TimeUnit.MILLISECONDS.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//write 用户853 在map中插入的元素是 853
//write 用户853 在map中插入的元素853 成功
//write 用户855 在map中插入的元素是 855
//write 用户855 在map中插入的元素855 成功
//读 用户311 开始读取数据 311
//读 用户310 开始读取数据 310
// 在加读写锁的情况下 每个用户在写数据的过程中其他的用户是不能插队的
// 但是在读取数据的时候其他用户是可以插队的
阻塞队列(BlockingQueue)
A Deque另外支持在检索元素时等待deque变为非空的阻塞操作,并等待存储元素时在deque中可用的空间。
BlockingDeque方法有四种形式,不同的方式处理不能立即满足的操作,但可能在将来的某个时间点满足:一个抛出异常,第二个返回一个特殊值( null或false ,具体取决于操作),第三个程序将无限期地阻止当前线程,直到操作成功为止,而第四个程序块在放弃之前只有给定的最大时限。
- 抛异常
public static void test1() {
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(3);
System.out.println("integers.add(1) = " + integers.add(1));
System.out.println("integers.add(1) = " + integers.add(1));
System.out.println("integers.add(1) = " + integers.add(1));
System.out.println("integers.add(1) = " + integers.add(1));
System.out.println("\"--------------\" = " + "--------------");
System.out.println("integers.remove() = " + integers.remove());
System.out.println("integers.remove() = " + integers.remove());
System.out.println("integers.remove() = " + integers.remove());
System.out.println("integers.remove() = " + integers.remove());
}
- 返回特殊值
public static void test2() {
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(3);
System.out.println("integers.offer(1) = " + integers.offer(1));
System.out.println("integers.offer(1) = " + integers.offer(1));
System.out.println("integers.offer(1) = " + integers.offer(1));
System.out.println("integers.offer(1) = " + integers.offer(1));
System.out.println("\"--------------\" = " + "--------------");
System.out.println("integers.poll() = " + integers.poll());
System.out.println("integers.poll() = " + integers.poll());
System.out.println("integers.poll() = " + integers.poll());
// System.out.println("integers.poll() = " + integers.poll());
}
//integers.offer(1) = true
//integers.offer(1) = true
//integers.offer(1) = true
//integers.offer(1) = false
//"--------------" = --------------
//integers.poll() = 1
//integers.poll() = 1
//integers.poll() = 1
- 阻塞等待
public static void test3() throws InterruptedException {
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(3);
readWriteLock.writeLock().lock();
integers.put(1);
System.out.println("插入成功");
integers.put(1);
System.out.println("插入成功");
integers.put(1);
System.out.println("插入成功");
// 程序执行到这里之后没有位置了 就会一直阻塞下去
// integers.put(1);
System.out.println("插入成功");
System.out.println("\"--------------\" = " + "--------------");
System.out.println("integers.take() = " + integers.take());
System.out.println("integers.take() = " + integers.take());
System.out.println("integers.take() = " + integers.take());
// 程序执行到这里之后如果一直没有元素 就会一直阻塞
// System.out.println("integers.take() = " + integers.take());
}
- 超时等待
public static void test4() throws InterruptedException {
ArrayBlockingQueue<Integer> integers = new ArrayBlockingQueue<>(90);
for (int i = 0; i < 100; i++) {
final int tempI = i;
new Thread(() -> {
try {
System.out.println("新增 = " + integers.offer(tempI, 2L, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
TimeUnit.SECONDS.sleep(3);
System.out.println("---------------");
for (int i = 0; i < 100; i++) {
System.out.println("删除 = " + integers.poll(2, TimeUnit.SECONDS));
}
}
线程池(重点)
- 线程池的好处
- 降低资源的消耗
- 提高响应的速度
- 方便管理。
- 线程复用、可以控制最大并发数、管理线程
- Executors 工具类、3大方法(但是不建议使用该方法)
- 创建一个单个线程的线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor();/ - 创建一个指定数量的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(5); - 创建一个具有缓存区的线程池(根据CPU的性能去创建,遇强则强,遇弱则弱)
ExecutorService threadPool = Executors.newCachedThreadPool();
- 7大参数
@Bean(value = "myThreadPoolExecutor")
public ThreadPoolExecutor sas() {
return new ThreadPoolExecutor(
2,// 队列没满时,线程最大并发数
4, // 队列满后线程能够达到的最大并发数
2, // 空闲线程过多久被回收的时间限制
TimeUnit.SECONDS,// keepAliveTime 的时间单位
new LinkedBlockingDeque(10), // 阻塞队列
Executors.defaultThreadFactory(), // 线程工厂:创建线程的,一般 不用动
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
}
corePoolSize,maximumPoolSize,workQueue之间关系。
当线程池中线程数小于corePoolSize时,新提交的任务将创建一个新的线程,即使此时线程池中存在空闲线程
当线程池中线程数达到corePoolSize时,新提交的任务将会存放到workQueue中,等待线程池任务调度执行
当workQueue已满,且maximumPoolSize>corePoolSize时,新提交的任务将会创建新的线程执行任务。
当提交的任务数大于maximumPoolSize+workQueue时,新提交的任务将由RejectedExecutionHandler处理
new ThreadPoolExecutor.AbortPolicy() // 银行满了,还有人进来,不处理这个人的,抛出异 常
new ThreadPoolExecutor.CallerRunsPolicy() // 哪来的去哪里!
new ThreadPoolExecutor.DiscardPolicy() //队列满了,丢掉任务,不会抛出异常!
new ThreadPoolExecutor.DiscardOldestPolicy() //队列满了,尝试去和最早的竞争,也不会 抛出异常!
CompletableFuture的使用
CompletableFuture supplyAsync(Supplier supplier, Executor executor);异步执行线程且有返回值
CompletableFuture runAsync(Runnable runnable, Executor executor) 异步执行线程没有返回值
// 异步任务1
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
System.out.println("线程future1开始执行");
int i = 10 / 1;
sleepSeconds(1);
System.out.println("线程future1执行结束");
return i;
}, myExecutor).whenComplete((res, ex) -> { // 并不异步执行 // 当上一个线程执行完成后执行,res->上一个线程的执行结果,ex->上一个线程的运行时异常;无返回值
System.out.println("res = " + res);
System.out.println("ex = " + ex);
}).exceptionally(throwable -> 110); // 当以上所有的线程有异常时执行,throwable ->异常信息;返回值->可以自定义返回值。
future.thenAcceptAsync(thenRes->{ //获取以上线程执行结果并异步执行 // 线程执行完成之后没有返回值
System.out.println("thenRes = " + thenRes);
},myExecutor);
// 异步任务2
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
System.out.println("线程future2开始执行");
int i = 101 / 1;
sleepSeconds(5);
System.out.println("线程future2执行结束");
return i++;
}, myExecutor);
// 异步任务3
future2.acceptEitherAsync(// 当future1或future2执行完成之后执行异步 新异步任务 前提是future1和future2的返回值必须一致 不然新的异步任务无法获取返回值并执行
future1, // 异步任务1
res -> { // 新异步任务
System.out.println("线程3333开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程3333执行结束:" + Thread.currentThread().getName());
},
myExecutor // 线程池
);
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程3333开始执行:pool-1-thread-1
//res = 10
//线程3333执行结束:pool-1-thread-1
//线程future2执行结束
future2.thenAcceptBoth(future1, (res2, res1) -> { // 当future2和future1都执行完成后执行新线程 其中future2结果对应res2 future1结果对应res1
System.out.println("线程4444开始执行:" + Thread.currentThread().getName());
System.out.println("res1 = " + res1);
System.out.println("res2 = " + res2);
sleepSeconds(1);
System.out.println("线程4444执行结束:" + Thread.currentThread().getName());
// 执行结果
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程future2执行结束
//线程4444开始执行:pool-1-thread-2
//res1 = 10
//res2 = 102
//线程4444执行结束:pool-1-thread-2
});
future2.applyToEither(future1, res -> { // 和acceptEither()方法的区别在于此方法是有返回值的
System.out.println("线程555开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程555执行结束:" + Thread.currentThread().getName());
return 322;
});
future2.thenCombine(future1,(res2,res1)->{ // 和thenAcceptBoth()方法的区别在于此方法是有返回值的
System.out.println("线程4444开始执行:" + Thread.currentThread().getName());
System.out.println("res1 = " + res1);
System.out.println("res2 = " + res2);
sleepSeconds(1);
System.out.println("线程4444执行结束:" + Thread.currentThread().getName());
return 322;
});
future2.runAfterEither(future1,()->{ // 不用接收结果去执行 两个线程只要有一个执行完成就行
System.out.println("线程666开始执行:" + Thread.currentThread().getName());
sleepSeconds(1);
System.out.println("线程666执行结束:" + Thread.currentThread().getName());
});
future2.runAfterBoth(future1,()->{ // 不用接收结果去执行 两个线程都需要执行完成
System.out.println("线程666开始执行:" + Thread.currentThread().getName());
sleepSeconds(1);
System.out.println("线程666执行结束:" + Thread.currentThread().getName());
});
//线程排版
future2.thenCompose(res -> {
System.out.println("线程7777开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(1);
System.out.println("线程7777执行结束:" + Thread.currentThread().getName());
return CompletableFuture.runAsync(() -> {
System.out.println("线程8888开始执行:" + Thread.currentThread().getName());
System.out.println("res = " + res);
sleepSeconds(10);
System.out.println("线程8888执行结束:" + Thread.currentThread().getName());
});
//线程future1开始执行
//线程future2开始执行
//线程future1执行结束
//res = 10
//ex = null
//线程future2执行结束
//线程7777开始执行:pool-1-thread-2
//res = 102
//线程7777执行结束:pool-1-thread-2
//线程8888开始执行:ForkJoinPool.commonPool-worker-1
//res = 102
//线程8888执行结束:ForkJoinPool.commonPool-worker-1
});