Java同步原语synchronized、volatile

1,195 阅读5分钟

synchronized

一般的多线程,执行顺序随机:

public class ThreadTest {
 
	public static void main(String[] args)
	{
		for(int i=0;i<3;i++)
		{
			MyThread temp=new MyThread();
			temp.start();
		}
	}
}
 
class MyThread extends Thread
{
	@Override
	public void run()
	{
		for(int i=0;i<5;i++)
		{
			System.out.println(Thread.currentThread().getName()+": ["+i+"]");
		}
	}
}

执行效果:

使用synchronized关键字修饰的多线程会顺序执行:

package 同步;

public class SynchronizedTest {

	static int number=0;
	//synchronized块(对象级)
	public String setNumber1()
	{
		 synchronized(this)
		 {
			number++;			
			return "setNumber1.:"+number;
		 }			
	}
	
	//synchronized块(类级别)
	public String setNumber2()
	{
		 synchronized(SynchronizedTest.class)
		 {
			number++;			
			return "setNumber2.:"+number;
		 }		
	}
	
	//synchronized 方法
	public synchronized String setNumber3()
	{		
		number++;			
		return "setnumber3.:"+number; 		
	}
	public String setNumber4() {
		return"setnumber4.:"+number;
	}
}

package 同步;
public class Test{
	public static void main(String[] args)
	{
		SynchronizedTest t = new SynchronizedTest();
		for(int i=0;i<3;i++)
		{
			MyThread2 temp=new MyThread2(t);
			temp.start();
		}
	}
}
class MyThread2 extends Thread{
	SynchronizedTest Object;
	public MyThread2(SynchronizedTest Object) {
		this.Object = Object;
	}
	@Override
	public void run() {
		// TODO Auto-generated method stub
	  try {
		Thread.sleep(0);
	} catch (InterruptedException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	  System.out.println(Thread.currentThread().getName()+"--"+Object.setNumber1());
	  System.out.println(Thread.currentThread().getName()+"--"+Object.setNumber3());
	}
	
}

执行效果:

所以使用锁进行多线程的同步就能控制执行的顺序。

volatile

volatile的读写操作与锁的释放和获取有着同样的效果

public class VolatileTest {
    volatile int number = 0;

	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}
    public int getandIncrement() {
    	number++;
    	return number;
    }
    public static void main(String args[]) {
 	   VolatileTest test = new VolatileTest();
 	   test.setNumber(5);
 	   System.out.println( test.getNumber());
 	 test.getandIncrement();
 	  System.out.println(test.getandIncrement());
    }
    
}

运行结果:

package 同步;

public class VolatileExample {
   int number = 0;
   public synchronized void setNumber(int number) {
	  this.number = number;
   }
   public int getandIncrement() {
   	int t =getNumber();
   	t+= 1;
   setNumber(t);
   return t;
   }
   public synchronized int getNumber() {
		return number;
	}
   public static void main(String args[]) {
	   VolatileExample test = new VolatileExample();
	   test.setNumber(5);
	   System.out.println( test.getNumber());
	 test.getandIncrement();
	  System.out.println(test.getandIncrement());
   }
}

运行结果:

从上面的例子中可以看出对一个 volatile变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同。锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个 volatile变量最后的写入。锁的语义决定了临界区代码的执行具有原子性。这意味着即使是 64 位的 long 型和double 型变量,只要它是volatile变量,对该变量的读写就将具有原子性。如果是多个volatile操作或类似于 volatile++这种复合操作,这些操作整体上不具有原子性。简而言之,volatile 变量自身具有下列特性:

 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。

 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++这 种复合操作不具有原子性

volatile 写-读建立的 happens before 关系

从内存语义的角度来说,volatile 的写-读与锁的释放-获取有相同的内存效果: volatile 写和锁的释放有相同的内存语义;volatile 读与锁的获取有相同的内存语 义。 请看下面使用 volatile 变量的示例代码:

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

假设线程 A 执行 writer()方法之后,线程 B 执行 reader()方法。根据 happens before 规则,这个过程建立的 happens before 关系可以分为两类:

  1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
  2. 根据 volatile 规则,2 happens before 3。
  3. 根据 happens before 的传递性规则,1 happens before 4。 上述 happens before 关系的图形化表现形式如下: 在上图中,每一个箭头链接的两个节点,代表了一个 happens before 关系。黑色 箭头表示程序顺序规则;橙色箭头表示 volatile 规则;蓝色箭头表示组合这些规则 后提供的 happens before 保证。 这里 A 线程写一个 volatile 变量后,B 线程读同一个 volatile 变量。A 线程在写 volatile 变量之前所有可见的共享变量,在 B 线程读同一个 volatile 变量后,将立 即变得对 B 线程可见。 volatile读写时就是几个线程通过主内存进行通信

volatile 写-读的内存语义 volatile 写的内存语义如下: .当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷 新到主内存。 以上面示例程序 VolatileExample 为例,假设线程 A 首先执行 writer()方法,随后 线程 B 执行 reader()方法,初始时两个线程的本地内存中的 flag 和 a 都是初始状 态。

线程 A 在写 flag 变量后,本地内存 A 中被线程 A 更新过的两个共享变量的值被刷新到主内存中。此时,本地内存 A 和主内存中的共享变量的值是一致的。

volatile 读的内存语义如下:

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

在读 flag 变量后,本地内存 B 包含的值已经被置为无效。此时,线程 B 必须从主内存中读取共享变量。线程 B 的读取操作将导致本地内存 B 与主内存中的共享变量的值也变成一致的了。如果我们把 volatile 写和 volatile 读这两个步骤综合起来看的话,在读线程 B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值 都将立即变得对读线程 B 可见。下面对 volatile 写和 volatile 读的内存语义做个总结:

线程 A 写一个 volatile 变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息。

线程 B 读一个 volatile 变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息。

线程 A 写一个 volatile 变量,随后线程 B 读这个 volatile 变量,这个过程实质上是线程 A 通过主内存向线程 B发送消息。