锁的简介
锁能够保护共享数据以实现线程安全,其作用包括保障原子性、保障可见性和保障有序性。
- 原子性:锁是通过互斥保证原子性,互斥就是一个锁在同一时刻只能被一个线程持有,持有的同时其他线程无法获得该锁,而只能等待该锁被释放后再申请。这样就保证了临界区的代码一次只能被一个线程访问,那么临界区的操作具有不可分割性,即具备了原子性。
- 可见性:锁的获得隐含刷新处理器缓存,即线程在执行临界区代码之前,将其他写线程对共享变量的更新同步到该线程的高速缓存中;锁的释放隐含冲刷处理器缓存,即将该线程将共享变量的更新推送到主内存,从而对读线程可同步,从而保证可见性。
- 有序性:由于锁对可见性的保障,写线程在临界区对任何共享变量的更新都对读线程可见,并且由于临界区具有原子性,临界区内多个共享变量的更新对读线程来说像是同一时间更新的,因此读线程也没法分辨什么顺序。
b = a + 1
c = 2
flag = true
一个读线程在临界区内能够看到c = 2,那么flag必然是true,b的值必然是比a大1,尽管过能保证有序性,但不保证临界区内不会发生重排序,只能保证临界区内的操作不会被重排序到临界区之外,临界区内的操作还是可以重排序。
锁的原子性和可见性结合,可以保证临界区内的代码能够读到共享数据的最新值,且对引用性共享变量,锁还可以保障临界区代码能够读取到该变量所引用对象字段(实例变量和类变量)的最新值(当然前提是对共享变量的访问都是在临界区,即锁内部)。
Java内部锁是通过synchronized关键字实现的,synchronized修饰的普通方法称为同步方法,修饰静态方法就称为静态同步方法,同步方法就是一个临界区。
Synchronized修饰方法或者修饰代码块有什么不同
修饰代码块,通过monitorenter和monitorexit来控制。
修饰方法,则会有ACC_SYNCHRONIZED的flag来进行控制。
修饰静态方法,则会有ACC_STATIC,ACC_SYNCHRONIZED的flag来进行控制。
修饰静态方法,方法内部没有monitorenter和monitorexit,只有ACC_STATIC, ACC_SYNCHRONIZED的flag标识
{
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
}
===========
修饰方法同样也没有monitorenter和monitorexit,而是通过ACC_SYNCHRONIZED来标识
public synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 5: 0
line 6: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lio/github/viscent/Test;
为什么称为内部锁
因为对锁的申请和释放都是有JVM负责的,且注意内部锁的使用并不会导致锁泄漏,因为就算临界区代码出现异常,内部锁仍然能释放(如下图所示)。
public class Test {
public void method(){
synchronized (Test.class){
System.out.println("hello world");
}
}
}
------------
public class io.github.guangping0215.Test {
public io.github.guangping0215.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void method();
Code:
0: ldc #2 // class io/github/guangping0215/Test
2: dup
3: astore_1
4: monitorenter //=============获得锁
5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #4 // String hello world
10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
13: aload_1
14: monitorexit //===========正常结束释放锁
15: goto 23
18: astore_2
19: aload_1
20: monitorexit //===========抛出异常时释放锁
21: aload_2
22: athrow
23: return
Exception table:
from to target type
5 15 18 any
18 21 18 any
}
内部锁的调度
JVM会为每个内部锁分配一个入口集(EntrySet),用于记录等待获得内部锁的线程。多个线程在申请同一个锁的时候,只有一个申请者能够持有该锁,其他线程申请锁失败,这些线程会进入BLOCKED状态,并被存入相应内部锁的EntrySet里,称为等待线程。那些持有锁的线程释放锁之后,该锁的EntrySet里面的任意一个等待线程会被JVM唤醒,从而获得再次申请锁的机会。且内部锁的调度策略是非公平策略,则该唤醒的线程不一定能获得锁,可能还会跟其他活跃线程一起再次争抢锁。
Synchronized总结
1.监视器对象
private static final Object lock = new Object();
Synchronized(object)
如果new一个Object来当监视器,那么需要注意的是最好写成static final的,如果这个object重新指向另一个对象,那么这个Synchronized就变成监视另一个对象,则之前的锁跟这个锁就不会互斥。
Synchronized不是锁代码块,而是锁对象,要么是this对象,要么是Class对象;Synchronized写在方法上或者Synchronized(this)锁定的是this对象;
Synchronized写在静态方法上或者Synchronized(this.class)锁定的是Class对象。
2.线程八锁
八种情况(打印one or two):
1).两个普通同步方法 . // one two
2).新增Thread.sleep()给getOne(). // one two
3).新增普通方法getThree(). // Three one two 同步和普通方法不存在竞态条件
4).两个普通同步方法,两个两个Number对象 .// two one
5).修改getOne()为静态同步方法,一个方法为同步,一个Number对象 . // two one 静态同步和普通同步不存在竞态条件
6).修改两个方法都为静态同步方法,一个Number对象. // one two
7).一个方法为静态同步方法,一个方法为同步,两个Number对象 // two one
8).修改两个方法都为静态同步方法,两个Number对象 //one two, 因为是对class加锁
非静态同步方法的锁默认是this对象(类似于方法里写synchronize(this),静态方法的锁为对应的Class实例,类对象本身)
网友评论