Java并发与线程安全2

volatile关键字

public static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
        int i=0;
        while(!stop){
        i++;
} 
System.out.println("输出结果:"+i);
});
    thread.start();
    Thread.sleep(1000);
    stop=true;
}

输出结果:

程序会陷入死循环

stop=true;对于线程thread而言是不可见的,因为线程thread已经从内存中拿到stop=false;所以程序才会死循环。

 public static void main(String[] args) throws InterruptedException {
    Thread thread=new Thread(()->{
        int i=0;
        while(!stop){
        i++;
//      System.out.println("rs:"+i);
		try {
		    Thread.sleep(0);
		} catch (InterruptedException e) {
		    e.printStackTrace();
		}
} 
System.out.println("输出结果:"+i);
});
    thread.start();
    Thread.sleep(1000);
    stop=true;
}

输出结果:

输出结果:685(一个任意随机数)

我们发现Thread.sleep(0)和System.out.println都可以让进程结束。不是说volatile可以解决吗,为什么sleep和println也可以解决可见性问题。

print就可以导致循环结束

活性失败. JIT深度优化
分2各层面来解释:
1、println底层用到了synchronized这个同步关键字,这个同步会防止循环期间对于stop值的缓存。
2、因为println有加锁的操作,而释放锁的操作,会强制性的把工作内存中涉及到的写操作同步到主 内存,可以通过如下代码去证明
3、第三个角度,从IO角度来说,print本质上是一个IO的操作,我们知道磁盘IO的效率一定要比CPU 的计算效率慢得多,所以IO可以使得CPU有时间去做内存刷新的事情,从而导致这个现象。比如 我们可以在里面定义一个new File()。同样会达到效果。

Thread.sleep(0)

Thread.sleep(0)导致线程切换,线程切换会导致缓存失效从而读取到了新的值。

volatile保证可见性

通过查看汇编指令,使用HSDIS工具。可以看到,使用volatile关键字之后,多了一个Lock指令
使用volatile:

0x00000000037028f3: lock add dword ptr [rsp],0h ;*putstatic stop

未使用volatile关键字:

0x0000000002b7ddab: push 0ffffffffc4834800h ;*putstatic stop ;-
com.example.threaddemo.VolatileDemo::<

什么是可见性

在单线程的环境下,如果向一个变量先写入一个值,然后在没有写干涉的情况下读取这个变量的值,那这个时候读取到的这个变量的值应该是之前写入的那个值。这本来是一个很正常的事情。
但是在多线程环境下,读和写发生在不同的线程中的时候,可能会出现:读线程不能及时的读取到其他线程写入的最新的值。这就是所谓的可见性

可见性从硬件层面来说

CPU/内存/IO设备
在整个计算机中他们的速度是递减的。

在这里插入图片描述
因为cpu高速缓存的存在,会导致一个缓存一致性问题。也就是我们所说的可见性问题。
这个时候我们可以锁定cpu0在加载数据的过程,这个期间cpu1是无法加载数据的。
这就是总线锁。

总线锁&缓存锁

总线锁:在多cpu下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出 一个LOCK#信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定 的开销比较大,这种机制显然是不合适的 。
因为这种串行的访问,会降低cpu的使用率。在X86架构的CPU后,引入了缓存锁来解决这个问题。
缓存锁:,就是指内存区域如果被缓存在处理器的缓存行中,并且在Lock期间被锁定,那么当它执 行锁操作回写到内存时,不再总线上加锁,而是修改内部的内存地址,基于缓存一致性协议保证操作的原子性

总线锁和缓存锁怎么选择,取决于很多因素,比如CPU是否支持、以及存在无法缓存的数据时 (比较大或者快约多个缓存行的数据),必然还是会使用总线锁

缓存一致性协议

MSI ,MESI 、MOSI … 为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有MSI,MESI,MOSI等。最常见的就是MESI协议。接下来给大家简单讲解一下MESI

MESI表示缓存行的四种状态,分别是

  1. M(Modify) 表示共享数据只缓存在当前CPU缓存中,并且是被修改状态,也就是缓存的数据和主内 存中的数据不一致
  2. E(Exclusive) 表示缓存的独占状态,数据只缓存在当前CPU缓存中,并且没有被修改
  3. S(Shared) 表示数据可能被多个CPU缓存,并且各个缓存中的数据和主内存数据一致
  4. I(Invalid) 表示缓存已经失效

MESI的优化&指令重排序

Store Bufferes是一个写的缓冲,对于上述描述的情况,CPU0可以先把写入的操作先存储到Store Bufferes中,Store Bufferes中的指令再按照缓存一致性协议去发起其他CPU缓存行的失效。而同步来 说CPU0可以不用等到Acknowledgement,继续往下执行其他指令,直到收到CPU0收到 Acknowledgement再更新到缓存,再从缓存同步到主内存。

这段代码,假设分别有两个线程,分别执行executeToCPU0和executeToCPU1,分别 由两个不同的CPU来执行。
引入Store Bufferes之后,就可能出现 b1返回true ,但是assert(a1)返回false。很多同学肯定会表 示不理解,这种情况怎么可能成立?那接下来我们去分析一下。

 executeToCPU0(){
    a=1;
	b=1; 
}
executeToCPU1(){
    while(b==1){
        assert(a==1);
    }
}

可能会出现b=1,而a=0;这是因为出现了指令重排序导致先执行b=1,而a=1未执行。
这种情况下,需要引入内存屏障来解决。

通过内存屏障禁止了指令重排序

X86的memory barrier指令包括lfence(读屏障) sfence(写屏障) mfence(全屏障)
1、Store Memory Barrier(写屏障) ,告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或 者写是可见的
2、Load Memory Barrier(读屏障) ,处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏 障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的
3、Full Memory Barrier(全屏障) ,确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障 后的读写操作

 volatile int a=0;
executeToCpu0(){
a=1; //storeMemoryBarrier()写屏障,写入到内存 
b=1;
// CPU层面的重排序 
//b=1;
//a=1;
}
executeToCpu1(){
while(b==1){ //true 
	loadMemoryBarrier(); //读屏障 
	assert(a==1) //false
} }

软件层面

在这里插入图片描述
2和3是cpu层面的重排序,1是编译器的重排序。
我们对一个变量的声明,在装载的过程之中它会去判断是不是volatile,如果是的话会调用storeload()。
storeload()就是一个内存屏障。
在这里插入图片描述
volatile底层的原理正是通过内存屏障和防止指令重排序的方式来解决可见性问题。

volatile的原理

首先volatile只是解决可见性问题,无法解决原子性问题。对于i++操作这是原子性操作。
导致可见性问题有两个因素,一个是高速缓存导致的可见性问题,另一个是指令重排序。那JMM是如何解决可见性和有序性问题的呢?
其实前面在分析硬件层面的内容时,已经提到过了,对于缓存一致性问题,有总线锁和缓存锁,缓存锁 是基于MESI协议。
而对于指令重排序,硬件层面提供了内存屏障指令。
而JMM在这个基础上提供了 volatile、final等关键字,使得开发者可以在合适的时候增加相应相应的关键字来禁止高速缓存和禁止 指令重排序来解决可见性和有序性问题。

双重检查锁防止获得不完整对象

public class DLCclass{
	public static volatile DLCclass dlcclass;
	private DLCclass(){}
	public static DLCclass getInstance(){
		if(dlcclass == null){
		synchronized(dlcclass){
			if(dlcclass == null){
			dlcclass = new DLCclass();
			}
		}
		}
		return dlcclass;
}
}

new关键字在底层是3个指令,一定会造成指令重排序。
因此一定要加volatile,否则有可能会获得不完整的对象。
volatile和synchronized双重检查锁DoubleLockCheck以防止不完整对象

Happens-Before模型

除了显示引用volatile关键字能够保证可见性以外,在Java中,还有很多的可见性保障的规则。
从JDK1.5开始,引入了一个happens-before的概念来阐述多个线程操作共享变量的可见性问题。所以 我们可以认为在JMM中,如果一个线程操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在 happens-before关系。这两个操作可以是同一个线程,也可以是不同的线程。

程序顺序规则(as-if-serial语义)

我们都知道指令是允许重排序的,但是有2个规则
1、不能改变程序的执行结果(在单线程环境下,执行的结果不变.)
2、依赖问题, 如果两个指令存在依赖关系,是不允许重排序

int a=0;
int b=0;
void test(){
    int a=1;    a
    int b=1;    b
    //int b=1;
    //int a=1;
    int c=a*b;  c
}

a happens -before b ; b happens before c
b和c是不允许重排序的。

传递性规则

a happens-before b , b happens- before c, 那么a happens-before c
a结果对b可见,b结果对c可见。那么a对于c也是可见的

volatile变量规则

volatile 修饰的变量的写操作,一定happens-before后续对于volatile变量的读操作.

public class VolatileDemo{
    int a=0;
    volatile boolean flag=false;
	public void writer(){
		a=1; //1
		flag=true; //修改 2
		}
	public void reader(){
    	if(flag){ //true              3
		inti=a; // 4 
		}
	}
}

由a线程执行writer方法,b线程执行reader方法。那么i的值等于多少?
1 happens-before 2 是否成立? 是
3 happens-before 4 是否成立? 是
2 happens -before 3 ->volatile规则
2优先于4执行。
1 happens-before 4 ;
最后结果:i=1

监视器锁规则

 int x=10;
synchronized(this){
//后续线程读取到的x的值一定12 if(x<12){
x=12; }
} x=12;

后续线程读到的x一定等于12。

start规则

 public class StartDemo{
    int x=0;
Thread t1=new Thread(()->{ 
	if(x==20){
	//读取x的值 一定是20 
	} 
});
    x=20;
    t1.start();
}    

Join规则

 public class Test{
    int x=0;
    Thread t1=new Thread(()->{
        x=200;
});
t1.start();
t1.join(); //join()保证结果的可见性。 
//在此处读取到的x的值一定是200.
}

final关键字提供了内存屏障的规则

死锁

死锁: 一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。
活锁: 指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试—失败—尝试—失败的过程。处于 的实体是在不断的改变状态, 有可能自行解开。

死锁发生的条件

四个条件同时满足,就会产生死锁。
互斥,共享资源 X 和 Y 只能被一个线程占用;
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

如何解决死锁问题

按照前面说的四个死锁的发生条件,我们只需要破坏其中一个,就可以避免死锁的产生。
其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥,其他三个条件都有办法可以破坏。

对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动 释放它占有的资源,这样不可抢占这个条件就破坏掉了。
对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序 的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环 了。
如果已经遇到死锁过程,该怎么办?
只能重启,去定位代码。因为没办法干预。

Thread.join

Thread.join的作用其实就是让线程的执 行结果对后续线程的访问可见。

public class JoinDemo {
    private static int i=10;
    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            i=30;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        System.out.println("i:"+i);
    }
}

输出结果:

i:10

如果我们想让t线程对i修改的结果在main线程中可见,就可以通过join()来做。
代码如下:

public class JoinDemo {


    private static int i=10;

    public static void main(String[] args) throws InterruptedException {
        Thread t=new Thread(()->{
            i=30;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
//        Thread.sleep(?); //你怎么知道多久能够执行完毕?
        //t线程中的执行结果对于main线程可见.
        t.join(); //Happens-Before模型 |
        //我希望t线程的执行结果可见
        System.out.println("i:"+i);
    }
}

输出结果:

i:30

可以看到结果是30。我们可以用sleep来做,但是我们不知道main线程要睡眠时间多久t线程才能执行完。
join()的本质是阻塞了main线程的执行,等到t线程执行完以后再去通知main线程。join底层用到的通信机制就是wait/notify。

在这里插入图片描述

ThreadLocal

ThreadLocal实际上一种线程隔离机制,也是为了保证在多线程环境下对于共享变量的访问的安全性。

public class ThreadLocalDemo {
    private static int num=0;
    public static void main(String[] args) {
        Thread[] thread=new Thread[5];
        for (int i=0;i<5;i++){
            thread[i]=new Thread(()->{
               num+=5;
                System.out.println(Thread.currentThread().getName()+"-"+num);
            });
        }
        for (int i = 0; i < 5; i++) {
            thread[i].start();
        }
    }
}

输出结果:

Thread-0-10
Thread-3-20
Thread-2-15
Thread-1-10
Thread-4-25

可以看到线程的值相互干扰,有线程结果是一样。
ThreadLocal可以让线程之间相互隔离。

public class ThreadLocalDemo {
//    private static int num=0;
    static ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        protected Integer initialValue(){
            return 0; //初始化一个值
        }
    };
    public static void main(String[] args) {
        Thread[] thread=new Thread[5];
        for (int i=0;i<5;i++){
            thread[i]=new Thread(()->{
                int num=local.get(); //获得的值都是0
                local.set(num+=5); //设置到local中  thread[0] ->thread[1] ->
                System.out.println(Thread.currentThread().getName()+"-"+num);
            });
        }
        for (int i = 0; i < 5; i++) {
            thread[i].start();
        }
    }
}

输出结果:
由于每个线程初始值都是0,所以结果都是5。

Thread-0-5
Thread-3-5
Thread-1-5
Thread-2-5
Thread-4-5

ThreadLocal原理分析

ThreadLocal用线性探测来解决hash冲突的一种策略.

线程并发常见问题

1、sleep , join() /yiled() 的区别
sleep 让线程睡眠指定时间, 会释放cpu时间片
join, 本质上是wait/notify, 让当前线程的执行结果可见
yiled 让出时间片. -> 触发重新调度.
sleep(0) -> 阻塞,然后会触发一次线程调度切换,作用和yiled差不多
2、Java中能够创建volatile数组吗?
可以创建, Volatile 对于引用可见,对于数组中的元素不具备可见性。 // volatile 缓存行的填充. ->性能问题
3、Java中的++操作是线程安全的吗?
不是线程安全的, 原子性、有序性、可见性。 ++操作无法满足原子性
4、线程什么时候会抛出InterruptedException()
t.interrupt() 去中断一个处于阻塞状态下的线程时(join/sleep/wait)
5、Java 中Runnable和Callable有什么区别
6、有T1/T2/T3三个线程,如何确保他们的执行顺序
join
7、Java内存模型是什么? **
JMM是一个抽象的内存模型
它定义了
共享内存中多线程程序读写操作的行为规范**:在虚拟机中把共享变量存储到内存以及从内 存中取出共享变量的底层实现细节。通过这些规则来规范对内存的读写操作从而保证指令的正确 性,它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的 可见性。
8、什么是线程安全
原子性、有序性、可见性(硬件层面(CPU高速缓存、指令重排序、JMM))
原子性:单个指令或多个指令在线程运行期间不允许被中断。我们必须保证单个或多个指令要么同时成功要么同时失败。
从应用层面来说,多个线程访问1个共享变量,实际运行结果和预期一致的,从而说明这个线程是安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值