发布于2021-05-30 19:26 阅读(934) 评论(0) 点赞(2) 收藏(1)
线程安全行:
线程安全性包括三个方面,①可见性,②原子性,③ 有序性。
volatile特性
参考文章: volatile关键字
volatile 保证线程可见性案例:使用Volatile关键字的案例分析
源码分析文章参考:java同步系列之volatile解析
通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
二者对比
使用场景:
对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果原子性的;
例:volatile int i = 0;
并且大量线程调用i
的自增操作,那么 volatile 可以保证变量的安全吗?
不可以保证!,volatile不能保证变量操作的原子性!
自增操作包括三个步骤,分别是:读取,加一,写入,由于这三个子操作的原子性不能被保证,那么n个线程总共调用n次i++
的操作后,最后的i
的值并不是大家想的n,而是一个比n小的数!
解释:
i
的初始值0
,然后就被阻塞了!i
的初始值0
,执行自增操作,此时i
的值为1
0
执行加1
与写入操作,执行成功后,i
的值被写成1
了!2
,可是输出的是1
,输出比预期小!代码实例:
public class VolatileTest {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String args[]) throws InterruptedException {
List<Thread> threadList = new ArrayList<>();
VolatileTest test = new VolatileTest();
for (int j = 0; j < 10000; j++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.increase();
}
});
thread.start();
threadList.add(thread);
}
// 等待所有线程执行完毕
for (Thread thread : threadList) {
thread.join();
}
System.out.print(test.i);// 输出9995
}
}
总结:
volatile不需要加锁,因此不会造成线程的阻塞,而且比synchronized更轻量级,而synchronized可能导致线程的阻塞!volatile由于禁止了指令重排,所以JVM相关的优化没了,效率会偏弱!
JAVA内存模型简称 JMM:
JMM规定所有的变量存在在主内存,每个线程有自己的工作内存,线程对变量的操作都在工作内存中进行,
不能直接对主内存就行操作。
使用volatile修饰变量,每次读取前必须从主内存属性最新的值,每次写入需要立刻写到主内存中,
volatile关键字修修饰的变量随时看到的自己的最新值,假如线程1对变量v进行修改,那么线程2是可以马上看见!
JVM在编译java代码或者CPU执行JVM字节码时,对现有的指令进行重新排序,主要目的是为了优化运行效率(不改变程序结果的前提)
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的
扩展:现行发生原则happens-before(了解即可~)
volatile 的内存可见性就体现了先行发生原则!
int num = 1; // 原子操作
num++; // 非原子操作,从主内存读取num到线程工作内存,进行+1,再把num写回到主内存,
// 除非用原子类:即,java.util.concurrent.atomic里的原子变量类
// 解决办法是可以用synchronized 或 Lock(比如ReentrantLock) 来把这个多步操作“变成”原子操作
// 这里不能使用volatile,前面有说到:对变量的写操作不依赖当前值,如多线程下执行a++,是无法通过volatile保证结果原子性的
public class XdTest {
// 方式1:使用原子类
// AtomicInteger num = 0;// 这种方式的话++操作就可以保证原子性了,而不需要再加锁了
private int num = 0;
// 方式2:使用lock,每个对象都是有锁,只有获得这个锁才可以进行对应的操作
Lock lock = new ReentrantLock();
public void add1(){
lock.lock();
try {
num++;
}finally {
lock.unlock();
}
}
// 方式3:使用synchronized,和上述是一个操作,这个是保证方法被锁住而已,上述的是代码块被锁住
public synchronized void add2(){
num++;
}
}
解决核心思想:把一个方法或者代码块看做一个整体,保证是一个不可分割的整体!
int a = 3; // step:1
int b = 4; // step:2
int c =5; // step:3
int h = a*b*c; // step:4
定义顺序: 1,2,3,4
计算顺序: 1,3,2,4 和 2,1,3,4 结果都是一样的(单线程情况下)
指令重排序可以提高执行效率,但是多线程上可能会影响结果!
假如下面的场景:
// 线程1
before();// 处理初始化工作,处理完成后才可以正式运行下面的run方法
flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
// 线程2
while(flag){
run(); // 执行核心业务代码
}
// -----------------指令重排序后,导致顺序换了,程序出现问题,且难排查-----------------
// 线程1
flag = true; // 标记资源处理好了,如果资源没处理好,此时程序就可能出现问题
// 线程2
while(flag){
run(); // 执行核心业务代码
}
before();// 处理初始化工作,处理完成后才可以正式运行下面的run方法
// 线程 A 执行
int num = 0;
// 线程 A 执行
num++;
// 线程 B 执行
System.out.print("num的值:" + num);
线程A执行 i++ 后再执行线程B,线程B可能有2个结果,可能是0和1。
因为i++
在线程A中执行运算,并没有立刻更新到主内存当中,而线程B就去主内存当中读取并打印,此时打印的就是0
;也可能线程A执行完成更新到主内存了,线程B的值是1
。
所以需要保证线程的可见性:
synchronized、lock 和 volatile 都能够保证线程可见性。
volatile 保证线程可见性案例:使用Volatile关键字的案例分析
独享锁,是指锁一次只能被一个线程持有。
共享锁,是指锁一次可以被多个线程持有。
ReentrantLock和synchronized都是独享锁,ReadWriteLock的读锁是共享锁,写锁是独享锁。
与独享锁/共享锁的概念差不多,是独享锁/共享锁的具体实现。
ReentrantLock和synchronized都是互斥锁,ReadWriteLock是读写锁
下面三种是Jvm为了提高锁的获取与释放效率而做的优化 针对Synchronized的锁升级,锁的状态是通过对象监视器在对象头中的字段来表明,是不可逆的过程
源码分析文章参考:java同步系列之synchronized解析
synchronized
是解决线程安全的问题,常用在同步普通方法、静态方法、代码块中使用!synchronized
非公平、可重入锁!这里推荐大家看一下这篇文章:Java并发基石CAS原理以及ABA问题,从简单 CAS的入门使用到分析原理!
synchronized
:
ReentrantLock
:
true
创建公平锁,如果传入的是false或没传参数则创建的是非公平锁state
和 FIFO 队列来控制加锁。synchronized
关键字可以保证原子性)synchronized
、volatile
关键字都可以保证可见性)Java 中解决线程安全的方式:
synchronized
关键字、Lock 锁,可以解决原子性问题。synchronized
关键字、volatile
关键字、Lock 锁,可以解决可见性问题。volatile
关键字修饰的变量,可以禁用指令重排,禁止的是加volatile
关键字变量之前的代码重排序,保证有序性问题。多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
可以通过thread.setDaemon(true)
方式将一个线程设置为守护线程。
注意①:必须在thread.start()
之前设置,否则会跑出一个 IllegalThreadStateException
异常。不能把正在运行的常规线程设置为守护线程。
注意②:由于守护线程的终止是自身无法控制的,因此千万不要把 IO、File 等重要操作逻辑分配给它;因为这些操作会随时可能抛出异常,守护线程也会随之结束!
总结的面试题也挺费时间的,文章会不定时更新,有时候一天多更新几篇,如果帮助您复习巩固了知识点,还请三连支持一下,后续会亿点点的更新!
为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 870mm x 560mm
,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,有兴趣的小伙伴可以了解一下,当然,不管怎样博主的文章一直都是免费的~
原文链接:https://blog.csdn.net/weixin_43591980/article/details/117396827
作者:bbjbbh
链接:http://www.phpheidong.com/blog/article/86795/23f27e903b30f2a4a6cf/
来源:php黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 php黑洞网 All Rights Reserved 版权所有,并保留所有权利。 京ICP备18063182号-4
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!