在并发编程中有三个典型问题:原子性问题,可见性问题,有序性问题。
原子性问题
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
如果这2个操作不具备原子性。从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
Java内存模型保证了基本读取和赋值是原子性操作,即这些操作是不可被中断的,如果要实现多线程的原子性,可以通过synchronized
和Lock
来实现。
可见性问题
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
普通的共享变量不能保证可见性,因为普通共享变量被修改之后,写入主存的时间是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
通过volatile
,synchronized
和Lock
也能够保证可见性,能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性问题
有序性:即程序执行的顺序按照代码的先后顺序执行。
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面可以通过volatile
关键字来保证一定的“有序性”。另外可以通过synchronized
和Lock
来保证有序性,很显然,synchronized
和Lock
保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
在面对Java并发中,volatile、synchronized和lock三个是比较常用的
Java的内存模型
image.pngvolatile
volatile
是一个类型修饰符,是Java中的一个关键字。用来修饰被不同线程访问和修改的变量,仅用于编译阶段
控制层面:内存和CPU高速缓存
当线程执行时,会先从主存当中读取值,然后复制一份到高速缓存当中,然后CPU执行指令操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。
控制机制:控制主内存和CPU高速缓存的一致性,当一个线程更改了某个内存变量时,会强制更新主内存,并通知其他CPU从主内存中重新获取变量值。
synchronized
synchronized叫做同步锁,Java语言的关键字,具有Java内置的特性。
可用来给对象和方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍然可以访问该object中的非加锁代码块。所以锁资源竞争相对较激烈
控制层面:JVM层面
控制机制:控制某个方法的执行者或者是某个变量的拥有者。如果是方法锁,则同一时间只能有一个线程使用该方法,其他线程阻塞;如果是对象锁,则哪个线程最先拥有锁对象,则哪个线程先执行,其他线程阻塞。
锁提供的两种特性:互斥性和可见性
1.互斥(mutual exclusion):一次就只有一个线程能够使用该共享数据。线程通过持有某个特定的锁,实现互斥访问资源。
2.可见性(visibility):确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的。
Lock
Lock是一个类,通过这个类可以实现同步访问;是synchronized
的升级版
ReentrantLock
,意思是“可重入锁”,是唯一实现了Lock
接口的类
控制层面 :CPU总线
控制机制:控制线程的执行的CPU,在总线之下只允许一个线程的CPU可以访问某个内存,当某个线程执行结束之后其他线程才能访问变量资源
volatile、synchronized、lock之间的区别
volatile和synchronized
1.volatile
本质是在告诉Jvm
当前变量在寄存器中的值是不确定的,需要从主存中读取,
synchronized
则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
2.volatile
仅能使用在变量级别,synchronized
则可以使用在变量,方法.
3.volatile
仅能实现变量的修改可见性,而synchronized
则可以保证变量的修改可见性和原子性.定义long
或double
变量时,如果使用volatile
关键字,就会获得(简单的赋值与返回操作)原子性。
4.volatile
不会造成线程的阻塞,而synchronized
可能会造成线程的阻塞.
5、当一个域的值依赖于它之前的值时,volatile
就无法工作了,如n=n+1
,n++
等。如果某个域的值受到其他域的值的限制,那么volatile
也无法工作,如Range
类的lower
和
upper
边界,必须遵循lower<=upper
的限制。
6、使用volatile
而不是synchronized
的唯一安全的情况是类中只有一个可变的域。
synchronized和lock
1.Lock
是一个接口,而synchronized
是Java
中的关键字,synchronized
是Java内置的语言实现;
2.synchronized
在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock
在发生异常时,如果没有主动通过unLock()
去释放锁,则很可能造成死锁现象,因此使用Lock
时需要在finally
块中释放锁;
3.Lock
可以让等待锁的线程响应中断,而synchronized
却不行,使用synchronized
时,等待的线程会一直等待下去,不能够响应中断;
4.通过Lock
可以知道有没有成功获取锁,而synchronized
却无法办到。
5.Lock
可以提高多个线程进行读操作的效率。在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,当大量线程同时竞争时Lock
的性能要远远优于synchronized
。所以说,在具体使用时要根据适当情况选择。
参考文章
关于volatile的实例应用可以参考
https://www.cnblogs.com/dolphin0520/p/3920373.html
关于synchronized的实例应用可以参考
https://www.cnblogs.com/dolphin0520/p/3923737.html
关于lock的实例应用可以参考https://www.cnblogs.com/dolphin0520/p/3923167.html
网友评论