前言:
大家好,我是良辰丫,这篇文章我将与大家一同去学习多线程中锁的知识点,认识线程安全问题,不多说,我们往下看.💞💞💞
🧑个人主页:良辰针不戳
📖所属专栏:javaEE初阶
🍎励志语句:生活也许会让我们遍体鳞伤,但最终这些伤口会成为我们一辈子的财富。
💦期待大家三连,关注,点赞,收藏。
💌作者能力有限,可能也会出错,欢迎大家指正。
💞愿与君为伴,共探Java汪洋大海。

目录
所谓的线程不安全可以认为是我们口中经常说的bug,本质上是因为线程之间的调度顺序不确定导致.
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}

我们搞了两个线程,去执行count++,我们理想的结果是让count最终的值等于10000,然而呢,我们惊讶的发现,count最终的值并不是我们想要的,结果不符合我们预料的,此时我们就认为这种情况下线程不安全.
举个简单的例子 : 就拿我们生活中的软件抢票,如果出现了两个人抢到了同一张票,并且在后台显示自己购票成功,这就是所谓的bug,线程不安全.
接下来我们就来解析一下count++这个操作.
我们肉眼看到的count++,其实分为三部分.
- load:把内存中的数据读到CPU寄存器中.
- add:寄存器中的值进行+1操作
- save:把寄存器的值写入内存中.
由于多个线程是并发执行的,那么可能t1线程的count++操作与t2的count++操作中的三部分混合执行,并且由于线程调度具有随机性,那么谁也无法判断会出现怎样的结果,往往来说,每次执行代码会有不同的结果.
线程调度具有随机性,每个线程都会抢着去执行某次调度,这就会导致各种各样的不确定情况,线程与线程之间穿插进行
- 一个线程
修改一个变量是安全的.- 多个线程
修改不同的变量是安全的.- 多个线程
读取同一个变量是安全的.- 多个线程修改同一个变量是不安全的.
上述操作的count++可以分成三部分,那么它就不具有原子性.
- 内存可见性引起线程不安全.
- 指令重排序引起线程不安全.
synchronized为java加锁的关键字,那么加锁到底是什么呢?加锁是为了保证原子性.比如上述的count++,我们稍作一下修改,就会有意外的惊喜,废话不多说,我们上代码.
public class Test26 {
public static int count = 0;
public void add(){
synchronized (this){
count++;
}
}
public static void main(String[] args) throws InterruptedException {
Test26 test = new Test26();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
}

我们惊奇的发现,结果与我们预期结果相同了,是不是很神奇呢?然后我们简单分析一下.
- 我们把count++的功能封装成一个方法.
- 对方法内的count++进行加锁,这时count++的三部分功能就相当于装到一个上锁的匣子里.
- 我们有两个线程,如果t1获取了锁对象,t1目前就有权利去执行完整的count++操作,此时t2线程没有权利获取锁对象,只能处于阻塞状态,只有当t1线程用完count++操作,释放了锁的时候,t2线程才有机会去获取锁对象.
接下来我们来认识一下synchronized的三种使用场所
1. 修饰实例方法
public class Test27 {
public static int count = 0;
public synchronized void add(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Test27 test = new Test27();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
}
修饰实例方法,用于当前实例加锁,进入同步代码要获得当前实例的锁.修饰对象中的实例方法.
2. 修饰静态方法
public static int count = 0;
public static synchronized void add(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Test27 test = new Test27();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前实例的锁.作用于静态方法,锁是当前class对象.
3. 修饰代码块
public class Test32 {
Test32 test = new Test32();
public static int count = 0;
public static synchronized void add(){
count++;
}
public static void main(String[] args) throws InterruptedException {
Test27 test = new Test27();
Thread t1 = new Thread(()->{
synchronized (test){
for (int i = 0; i < 5000; i++) {
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
test.add();
}
});
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println(count);
}
}
修饰代码块,指定加锁对象,进入同步代码块要获得指定对象的锁.
注意:
加锁需要有锁对象,下面代码中locker就是锁对象,写啥都行,但是不能写内置类型(基本类型)
public Object locker = new Object();
public void add(){
synchronized(locker){
count++;
}
}
public class Test30 {
//测试内存可见性
public static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (flag == 0){
}
System.out.println("t线程结束");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = sc.nextInt();
});
t.start();
Thread.sleep(1000);
t2.start();
}
}
预期效果,t通过flag等于0作为条件进行循环,t2通过控制台输入一个整数,一旦输入了非0的值,此时t线程就会结束.然而呢?实际效果
可能会出现输入非0值后t线程没有结束.(以前编译器会有这样的内存可见性问题,后面编译器可能做了修正,不一定会出现这样的问题),我们拿出来知识做简单讨论,在这里,我们不得不引入一个新的概念,内存可见性.
在上述操作中涉及两部分
- load:从内存读数据到CPU寄存器
- cmp 比较寄存器里的值是否为0
(load的时间开销远远大于cmp)
编译器发现,load开销很大,而每次load的结果都一样,这个时候编译器就做了一个大胆的操作,把load操作优化掉,只有在第一次执行load才真正执行了,后序循环都只cmp,不load(相当于是复用之前寄存器中load过的值)
简单介绍一下编译器优化
编译器优化是一个非常普遍的事情,开发JVM和编译器的程序员都是大佬中的大佬,你可以想一下,你会开车,你和会造车的人想比,你相差很多哈哈.
编译器优化就是能智能的调整你的代码的执行逻辑,保证程序结果不变的前提下,通过加减语句,通过语句变换,用过一系列的操作,让整个程序的执行效率大大提高.
- 我们可以选择开启编译器优化,也可以关闭编译器优化.
- 编译器优化对于单线程来说往往是非常准确的,但是对于多线程会出现误判,因为多线程调度具有随机性.
此时关键字volatile,可以很好的禁止优化,能够保证每次都是从内存上读取数据.
还有一种方式是sleep,因为sleep让执行速度变慢了,当循环次数速度变慢了,此时的load操作,就不再是负担了,编译器也没必要优化了.
public class Test30 {
//测试内存可见性,下面加了volatile关键字
volatile public static int flag = 0;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (flag == 0){
}
System.out.println("t线程结束");
});
Thread t2 = new Thread(()->{
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数:");
flag = sc.nextInt();
});
t.start();
t2.start();
}
}
注意:
volatile适合的场所是单线程,不保证原子性,它适合于一个线程读一个线程写,而synchronized是适合于多个线程写.volatile也能禁止指令重排序.
所谓内存可见性,t1频繁读取主内存,效率比较低,就被优化成自己的工作内存.t2修改了主内存的结果,由于t1没有读主内存,导致修改不能被识别到.
- 工作内存:CPU寄存器
- 主内存:内存
简述一下为什么Java官方使用工作内存这样的术语呢?
- java是跨平台的,兼容多种操作系统,兼容多种硬件设备,尤其是cpu,不同的硬件设备差别会很大,CPU与CPU之间也会有很大的差别.
- 以前的CPU只有寄存器,现在的CPU还有缓存,而且CPU缓存还有好几个,L1,L2等,目前常见的CPU都是3级缓存.
- 工作内存准确来说,代表CPU寄存器+缓存(CPU内部存储数据的空间)
线程的调度是随机的(无序的),但是也有一定的需求场景希望线程有序执行.wait就是让某个线程暂停下来等一等,notify就是把该线程唤醒,能够继续执行剩余的任务.
A,B,C去银行卡取存钱,但是只有一个取钱窗口,A优先抢到窗口,取钱的时候发现窗口没钱,没钱难道一直等下去嘛,或者出来明知道没钱还进去取嘛;如果B进去存钱了,此时窗口就有钱了,此时A就可以去取钱了.
像咱们以前学过的join局限性太大了,一个线程结束后别的线程才有机会,而结束的线程将不再苏醒.在这里wait与notify就很好的帮助解决上述问题,A发现没钱后进入线程阻塞,直到B存进去钱,A线程将被唤醒,此时A又可以进去取钱.
wait与notify是Object方法,只要你是一个类对象(不是内置类型/不是基本类型),都可以使用.
主要做三件事情(wait必须写到synchronized代码块里面,加锁的对象必须和wait的对象是同一个)
- 解锁.
- 阻塞等待
- 收到通知的时候,就唤醒,同时尝试获取锁.
public static void main(String[] args) throws InterruptedException {
Object locker = new Object();
Thread t1 = new Thread(() -> {
try {
System.out.println("wait 开始");
synchronized (locker) {
locker.wait();
}
System.out.println("wait 结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
synchronized (locker) {
System.out.println("notify 开始");
locker.notify();
System.out.println("notify 结束");
}
});
t2.start();
}

- t1先执行,执行到wait,进入阻塞状态.
- t2执行后,执行到notify,就会通知t1唤醒(notify是在synchronized内部就需要t2释放锁,t1才能继续往下走.)
注意:
notify也要放到synchronized中使用
- 必须先执行wait,然后再notify,才有效果.
- 如果没有wait就notify,此时notify不发挥任何作用,没有额外的副作用,但是代码的概念不能正确执行了.
可以有多个线程等待同一个对象,比如好几个线程都执行wait进入阻塞状态,如果调用了object.notifyAll就会把所有阻塞的线程唤醒,此时那些线程重新竞争锁对象.
①wait解决的是线程之间的顺序控制,sleep是让线程休眠一会
②sleep是Thread类的方法,wait是Object类的一个方法
③wait需要配合锁synchronized使用,调用wait后,线程进入阻塞状态,需要等待notify进行唤醒,而sleep与锁无关.
后序:
关于多线程线程安全的文章到这里就结束了,面试重点,synchronized锁,volatile,wait,希望小小的文章可以帮到大家.💌💌💌
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po
尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub
我正在编写一个小脚本来定位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
我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search
由于fast-stemmer的问题,我很难安装我想要的任何rubygem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=
我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("
当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub
我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
在Ruby中是否有Gem或安全删除文件的方法?我想避免系统上可能不存在的外部程序。“安全删除”指的是覆盖文件内容。 最佳答案 如果您使用的是*nix,一个很好的方法是使用exec/open3/open4调用shred:`shred-fxuz#{filename}`http://www.gnu.org/s/coreutils/manual/html_node/shred-invocation.html检查这个类似的帖子:Writingafileshredderinpythonorruby?