您的位置:首页 > 教育 > 培训 > 用手机制作游戏的app软件_百度网站名称和网址_产品营销方案案例范文_浏览器下载大全

用手机制作游戏的app软件_百度网站名称和网址_产品营销方案案例范文_浏览器下载大全

2025/4/2 4:21:13 来源:https://blog.csdn.net/Zz_waiting/article/details/146703565  浏览:    关键词:用手机制作游戏的app软件_百度网站名称和网址_产品营销方案案例范文_浏览器下载大全
用手机制作游戏的app软件_百度网站名称和网址_产品营销方案案例范文_浏览器下载大全

目录

观察多线程的不安全

线程安全的概念

线程不安全的原因

解决线程不安全问题

synchronized 关键字 - 监视器锁 monitor lock

synchronized 的特性:


观察多线程的不安全

如下代码,让 count 在两个线程中分别自增 5000次

我们预期结果应该是 100000 ,但打印的结果却和我们的预期不相同,,并且我们每次运行程序,结果都不相同,这就出现了 bug。

线程安全的概念

某个代码,无论是在单个线程下执行,还是在多个线程下执行,都不会产生 bug,这个情况就成为“线程安全”。

如果这个代码,单线程下运行正确,但是在多线程下,就可能产生 bug,这个情况就称为“线程不安全”或者存在“线程安全”问题。

线程不安全的原因

上述 count++ 代码其实相当于 += 1,这个 count++ 其实是三个 CPU 指令构成的(CPU需要读指令,解析指令,执行指令,此处暂时省略读指令,解析指令的过程...)

        1. load 从内存中读取数据到 CPU 的寄存器

        2. add 把寄存器中的值 + 1

        3.save 把寄存器的值写回到内存中

过程如下:

如果是一个线程执行上述的三个指令,当然没问题,但如果是两个线程,并发的执行上述操作,此时就会存在变数。(线程之间调度的顺序是不确定的!!!)

这里一共有多少种情况呢? ==》 其实是无数种情况,完全由可能,在 t2 执行一次 ++ 的时候,t1执行两次 ++,在 t2 执行一次 ++ 的时候, t1 执行三次++.....此时的排列组合,又会有很多种,所以,这里其实是有无数种情况。

我们作为程序员,就是要保证,上述任何一种执行顺序下的代码,得到的结果都是正确的才可以。(这里有一种复联四的感觉,奇异博士,要遍历千万种时间线, 才能找到其中的一种方式打败灭霸)我们要做的是,遍历千万种时间线,确保每种时间线,都没有bug,都能打败灭霸。。。

我们要分析清楚,什么样的顺序下,执行结果是对的(即执行两次 ++,得到的结果是 2 ),什么顺序啊下,执行结果是错的(即执行两次 ++, 得到的结果不是 2 )

由于这两个线程是并行执行 / 并发执行并不确定,但是即使是并发执行,在一个 CPU 核心上,两个线程有各自的上下文(各自一套寄存器的值,并不会互相影响)

所以,最关键的问题是,确保第一个线程 save 之后,第二个线程再 load,这个时候第二个线程 load 得到的,才是第一个线程自增后的结果,否则,第二个线程 load 得到的就是第一个线程自增前的结果了,所以两次自增,可能实际上就只增加了 1 次。 

总结:

        1.【根本原因】 操作系统上的线程是“抢占式执行”“随即调度” ==》 线程之间执行的顺序带来了很多变数。

        2. 代码结构,代码中多个线程,同时修改同一个变量,有线程不安全问题

                (一个线程修改一个变量,没事)

                (多个线程读取同一个变量,没事)

                (多个线程修改不同的变量,没事)

        3.【直接原因】上述线程修改操作,本身不是“原子的”

count++ 这个操作,是包含多个 CPU 指令的,一个线程执行这些指令,执行到一半,就有可能被调度走,从而给其他线程一些“可乘之机”,应该做到的是:每个 CPU 指令,要么都是“原子的”,要么不执行,要么就执行完。

        4.内存可见性问题...

        5.指令重排序问题...

解决线程不安全问题

知道了原因,就可以对症下药了。

针对原因1,无法做出任何改变,系统内部已经实现了抢占式执行了,我们无法干预。

针对原因2,分情况,有时候,我们可以调整代码结构,但有时候,我们要就是要实现,在多个线程中,同时修改同一个变量。

针对原因3,乍一看,count++,包含 3 个指令,咱们也无从干预,但是,实际上是有办法的。可以通过特殊手段,把这三个指令打包到一起,称为“整体”。

==》 加锁!!!锁具有“排他”“互斥”这样的特性。在 JAVA 中,加锁的方式,有好多种,最主要的使用的方式是:synchronized 关键字。

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来,B 也是可以进入这个房间的,打断 A 在房间里的隐私。但我们给房间加上一把锁之后,A 进去,就把们锁上,其他人是不是就进不来了。

加锁的目的,是为了把三个操作,打包成一个原子的操作。

synchronized 关键字 - 监视器锁 monitor lock

synchronized 的特性:

1. 互斥

synchronized 会起到互斥的效果,某个线程执行到某个对象的 synchronized 中时,其他线程如果也执行到同一个对象 synchronized 就会阻塞等待。

进入 synchronized 修饰的代码块,相当于 加锁。

推出 synchronized 修饰的代码块,相当于 解锁。

2. 可重入

synchronized 同步块对同一条线程来说是可重入的,不会出现自己把自己锁死的问题。

Java 中的 synchronized 是可重入锁,因此没有上述的问题。

进行加锁的时候,需要先准备好“锁对象”,加锁解锁的操作,都是依托于这里的”锁对象“展开的。

如果一个线程,针对一个对象加上锁之后,其他线程,也尝试对这个对象加锁,就会产生锁阻塞(BLOCKED),一直阻塞到,前一个线程释放锁为止。(在 JAVA 中,任何一个对象(注意是对象 Object / String / Integer...),都可以作为 锁对象)

synchronized 代码块中,进入 { } 就会加锁(lock),出了 { } 就会解锁(unlock),synchronized 是调用系统的 API 进行加锁的。系统 API 本质上是靠 CPU 上的特定指令完成加锁。

t1 先执行 lock,加锁可以成功, t2 后执行 lock 的时候,由于 锁已经被 t1 给加上了,t2 就会阻塞等待,阻塞到 t1 线程 unlock 了之后,t2 才能获取到锁。

前面讲,加锁是把 count++ 中的三步操变成原子了,但是很明显的是,并非是枷锁之后,执行三个操作过程中,线程就不调度了,是即使加锁的线程被调度走了,其他线程也无法”插队执行“,原子其实是不够严谨的,准确说,是通过锁竞争,让第二个线程的指令无法插入到第一个线程的执行指令中间,而不是禁止第一个线程被调度出 CPU。

还有就是,加锁之后,确实会影响到多线程的执行效率,但是,即使如此,也是比一个线程串行执行要更快的多,如下代码,在 for 循环中,还有更多的指令,每次循环都需要条件判定(条件跳转指令),i++ 也是 三个指令。

而此处的代码,加锁只是给 count++ 加锁的,t1 和 t2 的 count++ 部分是”串行“执行的,但是 for 循环部分,是可以并发执行的,这样做仍然是有意义的,仍然比所有代码都在串行执行要更快。

实际开发中,往往一个线程中,要完成很多工作 1 2 3 4 5,很多工作中,只有某几个,是需要加锁的,剩下的其他都是可以并发执行的。比如 1 2 3 4 5,其中,1 2 3 5 都能够并发执行,4 需要加锁串行执行。 

 关于 Object locker = new Object(); 这里任意一个 Object 都行,是什么对象不重要,重要的是,两个线程之间,是否使用的是同一个对象,是同一个对象,就会产生竞争,不是同一个,就不会有竞争。

补充问题:

        1. 如果一个线程加锁,一个线程不加锁,是否存在线程安全问题?

会出现线程安全问题。因为这种情况下,就不会出现锁竞争了。

        2.如果两个线程,针对不同的对象加锁,也会存在线程安全问题。

        3.针对加锁操作的一些混淆的理解:

        把count 放在一个 Test对象中,通过 add 方法来进行修改,加锁的时候,锁对象写作 this

打印结果 count 仍然等于 100000,这里需要理解的是 synchronized 的 ( ) 中,是不是同一个对象, t1 线程中,t.add() 中,this 指向的是 t 对象, 在 t2 线程中,t.add() 中,this 指向的也是 t 对象,两个this 指向的是同一个对象,仍然会存在锁竞争。

如果改成如下形式呢?

先看结果:count 仍然是等于 100000 的。为什么呢?

Test.calss 其实是获取到 Test 的类对象了,在一个 Java 进程中,一个类的对象都是只有一个的!!!因此,在一个线程中 拿到的类对象,和第二个线程中拿到的类对象,是同一个对象,因此锁竞争仍然是存在的,还是可以保障线程安全的。

我们编写一段Java代码后,会保存为 .java 文件,然后经过编译后为 .class 的字节码文件,JVM 执行 .class 的时候,就要先把这个 .class 文件读取到内存中,然后才能执行(类加载)。JVM在读取.class 文件到内存中,需要一些特定的数据结构,来表示加载好的这些数据,==》 类对象,类名.class 就会得到这个类的对象,每个类都会有一个类对象,类对象里包含了这个类的各种信息(类的名字是啥,有那些属性,每个属性叫什么名字,是什么类型,有什么方法,每个方法叫什么名字,有什么参数,参数是什么类型,有什么注解,继承自哪个类,实现了那些接口...)

类对象,是反射机制的一句。

synchronized(this),也可以等价写作,把 synchronized 加到方法上。

这两种情况是等价的。

如果 synchronized 是加到 static 方法上,就等价于 给 类对象加锁。

最主要的,最常见的,还是 synchronized () { }

补充:也不是说,加上 synchronized,就一定线程安全了,关键还是看代码需要,代码是怎么写的。像刚才,如果一个线程加锁,一个不加锁,或者两个线程给不同的对象加锁,虽然都有 synchronized,但仍然是线程不安全的...

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com