- 前言
- 一、线程通信
- 1.1JVM下的内存区域划分
- 1.2 代码中的存储理解
- 二、线程安全
- 2.1线程不安全举例
- 2.2线程不安全原因(重点知识、面试重点)
- 2.2.1站在开发者的角度
- 2.2.2系统角度解释
- 2.2.2.1原子性被破坏
- 2.2.2.2 内存可见性,导致某些线程读取“脏数据”
- 2.2.2.3 代码重排序
- 2.3线程安全问题小结
- 2.4常见类的线程安全问题
- 三、保护线程安全的措施——锁(lock)
- 3.1synchronized 锁 /同步锁/monitor锁(监视器锁)
- 3.1.1语法
- 3.1.2什么是锁
- 3.1.3判断一下那些会互斥
- 3.1.4 JVM中的锁
- 3.1.5synchronized 的作用
- 3.1.5.1保证临界区的原子性
- 3.1.5.2内存可见性
- 3.1.5.3增加约束代码的重排序
- 3.2JUC下的锁
- 3.2.1lock()
- 3.2.2.lockInterruptibly()
- 3.2.3try.lock()
- 3.2.4tryLock(5, TimeUnit.SECONDS)
- 3.3synchronized VS JUC下的锁
- 四、线程的状态
- 4.1阻塞状态
- 五、volatile修饰变量
- 5.1保护内存可见性
- 5.2原子性
- 5.3保证代码重排序
- 六、单列模式
- 6.1饿汉模式
- 6.2懒汉模式
- 七、生产-消费者模型、wait()和notify()
- 7.1wait()和notify()
- 7.2notify()唤醒
- 7.3notify()和wait小结
- 7.4与synchronized对应到lock也有相似的功能
- 八、阻塞队列
- 8.1阻塞队列实现
- 总结
前言
一、线程通信 1.1JVM下的内存区域划分
// 演示线程不安全的现象
public class Main {
// 定义一个共享的数据 —— 静态属性的方式来体现
static int r = 0;
// 定义加减的次数
// COUNT 越大,出错的概率越大;COUNT 越小,出错的概率越小
static final int COUNT = 100;
// 定义两个线程,分别对 r 进行 加法 + 减法操作
static class Add extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r++;
}
}
}
static class Sub extends Thread {
@Override
public void run() {
for (int i = 0; i < COUNT; i++) {
r--;
}
}
}
public static void main(String[] args) throws InterruptedException {
Add add = new Add();
add.start();
Sub sub = new Sub();
sub.start();
add.join();
sub.join();
// 理论上,r 被加了 COUNT 次,也被减了 COUNT 次
// 所以,结果应该是 0
//但是每次输出结果都不同
System.out.println(r);
}
}
2.2线程不安全原因(重点知识、面试重点)
2.2.1站在开发者的角度
2.2.2系统角度解释
2.2.2.1原子性被破坏
2.2.2.2 内存可见性,导致某些线程读取“脏数据”
一个是对当前对象加锁,一个是对当前类加锁
加锁操作使得互斥(synchronized和我们一起配合(我们需要真正的使用synchronized))
看4/24的开头和00:58~1:18
public class Main1 {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
lock.lock();
System.out.println("子线程进入临界区"); // 理论上,这句代码永远到达不了
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
t.join();
}
}
上述代码中的永远加不到锁的理解
使用lock.lockInterruptibly
输出结果:收到停止信号,停止运行
public class Main3 {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
try {
lock.lockInterruptibly();//允许停止的情况
System.out.println("子线程进入临界区"); // 理论上,这句代码永远到达不了
} catch (InterruptedException e) {
System.out.println("收到停止信号,停止运行");
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
t.interrupt();//直接使用尝试让子线程停下来,但实际会徒劳无功,所以用上面的捕获
t.join();
}
}
3.2.3try.lock()
使用lock.try.lock返回一个boolean,加锁失败后还可以有其他操作,更加灵活
输出:加锁失败
static class MyThread extends Thread {
@Override
public void run() {
boolean b = lock.tryLock();
if (b == true) {
// 加锁成功了
System.out.println("加锁成功");
System.out.println("子线程进入临界区"); // 理论上,这句代码永远到达不了
} else {
System.out.println("加锁失败");
}
}
}
3.2.4tryLock(5, TimeUnit.SECONDS)
使用lock.tryLock(5, TimeUnit.SECONDS),会等上5秒,更加灵活
public class Main5 {
private static final Lock lock = new ReentrantLock();
static class MyThread extends Thread {
@Override
public void run() {
boolean b = false;
try {
b = lock.tryLock(5, TimeUnit.SECONDS);//会等上5秒
if (b == true) {
// 加锁成功了
System.out.println("加锁成功");
System.out.println("子线程进入临界区");
} else {
System.out.println("5s 之后加锁失败");
}
} catch (InterruptedException e) {
System.out.println("被人打断了");
}
}
}
public static void main(String[] args) throws InterruptedException {
lock.lock();
MyThread t = new MyThread();
t.start();
// TimeUnit.SECONDS.sleep(2);
// t.interrupt(); // 休息2 秒,然后输出:被人打断
// TimeUnit.SECONDS.sleep(2);
// lock.unlock(); //2秒之后把锁释放,然后可以正常取到锁,所以会,输出:加锁成功,子线程进入临界区
TimeUnit.SECONDS.sleep(20);
lock.unlock(); //由于休息了20秒,才解锁,但是上面只等待5秒,所以加锁失败输出:5秒之后加锁失败
t.join();
}
}
3.3synchronized VS JUC下的锁
四、线程的状态
4.1阻塞状态
阻塞状态转换图
public class Main2 {
static Object lock = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (lock) { // 永远请求不到锁 —— 死锁 BLOCKED
System.out.println("永远到不了");
}
}
}
public static void main(String[] args) throws InterruptedException {
synchronized (lock) {
MyThread t = new MyThread();
t.start();
t.join(); // 等待的时候不会释放锁 WAITING
}
}
}
public class Main {
static class MyThread extends Thread {
@Override
public void run() {
try {
TimeUnit.DAYS.sleep(1); // TIMED_WAITING
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
// t.join(); // WAITING
t.join(1000 * 60); // 一分钟 TIMED_WAITING
}
}
五、volatile修饰变量
5.1保护内存可见性
没有volatile时,main即使改变quit,但是run()方法中的quit仍任是false,无法停止循环
有volatile时,main中改变quit,run()方法中quit改变,停止循环,输出结果
使得可以看到变量的更改
public class WithOutVolatile {
volatile static boolean quit = false;
static class MyThread extends Thread {
@Override
public void run() {
long r = 0;
while (quit == false) {
r++;
}
System.out.println(r);
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(5);
quit = true;
}
}
5.2原子性
5.3保证代码重排序
一开始就初始化(饿汉模式——等不及)
等用到的时候才初始化(懒汉模式)
天生就是线程安全的
public class StarvingMode {
// 是线程安全的
// 类加载的时候执行
// JVM 保证了类加载的过程是线程安全的
//所以只执行一次初始化
private static StarvingMode instance = new StarvingMode();
//谁要去调用这个对象,就要去调用下面这个静态方法
public static StarvingMode getInstance() {
return instance;//所以拿到的都是instance指向的对象,保证了整个应用过程中只有这一个对象
}
//为了保证没人可以用静态方法初始化其他对象
//用private修饰只能在本类中只用,其他想调用也无法使用
private StarvingMode() {}
}
6.2懒汉模式
public class LazyModeV1 {
private static LazyModeV1 instance = null;//先不实例化对象
public static LazyModeV1 getInstance() {
// 第一次调用这个方法时,说明我们应该实例化对象了
//之后则会直接返回instance,因此只初始化一次
if (instance == null) {
instance = new LazyModeV1(); // 只在第一次的时候执行
}
return instance;
}
private LazyModeV1() {}
由于if语句对之前instance既共享又有修改所以线程不安全,当原子性被破坏时多个线程可能进行多次初始化赋值
为了保证线程安全,可以对其进行加锁操作
public class LazyModeV2 {
private static LazyModeV2 instance = null;
public synchronized static LazyModeV2 getInstance() {
// 第一次调用这个方法时,说明我们应该实例化对象了
if (instance == null) {
instance = new LazyModeV2(); // 只在第一次的时候执行
}
return instance;
}
private LazyModeV2() {}
}
但是该方法会对每一个线程进行加锁,所以需要优化支队第一个线程进行加锁
仔细阅读每一个注释并理解
public class LazyModeV3 {
private volatile static LazyModeV3 instance = null;
public static LazyModeV3 getInstance() {
// 第一次调用这个方法时,说明我们应该实例化对象了
if (instance == null) {
// 只有 instance 还没有初始化时,才会走到这个分支
// 这里没有锁保护,所以理论上可以有很多线程同时走到这个分支
synchronized (LazyModeV3.class) { // 通过上面的条件,
// 让争抢锁的动作只在 instance
// 实例化之前才可能发生。
// 实例化之后就不再可能
// 加锁之后才能执行
// 第一个抢到锁的线程,看到的 instance 是 null
// 其他抢到锁的线程,看到的 instance 不是 null
// 保证了 instance 只会被实例化一次
if (instance == null) {
instance = new LazyModeV3(); // 只在第一次的时候执行
// 当重排序成 1 -> 3 -> 2 的时候可能出问题
// 通过 volatile 修复
}
}
}
return instance;
}
private LazyModeV3() {}
}
假设第一次执行初始化的时候,A线程执行1->3还没有走到2被切走了,又接着进行下一个线程继续执行。呢么初始化已经出现错误,后续依然是错误的,因此需要用volatile进行修复
import java.util.concurrent.TimeUnit;
public class Demo1 {
static class MyThread extends Thread {
private Object o;
MyThread(Object o) {
this.o = o;
}
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);//等待5秒钟
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (o) {
System.out.println("唤醒主线程");
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
Object o = new Object();
synchronized (o) {
MyThread t = new MyThread(o);//保证主线程先执行,先唤醒再等待
t.start();
o.wait(); // 1. 释放 o 锁;2.等待 ...... 3. 再次加锁
// wait() 过程中是不持有锁的,所以notify能加锁
System.out.println("永远不会到达");//若只有wait没有唤醒则永远不会到达,只有先唤醒才能wait成功
}
}
}
注意:
1.若只有wait没有notify则 永远不会执行System.out.println(“永远不会到达”);只有执行notify才能wait成功进而执行 System.out.println(“永远不会到达”);
先执行主线程加锁然后让wait释放锁,子线程才能加到锁然后执行notify,然后wait再次加锁再解锁
2.为什么wait和notify都能进行加锁?
因为,wait()1. 释放 o 锁;2.等待 … 3. 再次加锁,等待 过程中不持有锁
3. MyThread t = new MyThread(o)放入锁中可以避免
public class Demo2 {
static Object lock1 = new Object();
static Object lock2 = new Object();
static Object lock3 = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (lock3) {
System.out.println("lock3 成功");
}
}
}
public static void main(String[] args) throws InterruptedException {
synchronized (lock1) {
synchronized (lock2) {
synchronized (lock3) {
MyThread t = new MyThread();
t.start();
// 主线程持有 3 把锁
lock3.wait(); // 释放 lock3 锁
}
}
}
}
}
没有释放lock1、lock2
7.2notify()唤醒
1.随机唤醒
public class Demo3 {
static Object o = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (o) {
try {
o.wait();
System.out.println(getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
MyThread t = new MyThread();
t.start();
}
// 保证了子线程们先 wait,主线程就先休眠一会儿
TimeUnit.SECONDS.sleep(5);
synchronized (o) {
// o.notify();
o.notifyAll();
}
}
}
o.notify();随机唤醒一个
o.notifyAll();唤醒全部wait但是顺序不定
2.先wait再notify是正常执行的,但是如果先notify再wait 是不行的,因为wait无法感知notify,则wait会一直等下去
public class Demo4 {
static Object o = new Object();
static class MyThread extends Thread {
@Override
public void run() {
synchronized (o) {
System.out.println("notify");
o.notify();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
TimeUnit.SECONDS.sleep(2);
synchronized (o) {
System.out.println("wait");
o.wait();
}
}
}
7.3notify()和wait小结
7.4与synchronized对应到lock也有相似的功能
条件变量Condition
功能和wait和notify可以说“一模一样”
public class Demo {
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
condition.await(); // 等价于 wait
condition.signal(); //等价于notify
condition.signalAll();
lock.lock();//只有在lock锁住的情况下才能执行wait
try {
condition.await();
} finally {
lock.unlock();
}
}
}
八、阻塞队列
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class Main1 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue = new LinkedBlockingDeque<>();
String poll = queue.poll();
System.out.println(poll);
String take = queue.take(); // 只要队列中取不到元素,就会一直阻塞
System.out.println("永远到达不了");
BlockingQueue e = new ArrayBlockingQueue<>(2);
e.put("1");
e.put("2");
e.put("3");
}
}
8.1阻塞队列实现
public class MyArrayBlockingQueue {
private long[] array;
private int frontIndex; // 永远在队列的第一个元素位置
private int rearIndex; // 永远在队列的最后一个的下一个位置
private int size;
public MyArrayBlockingQueue(int capacity) {
array = new long[capacity];
frontIndex = 0;
rearIndex = 0;
size = 0;
}
public synchronized void put(long e) throws InterruptedException {
// 判断队列是否已经满了
while (array.length == size) {
this.wait();
}
// 预期:队列一定不是满的
array[rearIndex] = e;
rearIndex++;
if (rearIndex == array.length) {
rearIndex = 0;
}
size++;
notifyAll();
}
public synchronized long take() throws InterruptedException {
while (size == 0) {
wait();
}
long e = array[frontIndex];
frontIndex++;
if (frontIndex == array.length) {
frontIndex = 0;
}
size--;
notifyAll();
return e;
}
}
import java.util.Scanner;
public class Main {
static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);
static class MyThread extends Thread {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
long e = scanner.nextLong();
try {
queue.put(e);
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
long take = queue.take();
System.out.println(take);
}
}
import java.util.Scanner;
public class Main2 {
static MyArrayBlockingQueue queue = new MyArrayBlockingQueue(3);
static class MyThread extends Thread {
@Override
public void run() {
Scanner scanner = new Scanner(System.in);
long e = scanner.nextLong();
try {
queue.take();
} catch (InterruptedException interruptedException) {
interruptedException.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
MyThread t = new MyThread();
t.start();
queue.put(1);
queue.put(2);
queue.put(3);
queue.put(4); // 阻塞
System.out.println("4 被放入队列中");
}
}
总结



