synchronized与volatile的联系与区别

本文深入探讨Java中的synchronized关键字及其应用场景,解释了如何通过synchronized实现线程间的同步,并对比了volatile关键字的不同之处。此外,还详细解析了volatile如何确保变量的可见性和有序性。

synchronized

先来了解synchronized,首先我们知道互斥同步是一种常见的高并发保障手段。同步是指多个线程并发的访问数据时,保证共享数据在同一时刻只能被一个线程访问,而互斥是同步的实现手段之一。

而被synchronized修饰的代码或代码块,在同一时刻只能被一个线程访问。

在java中同步锁是依赖对象存在的。不同线程对于同步锁是互斥的(例如 一个线程拿到对象的同步锁,那么另一个线程是拿不到的,除非这个线程释放同步锁)。

特点:

1,当两个并发的线程同步访问被synchronized修饰的方法或代码块时,只有一个线程能访问,另一个将被阻塞。

2,当一个线程访问“某对象”中被synchronized修饰的方法或代码块时,其他线程可以访问非同步的代码块。

3,当一个线程访问“某对象”中被synchronized修饰的方法或代码块时,其他线程访问其他被synchronized修饰的方法或代码块将被阻塞。

4,以上规则对其他对象锁同样适用。

实现synchronized

1锁住对象

class TextDemoThread1 implements Runnable{
	public void run(){
		synchronized (this) {//synchronized锁住this对象
			for(int i = 0;i<5;i++){
				System.out.println(Thread.currentThread().getName()+"..执行.."+i);
			}
		}
	}
}
public class TextDemo1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TextDemoThread1 textDemo1 = new TextDemoThread1();
		Thread thread = new Thread(textDemo1);
		thread.setName("t1");
		
		TextDemoThread1 textDemo2 = new TextDemoThread1();
		Thread thread1 = new Thread(textDemo2);
		thread1.setName("t2");
		thread.start();
		thread1.start();
	}

}

执行结果:

t1..执行..0
t2..执行..0
t2..执行..1
t2..执行..2
t1..执行..1
t2..执行..3
t2..执行..4
t1..执行..2
t1..执行..3
t1..执行..4

2,锁住方法

class TextDemoThread1 implements Runnable{
	public synchronized void run(){//锁住当前对象
			for(int i = 0;i<5;i++){
				System.out.println(Thread.currentThread().getName()+"..执行.."+i);
			}
		
	}
}
public class TextDemo1 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TextDemoThread1 textDemo1 = new TextDemoThread1();
		Thread thread = new Thread(textDemo1);
		thread.setName("t1");
		
		TextDemoThread1 textDemo2 = new TextDemoThread1();
		Thread thread1 = new Thread(textDemo2);
		thread1.setName("t2");
		thread.start();
		thread1.start();
	}

}

运行结果

t1..执行..0
t2..执行..0
t2..执行..1
t2..执行..2
t2..执行..3
t2..执行..4
t1..执行..1
t1..执行..2
t1..执行..3
t1..执行..4

这两种加锁对象的区别是:

在给this对象加锁的时候,synchronized关键字经过编译之后,会在同步块前后分别形成monitorentermonitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。在执行monitorenter时,首先要尝试获取对象的锁。如果这个对象没被锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器加一,相应的。执行monitorexit时会将锁计数器减1,当计数器为0时就释放锁。如果获取对象锁失败,那么当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。(Java线程是映射到操作系统的原生线程上的,如果要阻塞或者唤醒一个线程,都需要操作系统帮忙完成,这就需要从 用户态转换到核心态中,因此状态转换需要耗费很多的处理器转换时间)所以synchronized也被称作重量级锁。

但是将锁加到run()方法时不会产生这两个字节码指令。

同步方法是在Class文件的方法表将该方法的access_flags字段中的synchronized标志位置设为1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示做为锁对象。

volatile

`volatile只能添加在变量前,确保了变量的可见性和有序性,不保证操作的原子性(一次执行,不可分割);

public class Demo2 {

public static volatile int num = 0;
	
	public static void main(String[] args) {
		// TODO Auto-generated method stu
		
		Thread[] thread = new Thread[20];
		for(int i = 0;i<20 ;i++){
			thread[i] = new Thread(new Runnable(){

				@Override
				public void run() {
					// TODO Auto-generated method stub
					for(int j = 0 ;j<100;j++){
						num++;
					}
				}
				
			});
			thread[i].start();
		}
		
		System.out.println(num);
	}

代码执行结果按道理是2000,但是实际上结果比2000小???

这是因为num++不是原子操作,他是分为几步进行的,当他走了一步的时候,其他线程可能已经将值修改了,但被volatile修饰的变量为了保证能拿到正确的值,还是拿取前面的值,所以结果比2000小。

可见性

在汇编层会对volatile修饰的变量加lock前缀,而lock前缀其实是一个屏障,内存屏障会提供三个功能:

  1. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  2. 它会强制将对缓存的修改操作立即写入内存
  3. 如果是写操作,它会导致其他CPU中对应的缓存行无效

在修改变量的过程中

  1. 将修改变量的副本写入主内存
  2. 其他线程的 副本置为无效

读取时,先判断volatile修饰的变量是否有效,如果有效直接取,否则就去主内存去读取。

有序性

1)当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯 定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;

2)在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也 不能把volatile变量后面的语句放到其前面执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值