java内存模型与线程
硬件效率与内存一致性问题
由于cpu的计算速度要远远超过内存的读取写入速度,因此在cpu和主内存之间存在多级缓存cache,缓存的速度相比内存快。但是和cpu相比还是差些,cpu如果要计算使用数据,只能先从主内存中把数据复制到缓存中。修改计算缓存中的数据后,在由缓存写回到主内存中。
除了增加缓存,cpu还会对代码进行排序优化。乱序执行代码。将执行后的代码结果重组,使得结果和正常顺序的一样。
java内存模型
java内存分为,主内存和工作内存。主内存记录那些共享变量。局部变量不会出现在主内存中。当java虚拟机需要使用到主内存中的内存时,会将主内存中的数据加载到工作内存中。每条线程都对应一个工作内存,
内存间的交互操作。
java内存模型中定义了8种原子操作。
lock ,标识主内存中的一个变量为线程独有的。
unlock 取消标记
read ,读取主内存中的变量值到工作内存
load ,加载read读取到的值,赋给工作内存中的变量副本
use ,使用load加载的值进行计算操作等。
assign,赋值给工作内存中的变量副本
store 把工作内存中的值传输给主内存
write 将store得到的值赋给主内存变量
volatile
被volatile修饰的变量 ,各个线程在使用它时会重新从主内存中读取,各个线程在给volatile变量赋值后,会立马同步到主内存中,使得各个线程立马得知变量的改变。同步主存这一步操作实际上是将变量所在的工作内存中变量全部同步。
volatile并不能 解决原子性。线程安全,他只能保证 每次线程在使用它时是最新值
比如线程1 读取到 volatile变量 a
线程2也同时读取到
线程2 修改后同步到主内存
这时候线程1使用的变量a就是错误的信息,也就i是有线程安全问题
指令重排序问题。
volatile修饰的变量在对其赋值操作时,他会在赋值操作后添加一个内存屏障来保证前面的代码不会重排序到后面,后面的代码不会重排序到前面
java内存模型中对volatile修饰的变量规定, read load use 必须连续且一起出现
assign store write 必须连续且一起出现。
针对long和double的变量规则
对于64位的long和double ,可能在读取他们到工作内存中时是采取读取两次,所以如果两个线程共同访问同一个变量,可能会出现线程安全问题,比如一个线程读取的是被其他线程修改的半个变量的数值。long出现的可能性比double大,double一般是用专门处理浮点数的浮点数运算器。除非我们明确知道long和double变量有明确竞争的,才回加volatile
原子性
对基本数据类型的访问都是原子的
可以加lock 和unlock 内部的代码块也是原子的
可见性
就是一个线程对变量的修改对另一个线程可见。就是可见性。volatiile就是保证了变量的可见性。还有synchronized修饰的变量。他们在解锁之前会把数据同步到主内存中,所以synchronized也能保证可见性,final关键字也可以保证,因为final字段一旦被初始化后,就不会在改变了,所以所有线程对其读取的都是一样的。也能保证可见性
有序性
java语言可能会造成指令重排序的问题,但是如果在一个线程内看,这个代码是有序的,因为最后结果是正确的,但是如果站在其他线程看就是乱序的。volatile的关键字也可以解决指令重排序问题。
先行发生原则
1,程序次序原则 在一个线程内,按照控制流,写在前面的内容对数据的修改对后面的代码可见
2,管程锁定原则 ,在unlock后,前面的对变量修改的操作,对lock后面的代码可见
3,volatile原则,在volatile赋值 对其他线程对volatile读操作可见
4 线程启动原则 ,thread 对象的start()方法前的对变量的修改,对thread内部可见。
5 线程终止规则 ,线程结束前对变量的修改对其他得知该线程结束的线程可见
6 线程打断原则 ,线程t1 在打断 t2 之前对变量的修改,对打断之后,所有知道t2线程被打断的线程可见
7 对象终结规则 ,一个对象初始化之前的操作,对于finalize()方法可见。
8 传递性。 如果A操作对B操作可见,B操作对C操作时可见的 那么A对C也是可见的
java线程实现
1内核线程
2用户线程
3混合线程
java线程调度
协同式和抢占式
协同式主要是在线程内部控制下一个调用的线程。只有当该线程干完自己的事情才能干其他线程的事,因此不会存在多线程安全问题。但是如果一个线程的代码有问题就会导致不停的阻塞的话,就可能导致整个程序崩溃
抢占式是有操作系统内核来决定调度哪个线程来执行。由操作系统进行线程的切换
线程的状态
NEW 线程刚创建,还没调用start
running 线程调用start后
blocked 线程竞争锁失败后,等待锁释放
timewaiting 调用sleep()或者 parkNanos()或者各种带有时间参数的waiting
waiting 调用join()调用不带时间参数的wait()
terminated 线程运行结束
java与协程
。。。。
线程安全与锁优化
线程安全
如果不同考虑线程的调度和交替执行。也不需要进行额外的同步,或者在调用方任何其他协调的操作,调用这个对象都可以获得正确的结果,则这个对象就是线程安全的
java里的线程安全
1不可变
final修饰的变量是不可变的无论怎样调用它,他都不会改变,如果final变量指向的是一个对象,他只能保证reference值不变,并不能保证对象内部数据不变
2绝对线程安全
无需采用任何协调的操作,调用类的方法,都会得到正确的结果
3相对线程安全
java中的大部分类都是相对线程安全的
如vector Hashtable Collections
4线程兼容
对象和类本身并不是线程安全的,但是可以通过同步手段来达到线程安全
5线程对立,
无论采取任何同步手段都无法达到线程安全
线程对立的例子 Thread.suspend() 和Thread.resume()
线程安全的实现方法
1互斥同步
就是使用锁机制使得共享数据只能在同一时间由一个或几个线程访问
在java里面就是采取synchronized 或者锁的lock
synchronized 可重入
Reentrantlock锁就是实现了lock接口的重入锁重入后需要记录重入的次数然后解锁也要解相应的次数
ReentrantLock
等待可中断,正在等待锁的线程,过了等待时间可以不等待。
公平锁,先来先执行,后来的排队
锁绑定多个条件,Condition.wait()
2非阻塞同步
是采取不加锁的方式,就是每次先进行操作。如果没有其他线程 共享数据就成功了,如果有其他线程先修改了数据,就不断重试,直到成功了为止
cas指令
unsafe 类可以通过反射获取,来进行使用cas来达到同步的操作
3无同步
一个方法本身不访问任何共享数据,因此它不需要同步
锁优化
自旋锁
当线程获取不到锁时不会立马进入阻塞状态,而是先自旋几次,再次尝试几次,看看持有锁的线程释放了没,如果尝试几次后还不释放,进入阻塞,如果释放了,则就获取到锁了。
自适应自旋
就是在自旋锁的过程中,如果一个锁总是在自旋的时候获取不到,那么他在下次获取不到时可能不会自旋或者自旋次数减少,直接进入阻塞,又或者一个锁每次自旋都能获取到,那么在下次获取锁时,可以自旋次数多一些,增加自旋次数
锁消除
如果一个共享资源,只在一个线程中访问使用,其他的线程没有访问这个共享资源,则java虚拟机进行优化为不加锁的情况,因为同一个线程访问,不存在线程安全问题
锁粗化
如果对一段代码对一个对象频繁的加锁解锁,虚拟机就会把这一串的加锁解锁操作换成一个大的范围加锁解锁
轻量级锁
在没有竞争的情况下,不会采用monitor来进行加锁,而是采取轻量级锁,在线程的栈中创建一个锁记录,锁记录来cas交换对象头的markword,并且改变对象头的状态位,改成轻量锁的,如果此时来一个竞争的线程,则会把轻量级锁升级成重量级锁,
偏向锁
偏心一个线程。在初次使用对象的时,改变对象头的markword记录线程id,此后如果访问该对象就会检测是不是本线程,如果不是就撤销偏向锁,改为轻量级锁,如果是本线程则不用在进行同步了