• Java学习笔记——并发编程(三)


    一、wait和notify

    wait和notify原理

    在这里插入图片描述

    • Owner线程发现条件不满足,调用wait方法,即可进入WaitSet变为WAITING状态
    • BLOCKED和WAITING的线程都处于阻塞状态,不占用CPU时间片
    • BLOCKED线程会在Owner线程释放锁时唤醒
    • WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后并不意味着立刻获得锁,仍需进入EntryList重新竞争

    API介绍

    • obj.wait()让进入object监视器的线程到waitSet等待
    • obj.notify()在object上正在waitSet等待的线程中 挑一个唤醒
    • obj.notifyAll()让object上正在waitSet等待的对象全部唤醒

    它们都是线程之间协作的手段,都属于Object对象的方法。必须获得此对象的锁,才能调用这几个方法。

    final static Object obj = new Object();
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(obj){
    			log.debug("执行....");
    			try{
    				obj.wait();  //让线程在obj上一直等待下去
    			}catch(InterruptedException e){
    				e.printStackTrace();
    			}
    			log.debug("其他代码....");
    		}
    	},"t1").start();
    	
    	new Thread(()->{
    		synchronized(obj){
    			log.debug("执行....");
    			try{
    				obj.wait();  //让线程在obj上一直等待下去
    			}catch(InterruptedException e){
    				e.printStackTrace();
    			}
    			log.debug("其他代码....");
    		}
    	},"t2").start();
    	sleep(2);
    	log.debug("唤醒obj上其他线程");
    	synchronized(obj){
    		obj.notify();  //唤醒obj上一个线程
    		//obj.notifyAll(); //唤醒obj上所有等待线程
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    notify的一种结果

    20:00:53.096 [Thread-0] c.TestWaitNotify - 执行…
    20:00:53.099 [Thread-1] c.TestWaitNotify - 执行…
    20:00:55.096 [main] c.TestWaitNotify - 唤醒obj上其他线程
    20:00:55.096 [Thread-0] c.TestWaitNotify - 其他代码…

    notifyAll的结果

    19:58:15.457 [Thread-0] c.TestWaitNotify - 执行…
    19:58:15.460 [Thread-1] c.TestWaitNotify - 执行…
    19:58:17.456 [main] c.TestWaitNotify - 唤醒obj上其他线程
    19:58:17.456 [Thread-1] c.TestWaitNotify - 其他代码…
    19:58:17.456 [Thread-0] c.TestWaitNotify - 其他代码…

          wait()方法会释放对象的锁,进入WaitSet等待区,从而让其他线程有机会获取对象的锁。无限制等待,直到notify为止;wait(long n)有时限的等待,到n毫秒后结束等待,或是被notify。

    sleep(long n)和wait(long n)的区别
    1)sleep是Thread方法,而wait是Object方法
    2)sleep不需要强制和synchronized配合使用,但wait需要和synchronized一起用
    3)sleep在睡眠的同时,不会释放对象锁,但wait在等待的时候会释放对象锁

          下述代码中有一个"小南"线程,五个"其他人"线程,一个"送烟的"线程。在没有烟时,小南使用sleep,不会释放room,也就是这个时候,其他人不会开始工作。

    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(room){
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				sleep(2);
    			}
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(hasCigarette){
    				log.debug("可以开始干活了");
    			}
    		}
    	},"小南").start();
    	for(int i = 0;i < 5;i++){
    		new Thread(()->{
    			synchronized(room){
    				log.debug("可以开始干活了");
    			}
    		},"其他人").start();
    	}
    	sleep(1);
    	new Thread(()->{
    		hasCigarette = true;
    		log.debug("烟到了哦!");
    	},"送烟的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    输出
    在这里插入图片描述
          下述代码在小南没有烟时,使用wait,此时会释放掉room的锁,其他人可以开始干活,当烟到的时候,调用room.notify(),唤醒线程(注意:此时因为“休息室”只有小南一人,所以notify唤醒的一定是小南)。

    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(room){
    			log.debug("有烟没?[{}]",hasCigarette );
    			if(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(hasCigarette){
    				log.debug("可以开始干活了");
    			}
    		}
    	},"小南").start();
    	for(int i = 0;i < 5;i++){
    		new Thread(()->{
    			synchronized(room){
    				log.debug("可以开始干活了");
    			}
    		},"其他人").start();
    	}
    	sleep(1);
    	new Thread(()->{
    		synchronized(room){
    			hasCigarette = true;
    			log.debug("烟到了哦!");
    			room.notify();
    		}
    		
    	},"送烟的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    输出
    在这里插入图片描述

          解决了其他干活的线程阻塞的问题,但如果有其他线程也在等待条件呢(即“休息室”不止小南一人呢)?
          下述代码有一个“小南”线程,一个“小女”线程,一个“送外卖的”线程。当没烟没外卖时,小南小女都会在“休息室”等待;当外卖送到时,触发room.notify(),按照jvm规范随机唤醒。

    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(room){
    			log.debug("有烟没?[{}]",hasCigarette );
    			if(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(hasCigarette){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小南").start();
    	new Thread(()->{
    		synchronized(room){
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(!hasTakeout ){
    				log.debug("没外卖,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(hasTakeout ){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小女").start();
    	sleep(1);
    	new Thread(()->{
    		synchronized(room){
    			hasTakeout = true;
    			log.debug("外卖到了哦!");
    			room.notify();
    		}
    		
    	},"送外卖的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    输出
    在这里插入图片描述
          下述代码有一个“小南”线程,一个“小女”线程,一个“送外卖的”线程。当没烟没外卖时,小南小女都会在“休息室”等待;当外卖送到时,触发room.notifyAll(),唤醒“休息室”全部线程(这里即唤醒小南小女)。

    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(room){
    			log.debug("有烟没?[{}]",hasCigarette );
    			if(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(hasCigarette){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小南").start();
    	new Thread(()->{
    		synchronized(room){
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(!hasTakeout ){
    				log.debug("没外卖,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(hasTakeout ){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小女").start();
    	sleep(1);
    	new Thread(()->{
    		synchronized(room){
    			hasTakeout = true;
    			log.debug("外卖到了哦!");
    			room.notifyAll();
    		}
    		
    	},"送外卖的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    输出
    在这里插入图片描述
          观察输出,可以发现"小南"线程被虚假唤醒了,这时把if改为while

    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    
    public static void main(String[] args){
    	new Thread(()->{
    		synchronized(room){
    			log.debug("有烟没?[{}]",hasCigarette );
    			while(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("有烟没?[{}]",hasCigarette);
    			if(hasCigarette){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小南").start();
    	new Thread(()->{
    		synchronized(room){
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(!hasTakeout ){
    				log.debug("没外卖,先歇会!");
    				try{
    					room.wait();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(hasTakeout ){
    				log.debug("可以开始干活了");
    			}else{
    				log.debug("没干成活...");
    			}
    		}
    	},"小女").start();
    	sleep(1);
    	new Thread(()->{
    		synchronized(room){
    			hasTakeout = true;
    			log.debug("外卖到了哦!");
    			room.notifyAll();
    		}
    		
    	},"送外卖的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    输出
    在这里插入图片描述
          用notifyAll仅解决某个线程的唤醒问题,但使用if+wait判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了。
          解决方法,用while + wait,当条件不成立,再次wait。
    格式

    synchronized(lock){
    	while(条件不成立){
    		lock.wait();
    	}
    	//干活
    }
    //另一个线程
    synchronized(lock){
    	lock.notifyAll();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    二、ReentrantLock

    相对于synchronized它具备如下特点

    • 可中断
    • 可以设置超时时间
    • 可以设置为公平锁
    • 支持多个条件变量

    与synchronized一样,都支持可重入
    基本语法

    //获取锁
    reentrantLock.lock();
    try{
    	//临界区
    }finally{
    	//释放锁
    	reentrantLock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    1.可重入

          可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

    static ReentrantLock lock = new ReentrantLock();
    
    public static void main(String[] args){
    	lock.lock();
    	try{
    		log.debug("enter main");
    		m1();
    	}finally{
    		lock.unlock();
    	}
    }
    public static void m1(){
    	lock.lock();
    	try{
    		log.debug("enter m1");
    		m2();
    	}finally{
    		lock.unlock();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    [main] - enter main
    [main] - enter m1
    [main] - enter m2

    2.可打断

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
    	Thread t1 = new Thread(()->{
    		try{
    			//如果没有竞争那么此方法就会获取lock的对象锁
    			//如果有竞争就进入阻塞队列,可以被其它线程用interruput方法打断
    			log.debug("尝试获取锁");
    			lock.lockInterruptibly();
    		}catch(InterruptedException e){
    			e.printStackTrace();
    			log.debug("没有获得锁,返回");
    			return;
    		}
    		try{
    			log.debug("获取到锁");
    		}finally{
    			lock.unlock();
    		}
    	},"t1");
    	t1.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    [ t1 ] - 尝试获取锁
    [ t1 ] - 获取到锁

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
    	Thread t1 = new Thread(()->{
    		try{
    			//如果没有竞争那么此方法就会获取lock的对象锁
    			//如果有竞争就进入阻塞队列,可以被其它线程用interruput方法打断
    			log.debug("尝试获取锁");
    			lock.lockInterruptibly();
    		}catch(InterruptedException e){
    			e.printStackTrace();
    			log.debug("没有获得锁,返回");
    			return;
    		}
    		try{
    			log.debug("获取到锁");
    		}finally{
    			lock.unlock();
    		}
    	},"t1");
    	
    	lock.lock();
    	t1.start();
    	
    	sleep(1);
    	log.debug("打断 t1");
    	t1.interrupt();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    输出
    在这里插入图片描述

    3.锁超时

    1> tryLock()

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
    	Thread t1 = new Thread(()->{
    		log.debug("尝试获得锁");
    		if(!lock.tryLock()){
    			log.debug("获取不到锁");
    			return;
    		}
    		try{
    			log.debug("获得到锁");
    		}finally{
    			lock.unlock();
    		}
    	},"t1");
    	t1.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
    	Thread t1 = new Thread(()->{
    		log.debug("尝试获得锁");
    		if(!lock.tryLock()){
    			log.debug("获取不到锁");
    			return;
    		}
    		try{
    			log.debug("获得到锁");
    		}finally{
    			lock.unlock();
    		}
    	},"t1");
    
    	lock.lock();
    	log.debug("获得到锁");
    	t1.start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    在这里插入图片描述
    2>tryLock(long timeout, TimeUnit unit)

    private static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args){
    	Thread t1 = new Thread(()->{
    		log.debug("尝试获得锁");
    		try{
    			if(!lock.tryLock(2,TimeUnit.SECONDS)){
    				log.debug("获取不到锁");
    				return;
    			}
    		}catch(InterruptedException e){
    			e.printStackTrace();
    			log.debug("获取不到锁");
    			return;
    		}
    		try{
    			log.debug("获得到锁");
    		}finally{
    			lock.unlock();
    		}
    	},"t1");
    
    	lock.lock();
    	log.debug("获得到锁");
    	t1.start();
    	sleep(1);
    	log.debug("释放了锁");
    	lock.unlock();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述

    4.条件变量

    synchronized中也有条件变量,就是我们讲原理时那个waitSet休息室,当条件不满足时进入waitSet等待
    ReentrantLock的条件变量比synchronized强大之处在于,它是支持多个条件变量的,这就好比

    • synchronized是那些不满足条件的线程都在一间休息室等消息
    • 而ReentrantLock支持多间休息室,有专门的等烟的休息室、专门放早餐的休息室、唤醒时也是按休息室来唤醒

    使用流程

    • await前需要获得锁
    • await执行后,会释放锁,进入conditionObject等待
    • await的线程被唤醒(或打断、或超时)取重新竞争lock锁
    • 竞争lock锁成功后,从await后继续执行
    static final Obejct room = new Object();
    static boolean hasCigarette = false; //有没有烟
    static boolean hasTakeout = false;   //有没有外卖
    static ReentrantLock ROOM = new ReentrantLock();
    //等待烟的休息室
    static Condition waitCigaretteSet = ROOM.newCondition();
    //等外卖的休息室
    static Condition waitTakeoutSet = ROOM.newCondition();
    
    public static void main(String[] args){
    	new Thread(()->{
    		ROOM.lock();
    		try{
    			log.debug("有烟没?[{}]",hasCigarette );
    			while(!hasCigarette){
    				log.debug("没烟,先歇会!");
    				try{
    					waitCigaretteSet.await();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("可以开始干活了");
    		}finally{
    			ROOM.unlock();
    		}
    	},"小南").start();
    	new Thread(()->{
    		ROOM.lock();
    		try{
    			log.debug("外卖送到没?[{}]",hasTakeout);
    			if(!hasTakeout ){
    				log.debug("没外卖,先歇会!");
    				try{
    					waitTakeoutSet.await();
    				}catch(InterruptedException e){
    					e.printStackTrace();
    				}
    			}
    			log.debug("可以开始干活了");
    		}finally{
    			ROOM.unlock();
    		}
    	},"小女").start();
    	sleep(1);
    	new Thread(()->{
    		ROOM.lock();
    		try{
    			hasTakeout = true;
    			waitTakeoutSet.signal();
    		}finally{
    			ROOM.unlock();
    		}
    	},"送外卖的").start();
    	sleep(1);
    	new Thread(()->{
    		ROOM.lock();
    		try{
    			hasCigarette = true;
    			waitCigaretteSet.signal();
    		}finally{
    			ROOM.unlock();
    		}
    	},"送烟的").start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    在这里插入图片描述

    三、共享模型之内存

    1.Java内存模型

    JMM即Java MemoryModel,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。
    JMM体现在以下几个方面

    • 原子性-保证指令不会受到线程上下文切换的影响
    • 可见性-保证指令不会受cpu缓存的影响
    • 有序性-保证指令不会受cpu指令并行优化的影响

    2.可见性

    退不出的循环
    先来看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止

    static boolean run = true;
    
    public static void main(String[] args)throws InterruptedException{
    	Thread t = new Thread(()->{
    		while(run){
    			//....
    		}
    	});
    	t.start();
    	
    	sleep(1);
    	run = false;  //线程t不会如预想的停下来
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为什么呢?分析一下
          1.初始状态,t线程刚开始从主内存读取了run的值到工作内存
    在这里插入图片描述
          2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率
    在这里插入图片描述
          3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
    在这里插入图片描述
    解决方法
    volatile(易变关键字)
          它可以用来修饰成员变量和静态成员变量,可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

    volatile static boolean run = true;
    
    public static void main(String[] args)throws InterruptedException{
    	Thread t = new Thread(()->{
    		while(run){
    			//....
    		}
    	});
    	t.start();
    	
    	sleep(1);
    	run = false;  //线程t不会如预想的停下来
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    static boolean run = true;
    //锁对象
    final static Object lock = new Object();
    
    public static void main(String[] args)throws InterruptedException{
    	Thread t = new Thread(()->{
    		while(true){
    			//....
    			synchronized(lock){
    				if(!run){
    					break;
    				}
    			}
    		}
    	});
    	t.start();
    	
    	sleep(1);
    	log.debug("停止t");
    	synchronized(lock){
    		run = false;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

          synchronized和volatile都可以解决可见性问题,但是synchronized需要创建monitor,属于重量级操作,volatile相对更轻量,建议使用volatile。

    可见性vs原子性
    前面例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况:
    上例从字节码理解是这样的:
    在这里插入图片描述
    比较一下之前我们讲线程安全时举的例子:两个线程一个i++一个i–,只能保证看到最新值,不能解决指令交错
    在这里插入图片描述
    注意
    synchronized语句块既可以保证代码块的原子性,也同时保证代码块内变量的可见性。但缺点是synchronized是属于重量级操作,性能相对更低。
    如果在前面示例的死循环中加入System.out.println()会发现即使不加volatile修饰符,线程t也能正确看到对run变量的修改了,想一想为什么?

    3.有序性

    JVM会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

    static int i;
    static int j;
    
    //在某个线程内执行如下赋值操作
     i = ...;
     j = ...;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

    i = …;
    j = …;

    也可是

    j = …;
    i = …;

    这种特性称之为【指令重排】,多线程下【指令重排】会影响正确性。为什么要有重排指令这项优化呢?从CPU执行指令的原理来理解一下把
    诡异的结果

    int num = 0;
    boolean ready = false;
    
    //线程1执行此方法
    public void actor1(I_Result r){
    	if(ready){
    		r.r1 = num + num;
    	}else{
    		r.r1 = 1;
    	}
    }
    //线程2执行此方法
    public void actor2(I_Result r){
    	num = 2;
    	ready = true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    I_Result 是一个对象,有一个属性r1用来保存结果,问,可能的结果有几种
    有同学这么分析

    • 情况1:线程2先执行,这时ready = false,所以进入else分支结果为1
    • 情况2:线程2线执行num = 2,但没来得及执行ready = true,线程1执行,还是进入else分支,结果为1
    • 情况3:线程2执行到ready = true,线程1执行,这回进入if分支,结果为4

    但我告诉你,结果还有可能是0,信不信吧!
    这种情况下是:线程2执行ready = true,切换到线程1,进入if分支,相加为0,再切回线程2执行num = 2
    相信很多人已经晕了。

    指令重排序的验证,可以借助java并发压测工具jcstress

    那如何禁用指令重排序呢?加volatile

    int num = 0;
    volatile boolean ready = false;
    
    //线程1执行此方法
    public void actor1(I_Result r){
    	if(ready){
    		r.r1 = num + num;
    	}else{
    		r.r1 = 1;
    	}
    }
    //线程2执行此方法
    public void actor2(I_Result r){
    	num = 2;
    	ready = true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    为什么加在了ready上而不是num上,现在先简单提一下,此处的volatile相当于在ready指令下加了一个屏障,禁止ready上面的语句掉到屏障下面,即保证了先执行num = 2,再执行ready = true。

    四、volatile原理

    volatile的底层实现原理时内存屏障,Memory Barrier(Memory Fence)

    • 对volatile变量的写指令后会加入写屏障
    • 对volatile变量的读指令前会加入读屏障

    1.如何保证可见性

    • 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    public void actor2(I_Result r){
    	num = 2;
    	ready = true;  //ready是volatile赋值带写屏障
    	//写屏障
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
    public void actor1(I_Result r){
    	//读屏障
    	//ready是volatile读取值读屏障
    	if(ready){
    		r.r1 = num + num;
    	}else{
    		r.r1 = 1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    2.如何保证有序性

    • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    public void actor2(I_Result r){
    	num = 2;
    	ready = true;  //ready是volatile赋值带写屏障
    }
    
    • 1
    • 2
    • 3
    • 4
    • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
    public void actor1(I_Result r){
    	//ready是volatile读取值读屏障
    	if(ready){
    		r.r1 = num + num;
    	}else{
    		r.r1 = 1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    还是那句话,不能解决指令交错:

    • 可见性仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
    • 而有序性的保证也只是保证了本线程内相关代码不被重排序

    在这里插入图片描述
          synchronized可以保证可见、共享、有序性,前提:共享变量都交给synchronized来管理

    学习视频
    黑马程序员全面深入学习Java并发编程,JUC并发编程全套教程

  • 相关阅读:
    ArcGIS数据处理与制图
    python使用蓝牙库选择
    AVL树简介
    Kubernetes来去今生与基础理论
    spring boot 1.封装分页插件pagehelp并做统一做返回值 2.封装SQL 3.redis在mybatis里面使用(大综合)
    IP初学习
    工业CT检测技术及工业CT基本组成
    【计组 期末版】计算机组成原理笔记目录
    同余方程(记录)
    LiveGBS流媒体平台GB/T28181常见问题-海康大华宇视硬件NVR摄像头通道0未获取到视频通道如何排查如何抓包分析
  • 原文地址:https://blog.csdn.net/l903445981/article/details/126040237