});
t1.start();
// --
Thread t2 = new Thread(() -> {
for (int i = 0; i < MAX_COUNT; i++) {
number--;
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + number);
}
}
运行结果:
![](https://img-blog.csdnimg.cn/9fc43be5b95e4921977afee978c47803.png)
> 结果并不是预期的0,是线程不安全的。
**线程安全代码(AtomicInteger):**
public class CASDemo2 {
private static final int MAX_COUNT = 100000;
private static final AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// ++
Thread t1 = new Thread(() -> {
for (int i = 0; i < MAX_COUNT; i++) {
atomicInteger.getAndIncrement();
}
});
t1.start();
// --
Thread t2 = new Thread(() -> {
for (int i = 0; i < MAX_COUNT; i++) {
atomicInteger.getAndDecrement();
}
});
t2.start();
t1.join();
t2.join();
System.out.println("最终结果:" + atomicInteger.get());
}
}
运行结果:
![](https://img-blog.csdnimg.cn/a664c610af244b7bbf58c100b2445475.png)
> 结果为0,证明使用了**AtomicInteger**是线程安全的。
### 1.1.4 CAS缺点(ABA问题)
ABA 转账问题,X 给 Y 转账,系统卡顿点击了两次转账按钮,X 原来是 300,正常是转完账(100元) 还剩下200,第⼀次转账成功之后变成了 200,此时 Z 给 X 转了 100 元,余额又变回了 300,第⼆次CAS 判断(300,300,200)成功,于是又扣了X 100 元,X直接亏了100元。
![](https://img-blog.csdnimg.cn/img_convert/61555b0393b51a650f5101f324b07a32.gif)
**ABA代码演示:**
public class ABADemo1 {
private static final AtomicInteger money = new AtomicInteger(300);
public static void main(String[] args) throws InterruptedException {
// 第一次转账点击按钮(-100)
Thread t1 = new Thread(() -> {
// 先得到余额
int oldMoney = money.get();
// 执行耗时 2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldMoney, oldMoney - 100);
});
t1.start();
// 第二次点击按钮(-100)(不小心点击的,因为第一次点击完没反应,所以又点了一次)
Thread t2 = new Thread(() -> {
int oldMoney = money.get();
money.compareAndSet(oldMoney, oldMoney - 100);
});
t2.start();
// 给账户 +100
Thread t3 = new Thread(() -> {
int oldMoney = money.get();
// 执行耗时 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldMoney, oldMoney + 100);
});
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("最终余额:" + money.get());
}
}
运行结果:
![](https://img-blog.csdnimg.cn/0c2d628e0ef54f5e90d38132e2bbf6ca.png)
> 可以看到,这波属实亏大了!
**解决方案(AtomicStampedReference)**:引入了一个版本号,每次操作后让版本号+1,执行的时候判断版本号和值。
public class ABADemo2 {
private static final AtomicStampedReference<Integer> money = new AtomicStampedReference<>(300, 0);
public static void main(String[] args) throws InterruptedException {
// 第一次转账点击按钮(-100)
Thread t1 = new Thread(() -> {
// 先得到余额
int oldMoney = money.getReference();
// 得到版本号
int version = money.getStamp();
// 执行耗时 2s
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldMoney, oldMoney - 100, version, version + 1);
});
t1.start();
// 第二次点击按钮(-100)(不小心点击的,因为第一次点击完没反应,所以又点了一次)
Thread t2 = new Thread(() -> {
int oldMoney = money.getReference();
int version = money.getStamp();
money.compareAndSet(oldMoney, oldMoney - 100, version, version + 1);
});
t2.start();
// 给账户 +100
Thread t3 = new Thread(() -> {
int oldMoney = money.getReference();
int version = money.getStamp();
// 执行耗时 1s
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
money.compareAndSet(oldMoney, oldMoney + 100, version, version + 1);
});
t3.start();
t1.join();
t2.join();
t3.join();
System.out.println("最终余额:" + money.getReference());
}
}
运行结果:
![](https://img-blog.csdnimg.cn/e3c7d610b5b34707bed933d77f23f2b4.png)
> 成功解决ABA问题。
1.2 悲观锁
-------
### 1.2.1 悲观锁定义
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。
### 1.2.2 悲观锁应用
之前介绍过的 synchronized、Lock 都是悲观锁。
详情请看:[Java多线程(三):线程安全问题与解决方法\_澄白易的博客-CSDN博客]( )
2. 公平锁&非公平锁
===========
> 假设三个线程 A, B, C。 A 先尝试获取锁, 获取成功. 然后 B 再尝试获取锁, 获取失败, 阻塞等待; 然后C 也尝试获取锁, C 也获取失败, 也阻塞等待.当线程 A 释放锁的时候, 会发生啥呢?
* **公平锁**: 遵守 "先来后到"。B 比 C 先来的,当 A 释放锁的之后, B 就能先于 C 获取到锁;
* **非公平锁**: 不遵守 "先来后到"。 B 和 C 都有可能获取到锁。
注意:
1. 操作系统内部的线程调度就可以视为是随机的. 如果不做任何额外的限制, 锁就是**非公平锁**. 如果要想实现**公平锁**, 就需要依赖额外的数据结构, 来记录线程们的先后顺序;
2. **公平锁**和**非公平锁**没有好坏之分, 关键还是看适用场景。
3\. 读写锁
=======
3.1 读写锁的定义
----------
**读写锁(Readers-Writer Lock)****顾名思义就是⼀把锁分为两部分:读锁和写锁**,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得(写锁),并且写操作和读操作也是互斥的,总结来说,**读写锁的特点是:读读不互斥、读写互斥、写写互斥。**
Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁:
* ReentrantReadWriteLock.ReadLock 类表示⼀个读锁. 这个对象提供了 lock / unlock 方法进行加锁解锁;
* ReentrantReadWriteLock.WriteLock
[video(video-8PGqMbZO-1716354551447)(type-bilibili)(url-https://player.bilibili.com/player.html?aid=1804892953)(image-https://img-blog.csdnimg.cn/img_convert/d03a705143646f4f3f6ea879d59b2788.png)(title-必看视频!获取2024年最新Java开发全套学习资料 备注Java)]
类表示⼀个写锁. 这个对象也提供了 lock / unlock方法进行加锁解锁.
**3.2 读写锁示例**
-------------
public class ReadWriteLockDemo {
public static void main(String[] args) {
// 创建读写锁
final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
// 创建读锁
final ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
// 创建写锁
final ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
// 线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100));
// 启动新线程执行读任务
executor.submit(() -> {
// 加锁操作
readLock.lock();
try {
System.out.println("执行读锁1:" + LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
});
// 启动新线程执行读任务2
executor.submit(() -> {
readLock.lock();
try {
System.out.println("执行读锁2:" + LocalDateTime.now());
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
});
// 启动新线程执行写操作
executor.submit(() -> {
writeLock.lock();
try {
System.out.println("执行写锁1:" + LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
});
// 启动新线程执行写操作2
executor.submit(() -> {
writeLock.lock();
try {
System.out.println("执行写锁2:" + LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
});
executor.shutdown();
}
}
运行结果:
# 总结
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
![](https://img-blog.csdnimg.cn/img_convert/0842a7fd074a8ee36e9dd91ef6a1d651.webp?x-oss-process=image/format,png)
行写锁2:" + LocalDateTime.now());
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
});
executor.shutdown();
}
}
运行结果:
总结
总体来说,如果你想转行从事程序员的工作,Java开发一定可以作为你的第一选择。但是不管你选择什么编程语言,提升自己的硬件实力才是拿高薪的唯一手段。
如果你以这份学习路线来学习,你会有一个比较系统化的知识网络,也不至于把知识学习得很零散。我个人是完全不建议刚开始就看《Java编程思想》、《Java核心技术》这些书籍,看完你肯定会放弃学习。建议可以看一些视频来学习,当自己能上手再买这些书看又是非常有收获的事了。
[外链图片转存中…(img-sf2fiF51-1716458140733)]