异步编程学习之路(二)-通过Synchronized实现线程安全的多线程

}

public Boolean allThreadFinish(List threads) {

for (Thread thread : threads) {

if (thread.isAlive()) {

return false;

}

}

return true;

}

}

运行结果:

70267

正确结果应该是10万。

其实volatile关键字只能够保证多线程之间的内存可见性,而不能保证多个线程之间的有序性。

为什么synchronized能够保证线程安全而volatile不行呢?

二、JMM内存模型


Java 内存模型来屏蔽掉各种硬件和操作系统的内存差异,达到跨平台的内存访问效果。JLS(Java语言规范)定义了一个统一的内存管理模型JMM(Java Memory Model)

Java内存模型规定了所有的变量都存储在主内存中,此处的主内存仅仅是虚拟机内存的一部分,而虚拟机内存也仅仅是计算机物理内存的一部分(为虚拟机进程分配的那一部分)。

Java内存模型分为主内存,和工作内存。主内存是所有的线程所共享的,工作内存是每个线程自己有一个,不是共享的。

每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值),都必须在工作内存中进行,而不能直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者之间的交互关系如下图:

Java内存间交互操作

JLS定义了线程对主存的操作指令:lock,unlock,read,load,use,assign,store,write。这些行为是不可分解的原子操作,在使用上相互依赖,read-load从主内存复制变量到当前工作内存,use-assign执行代码改变共享变量值,store-write用工作内存数据刷新主存相关内容。

  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用

  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。

  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。

  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

为什么需要要多线程 (充分利用CPU)

举一个栗子,假设现在要10000条数据,总共需要100分钟。如果是单线程的串行操作,需要100分钟。那么如果同时开10个线程,每一个线程运行100条数据,那么只需要10分钟就可以完成所有的操作。(总之是充分利用物理资源CPU)

三、多线程特性


1、原子性(Atomicity)

原子性是指一个原子操作在cpu中不可以暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行。原子操作保证了原子性问题。

x++(包含三个原子操作)a.将变量x 值取出放在寄存器中 b.将将寄存器中的值+1 c.将寄存器中的值赋值给x

由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、store和write六个,大致可以认为基础数据类型的访问和读写是具备原子性的。

如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock与unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐匿地使用这两个操作。

这两个字节码指令反映到Java代码中就是同步块—synchronized关键字,因此在synchronized块之间的操作也具备原子性。

2、可见性(Visibility)

java 内存模型的主内存和工作内存,解决了可见性问题。

volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。

可见性就是指当一个线程修改了线程共享变量的值,其它线程能够立即得知这个修改。

无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是volatile的特殊规则保证了新值能立即同步到主内存,以及每使用前立即从内存刷新。

因为我们可以说volatile保证了线程操作时变量的可见性,而普通变量则不能保证这一点。

3、有序性(Ordering)

Java内存模型中的程序天然有序性可以总结为一句话:如果在本线程内观察,所有操作都是有序的;如果在一个线程中观察另一个线程,所有操作都是无序的。前半句是指“线程内表现为串行语义”,后半句是指“指令重排序”现象和“工作内存主主内存同步延迟”现象。

注意:

1. Java语言作为高级语言支持多线程的操作,主要是为了解决单线程因阻塞而带来的效率问题,同时也充分利用多核CPU的优势。使用多线程也带了问题,线程之间如何通信?线程之间如何和同步?

2. 线程之间的通信是依靠共享内存和线程方法的调用来实现。在多线程的体系下,Java的内存模型分为主内存和共享内存,通过内存之间的数据交换,依赖多线程的可见性,实现线程之间的通信;线程具有基本状态,主动调用线程的wait、notify方法也可以实现线程之间的通信。

3.线程的同步也是并发线程操作共享变量所带来的问题。多线程允许使用synchronize、volatile、ThreadLocal来保证多线程的安全。synchronize是一个重量级的锁,直接使得线程阻塞,单线程顺利执行,对于更新变量不会有并发操作,很大程度的降低的系统的性能。volatile是一个轻量级的原子锁,对于volatile修饰的变量,每一次的读和写,都必须和主内存交互,他禁止了编译器和处理器的一些重排序优化。

四、Volatile非线程安全的原因


示例中count变量在内存中如下图:

由上,我们可以得出以下结论。

  1. read和load阶段:从主存复制变量到当前线程工作内存;

  2. use和assign阶段:执行代码,改变共享变量值;

  3. store和write阶段:用工作内存数据刷新主存对应变量的值。

在多线程环境中,use和assign是多次出现的,但这一操作并不是原子性,也就是在read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步,所以计算出来的结果会和预期不一样,也就出现了非线程安全问题。对于用volatile修饰的变量,JVM虚拟机只是保证从主内存加载到线程工作内存的值是最新的,例如线程1和线程2在进行read和load的操作中,发现主内存中count的值都是5,那么都会加载这个最新的值。也就是说,volatile关键字解决的是变量读时的可见性问题 ,但无法保证原子性,对于多个线程访问同一个实例变量还是需要加锁同步。

四、Synchronized原理分析


synchronized是平时用的比较多的多线程问题的解决方案,但各位知道吗,synchronize也有失效的情况存在,下面我就逐个来分析synchronized失效的种种情况以及它的底层原理。

情况1:同一个对象在两个线程中分别访问该对象的两个同步方法。

结果:会产生互斥。

分析:因为锁针对的是对象,当对象调用一个synchronized方法时,其他同步方法需要等待其执行结束并释放锁后才能执行。

情况2:不同对象在两个线程中调用同一个同步方法。

结果:不会产生互斥。

分析:因为是两个对象,锁针对的是对象,并不是方法,所以可以并发执行,不会互斥。形象的来说就是因为我们每个线程在调用方法的时候都是new 一个对象,那么就会出现两个空间,两把钥匙。

情况3:.Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”,用类直接在两个线程中调用两个不同的同步方法。

结果:会产生互斥。

**分析:因为对静态对象加锁实际上对类(.class)加锁,类对象只有一个,可以理解为任何时候都只有一个空间,里面有N个房间,一把锁,因此房间(同步方法)之间一定是互斥的。

注:上述情况和用单例模式声明一个对象来调用非静态方法的情况是一样的,因为永远就只有这一个对象。所以访问同步方法之间一定是互斥的。**

情况4:用一个类的静态对象在两个线程中调用静态方法或非静态方法。

结果:会产生互斥。

分析:因为是一个对象调用,同上。

情况5:一个对象在两个线程中分别调用一个静态同步方法和一个非静态同步方法。

结果:不会产生互斥。

分析:因为虽然是一个对象调用,但是两个方法的锁类型不同,调用的静态方法实际上是类对象在调用,即这两个方法产生的并不是同一个对象锁,因此不会互斥,会并发执行。

情况6:一个对象,两个线程,对象方法是Runnable返回值,并且该方法上有synchronized关键字,两个线程都调用这个对象的这个方法返回的Runnable来做实际执行逻辑。

结果:synchronized同步锁失效。

分析:具体原因不明。

1、实践一,synchronized使用在一般方法内。

/**

  • @Description:Synchronized原理分析

  • @Author:zhangzhixiang

  • @CreateDate:2018/12/21 20:30:54

  • @Version:1.0

*/

public class User {

public synchronized void test1(){

try {

System.out.println(“test1 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test1 end”);

}

public synchronized void test2(){

try {

System.out.println(“test2 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test2 end”);

}

public static void main(String[] args) {

final User user = new User();

new Thread(new Runnable() {

@Override

public void run() {

user.test1();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

user.test2();

}

}).start();

}

}

运行结果:

test2 start

test2 end

test1 start

test1 end

分析:对于同一个对象user,可以看见对于普通方法的时,每一次只能执行一个方法,类似于串行执行的效果。

2、实践二,synchronized使用在类方法内。

/**

  • @Description:Synchronized原理分析

  • @Author:zhangzhixiang

  • @CreateDate:2018/12/21 20:30:54

  • @Version:1.0

*/

public class Test {

public synchronized static void test1(){

System.out.println(“test1 in”);

try{

System.out.println(“test1 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test1 end”);

}

public synchronized static void test2(){

System.out.println(“test2 in”);

try{

System.out.println(“test2 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test2 end”);

}

public static void main(String[] args) {

new Thread(new Runnable() {

@Override

public void run() {

Test.test1();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

Test.test2();

}

}).start();

}

}

运行结果:

test1 in

test1 start

test1 end

test2 in

test2 start

test2 end

分析:可以看见对于类方法,依然是起到了锁的作用,但是要注意,这边使用的仍然是Test这个Class,可以知道此时的锁加在了Class上了。

3、实践三,synchronized混合使用与类方法和普通方法。

/**

  • @Description:Synchronized原理分析

  • @Author:zhangzhixiang

  • @CreateDate:2018/12/21 20:30:54

  • @Version:1.0

*/

public class Test {

public synchronized static void test1(){

System.out.println(“test1 in”);

try{

System.out.println(“test1 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test1 end”);

}

public synchronized void test2(){

System.out.println(“test2 in”);

try{

System.out.println(“test2 start”);

Thread.sleep(3000);

}catch (Exception e){

}

System.out.println(“test2 end”);

}

public static void main(String[] args) {

new Thread(new Runnable() {

@Override

public void run() {

Test.test1();

}

}).start();

new Thread(new Runnable() {

@Override

public void run() {

new Test().test2();

}

}).start();

}

}

运行结果:

test1 in

test1 start

test2 in

test2 start

test1 end

test2 end

分析:这边可以看出运行结果的不同了,发现两个方法并没有被锁住,而是同时执行了。这是为什么呢???这边先简单解释下吧。这是因为static test1方法是类方法,也就是它属于的是Class,而不是属于这个Class产生的具体的对象。而test2方法是由Class产生出的具体对象才能调用的,因此它属于的是Class产生的对象,而不是Class本身。这是两个概念,所以简单来说就是这两种情况锁的条件不一样,因此产生了锁不住的效果。。(具体原因需要分析源码或者底层原理才能给出更规范的解释)。

4、实践四,synchronized修饰普通方法时重入的效果。

/**

  • @Description:Synchronized原理分析

  • @Author:zhangzhixiang

  • @CreateDate:2018/12/21 20:30:54

  • @Version:1.0

*/

public class Test {

public synchronized void test1(){

System.out.println(“test1 in”);

try{

System.out.println(“test1 start”);

test2();

}catch (Exception e){

}

System.out.println(“test1 end”);

}

public synchronized void test2(){

System.out.println(“test2 in”);

try{

System.out.println(“test2 start”);

}catch (Exception e){

}

System.out.println(“test2 end”);

}

public static void main(String[] args) throws Exception{

final Test t1 = new Test();

new Thread(new Runnable() {

@Override

public void run() {

t1.test1();

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数网络安全工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年网络安全全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
img

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

rlgg8-1713065545582)]
[外链图片转存中…(img-tqZedduU-1713065545582)]
[外链图片转存中…(img-Egik2AaP-1713065545582)]
[外链图片转存中…(img-Q3UdlmzF-1713065545583)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上网络安全知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加VX:vip204888 (备注网络安全获取)
[外链图片转存中…(img-mU3Pjf7I-1713065545583)]

学习路线:

这个方向初期比较容易入门一些,掌握一些基本技术,拿起各种现成的工具就可以开黑了。不过,要想从脚本小子变成黑客大神,这个方向越往后,需要学习和掌握的东西就会越来越多以下是网络渗透需要学习的内容:
在这里插入图片描述

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-pVhTlBKR-1713065545583)]

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值