目录
一、对象锁和类锁
1. 对象锁
在 Java 中,每个对象都会有一个 monitor 对象,这个对象其实就是 Java 对象的锁,通常会被称为“内置锁”或“对象锁”。类的对象可以有多个,所以每个对象有其独立的对象锁,互不干扰。
2. 类锁
在 Java 中,针对每个类也有一个锁,可以称为“类锁”,类锁实际上是通过对象锁实现的,即类的 Class 对象锁。每个类只有一个 Class 对象,所以每个类只有一个类锁。
二、应用举例
synchronized用到不同地方对代码产生的影响:
1. synchronized关键字修饰方法
假设P1、P2是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2就都能够调用他们。
public synchronized void method(){
//
}
这也就是同步方法,那这时synchronized锁定的是调用这个同步方法对象(this)。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,他们之间会形成互斥,达到同步的效果。同时如果该对象中有多个同步方法,则当一个线程获执行对象中的一个synchronized方法,则该对象中其它同步方法也不允许别的线程执行。但是这个对象所属的Class所产生的另一对象P2却能够任意调用这个被加了synchronized关键字的方法。
上边的示例代码等同于如下代码:
public void method() {
synchronized (this)
{
//..
}
}
此次就是一个P1对象的对象锁,哪个拿到了P1对象锁的线程,才能够调用P1的同步方法,而对P2而言,P1这个锁和他毫不相干,程式也可能在这种情形下摆脱同步机制的控制,造成数据混乱。
2.同步块,示例代码如下:
public void method(SomeObject so) {
synchronized(so)
{
//..
}
}
这时,锁就是so这个对象,每个对象对应一个唯一的锁,所以哪个线程拿到这个对象锁谁就能够运行他所控制的那段代码。当有一个明确的对象作为锁时,就能够这样写程式,但当没有明确的对象作为锁,只是想让一段代码同步时,能够创建一个特别的instance变量(他得是个对象)来充当锁:
private byte[] lock = new byte[0];
Public void method(){
synchronized(lock)
{
//同步块里的代码
//释放锁
lock.wait()
}
//后面的代码
}
PS:零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。
在B线程先占用so对象后,A线程只能等A执行完synchronized同步代码块释放锁之后,才能执行synchronized代码块,而不会跳过synchronized代码块而去执行后面的代码。
3.将synchronized作用于static 函数,示例代码如下:
Class Foo
{
public synchronized static void method1()
{
//.
}
public void method2()
{
synchronized(Foo.class)
//
}
public synchronized void method3()
{
//.
}
}
method1和method2 的锁都是Foo.class 构成同步
method3的锁是this对象和method1,method2不构成同步
下面是一个经典问题:
当一个线程进入一个对象的一个synchronized 方法后,其它线程是否可
入此对象的其它方法?
几种情况:
1. 其他方法前是否加了 synchronized 关键字,如果没加,则能。
2. 如果这个方法内部调用了 wait ,则可以进入其他 synchronized 方法。
3. 如果其他个方法都加了 synchronized 关键字,并且内部没有调用 wait ,则不能。
4. 如果其他方法是 static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是 this 。
三、使用总结
1、对于静态方法,由于此时对象还未生成,所以只能采用类锁;
2、只要采用类锁,就会拦截所有线程,只能让一个线程访问。
3、对于对象锁(this),如果是同一个实例,就会按顺序访问,但是如果是不同实例,就可以同时访问。
4、如果对象锁跟访问的对象没有关系,那么就会都同时访问。
四、实际应用
public class Main {
/**
* 当前打印到的数字
*/
private static volatile int curNum = 0;
/**
* 打印到的最大数字
*/
private static final int MAX_NUM = 100;
/**
* 线程数
*/
private static final int THREAD_NUM = 10;
/**
* 共享的锁
*/
private static final Object lock = new Object();
public static void main(String[] args) {
for (int i = 1; i <= THREAD_NUM; i++) {
new Thread(new Turn(i), "线程" + i).start();
}
}
private static class Turn implements Runnable {
private int index;
public Turn(int index) {
this.index = index;
}
@Override
public void run() {
while (curNum <= MAX_NUM) {
synchronized (lock) {
if ((curNum % THREAD_NUM + 1) != index) {
try {
//释放锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + ":" + curNum++);
lock.notifyAll();
}
}
}
}
}
}
/*
线程1:0
线程2:1
线程3:2
线程4:3
线程5:4
线程6:5
线程7:6
线程8:7
线程9:8
线程10:9
线程1:10
线程2:11
线程3:12
线程4:13
线程5:14
线程6:15
线程7:16
线程8:17
线程9:18
线程10:19
线程1:20
线程2:21
线程3:22
线程4:23
线程5:24
线程6:25
线程7:26
线程8:27
线程9:28
线程10:29
线程1:30
线程2:31
线程3:32
线程4:33
线程5:34
线程6:35
线程7:36
线程8:37
线程9:38
线程10:39
线程1:40
线程2:41
线程3:42
线程4:43
线程5:44
线程6:45
线程7:46
线程8:47
线程9:48
线程10:49
线程1:50
线程2:51
线程3:52
线程4:53
线程5:54
线程6:55
线程7:56
线程8:57
线程9:58
线程10:59
线程1:60
线程2:61
线程3:62
线程4:63
线程5:64
线程6:65
线程7:66
线程8:67
线程9:68
线程10:69
线程1:70
线程2:71
线程3:72
线程4:73
线程5:74
线程6:75
线程7:76
线程8:77
线程9:78
线程10:79
线程1:80
线程2:81
线程3:82
线程4:83
线程5:84
线程6:85
线程7:86
线程8:87
线程9:88
线程10:89
线程1:90
线程2:91
线程3:92
线程4:93
线程5:94
线程6:95
线程7:96
线程8:97
线程9:98
线程10:99
线程1:100
*/
五、底层原理
package com.paddx.test.concurrent;
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("Method 1 start");
}
}
}
关于这两条指令的作用,我们直接参考JVM规范中描述:
每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
1、如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
2、如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
3.如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。
理解Java对象头与Monitor
在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:
-
实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。
-
填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。
monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因
而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:
虚拟机位数 | 头对象结构 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode、锁信息或分代年龄或GC标志等信息 |
32/64bit | Class Metadata Address | 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。 其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构 |
其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit 锁标志位 |
---|---|---|---|---|
无锁状态 | 对象HashCode | 对象分代年龄 | 0 | 01 |
由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构: