多线程基础之synchronized和volatile

303 阅读6分钟

多线程安全三大特性:

(1)原子性:

指一系列操作要么一起执行完成,要么一起不执行。例如i++操作其实并不是原子的,线程需要先获取到i的值然后在线程内存中对i的值进行+1再刷新到主内存中,在这个期间可能有别的线程对i的值进行了修改,这样得出的结果就是错误的,所以我们需要同步锁Synchronized(Volatile并不是原子性)。

(2)可见性:

线程之间变量相互可见。假设有一个全局变量i的值为0,同时有线程A和线程B对一个全局变量i执行++操作那么在JMM(JAVA内存模型)中由A线程获取全局变量i的值后在线程内存中对i执行++操作(由于线程互相之间不可见此时对B线程来说i还是0),此时B线程同时进行与A线程一样的操作,在最后刷新到主内存中那么i的值就为1而不是2。这就是线程之间互相不可见造成的线程不安全。


                                                    线程安全情况下





                                                    线程不安全情况下

(3)有序性:

即程序执行的顺序按照代码的先后顺序执行。在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。


synchronized:

java中自带的同步锁,可修饰方法或用来同步代码块

public class ThreadDemo implements Runnable {
    static int a=100;
	Object obj=new Object();
	@Override
	public void run() {
		test3();
	}
	/**
	 * Synchronzed修饰方法 此时相当于使用Synchronized(this){...}包裹了方法中的所有代码
	 */
	public  synchronized void test1() {
		System.out.println();
	}
	
	/**
	 *  synchronized 代码块选择一个对象作为对象锁 只有获取到该对象锁的线程才可以访问代码块
	 */
	public void test2() {
		synchronized(obj){
			System.out.println();
		}
	}
	
	/**
	 *  synchronized 修饰静态方法 相当于synchronized(ThreadDemo.class) 以类字节码文件作为对象锁  所有该类的对象想访问代码都必须获取同一个锁
	 */
	public synchronized static void test3() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300);
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a--;
			System.out.println(Thread.currentThread().getName()+"----"+a);
		}
	}

} 

1.修饰方法:

等于synchronized(this){...},一个对象实例中同时只能有一条线程访问该方法,如果该对象实例中有其它修饰了synchronized的方法,那么它们将共用一把锁。


2.同步代码块:

语法:synchronized(obj){...},一个对象实例中同时只能有一条线程访问该同步代码块,如果该对象实例中有其它使用了这个obj对象锁修饰的代码块,那么它们将共用一把锁。


3.修饰静态方法:

等于synchronized(xxx.class){...},所有该class的实例对象共用同一把锁,以class字节码文件作为对象锁。

代码如下:

public class ThreadDemo implements Runnable {
    private static int a=0;

	@Override
	public void run() {
		add();
	}
   
	public static void main(String[] args) {
		Thread t1=new Thread(new ThreadDemo(),"线程1");
		Thread t2=new Thread(new ThreadDemo(),"线程2");
		t1.start();
		t2.start();
	}
	
	public synchronized static void add() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300); /**非线程安全情况下让线程休眠堆积 重新调度 产生竟态条件**/
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a++;
			System.out.println(Thread.currentThread().getName()+":----"+a);
		}
		
	}

}

运行结果:

线程1:----1
线程1:----2
线程1:----3
线程1:----4
……
线程1:----40
线程1:----41
线程1:----42
线程1:----43
线程1:----44
线程1:----45
线程1:----46
线程1:----47
线程1:----48
线程1:----49
线程1:----50
线程2:----51
线程2:----52
线程2:----53
线程2:----54
线程2:----55
线程2:----56
线程2:----57
线程2:----58
线程2:----59
……
线程2:----96
线程2:----97
线程2:----98
线程2:----99
线程2:----100

synchronized在使用时需要注意性能问题,应自己衡量好性能与线程安全之间的平衡。

volatile:

java自带的关键字,用来修饰变量,被声明的变量对所有线程可见同时禁止重排序,但不保证原子性,假设有多条线程同时对变量值进行修改还是会出现线程不安全。

修改上述代码:

public class ThreadDemo implements Runnable {
    private volatile static int a=0;

	@Override
	public void run() {
		add();
	}
   
	public static void main(String[] args) {
		Thread t1=new Thread(new ThreadDemo(),"线程1");
		Thread t2=new Thread(new ThreadDemo(),"线程2");
		t1.start();
		t2.start();
	}
	
	public  static void add() {
		for (int i = 0; i < 50; i++) {
			try {
				Thread.sleep(300); /**非线程安全情况下让线程休眠堆积 重新调度 产生竟态条件**/
			} catch (InterruptedException e) {
				
				e.printStackTrace();
			}
			a++;
			System.out.println(Thread.currentThread().getName()+":----"+a);
		}
		
	}

}

运行结果:

线程2:----1
线程1:----2
线程2:----4
线程1:----4
线程2:----5
线程1:----6
线程2:----7
线程1:----8
线程2:----9
线程1:----10
线程1:----11
线程2:----12
线程1:----13
线程2:----14
线程1:----16
线程2:----16
线程1:----18
线程2:----18
线程1:----19
线程2:----20
线程1:----21
线程2:----22
线程1:----23
线程2:----23
线程1:----24
线程2:----25
线程1:----26
线程2:----27
线程2:----29
线程1:----29
线程1:----30
线程2:----31
线程1:----32
线程2:----33
线程1:----34
线程2:----35
线程1:----36
线程2:----37
线程1:----38
线程2:----39
线程1:----40
线程2:----41
线程1:----42
线程2:----43
线程1:----44
线程2:----45
线程1:----46
线程2:----47
线程1:----48
线程2:----49
线程1:----50
线程2:----51
线程1:----52
线程2:----53
线程1:----54
线程2:----55
线程1:----56
线程2:----57
线程1:----58
线程2:----59
线程1:----60
线程2:----61
线程1:----62
线程2:----63
线程1:----64
线程2:----65
线程1:----66
线程2:----67
线程1:----68
线程2:----69
线程1:----70
线程2:----71
线程1:----72
线程2:----73
线程1:----74
线程2:----75
线程1:----76
线程2:----77
线程1:----78
线程2:----79
线程1:----80
线程2:----81
线程1:----82
线程2:----83
线程1:----84
线程2:----85
线程1:----86
线程2:----87
线程1:----88
线程2:----89
线程1:----90
线程2:----91
线程1:----92
线程2:----93
线程1:----94
线程2:----95
线程2:----97
线程1:----97
线程1:----98
线程2:----99

线程1和线程2两个线程还是会得出相同的值,这就是因为同时有2个线程修改了值。


总结:
volatile只保证线程之间的可见性和禁止重排序,但是不能保证原子性。synchronized可以保证原子性但是synchronized多了会影响性能,jdk1.6后对synchronized有了优化,如何使用需要看实际情况自己衡量。