• 多线程之wait,notify,volatile,synchronized,sleep


      最近在学习多线程,现在进行总结一下吧。首先要了解一下以下几个名词。

      (1)wait:当线程调用wait()方法时,当前该线程会进入阻塞状态,且释放锁,使用wait方法的时候,必须配合synchronized使用

      (2)notify:当线程调用notify()方法时,会唤醒一个处于等待该对象锁的线程,不释放锁,使用notify方法的时候,必须配合synchronized使用

      (3)sleep:当线程调用sleep()方法时,会让出CPU执行权,不释放锁。当指定的时间到了后,会自动恢复运行状态。

      (4)volatile:可见性,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性。(当多个线程操作同一个成员变量的时候,为了提高效率,JVM为每个线程单独复制了一份,这样会导致各个线程读取的数据出现脏数据,所以使用volatile关键字可以解决脏数据问题)。

      (5)synchronized:synchronized为一段操作或内存进行加锁,它具有互斥性。当线程要操作被synchronized修饰的内存或操作时,必须首先获得锁才能进行后续操作;但是在同一时刻只能有一个线程获得相同的一把锁(对象监视器),所以它只允许一个线程进行操作。

      (6)原子性:类似"a += b"这样的操作不具有原子性,在JVM中"a += b"可能要经过这样三个步骤:

        (1)取出a和b

        (2)计算a+b

        (3)将计算结果写入内存

      如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的买碘片例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。类似的,像"a++"这样的操作也都不具有原子性,需要配合synchronized实现原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

      (7)调用start()方法的顺序不代表线程的启动顺序,线程启动的顺序具有不确定性。

      下面写一个简单的例子,线程t1向集合list添加数据,当集合的大小等于5的时候,通知t2线程向集合添加数据。

    public class ListAdd {
    	//集合
    	public static volatile List<String> list=new ArrayList<String>();
    	//向集合增元素
    	public void add(){	
    	  list.add("gdpuzxs");
    	}
    	//返回集合大小
    	public int size(){	
    	  return list.size();
    	}
    	public static void main(String[]args){
    		final ListAdd listAdd=new ListAdd();
    		//创建锁
    		final Object lock =new Object();
    		//线程t1
    		Thread t1=new Thread(new Runnable() {
    			public void run() {
    				System.out.println("进入t1");
    				synchronized (lock) {
    					for(int i=0;i<10;i++){
    						listAdd.add();
    						try {
    							Thread.sleep(500);
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    						System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素");
    						if(listAdd.size()==5){
    							System.out.println("发出唤醒通知!");
    							lock.notify();
    						}
    					}
    				}
    			}
    		},"t1");
    		//线程t2
    		Thread t2=new Thread(new Runnable() {
    			public void run() {
    				System.out.println("进入t2");
    				synchronized (lock) {
    					if(listAdd.size()!=5){
    						try {
    							lock.wait();
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    					}
    					System.out.println("当前线程:"+Thread.currentThread().getName()+"已经收到通知!");
    					for(int i=0;i<5;i++){
    						listAdd.add();
    						System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素");
    					}
    				}
    			}
    		},"t2");
    		t2.start();
    		t1.start();
    	}
    }
    

      运行结果如下:

      

      分析结果:运行多次会出现上面两个不同的结果集。(证明调用start()方法的顺序不代表线程的启动顺序,线程启动的顺序具有不确定性。)

      结果(1):线程t2先抢到CPU操作权且拿到了对象锁,然后判断size!=5,调用了wait方法,进入阻塞状态,释放锁。线程t1拿到CPU操作权,向集合添加了5个元素后,size=5,调用notify方法唤醒线程t1,由于nofity方法不释放锁,所以t1继续添加5个元素。当t1执行完毕后,释放锁,这是t2拿到锁后继续添加5个元素。

      结果(2):线程t1先抢到CPU操作权且拿到了对象锁,然后添加了5个元素,当size=5的时候,调用了notify方法(这时t1没有线程处于阻塞状态,所以调用该方法一点作用都没有),接着t1继续添加5个元素。当t1执行完毕,释放锁。t2拿到对象锁后,判断size!=5为true,调用了wait方法进入阻塞状态。下面代码没有执行。程序一直处于运行状态。

      这个例子由于notify不释放锁,导致线程之间存在一个时效性。即t1虽然唤醒t2,但是由于t1不释放锁,所以t1执行完synchronized方法里面的代码后,t2获取锁才能继续执行。可以使用countDownLatch解决时效性的问题。代码如下:

      

    public class ListAdd2 {
    	//集合
    	public static volatile List<String> list=new ArrayList<String>();
    	//向集合增元素
    	public void add(){	
    		list.add("gdpuzxs");
    	}
    	//返回集合大小
    	public int size(){	
    		return list.size();
    	}
    	public static void main(String[]args){
    		final ListAdd2 listAdd2=new ListAdd2();
    		
    		final CountDownLatch countDownLatch=new CountDownLatch(1);
    		//线程t1
    		Thread t1=new Thread(new Runnable() {
    			public void run() {
    				System.out.println("进入t1");
    					for(int i=0;i<10;i++){
    						listAdd2.add();
    						try {
    							Thread.sleep(500);
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    						System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素");
    						if(listAdd2.size()==5){
    							System.out.println("发出唤醒通知!");
    							countDownLatch.countDown();
    						}
    					}
    				}
    		},"t1");
    		//线程t2
    		Thread t2=new Thread(new Runnable() {
    			public void run() {
    				System.out.println("进入t2");
    					if(listAdd2.size()!=5){
    						try {
    							countDownLatch.await();
    						} catch (InterruptedException e) {
    							e.printStackTrace();
    						}
    					}
    					System.out.println("当前线程:"+Thread.currentThread().getName()+"已经收到通知!");
    					for(int i=0;i<5;i++){
    						listAdd2.add();
    						System.out.println("当前线程:"+Thread.currentThread().getName()+"添加了一个元素");
    					}
    				}
    		},"t2");
    		t1.start();
    		t2.start();
    	}
    }

      结果如下:使用countDownLatch.countDown(),countDownLatch.await()释放锁。

      

      参考网址:http://www.cnblogs.com/mengyan/archive/2012/08/22/2651575.html

  • 相关阅读:
    JAVA-初步认识-第九章-抽象类-特点
    JAVA-初步认识-第九章-抽象类-概述
    JAVA-初步认识-第九章-面向对象-final关键字
    JAVA-初步认识-第九章-继承-子父类中的构造函数-子类的实例化过程-内存图解
    JAVA-初步认识-第九章-继承-子父类中的构造函数-子类的实例化过程-细节
    JAVA-初步认识-第九章-继承-子父类中的构造函数-子类的实例化过程
    JAVA-初步认识-第八章-继承-子父类中成员变量的特点-覆盖的应用
    JAVA-初步认识-第八章-继承-子父类中成员函数特点-覆盖
    JAVA-初步认识-第八章-继承-子父类中成员变量的内存图解
    JAVA-初步认识-第八章-继承-子父类中成员变量的特点
  • 原文地址:https://www.cnblogs.com/gdpuzxs/p/6665590.html
Copyright © 2020-2023  润新知