Java中共享变量的内存可见性问题:
在java内存模型中规定,所有的变量都放在主内存中,当使用变量时,会把主内存中的变量复制到线程自己的工作空间或叫工作内存中,线程读写时操作的是自己工作内存中的变量。
如上图所示是一个双核的cpu系统架构,每个核都有自己的控制器和运算器,有自己的L1级缓存,有些架构里还有一个所有核共享的L2级缓存,那么java内存模型里的共享内存就是这里的L1或L2级缓存或者cpu寄存器。
当一个线程操作共享变量时,先将共享变量复制到自己的内存空间,处理完之后更新到主内存。当两个不同的线程同时操作一个共享变量时,由于缓存更新滞后,就导致了内存不可见问题。
关于synchronized关键字:
1.这是java提供的一个原子性内置锁,也叫监视器锁,每个对象都可以把它当成同步锁来用。
这个内置锁是一种排它锁,当一个线程获取到锁后,其他线程必须等待释放锁之后才能获取。
2.由于java中的线程与操作系统的原生线程一一对应,所有当阻塞一个线程时,需要由用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized就会导致上下文切换。
3.sychronized的内存语义:进入sychronized的内存语义是把sychronized块内的变量从工作内存中清除,直接从主内存中拿,而退出sychronized的语义是把修改后的变量刷新到主内存中。这也是加锁和释放锁的语义,加锁会清空锁内在工作内存中的共享变量,并加载主内存中的共享变量,释放锁会将共享变量刷新到主内存中。
关于volatile关键字:
上面介绍使用锁的方式可以解决内存可见性的问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销,对于内存可见性的问题,java提供了volatile关键字的方法,它可以确保一个变量的更新对其他线程立马可见。一般在什么时候使用这个关键字呢:
l 写入变量值不依赖变量的当前值。如果依赖于当前值,将变成获取-计算-写入三步操作,这三步不是原子性的,而volatile不保证原子性。
l 读写变量值时没有加锁。因为加锁操作已经保证了内存可见性,不需要再声明volatile。
l Java内存模型允许编译器和处理器对指令重排序以提高程序运行性能,这在单线程下是可以的,在多线程下就会存在问题,通过添加volatile关键字可以禁用重排序。
Java中的原子性操作:
所谓原子操作,就是执行一系列操作时,要么全部执行,要么全部失败,不存在执行一部分的情况。
CAS操作:锁在并发中使用很广泛,但锁有一个不好的地方,当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。即使使用volatile也只是保证了可见性,并不能保证原子性。CAS(Compare and Swap)是jdk提供的非阻塞原子性操作,它通过硬件保证了比较-更新操作的原子性。
伪共享:
现代cpu为了解决cpu和内存之间速度差的问题,往往会在两者之间添加一级或多级缓存(cache),这个cache一般是集成到cpu内部的,也叫cpu cache,在cache内部是按行存储的,其中每一行称为一个cache行,这是cache与主内存进行数据交换的单位,一般为2的幂次方字节大小。
由于存放在cache行中的是内存块而不是变量,所以可能会把多个变量存放在一个cache行中,当多个线程同时修改一个cache行中的多个变量时,由于同时只能有一个线程操作cache行,所以相比将每个变量放到不同的cache行,性能会有所下降,这就是伪共享。
由于cache行中存放的都是内存连续的多个变量,因此在java8之前,一般都是字节填充的方式来避免该问题,填满一个cache行来解决问题,在java8中可以使用@sun.misc.Contended注解来解决伪共享问题。
锁个概述:
- 乐观锁和悲观锁
乐观锁和悲观锁是在数据库中引入的名词,但是在并发包锁里面也引入了类似的思想。
悲观锁是指数据对外界修改持保守态度,认为数据很容易被其他线程修改,所以在数据处理前先加锁,并在整个数据处理过程中,使数据处于锁定状态。悲观锁的实现往往依靠数据库的锁机制,在数据库中,对数据记录操作前先加上排它锁,如果获取锁失败,说明数据正在被其他线程修改。如果获取锁成功,则对数据修改然后提交事务并释放排它锁。
栗子:
//使用悲观锁获取指定记录
Select * from table1 where id =#{id} for update
//修改数据记录
.......
//update操作
Update table1 set name = #{name},age=#{age} where id=#{id}
只有一个线程能执行select成功,其他线程都被阻塞,一直到update操作完并提交事务成功后,悲观锁才会释放。
乐观锁是相对于悲观锁来说的,它认为数据在一般情况下不会有冲突,因此在访问数据记录前不会加排它锁,而是在提交数据更新的时候,才会正式对冲突进行检测,也就是说让用户根据返回的更新条数判断时候修改成功。上述代码改为乐观锁为:
//使用乐观锁获取指定内容
Select * from table2 where id= #{id}
//修改数据记录内容
........
//update操作
Update table2 set name = #{name} , age = #{age}, version=#{version}+1 where id=#{id} and version = #{version}
多个线程可以同时执行查询操作获取数据,并在各自线程栈上修改,在执行提交时,每个提交都加上了version字段, set中多了vesion+1的操作,如果数据没有变,则在原来的基础上增加一个版本号,否则就返回0条更新记录,有点CAS的意思,乐观锁直到提交时才锁定,因此不会有死锁问题。
- 公平锁和非公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,所谓公平锁就是多个线程获取锁的顺序是按照请求的时间早晚决定,也就是先来后到的顺序,而非公平锁则是随机性调度,不严格遵从时间顺序。由于公平锁会带来性能开销,应该优先使用非公平锁。
- 独占锁和共享锁
根据锁只能被单个线程持有还是能被多个线程持有,锁可以分为独占锁和共享锁。
- 可重入锁
当一个线程获取一个被其他线程持有的独占锁时,会被阻塞,但如果获取的是被自己占有的锁时呢?如果不被阻塞,则说这个锁是可重入锁,否则就是不可重入锁。Synchronized也是一种可重入锁。
- 自旋锁
由于java线程和操作系统的线程是一一对应的,所以当一个线程获取锁失败后,会被切换到内核态挂起。获取到锁之后又要切换到用户态,这样的切换的性能开销是比较大的,也会影响并发性能。而自旋锁则是,在发现锁被其他线程占用时,不会马上阻塞自己,在不放弃cpu执行权的情况下,多次尝试获取(默认是10次,在-XX:PreBlockSpinsh设置),很有可能在之后几次尝试中获取到了锁,如果没有,仍然进入阻塞状态被挂起。
原文地址:https://www.cnblogs.com/loveBolin/p/10200454.html