目录
2.2 线程安全的单例模式(!!!!安全的懒汉模式(重要))
一、线程安全
操作系统中,由于线程的调度是随机性的(抢占式执行),由此带来了一些安全方面的问题。
线程不安全的原因:
- 1.线程之间的抢占式执行(无法确定顺序,具有随机性)
- 2.多个线程 修改 同一个变量(规避:一个线程只改一个变量)
- 3.原子性:操作只有一个步骤,多步骤互相排斥,加锁即保证了原子性(同步互斥,不能同时进行两个操作,A作业时B不能进行打断)
- 4.内存可见性(和原子性类似):一个线程对共享变量值的修改,能够及时地被其他线程看到.(一个线程频繁读,另一个频繁写数据,则会存在可见性问题)
- 指令重排序:代码执行顺序也会影响线程安全
给方法直接加synchronized关键字进行加锁,进入此方法自动加锁,离开方法,自动解锁,便可保证下面代码自增结果始终为100000
当一个线程加锁成功,其他线程尝试枷锁便会出发阻塞等待(BLOCKED),持续到占用所得线程释放锁为止。
class Counter {
public int count;
synchronized public void increase() {
count++;
}
}
public class thread {
private static Counter counter = new Counter(); //创建一个实例counter
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
Thread t2 = new Thread(() ->{
for (int i = 0; i < 50000; i++) {
counter.increase();
}
});
t1.start();
t2.start();
t1.join(); //进行一个线程等待(俩线程执行完毕,再打印)
t2.join();
System.out.println(counter.count); //100000
}
}
执行计数器++,的步骤:
- 1.先把内存中count的值加载到cpu寄存器中
- 2.再给寄存器中的值+1
- 3.再把寄存器的值写回到count种
synchronized可保证原子性、可见性、有序但不能禁止指令重排;
volatile保证可见性、禁止指令重排但不保证原子性
此处的有序并不代表指令重排序
1.1synchronized的使用
Java中每个类都是继承自Object,new出来的实例包含了自己安排的属性,也包含了“对象头”,对象的一些元数据。(加锁操作也就是给对象头里设置标志位)。两个线程针对同一个变量进行加锁才具有竞争,若是不同变量则不需竞争
- 进入 synchronized 修饰的代码块, 相当于 加锁
- 退出 synchronized 修饰的代码块, 相当于 解锁
监视器锁:monitor lock
- 1.直接修饰普通方法:(针对某个对象(this)加锁)
synchronized public void increase() {
count++;
}
- 2.修饰一个代码块:需要显式指定那个对象加锁,(java中任意对象都可作为锁对象)
public void increase() {
synchronized(this) { //锁对象需要手动指定
count++;
}
}
- 3.修饰一个静态方法:对类对象进行加锁
public static void func() {
synchronized (Counter.class) { //针对类对象(Counter是类名)进行加锁
}
}
连续进行两次锁,则会造成死锁的状态:
- 外层锁:进入方法则开始枷锁,加锁成功,因为当前锁无占用
- 内层锁:进入代码块,开始加锁,加锁失败(外层占用锁,需要其释放才可加锁)
synchronized public void increase() {
synchronized(this) { //锁对象需要手动指定
count++;
}
}
为了解决死锁的问题,引入了可重入锁(记录当前锁被占用的线程,记录一个加锁次数)
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息。
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取到锁, 并让计数器自增.;
- 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
static class Counter {
public int count = 0;
synchronized void increase() {
count++;
}
synchronized void increase2() {
increase();
}
}
死锁的四个必要条件:
- 互斥使用:一个锁被占用后,其他线程不能占用(原子性)
- 不可抢占:一个所被占用后,其他线程不能抢走
- 请求和保持:一个线程占据多把锁之后,除非进行显式的释放锁,否则锁只能被该线程持有
- 环路等待:等待关系(实际中,要尽量避免出现循环等待,给固定的顺序,就可避免死锁)
1.2Java 标准库中的线程安全类
Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施:
- ArrayList
- LinkedList
- HashMap
- TreeMap
- HashSet
- TreeSet
- StringBuilder
线程安全的 :
- Vector (不推荐使用)
- HashTable (不推荐使用)
- ConcurrentHashMap
- StringBuffer (StringBuffer 的核心方法都带有 synchronized)
- String(线程安全,非synchronized,是不可变对象(没有提供public 的修改属性操作)),不可变对象和常量/final没有联系
1.3volatile 关键字
volatile 修饰的变量, 能够保证 "内存可见性".。不会引起线程阻塞。
代码在写入 volatile 修饰的变量的时候:
- 改变线程工作内存中volatile变量副本的值
- 将改变后的副本的值从工作内存刷新到主内存
代码在读取 volatile 修饰的变量的时候:
- 从主内存中读取volatile变量的最新值到线程的工作内存中
- 从工作内存中读取volatile变量的副本
直接访问工作内存(实际是 CPU 的寄存器或 CPU 的缓存), 速度非常快, 但是可能出现数据不一致的情况。加上 volatile , 强制读写内存. 速度是慢了, 但是数据变的更准确了.(缓存是介于CPU和寄存器之间的,空间比寄存器大,速度比CPU快)
static class Counter {
public int flag = 0;
}
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
while (counter.flag == 0) {
// do nothing
}
System.out.println("循环结束!");
});
Thread t2 = new Thread(() -> {
Scanner scanner = new Scanner(System.in);
System.out.println("输入一个整数:");
counter.flag = scanner.nextInt();
});
t1.start();
t2.start();
}
// 执行效果
// 当用户输入非0值时, t1 线程循环不会结束. (这显然是一个 bug)
t1 读的是自己工作内存中的内容;当 t2 对 flag 变量进行修改, 此时 t1 感知不到 flag 的变化.
1.4 wai和notify
由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序具有随机性。
wai和notify都是 Object 对象
wait 做的事情:
<
- 释放当前的锁
- 使当前执行代码的线程进行等待. (把线程放到等待队列中)
- 满足一定条件时被唤醒, 重新尝试获取这个锁.

本文深入探讨Java多线程核心技术,包括线程安全、synchronized关键字、wait和notify方法、死锁预防、线程安全的单例模式及其实现、阻塞队列的应用和生产者-消费者模型、自定义定时器的设计与实现、线程池的原理和使用等。
最低0.47元/天 解锁文章
393





