高并发编程基础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);
	}
}

  

  

posted @ 2018-11-05 13:31  吴振照  阅读(595)  评论(0编辑  收藏  举报