java锁的分类和锁的内存语义

                                     java锁的分类和锁的内存语义

java锁的分类:

java对象锁有两种:对象锁类锁

对象锁:在非静态方法上加锁。声明了一个对象锁。类锁:在静态方法上加锁,声明了一个类锁。

经过大量的实验总结出以下结论:

1、想要保证能够锁住对象,需要在对应的的普通方法上加上synchronized关键字。

2、想要保证能够锁住对象,需要在对应的的普通方法上加上synchronized关键字。

3、非静态函数用关键字synchronized不会对普通方法有影响。

4、普通函数用关键字synchronized不会对静态方法有影响。

然后我们来做一个实验:

1、我们先声明一个类对象,

2、声明了两个普通方法,一个method1用synchronized关键字修饰,另一个method2没有锁(没有用synchronized修饰)

3、两个函数都调用另一个普通函数method3,函数method让对象的属性加一。循环一万次。

4、有两个线程分别执行method1和method2。那么执行结果是什么呢?

线程类:

package Test;

public class Syn extends Thread{

	int i;
	private TestSyn syn; 
	public Syn(int i ,TestSyn syn) {
		this.i=i;
		this.syn=syn;
	}
	@Override
	public void run() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(i%2==0) {
			syn.method1(syn);
		}
		else {
			syn.method2(syn);
		}
	}
}

测试类:

package Test;

public class TestSyn {
	
	private int i;
	public TestSyn(int i) {
		this.i=i;
	}
	
	public synchronized void method1(TestSyn aSyn) {
		System.out.println("1");
		method3(aSyn);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("变化的i"+aSyn.i);
}
	public void method2(TestSyn aSyn) {
		System.out.println("2");
		method3(aSyn);
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		System.out.println("变化的i"+aSyn.i);
	}
	public void method3(TestSyn aSyn) {
		for(int i=0;i<10000;i++) {
		aSyn.i++;
		}
	}
	public static void main(String[] args) {
		Syn []a = new Syn[2];
		TestSyn aSyn=new TestSyn(0);
		a[0]=new Syn(1,aSyn);
		a[1]=new Syn(2,aSyn);
		a[1].start();
		a[0].start();
	}

}

理论上是20000,实际上会少很多,所以这种方式是线程不安全的。

所以如果想让两个函数互斥的访问某些资源,在对应的函数访问的时候都要加上锁。这样才能保证数据的正确性。

java锁的内存语义:

我们在访问共享内存的时候,都是用关键字synchronized声明,但我们常常忽略他是怎么实现的,这里来了解一下。

锁的释放和获取的内存语义:

当线程释放锁的时候,JMM会把本地内存的共享变量的值刷会到主内存中,当另一个线程访问临界区的资源的时候,

JMM会使本地内存的值置为无效,使临界区的代码必须去主内存中读取新的值。从这种关系上看,

锁的释放和volatile变量的写有相同的语义,锁的获取与volatile变量的读具有相同的语义。

线程A释放锁,线程B获取锁。实际上是线程A通过主内存给线程B发送消息。

volatile的详情可以看我的另一篇博客:java中的volidate用法及注意事项

锁的内存语义的实现:

这里我们来了解ReentrantLock类,这个类对象可以调用方法lock实现同步,不过需要注意的是要在finally调用释放

锁(unlock)的方法。否则访问资源的线程一旦崩溃,所有线程都无法访问到共享资源。而synchronized不需要我们手

动释放锁,这是区别与synchronized的一大特点。ReentrantLock类实现了 Lock ,它拥有与 synchronized相同的

并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用

情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间

用在执行线程上。)

ReentrantLock公平锁:

加锁方法lock方法的调用轨迹:

1、ReentrantLock:lock()

2、FairSync:lock()

3、AbstractQueuedSynchronizer:acquire(int arg)

4、ReentrantLock:tryAcquire(int acquires)

第四步是开始真正加锁,并在一开始读取volatile变量的state。

解锁方法unlock方法的调用轨迹:

1、ReentrantLock:unlock()

2、AbstractQueuedSynchronizer:release(int arg)

3、Sync:tryRelease(int release)

第三步是开始释放锁,并在对volatile变量的state进行写入。

ReentrantLock非公平锁:

加锁方法lock方法的调用轨迹:

1、ReentrantLock:lock()

2、NonFairSync:lock()

3、AbstractQueuedSynchronizer:compareAndSetState(int except,int update)

第三步开始真正加锁。并以原子方式更新volatile变量state。此操作具有volatile的读和写的内存语义。

编译器不会对volatile读后面的操作和volatile写前面的操作进行重排序,所以CAS操作不允许编译器和其前后的操作进

行重排序。

解锁方法unlock方法的调用轨迹:

1、ReentrantLock:unlock()

2、AbstractQueuedSynchronizer:release(int arg)

3、Sync:tryRelease(int release)

第三步是开始释放锁,并在对volatile变量的state进行写入。

确保对内存的操作的读-写原子性:inter公司在奔腾和奔腾之前的处理机的处理方式是,调用lock方法会锁住整个总线,

但是这样的开销太大。所以从奔腾4以后的处理器都采用缓存锁定的方式来保证指令执行的原子性。锁住了缓存之后,

其他的对volatile的读操作的线程根据happens-before规则就需要等待该线程把缓存(本地内存)中的数据刷新到主内存中。

总结:锁的释放-获取的内存语义的实现需要利用volatile变量的写-读所具有的内存语义和利用CAS所附带的volatile读

和volatile写的内存语义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值