课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
随着互联网的不断发展,越来越多程序员都开始掌握多线程编程开发的一些技术和方法。今天我们就一起来了解一下,关于多线程编程开发都有哪些常见特性。
一、原子性
原子性是指一个操作或者一系列操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。其实这句话就是在告诉你,如果有多个线程执行相同一段代码时,而你又能够预见到这多个线程相互之间会影响对方的执行结果,那么这段代码是不满足原子性的。结合到实际开发当中,如果代码中出现这种情况,大概率是你操作了共享变量。
针对这个情况网上有个很的例子,银行转账问题:
比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的终余额为30万,而非预期的40万。如果A和B两个转账操作是在不同的线程中执行,而C的账户就是你要操作的共享变量,那么不保证执行操作原子性的后果是十分严重的。
OK,上面的状况我们理清楚了,由此可以引申出下列三个问题
1、哪些是共享变量
从JVM内存模型的角度上讲,存储在堆内存上数据都是线程共享的,如实例化的对象、全局变量、数组等。存储在线程栈上的数据是线程独享的,如局部变量、操作栈、动态链接、方法出口等信息。
举个通俗的例子,如果你的执行方法相当于做菜,你可以认为每个线程都是一名厨师,方法执行时会在虚拟机栈中创建栈帧,相当于给每个厨师分配一个单独的厨房,做菜也就是执行方法的过程中需要很多资源,里面的锅碗瓢盆各种工具,就诸如你在方法内的局部变量是每个厨师独享的;但如果需要使用水电煤气等公共资源,就诸如全局变量一般是共享的,使用时需要保证线程安全。
2、哪些是原子操作
既然是要保证操作的原子性,如何判断我的操作是否符合原子性呢,一段代码肯定是不符合原子性的,因为它包含很多步操作。但如果只是一行代码呢,比如上面的银行转账的例子如果没有这么复杂,共享变量“C的账户”只是一个简单的count++操作呢?针对这个问题,先我们要明确,看起来十分简单的一句代码,在JMM(java线程内存模型)中可能是需要多步操作的。
3、如何保证操作的原子性
使用较多的三种方式:
内置锁(同步关键字):synchronized;
显示锁:Lock;
自旋锁:CAS;
当然这三种实现方式和保证同步的机制上都有所不同,在这里我们不做深入的说明。
二、可见性
可见性是一种复杂的属性,因为可见性的错误通常比较隐蔽并且违反我们的直觉。
如果你直接运行上面的代码,那么你永远也看不到number的输出的,线程将会无限的循环下去。你可能会有疑问代码当中明明已经把isOver设置为了false,为什么循环还不会停止呢?这正是因为多线程之间可见性的问题。在单线程环境中,如果向某个变量写入某个值,在没有其他写入操作的影响下,那么你总能取到你写入的那个值。然而在多线程环境中,当你的读操作和写操作在不同的线程中执行时,情况就并非你想象的理所当然,也就是说不满足多线程之间的可见性,所以为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。
三、有序性
理解多线程的有序性其实是比较困难的,因为你很难直观的去观察到它。
有序性的本义是指程序在执行的时候,程序的代码执行顺序和语句的顺序是一致的。但是在Java内存模型中,是允许编译器和处理器对指令进行重排序的,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。也就是说在多线程中代码的执行顺序,不一定会与你直观上看到的代码编写的逻辑顺序一致。
四、happen-before规则
上面我们也提到了,多线程的可见性与有序性之间其实是有联系的,如果程序没有按你希望的顺序执行,那么可见性也就无从谈起。JMM(Java线程内存模型)中的happen-before规则,该规则定义了Java多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。
按照官方的说法:
当一个变量被多个线程读取并且至少被一个线程写入时,如果读操作和写操作没有happen-before关系,则会产生数据竞争问题。要想保证操作B的线程看到操作A的结果(无论A和B是否在一个线程),那么在A和B之间必须满足HB原则,如果没有,将有可能导致重排序。当缺少happen-before关系时,就可能出现重排序问题。
【免责声明】:本内容转载于网络,转载目的在于传递信息。文章内容为作者个人意见,本平台对文中陈述、观点保持中立,不对所包含内容的准确性、可靠性与完整性提供形式地保证。请读者仅作参考。