什么是线程安全问题?
当多个线程共享一个全局变量,对其做写操作时,可能会受到其他线程的干扰,从而引发线程安全问题
内置锁(synchronized)
内置锁也叫互斥锁,可以保证线程的原子性,当线程进入方法时,会自动获得一个锁,一旦锁被获得,其他线程必须等待获得锁的线程执行完代码释放锁,会降低程序的执行效率
使用方式:
同步方法
public synchronized void sale() {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "票" + (100 - trainCount + 1) + "张票");
trainCount --;
}
}
// 非静态同步方法使用this锁
// 静态同步方法使用当前字节码文件
同步代码块
private Object obj = new Object();
....
public void sale() {
// 参数为任意全局对象
synchronized (obj) {
if (trainCount > 0) {
System.out.println(Thread.currentThread().getName() + "票" + (100 - trainCount + 1) + "张票");
trainCount --;
}
}
}
注意事项:
public static void main(String[] args) {
ThreadDemo1 threadDemo1 = new ThreadDemo1();
ThreadDemo1 threadDemo2 = new ThreadDemo1();
Thread t1 = new Thread(threadDemo1, "窗口1");
Thread t2 = new Thread(threadDemo2, "窗口2");
t1.start();
t2.start();
}
// 这样的话也会产生线程安全问题
// 那是因为两个线程分别由不同的线程创建的,它们之间的变量不共享,产生了两把不同的锁
// 解决方法是在全局变量上加上 static 关键字,静态变量存在方法区,这个类中的所有对象都共享同一个变量
重入锁和不可重入锁
重入锁:即获得锁的线程可以进入它拥有的锁的同步代码块
不可重入锁:即获得锁的线程,在方法中尝试再次获得锁时,获取不到进入阻塞状态
死锁产生的原因
同步中嵌套同步,同步锁是一个重入锁,就很有可能发生死锁
ThreadLocal
为每个线程提供局部变量,解决线程安全问题
ThreadLocal 底层采用 Map 来实现,将当前线程作为key,将值存储到这个 map 中
class Res {
private Integer count = 0;
ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public Integer getCount() {
int count = integerThreadLocal.get() + 1;
integerThreadLocal.set(count);
return count;
}
}
多线性特性
什么是原子性?
即一个或一组操作,要么全部执行,执行过程中不会被其他线程打断,要么全部不执行
什么是可见性?
多线程操作中一个线程修改了全局共享变量的值,其他线程能立马得到修改后的值
什么是有序性?
程序执行的顺序按照代码的先后顺序执行,一般来说处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的
Java 内存模型(JMM)
JMM 决定一个线程对一个共享变量做写操作时,能否对另一个线程可见
主内存:共享变量
本地内存:共享变量副本
多线程做修改操作时,首先从主内存中拷贝一份副本到本地内存中,当线程修改本地内存的值后,首先在本地内存修改成功,然后再将修改后的结果刷新到主内存中
Volatile
可见性就是说一旦某个线程修改了被 Volatile 修饰的变量,其他线程能立马获取到修改后的新值,在 Java 中为了加快程序的运行效率,对一些变量的操作通常是在该线程的寄存器或者 CPU 缓存中进行的,之后才会同步到主存,而加了 Volatile 关键字后会直接读写内存
注意:虽然该关键字能够保证可见性,但不能保证原子性
特性:
- 保证可见性
- 禁止指令重排(CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理),使用Volatile 修饰的变量,赋值后多执行了一个"load addl $0x0, (%esp)"操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障。
Volatile 与 Synchronized 区别:
Volatile 虽然能保证可见性,但不能保证原子性
Synchronized 防止多个线程执行同一块代码,影响执行效率,就性能而言,Volatile 是高于 Synchronized 的。
但是 Volatile 是不能取代 Synchronized 的,因为 Volatile 不能保证原子性。
重排序
数据依赖
如果两个操作同时操作一份变量,且这两个操作其中有一个写操作,此时这两个操作之间就存在数据依赖
名称 | 代码示例 | 说明 |
---|---|---|
写后读 | a = 1;b = a; | 写一个变量之后,再读这个位置。 |
写后写 | a = 1;a = 2; | 写一个变量之后,再写这个变量。 |
读后写 | a = b;b = 1; | 读一个变量之后,再写这个变量 |
上面三种情况,只要更改操作的执行顺序,结果就会发生改变,编译器和处理器可能会做重排序,在做重排序时会尊徐数据依赖,所以编译器和处理器不能对数据依赖的操作重排序,这里的数据依赖性仅针对单个处理器中执行的指令序列和单线程的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑
as-if-serial语义
不管怎么重排,结果都不能改变
对多线程的影响
在单线程程序中,对存在数据依赖的操作做重排序不会影响结果,因为单线程尊徐 as-if-serial 语义,但 as-if-serial 语义提到"不考虑不同处理器之间和不同线程之间的数据依赖",所以,指令重排可能会对多线程的结果产生影响