大家好,又见面了,我是你们的朋友全栈君。
参考文章:https://blog.csdn.net/huyiju/article/details/97126274
一、voliate相关
1:java内存模型
1.1:计算机的内存模型
在计算机的内存模型中cpu和内存之间的速度存在数量级所以引入了高速缓存,告诉缓存会导致到底以哪个处理器的缓存为主,同步到主内存,这个时候有有了缓存一致性协议,来保证缓存一致性。
指令重排:例如一下五行代码,前四行的在计算机cpu的执行顺序不一定是12345,也可以是13245或者34125,但是第五步的顺序不会变,这种指令重排不会影响最后的计算结果。
int a=1;
a++;
int b=5;
b++;
int c=a+b;
1.2:java内存模型
java的内存模型屏蔽掉了计算机硬件和操作系统的差异,但是没有限制处理器使用缓存和主内存进行交互,也没有限制编译器在执行过程中的指令重排这类优化措施。
2:Voliate关键字
voliate关键字有两个作用(可见性和禁止指令重排)
1:可见性:保证在多个线程的情况下,线程一把int a的值修改为5的时候,其他线程也能立即知道int a=5,实现的方法是线程1把int a=5,会把a=5的是立马通过缓存同步到主内存,然后其他线程使用a之前会从主内存刷新一次,得到被线程1修改的值为5.
2:有序性:有序性的意思的在本线程内观察,所有操作都是有序的(线程内是串行的),但是在另一个线程观察本线程,所有操作都是无序的(主要是指指令重排现象和工作内存与主内存同步)。voliate和synchronize两个关键字来保证线程操作之间的有序性。
2.1:voliate不能保证线程安全(可见性分析)
预期结果:10个线程同时执行,每一个此线程都把i累加到1000.最后的结果应该是10000.
实际结果:多次运行,大部分结果都不足10000
结论分析:i++,不是原子性操作,线程1把i的值赋值的10,这个时候后线程2根据可见性也得到了10,但是线程1接着执行累加把i的值赋值到250,这个时候线程2依然拿到的值是10是过期数据,入栈++后的值为11,把11同步到了主内存,触发了线程1从主内存同步的得到值为11,这个时候会导致线程一的结果错误。
public class VoliteTest {
//voliate关键字保证了此变量在各个线程中是一致的
public static volatile int id=1;
public static void main(String[] args) throws InterruptedException {
// TODO Auto-generated method stub
Thread[] ths=new Thread[10];
for(int i=0;i<10;i++) {
//10个线程同时执行
ths[i]=new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
add();
}
});
ths[i].start();
}
//累加线程执行完毕
while (Thread.activeCount()>1) {
Thread.yield();
}
System.out.println("========================");
System.out.println("ID的值是:"+id);
}
//该方法可以通过加锁实现线程安全
public static void add() {
for(int i = 0;i<1000;i++) {
//id++不是原子性的
//第一步:假如线程1同步id=5,这个时候要++,其他线程切换了进来
//第二部:其他比如线程2同步id=5,执行了很多操作,id=100了
//第三部:线程1执行++,得到了id=6,这个时候的id为5已经是过期数据了,
//第四部:线程1将结果6同步到内存共享,其他的线程得到id=6.会导致最后的值不等于10001
id++;
}
}
}
2.2:voliate禁止指令重排
voliate保证cpu能够保证在这个关键字之前和之后的代码不会指令重排,保证按照书写顺序执行代码。
之所以能实现指令重排是因为voliate的可见性决定的,这个关键字保证了从缓存到主内存之间的同步,就像内存之间的一道屏障,保证了之前和之后的代码无法越过这个 内存栅栏。
单例代码案例:保证线程安全和只有一个实例
public class Danli {
private volatile static Danli danli;
//私有构造,防止通过外部new创建对象
private Danli() {
}
//懒汉模式 需要的时候get
//(方法加锁导致多线程效率低)
public static synchronized Danli getDanli() {
if(danli==null) {
//在此处判断加锁,防止线程1判断为空后,线程二创建了实例
//在加锁方法之内再次判断一次
synchronized (Danli.class) {
if(danli==null) {
//再次判断
danli=new Danli();//内存屏障,保证写完之后其他线程读取
//1:在工作内存创建,(store存储)到主内存
//在此之间可能有其他线程插入
//2:主内存的danli(write写入)
//这是一个内存栅栏
}
}
}
return danli;
}
}
二、synchronize关键字
2.1:说一下对synchronize关键字的理解
synchronize关键字解决了多线程的资源同步性,该关键字保证了在多线程的条件下,同时是有一个线程能够获取到资源。
在jdk的早起版本中,这个关键字是重量级锁,这个关键字在编译之后,在同步块的前后会形成monitorenter和monitorexit(监视器进入和退出,通过获取对象锁计数器加一减一来实现锁机制)两个节码指令,其他线程在竞争的时候会挂起,效率低下,但是随着jdk的发展,通过对synchronize底层的发展,不必每次线程都挂起。来大大提升了效率
比如采用偏向锁、轻量级锁、锁粗化、锁自旋、锁自旋、锁消除等手段来提升效率
2.2:synchronize在用项目中用到了吗?
2.2.1:synchronize用法
1:用来修饰静态方法(给类加了锁,无论new多少个对象,都会产生竞争,线程1new的对象获取普通加锁方法和线程2获取类的静态加锁方法不会产生竞争)
2:用来修饰静态代码块(也是给类加了锁)
3:用来修饰普通方法(锁给了对象,只用new的同一对象才有竞争)
2.2.2:项目中的使用(懒汉单例模式)
package com.thit.connpool;
public class LazySimple {
//voliate修饰,禁止指令重排
private static volatile LazySimple lazySimple;
//私有构造防止new创建对象
private LazySimple() {
}
//静态方法外部得到实例
public static LazySimple getDanli() {
//首先判断是否为空
if (lazySimple==null) {
synchronized (LazySimple.class) {
//再次判断是否为空
if(lazySimple==null) {
//new 对象非原子性操作
//1:分配内存空间
//2:初始化值(构造器)
//3:将对象指向内存地址
lazySimple=new LazySimple();
}
}
}
return lazySimple;
}
}
代码分析:
1:其中 volatile修饰lazySimple;是为了防止指令重排,其中lazySimple=new LazySimple();这段代码不是原子性操作,实际分为3步
//1:分配内存空间
//2:初始化值
//3:将引用指向内存地址
lazySimple=new LazySimple();
由于jvm有指令重排的优化,所以代码执行顺序可能是1>3>2,当线程1执行1>3的时候没有执行到2初始化赋值的时候,线程2进入执行,这个时候判断到lazySimplenull,会在创造一个对象,就不是单例了。加了voliate关键字修饰,会因为可见性而防止指令重排,因为其他的线程想要判断lazySimplenull的时候,会在1>2>3执行完成,在线程1修改lazySimple的时候,没写入之前内存像是有屏障一般。这边形成了指令重排无法越过的内存屏障。
三、说说synchronize关键字和voliate关键字
1:voliate修饰变量多线程可见性但是没有原子性,synchronize修饰方法能保证可见性和原子性。
2:voliate的效率更高一点,线程不会阻塞,但是synchronize线程会阻塞
3:voliate用来解决变量的多线程可见性,但是synchronize用来解决多线程资源访问问题
四、lock接口
4.1:为什么需要Lock接口?
我们知道锁是来控制多线程并发访问资源的竞争,在Lock接口出现之前我们使用synchronize关键字来实现锁功能,但是synchronize获取锁是隐式的,我们无法控制锁的获取,释放中断的可操作性性,由于synchronize获取锁固话,所以创建了Lock接口来实现更领过的锁。
Lock和synchronize都是重入锁
1:尝试非中断的获取锁,
2:可以实现中断,与synchronize关键字不同,当获取锁的线程能够响应中断,中断的时候,异常内抛出,释放锁
3:能指定时间获取锁,在指定时间内无法获取锁的时候返回
4.2:代码实现
Lock lock=new ReentrantLock();
lock.lock();//获取锁
try {
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();//finally修饰,异常处理,一定要释放锁
}
原文链接:https://blog.csdn.net/huyiju/article/details/97646152
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/139668.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...