想进大厂?这些线程安全的知识你应该知道
线程安全的概念
官方: 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
我的理解:如果多线程环境下代码运行的结果符合我们预期的,即在单线程环境应该的结果,则说这个线程是线程安全的。
线程不安全的原因
在多线程的运行环境修改共享资源,可能会导致各种问题,最终导致线程不安全,那么都有那些问题呢?
1、原子性
不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。这个现象也叫同步互斥,表示操作是相互排斥的。
一条Java语句不一定是原子的,也不一定只是一条指令
例如n++,其实是由三步操作组成的
1、从内存把数据读到CPU
2、进行数据更新
3、把数据写回到CPU
如果不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量操作,中途其他线程插入进来,如果这个操作被打断了,结果可能就会出现错误
补充:这点也和线程的抢占式调度密切相关。
2、可见性
可见性指,一个线程对共享变量值的修改,能够及时被其他线程看到
Java内存模型:Java虚拟机规范中定义了Java内存模型。
目的是屏蔽硬件和操作系统的内存访问差异,以便Java程序在各种平台下都能达到一致的并发效果。
①线程之间共享变量存在 主内存中
②每一个线程都有自己的 “工作内存”
③当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
④当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,在同步回主内存。
注意:如果某一个线程的工作内存进行了修改,发送了调度没有及时同步到主内存,其他线程可能会进行修改然后同步到主内存,最后第一个线程回来再同步时,结果就出现了错误。
3、重排序
例如:
1.出门买可乐
2.回宿舍睡觉
3.出门买薯片
如果在单线程情况下,JVM,VPU指令***对其进行优化,如果按1->3->2方式执行,就可以让代码得到一定的优化。
编译器对于重排序的前提是“保持逻辑不发生变化”,但是在多线程环境下就不太好保证,多线程代码执行复杂度更高,编译器很难再编译阶段的执行效果进行预测,因此激进的重排序很同意导致优化后的逻辑与之前不等价
解决线程安全的方法
1、使用synchronized锁
①互斥功能:当某个线程执行到某个对象的synchronized时,多个线程如果也执行到同一个对象synchronized就会阻塞等待。
保证了临界区的原子性
②synchronized语法:
synchronized(lock)//针对当前对象上锁
//进入代码块,相当于加锁
{
执行操作
}
//退出代码块,相当于解锁
synchronized修饰普通方法则被视为对“当前对象”加锁
//锁的SynchronizeDemo对象
public class SynchronizeDemo{
public synchronized void methond(){
}
}
synchronized修饰静态方法则被视为对对象的类“”加锁
//锁的SynchronizeDemo类的对象
public class SynchronizeDemo{
public synchronized static void methond(){
}
}
synchronized修饰代码块则被视为对“指定对象”加锁
//锁的当前对象
public class SynchronizeDemo{
public void methond(){
synchronized(this){
}
}
}
③synchronized工作流程
获得互斥锁
从主内存拷贝变量的最新副本到工作内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
所以synchronized在有限程度上保证了内存的可见性
④可重入
synchronized同步块对同意条线程来说是可重入的,不会出现自己把自己锁死的问题;
在可重入锁内部,包含“线程持有者”和“计数器”两个信息
如果某个线程加锁的时候,发现所以就被人占用,但是恰好占用的是自己,那么仍然可以加锁,并让计数器自增。
解锁的时候计数器会递减,为0的时候才真正释放锁(才能被别的线程获取)
“不可重入锁”:一个线程加锁后没有释放锁,然后由尝试再次加锁,需要等待第一次的锁释放后才能进行加锁,如果第一次的线程无法解锁,那么就会产生死锁现象。
2、使用Lock锁
基本语法
Lock lock = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.ublock();
synchronized锁和Lock锁的区别
synchronized锁 Lock锁
synchronized是自动锁
需要手动解锁
可以在一个方法加锁,再到另一个方法解锁
只有一种类型的锁可重入锁 公平锁/非公平锁、读写锁/独占锁、可重入锁/不可重入锁
一直请求锁 1、一直请求锁 2、带中断 3、尝试请求 4、带超时的尝试
volatile关键字
volatile修饰的变量,能够保证"内存可见性"但是不能保证原子性。
代码在写入violatile修饰的变量时
改变线程工作内存中volatile变量副本的值
将改变后副本的值从工作内存刷新到主内存
代码在读取violatile修饰的变量时
从主内存中读取vola变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
代码示例
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的变化
//解决策略
static class Counter {
public volatile int flag = 0;
}
// 执行效果 // 当用户输入非0值时, t1 线程循环能够立即结束.
#Java开发#
官方: 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。 多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他操作,调用这个对象的行为都可以获得正确的结果,那么这个对象就是线程安全的。
我的理解:如果多线程环境下代码运行的结果符合我们预期的,即在单线程环境应该的结果,则说这个线程是线程安全的。
线程不安全的原因
在多线程的运行环境修改共享资源,可能会导致各种问题,最终导致线程不安全,那么都有那些问题呢?
1、原子性
不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。这个现象也叫同步互斥,表示操作是相互排斥的。
一条Java语句不一定是原子的,也不一定只是一条指令
例如n++,其实是由三步操作组成的
1、从内存把数据读到CPU
2、进行数据更新
3、把数据写回到CPU
如果不保证原子性会给多线程带来什么问题?
如果一个线程正在对一个变量操作,中途其他线程插入进来,如果这个操作被打断了,结果可能就会出现错误
补充:这点也和线程的抢占式调度密切相关。
2、可见性
可见性指,一个线程对共享变量值的修改,能够及时被其他线程看到
Java内存模型:Java虚拟机规范中定义了Java内存模型。
目的是屏蔽硬件和操作系统的内存访问差异,以便Java程序在各种平台下都能达到一致的并发效果。
①线程之间共享变量存在 主内存中
②每一个线程都有自己的 “工作内存”
③当线程要读取一个共享变量的时候,会先把变量从主内存拷贝到工作内存,再从工作内存读取数据
④当线程要修改一个共享变量的时候,也会先修改工作内存中的副本,在同步回主内存。
注意:如果某一个线程的工作内存进行了修改,发送了调度没有及时同步到主内存,其他线程可能会进行修改然后同步到主内存,最后第一个线程回来再同步时,结果就出现了错误。
3、重排序
例如:
1.出门买可乐
2.回宿舍睡觉
3.出门买薯片
如果在单线程情况下,JVM,VPU指令***对其进行优化,如果按1->3->2方式执行,就可以让代码得到一定的优化。
编译器对于重排序的前提是“保持逻辑不发生变化”,但是在多线程环境下就不太好保证,多线程代码执行复杂度更高,编译器很难再编译阶段的执行效果进行预测,因此激进的重排序很同意导致优化后的逻辑与之前不等价
解决线程安全的方法
1、使用synchronized锁
①互斥功能:当某个线程执行到某个对象的synchronized时,多个线程如果也执行到同一个对象synchronized就会阻塞等待。
保证了临界区的原子性
②synchronized语法:
synchronized(lock)//针对当前对象上锁
//进入代码块,相当于加锁
{
执行操作
}
//退出代码块,相当于解锁
synchronized修饰普通方法则被视为对“当前对象”加锁
//锁的SynchronizeDemo对象
public class SynchronizeDemo{
public synchronized void methond(){
}
}
synchronized修饰静态方法则被视为对对象的类“”加锁
//锁的SynchronizeDemo类的对象
public class SynchronizeDemo{
public synchronized static void methond(){
}
}
synchronized修饰代码块则被视为对“指定对象”加锁
//锁的当前对象
public class SynchronizeDemo{
public void methond(){
synchronized(this){
}
}
}
③synchronized工作流程
获得互斥锁
从主内存拷贝变量的最新副本到工作内存
执行代码
将更改后的共享变量的值刷新到主内存
释放互斥锁
所以synchronized在有限程度上保证了内存的可见性
④可重入
synchronized同步块对同意条线程来说是可重入的,不会出现自己把自己锁死的问题;
在可重入锁内部,包含“线程持有者”和“计数器”两个信息
如果某个线程加锁的时候,发现所以就被人占用,但是恰好占用的是自己,那么仍然可以加锁,并让计数器自增。
解锁的时候计数器会递减,为0的时候才真正释放锁(才能被别的线程获取)
“不可重入锁”:一个线程加锁后没有释放锁,然后由尝试再次加锁,需要等待第一次的锁释放后才能进行加锁,如果第一次的线程无法解锁,那么就会产生死锁现象。
2、使用Lock锁
基本语法
Lock lock = new ReentrantLock();
lock.lock();
try{
//可能会出现线程安全的操作
}finally{
//一定在finally中释放锁
//也不能把获取锁在try中进行,因为有可能在获取锁的时候抛出异常
lock.ublock();
synchronized锁和Lock锁的区别
synchronized锁 Lock锁
synchronized是自动锁
需要手动解锁
可以在一个方法加锁,再到另一个方法解锁
只有一种类型的锁可重入锁 公平锁/非公平锁、读写锁/独占锁、可重入锁/不可重入锁
一直请求锁 1、一直请求锁 2、带中断 3、尝试请求 4、带超时的尝试
volatile关键字
volatile修饰的变量,能够保证"内存可见性"但是不能保证原子性。
代码在写入violatile修饰的变量时
改变线程工作内存中volatile变量副本的值
将改变后副本的值从工作内存刷新到主内存
代码在读取violatile修饰的变量时
从主内存中读取vola变量的最新值到线程的工作内存中
从工作内存中读取volatile变量的副本
代码示例
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的变化
//解决策略
static class Counter {
public volatile int flag = 0;
}
// 执行效果 // 当用户输入非0值时, t1 线程循环能够立即结束.
#Java开发#