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写的内存语义。