zoukankan      html  css  js  c++  java
  • 高并发编程基础Synchronized与Volatile(操作层面)

    关键字Synchronized:

      当使用Synchrnized (o) ,锁定 o 的时候,锁定的是 o 指向的堆内存中 new 出来的对象,而非 o 引用,当锁定 o 以后,一旦 o 指向了其他对象,这个时候锁定的对象也会发生改变。在工作开发中经常 new 出一个对象当锁太麻烦了,常用的方法是锁定执行方法的对象,即 Synchronized (this)。任何线程要执行同步的代码,必须先获得 this 的锁。锁定 this 对象还有一种写法就是写在方法申明上 public synchronized void method(){} .需要注意的是对于静态(static)方法中不可以使用Synchronized (this),因为静态的属性或方法不需要 new 出来对象来访问的,也就是没有 this 引用的存在 。Synchronized所同步的代码块越少越好,细粒度的锁能提高效率 下面看一些例子要进一步认识Synchronized。

    public class Test5  implements Runnable{
    
    	private int count =10;
    	
    	@Override
    	public /*synchronized*/ void run() {// synchronized的代码块是原子操作,不可分,只要当前线程操作完了,其他线程才能访问
    		
    		count --;
    		// 线程重入 当线程1执行到这里,线程2,3.。也执行到这里,所有控制台有可能输出不一致问题。控制台打印如下。
    //		Thread0count:8
    //		Thread4count:5
    //		Thread2count:6
    //		Thread1count:8
    //		Thread3count:7
    		System.err.println(Thread.currentThread().getName()+ "count:"+count);
    	}
    
    	public static void main(String[] args) {
    		Test5 t =new Test5();
    		for(int i=0;i<5;i++) {
    			new Thread(t,"Thread"+i ).start();
    		}
    	}
    }
    

      要解决以上线程重入,只需要在方法上添加关键字Synchronized 即可。 因为Synchronized的代码块是原子操作,不可分,不可以被打断。就能避免线程重入。

    public class Test6{
    	
    	public synchronized void m1() {
    		System.err.println(Thread.currentThread().getName()+ "m1  start:");
    		try {
    			Thread.sleep(10000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.err.println(Thread.currentThread().getName()+ "m1  end:");
    	}
    	public void m2() {
    		System.err.println(Thread.currentThread().getName()+ "m2  start:");
    		try {
    			Thread.sleep(5000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.err.println(Thread.currentThread().getName()+ "m2  end:");
    	}
    	
    	public static void main(String[] args) {
    		Test6 t =new Test6();
    		//再调用m1的过程之中能否访问m2  当然可以
    		new Thread(t::m1,"t1").start();
    		new Thread(t::m2,"t2").start();
    	}
    }
    

      上面这个小例子要说明的是,当同步方法被调用的过程中能否调用非同步方法,通过执行以上代码可以发现,是可以的。

    public class Account{
    	String name;
    	double balance;
    	
    	public synchronized void set(String name ,double balance) {
    		this.name=name;
    		try { // 放大线程执行的时间差,表明有可能在执行过程中有其他线程来通过getBalance()方法获取balance;
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		this.balance=balance;
    	}
    	public  double getBalance(String name) {
    		
    		return this.balance;
    	}
    	
    	public static void main(String[] args) {
    		Account a =new Account();
    		new Thread(()->a.set("zhangsan",100.0)).start();
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println(a.getBalance("zhangsan"));
    		try {
    			TimeUnit.SECONDS.sleep(2);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.out.println(a.getBalance("zhangsan"));
    	}
    }
    

      上述代码对业务写方法加锁,读方法不加锁,容易造成脏读,要解决以上问题,只要在getBalance()上加Synchronized就可以。

    public class Test7{
    	public synchronized void m1() {
    		System.err.println(Thread.currentThread().getName()+ "m1  start:");
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		m2();
    	}
    	public synchronized void m2() {
    		System.err.println(Thread.currentThread().getName()+ "m2  start:");
    		try {
    			TimeUnit.SECONDS.sleep(2);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.err.println(Thread.currentThread().getName()+ "m2  end:");
    	}
    
    	public static void main(String[] args) {
    		Test7 t =new Test7();
    		//再调用m1的过程之中能否访问m2  当然可以
    		new Thread(t::m1,"t1").start();
    	}
    }
    

      上诉代码阐述了一个问题,一个同步方法可以调用另外一个同步方法,一个线程已经拥有某个对象的锁,再次申请的时候还会得到该对象的锁,也就是说synchronized的锁是可以重入的。由于这里锁定是同一个对象this.所以不会产生死锁,产生死锁的情况有很多,其中最简单的一种情况入下:

    	public static void main(String[] args) {
    		Object a =new Object();
    		Object b =new Object();
    		new Thread(()->{
    			synchronized(a) {
    				System.out.println("锁定a");
    				try {
    					TimeUnit.SECONDS.sleep(2000);
    				} catch (InterruptedException e) {
    					// TODO Auto-generated catch block
    					e.printStackTrace();
    				}
    				synchronized(b) {
    					System.out.println("锁定b");
    				}
    			}
    			
    		},"t1").start();
    		new Thread(()->{
    			synchronized(b) {
    				System.out.println("锁定b");
    				synchronized(a) {
    					System.out.println("锁定a");
    				}
    			}
    		},"t2").start();
    	}
    

      重入锁还有另外一种情形,即子类的同步方法调用父类的同步方法,其本职也是锁定this对象。代码如下:

    public class Test8{
    	public synchronized void m1() {
    		System.err.println( "m1  start:");
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		System.err.println( "m1  end:");
    	}
    	
    	public static void main(String[] args) {
    	    new TT().m1();;
    	}
    }
    class TT extends Test8{
    	
    	@Override
    	public synchronized void m1() {
    		System.err.println( " child m1  start:");
    		super.m1();
    		System.err.println( " child m1  end:");
    	}
    }
    

      下面来看一下另外一个问题,当线程在执行过程中如果有异常抛出的话会产生什么样的后果呢?

    public class Test9{
    
    	int count =0;
    	public synchronized void m1() {
    		System.err.println(Thread.currentThread().getName()+ "  start:");
    		while(true) {
    			count ++;
    			System.err.println(Thread.currentThread().getName()+ " count :"+count);
    			try {
    				TimeUnit.SECONDS.sleep(1);
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    			if(count ==5) {
    				int i=1/0;
    			}
    		}
    		
    	}
    	
    	public static void main(String[] args) {
    		Test9 t =new Test9();
    		//再调用m1的过程之中能否访问m2  当然可以
    		new Thread(t::m1,"t1").start();
    		try {
    			TimeUnit.SECONDS.sleep(3);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		new Thread(t::m1,"t2").start();
    	}
    }
    

      上述代码执行过程中抛出了 ArithmeticException ,抛出异常后该线程立马会释放锁。可以看到运行该程序后,在异常抛出后,t2线程会执行,即证明了t1释放锁。然后t2会拿着t1执行了一半的数据再去处理自己的业务,在程序中这样会发生很严重的数据问题。所以在同步方法内如果会抛出异常,一定要堆异常进行适当的处理,避免类似问题的出现。

    public class Test12 {
    
    	Object o = new Object();
    
    	void m() {//
    		synchronized (o) {//锁的是堆内存里new出来的对象,一旦o指向了另外的对象,那么原来的锁将被释放
    			while (true) {
    				try {
    					TimeUnit.SECONDS.sleep(1);
    				} catch (InterruptedException e) {
    					e.printStackTrace();
    				}
    				System.err.println(Thread.currentThread().getName() );
    			}
    		}
    	}
    
    	public static void main(String[] args) {
    		Test12 t = new Test12();
    		new Thread(t::m,"t1").start();//启动第一个线程
    		try {
    			TimeUnit.SECONDS.sleep(2);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		//启动第二个线程
    		Thread t2 = new Thread(t::m,"t2");//锁对象发生改变 t2才能执行
    		t.o =new Object();
    		t2.start();
    		
    	}
    }
    

      上述小程序描述了synchronized 锁,锁的是堆内存里new出来的对象,一旦o指向了另外的对象,那么原来的锁将被释放,通过上述小程序运行发现当执行完t.o =new Object(); t2线程会随即运行,不然一定要等t1线程释放锁t2才得以运行。

    注释掉 t.o =new Object(); 会发现t2是无法运行的。

     关键字 Volatile :

      先来看一下一段小程序:

    public class Test10{
    
    	/*volatile*/ boolean running =true; //对比有无 volatile的情况下,整个程序的执行结果
    	public synchronized void m1() {
    		System.err.println(Thread.currentThread().getName()+ " start:");
    		while(running) {
    			
    		}
    		System.err.println(Thread.currentThread().getName()+ " end:");
    		
    	}
    	
    	public static void main(String[] args) {
    		Test10 t =new Test10();
    		new Thread(t::m1,"t1").start();
    		try {
    			TimeUnit.SECONDS.sleep(1);
    		} catch (InterruptedException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    		t.running=false;
    	}
    }
    

      执行小程序,我们会发现在没有 volatile 的情况下,程序一直会处于运行的情况,也就是其中变量running一直是 true 的状态,从而导致方法 m1 一直处于死循环的状态,当加上了关键字 volatile 后,程序会结束,这是为什么呢? 其实这其中就涉及了线程中 running 这个变量可见性的问题,也就是线程之间的通讯问题,这里涉及到 JAVA 对于线程处理的内存模型(Java Memory Model),在Java Memory Model里面有一个内存叫主内存,我们所说的栈内存,堆内存,都可以认为是主内存,每一个线程在执行的过程中,都会有自己的一块内存,而这块内存不是真正的内存,实际上是CPU上的一块缓冲区,其实就是存放线程自己的变量的一块内存区,它的作用是在线程运行时,将主内存中的内容读过来在缓冲区内做修改,执行完修改动作了再将结果写回到主内存。但是在处理的过程中线程不会再去到主内存中读取该内容,上述代码中由于while死循环使得CPU非常的繁忙,他就不再去主内存中读取running的值,而是直接再自己的缓冲区中读取该变量的值,但是在其他情况下,当CPU并不是那么的繁忙的时候,还是会去读一下的。主线程把 running 改成了 false ,但是t1 线程没有去主内存中重新获取running的值,由于缓冲区中running的值是true,所以导致线程一直处于死循环。加了 volatile 之后,在 running 的值发生了改变,会通知其他线程 ,你们的缓冲区中  running 的值过期了,这个时候,其他线程才会去主内存中重新获取 running 的值,从而才能使线程结束。

      如果不使用 volatile 的话,可以使用 synchornized ,但是性能方面会大幅度降低, volatile 的性能比 synchronized的性能要好得多。volatile 可以说使无锁同步,使得两个线程之前的变量的可见性。

      volatile不能保证多个线程共同修改running的值带来的不一致问题,也就是说 volatile 不能代替 synchronized ,synchronized即保证了原子性,也保证了可见性,而volatile仅仅保证了可见性,来看一下下一个小程序:

    public class Test11{
    
    	volatile int count =0;//只保证可见性
    	void m() {
    
    		for(int i=0;i<10000;i++) {
    			count ++;
    		}
    		
    	}
    	
    	public static void main(String[] args) {
    		Test11 t =new Test11();
    		 List<Thread> threads =new  ArrayList<Thread>();
    		 
    		 for(int i=0;i<10;i++) {
    			 threads.add(new Thread(t::m,"thread"+i));
    		 }
    		 threads.forEach((o)->o.start());
    		 threads.forEach((o)->{
    			 
    			 try {
    				o.join();
    			} catch (InterruptedException e) {
    				e.printStackTrace();
    			}
    		 });
    		 System.out.println(t.count);
    	}
    }
    

      理论上上述小程序输出的结果会是100000,但是结果并不是如此,为什么会这样呢? 因为volatile 仅仅保证可见性,当两个线程同时读取到count为 10的时候,线程1 对count 执行完++ 以后,将11写回,此刻线程2也将自己的++以后的结果写回,这个时候就会出现这种问题,线程2覆盖了线程1 的结果。实际上就加了一遍,如果有多个线程,可能发生多次覆盖。要解决这个问题,可以使用 synchronized ,在方法 m 前 加上synchronized,去掉 count 的volatile即可。如果程序中仅仅涉及数字的简单加减。可以使用JAVA提供的原子类 AtomicXXX来进行操作。因为AtomicXXX这些类所提供的方法都是原子性的,但是AtomicXXX类两个方法之间是不具备原子性的,比如AtomicInteger 的++  可以使用incrementAndGet()方法等等。修改以上代码如下:

    public class Test11{
    
    //	/*volatile*/ int count =0;//只保证可见性
    	AtomicInteger count =new AtomicInteger(0);
    //	AtomicBoolean ,AtomicLong
    	/*synchronized*/ void m() {
    
    		for(int i=0;i<10000;i++) {
    			//count ++;
    			//if count.get() <1000  再这句中间和下面一行代码之间是不具备原子性的
    			count.incrementAndGet();// 具备原子性  代替count++
    		}
    		
    	}
    	
    	public static void main(String[] args) {
    		Test11 t =new Test11();
    		 List<Thread> threads =new  ArrayList<Thread>();
    		 
    		 for(int i=0;i<10;i++) {
    			 threads.add(new Thread(t::m,"thread"+i));
    		 }
    		 threads.forEach((o)->o.start());
    		 threads.forEach((o)->{
    			 
    			 try {
    				o.join();
    			} catch (InterruptedException e) {
    				// TODO Auto-generated catch block
    				e.printStackTrace();
    			}
    		 });
    		 System.out.println(t.count);
    	}
    }
    

      

      

  • 相关阅读:
    SQL 使用identity(int,1,1)来产生行号。
    SQL DateName\DatePart 返回表示指定date的指定datepart的字符串
    让我们受用一生的好习惯
    SCRUM软件开发过程(转)
    计算机英语词汇
    oral English英语绕口令(转)
    Setup相关经验总结
    与老外吵架之必会109句
    BAT批处理文件语法(转)
    SQL Server 2005之PIVOT/UNPIVOT行列转换(转)
  • 原文地址:https://www.cnblogs.com/wuzhenzhao/p/9908663.html
Copyright © 2011-2022 走看看