
🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔🦾🦾🦾
目录
2.1 引入关键字synchronized解决线程不安全问题
总结1:认识同步监视器(锁) ----- synchronized(同步监视器){ }
🦕在多线程环境下如果说代码运行的结果是符合我们预期的,即该代码在单线程中运行得到的结果,那么就说这个程序是线程安全的,否则就是线程不安全的.下面带大家仔细给大家讲解一下线程不安全问题!
1)抢占式执行,调度过程随机(也是万恶之源,无法解决)
2)多个线程同时修改同一个变量(可以适当调整代码结构,避免这种情况)
3)针对变量的操作,不是原子的(加锁,synchronized)
4)内存可见性,一个线程频繁读,一个线程写(使用volatile)
5)指令重排序(使用synchronized加锁或者volatile禁止指令重排序)
案例引入
我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的!
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁,A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令(例如++操作,内部三条指令构成)
原子性是指一个操作或者多个操作,要么全部执行,并且执行的过程不会被任何因素打断,要么就都不执行。
一组操作(一行或多行代码)是不可拆分的最小执行单位,就表示这组操作是具有原子性的
多个线程多次的并发并行的对一个共享变量操作时,该操作就不具有原子性
在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
上面一句话虽然看起来简单,但是理解起来并不是那么容易。看下面一个例子:
x = 10; //语句1
y = x; //语句2
x++; //语句3
x = x + 1; //语句4
注意:其实只有语句1是原子性操作,其他三个语句都不是原子性操作。
也就是说,只有简单的读取、赋值(而且必须是将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。
由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
多个线程工作的时候都是在自己的工作内存中来执行操作的,线程之间是不可见的
1. 线程之间的共享变量存在主内存(实际内存)
2. 每一个线程都有自己的工作内存(CPU寄存器+缓存)
3. 线程读取共享变量时,先把变量从主存拷贝到工作内存,再从工作内存读取数据
4. 线程修改共享变量时,先修改工作内存中的变量值,再同步到主内存
注意:
(1)线程对共享变量的所有操作都必须在自己的工作内存中进行,不能绕过工作内存直接从主内存中读写变量
(2)不同线程之间无法直接访问其他线程工作内存中的变量,线程之间变量值的传递需要通过主内存来完成
此时引入了两个问题
为啥要整这么多内存?
为啥要这么麻烦的拷来拷去?
1) 为啥整这么多内存?
实际并没有这么多 "内存". 这只是 Java 规范中的一个术语, 是属于 "抽象" 的叫法.
所谓的 "主内存" 才是真正硬件角度的 "内存". 而所谓的 "工作内存", 则是指 CPU 的寄存器和高速缓存
2) 为啥要这么麻烦的拷来拷去?
因为 CPU 访问自身寄存器的速度以及高速缓存的速度, 远远超过访问内存的速度(快了 3 - 4 个数量级, 也就是几千倍, 上万倍).
比如某个代码中要连续 10 次读取某个变量的值, 如果 10 次都从内存读, 速度是很慢的. 但是如果只是第一次从内存读, 读到的结果缓存到 CPU 的某个寄存器中, 那么后 9 次读数据就不必直接访问内存了. 效率就大大提高了.
那么接下来问题又来了, 既然访问寄存器速度这么快, 还要内存干啥??
答案就是一个字: 贵
值的一提的是, 快和慢都是相对的. CPU 访问寄存器速度远远快于内存, 但是内存的访问速度又远远快于硬盘.对应的, CPU 的价格最贵, 内存次之, 硬盘最便宜
线程1对共享变量的修改要想被线程2及时看到,必须要经过如下的2个步骤
(1)把工作内存1中更新过的共享变量刷新到主内存中
(2)将主内存中最新的共享变量的值更新到工作内存2中
变量传递顺序
JVM翻译字节码指令,CPU执行机器码指令,都可能发生重排序来优化执行效率
比如有这样三步操作:(1) 去前台取U盘 (2) 去教室写作业 (3) 去前台取快递
JVM会对指令优化,也就是重排序,新的顺序为(1)(3)(2),这样就可以少跑一次前台,以此提高效率,这就叫做指令重排序.
编译器对于指令重排序的前提是 "保持逻辑不发生变化". 这一点在单线程环境下比较容易判断, 但
是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高, 编译器很难在编译阶段对代
码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价.
重排序是一个比较复杂的话题, 涉及到 CPU 以及编译器的一些底层工作原理, 此处不做过多讨论
引入count++问题
class Counter {
private int count =0;
public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter =new Counter();
Thread t1 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
运行上述代码我们会发现每次都结果是小于100000的,因为上面两个线程在实际对count进行++操作的时候并不满足原子性,导致最终的结果一直不是我们想要的,这就是由于不满足原子性所导致的线程不安全问题!!!
count++操作,本质上是有三个CPU指令构成
1.load,把内存中的数据读到CPU寄存器中
2.add,就是把寄存器中的值进行+1运算
3.save,把寄存器中的值写回到内存中

由于CPU的抢占式执行,导致两个线程同时进行count++操作的时候,内部的三个CPU指令不能完整一次性执行完,例如在第一个线程在执行的时候先读取共享变量count的值到自己的寄存器中,还没来得及修改,第二个线程获取到了CPU的执行权开始执行,此时线程2线读取共享变量到自己的工作内存(寄存器中)进行修改,最后再同步到主内存(就是更新共享变量count的值),当线程2执行完毕后,线程1再次获得CPU的执行权继续执行未完成的操作,将自己寄存器中的count进行修改再同步到主内存中,此时由于两次修改实际上只修改成功一次,这就是由于原子性引起的线程不安全问题!
修饰方法:修饰普通方法时,关键字在public前后都可,锁对象是 this,也就是谁调用谁上锁。修饰静态方法时,锁对象是类对象。
修饰代码块:修饰代码块时,显式(手动)指定锁对象。
对于构造方法来说,如果加锁,不能直接加在方法上,但是内部可以使用代码块的方法,来加锁。
代码演示
//修饰普通方法
public synchronized void doSomething(){
//...
}
//修饰代码块
public void doSomething(){
synchronized (this) {
//...
}
}
//修饰静态方法(与下面效果相同都是锁类对象)
public static synchronized void doSomething(){
//...
}
//修饰静态方法
public static void doSomething(){
synchronized (A.class) {
//...
}
}
sychronized是基于对象头加锁的,特别注意:不是对代码加锁,所说的加锁操作就是给这个对象的对象头里设置了一个标志位
一个对象在同一时间只能有一个线程获取到该对象的锁
sychronized保证了原子性,可见性,有序性(这里的有序不是指指令重排序,而是具有相同锁的代码块按照获取锁的顺序执行)
(2.1) 互斥性
synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到同一个对象 synchronized 就会阻塞等待
进入 synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
下面图加深理解:

阻塞等待:
针对每一把锁, 操作系统内部都维护了一个等待队列. 当这个锁被某个线程占有的时候,其他线程尝试进行加锁, 就加不上了,就会阻塞等待, 一直等到之前的线程解锁之后, 由操作系统唤醒一个新的线程, 再来获取到这个锁!
(2.2) 刷新主存
synchronized锁住共享变量时的工作流程:
🐳获得互斥锁
🐳从主存拷贝最新的变量到工作内存
🐳对变量执行操作
🐳将修改后的共享变量的值刷新到主存
🐳释放互斥锁
(2.3) 可重入性
synchronized是可重入锁
同一个线程可以多次申请成功一个对象锁
在可重入锁的内部, 包含了 "线程持有者" 和 "计数器" 两个信息.
如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
到锁, 并让计数器自增.
解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到)
可重入锁的意义就是降低程序员负担(使用成本来提高开发效率),代价就是程序的开销增大(维护锁属于哪个线程,并且加减计数,降低了运行效率)
如下图:

class Counter {
private int count =0;
synchronized public void add() {
count++;
}
public int getCount() {
return count;
}
}
public class ThreadDemo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter =new Counter();
Thread t1 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 =new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount());
}
}
🐳必须是引用数据类型,不能是基本数据类型
🐳也可以创建一个专门的同步监视器,没有任何业务含义
🐳一般使用共享资源做同步监视器即可
🐳在同步代码块中不能改变同步监视器对象的引用🐳尽量不要String和包装类Integer做同步监视器
🐳建议使用final修饰同步监视器

1)第一个线程来到同步代码块,发现同步监视器open状态,需要close,然后执行其中的代码
2)第一个线程执行过程中,发生了线程切换(处于阻塞就绪状态),第一个线程失去了cpu,但是没有开锁(open)
3)第二个线程获取了cpu,来到了同步代码块,发现同步监视器close状态,无法执行其中的代码,第二个线程也进入阻塞状态
4)第一个线程再次获取CPU,接着执行后续的代码;同步代码块执行完毕,释放锁open
5)第二个线程也再次获取cpu,来到了同步代码块,发现同步监视器open状态,拿到锁并且上锁,由阻塞状态进入就绪状态,再进入运行状态,重复第一个线程的处理过程(加锁)
强调:同步代码块中能发生CPU的切换吗?能!!! 但是后续的被执行的线程也无法执行同步代码块(因为锁仍旧close)
1)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
2)多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
引入一个线程不安全的场景:
当一个线程对一个变量进行读取操作,同时另一个线程针对这个变量进行修改,此时读到的值不一定是修改后的值,这是编译器在多线程环境下优化时产生了误判,从而引起了bug
代码演示:
class Sign{
public boolean flag = false;
}
public class ThreadDemo4{
public static void main(String[] args) {
Sign sign = new Sign();
Thread t1 = new Thread(()->{
while(!sign.flag){
}
System.out.println("执行完毕");
});
Thread t2 = new Thread(()->{
sign.flag = true;
});
t1.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
t2.start();
}
}
运行上述代码我们会发现,程序会一直运行,while感知不到flag的变化。原因就是,执行到线程2的时候,while一直循环跑了好多遍,flag一直是false,所以编译器对代码进行优化,默认为程序不变,不再从内存中读取flag的值,而是读取寄存器中不变的flag的值,等到线程2执行到flag变量后,尽管修改掉了内存中flag的值,但是寄存器中的flag依旧为原来的值,所以while一直感知到的flag是没变的,一直循环跑。
那么如何解决该问题呢?
用volatile来修饰变量,通过保证内存可见性来解决上述问题,每次读取用volatile修饰的变量的值,都会从主内存中读取该变量。
通俗地讲:volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最新的值刷新到主内存。这样在任何时刻,不同的线程总能看到该变量的最新值。
那么,线程修改volatile变量的过程:
(1)改变线程工作内存中volatile变量副本的值
(2)将改变后的副本的值从工作内存刷新到主内存
线程读volatile变量的值的过程:
(1)从主内存中读取volatile变量的最新值到线程的工作内存中
(2)从工作内存中读取volatile变量的副本
我们这里拿实例化一个对象举例
SomeObject s=new SomeObject(); //保证对象实例化正确
1.堆里申请内存空间,初始化为0x0
2.对象初始化工作:构造代码块,属性的定义时初始化,构造方法(这才算是一个正确对象)
3.赋值给s
volatile禁止重排序,只能1->2->3,如果1->3->2在3到2之间有线程(线程调度随机)使用对象,其对象是错的即出现问题。
能准确的表明其作用是单列模式:(这个我们后面会再讲)
单列模式分为饿汉模式(在类加载期间就进行对象实例化),懒汉模式(第一次用到时进行对象的实例化)
其懒汉模式实现如下:假如多个线程走先判断对象没有实例化,对类加锁(一个线程持有锁,但这是不知道是否实例化),所以要再判断是否实例化,没有实例化进行实例化,实例化了就返回对象,这里volatile就是要确保实例化正确。
在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',
我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?
1.postman介绍Postman一款非常流行的API调试工具。其实,开发人员用的更多。因为测试人员做接口测试会有更多选择,例如Jmeter、soapUI等。不过,对于开发过程中去调试接口,Postman确实足够的简单方便,而且功能强大。2.下载安装官网地址:https://www.postman.com/下载完成后双击安装吧,安装过程极其简单,无需任何操作3.使用教程这里以百度为例,工具使用简单,填写URL地址即可发送请求,在下方查看响应结果和响应状态码常用方法都有支持请求方法:getpostputdeleteGet、Post、Put与Delete的作用get:请求方法一般是用于数据查询,
Ⅰ软件测试基础一、软件测试基础理论1、软件测试的必要性所有的产品或者服务上线都需要测试2、测试的发展过程3、什么是软件测试找bug,发现缺陷4、测试的定义使用人工或自动的手段来运行或者测试某个系统的过程。目的在于检测它是否满足规定的需求。弄清预期结果和实际结果的差别。5、测试的目的以最小的人力、物力和时间找出软件中潜在的错误和缺陷6、测试的原则28原则:20%的主要功能要重点测(eg:支付宝的支付功能,其他功能都是次要的)80%的错误存在于20%的代码中7、测试标准8、测试的基本要求功能测试性能测试安全性测试兼容性测试易用性测试外观界面测试可靠性测试二、质量模型衡量一个优秀软件的维度①功能性功
ES一、简介1、ElasticStackES技术栈:ElasticSearch:存数据+搜索;QL;Kibana:Web可视化平台,分析。LogStash:日志收集,Log4j:产生日志;log.info(xxx)。。。。使用场景:metrics:指标监控…2、基本概念Index(索引)动词:保存(插入)名词:类似MySQL数据库,给数据Type(类型)已废弃,以前类似MySQL的表现在用索引对数据分类Document(文档)真正要保存的一个JSON数据{name:"tcx"}二、入门实战{"name":"DESKTOP-1TSVGKG","cluster_name":"elasticsear
我正在尝试使用ruby编写一个双线程客户端,一个线程从套接字读取数据并将其打印出来,另一个线程读取本地数据并将其发送到远程服务器。我发现的问题是Ruby似乎无法捕获线程内的错误,这是一个示例:#!/usr/bin/rubyThread.new{loop{$stdout.puts"hi"abc.putsefsleep1}}loop{sleep1}显然,如果我在线程外键入abc.putsef,代码将永远不会运行,因为Ruby将报告“undefinedvariableabc”。但是,如果它在一个线程内,则没有错误报告。我的问题是,如何让Ruby捕获这样的错误?或者至少,报告线程中的错误?
我正在使用ruby2.1.0我有一个json文件。例如:test.json{"item":[{"apple":1},{"banana":2}]}用YAML.load加载这个文件安全吗?YAML.load(File.read('test.json'))我正在尝试加载一个json或yaml格式的文件。 最佳答案 YAML可以加载JSONYAML.load('{"something":"test","other":4}')=>{"something"=>"test","other"=>4}JSON将无法加载YAML。JSON.load("
我是ruby的新手,我认为重新构建一个我用C#编写的简单聊天程序是个好主意。我正在使用Ruby2.0.0MRI(Matz的Ruby实现)。问题是我想在服务器运行时为简单的服务器命令提供I/O。这是从示例中获取的服务器。我添加了使用gets()获取输入的命令方法。我希望此方法在后台作为线程运行,但该线程正在阻塞另一个线程。require'socket'#Getsocketsfromstdlibserver=TCPServer.open(2000)#Sockettolistenonport2000defcommandsx=1whilex==1exitProgram=gets.chomp