一 .简单介绍synchronized
关键字 synchronized可以在多线程并发中使用,保证同步。
非常重要的一点,保证线程安全的三个特性:原子性、可见性、有序性。而synchronized可以保证原子性和可见性。
synchronized主要有3种用法:
(1)修饰普通方法:作用于实例对象
(2)修饰静态方法:作用于类对象
(3)修饰代码块:可以指定作用的对象
二. 用Demo说明synchronized主要的用法
1.模拟多线程环境
Thread thread = new Thread(runnable); // runnable里面做操作
thread.start(); // 用Thread才能模拟
基本来说,synchronized修饰一个方法,多线程调用这个方法的时候,肯定会同步,这个大家都知道,就不用写Demo来说明了
2.多个普通方法使用synchronized 修饰(示例1)
假设有两个方法
(1)一个方法用synchronized修饰,一个不用
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.one("AA");
}
});
thread.start();
test.two();
public synchronized void one(String tag){
for (int i = 0; i < 100; i++) {
Log.v("mmp","one 【"+tag+" "+i);
}
}
public void two(){
for (int i = 0; i < 100; i++) {
Log.v("mmp","two "+i);
}
}
这样得到的结果是两个方法不会同步,打印的结果是乱的。
(2)两个方法都加锁
两个方法都用synchronized 修饰
public synchronized void one(String tag){
for (int i = 0; i < 10000; i++) {
Log.v("mmp","one 【"+tag+" "+i);
}
}
public synchronized void two(){
for (int i = 0; i < 10000; i++) {
Log.v("mmp","two "+i);
}
}
得到的结果是数据没有乱,两个方法实现了同步
(3)synchronized修饰的方法调用不被synchronized修饰的方法
public synchronized void one(String tag){
for (int i = 0; i < 10000; i++) {
Log.v("mmp","one 【"+tag+" "+i);
}
three();
}
public synchronized void two(){
for (int i = 0; i < 10000; i++) {
Log.v("mmp","two "+i);
}
}
public void three(){
for (int i = 0; i < 10000; i++) {
Log.v("mmp","three "+i);
}
}
这种情况数据也不会乱,和上面的一样,调用two的时候,one都没进去,更不可能调用three
从示例1可以有一种感觉,synchronized 作用的是对象,一个对象只有一个锁。
3.静态方法用synchronized修饰
(1)一个线程调普通的方法,主线程调这个类的静态方法,两个方法都用synchronized修饰(示例2)
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test.one("AA");
}
});
thread.start();
Test.three();
public synchronized void one(String tag){
for (int i = 0; i < 100; i++) {
Log.v("mmp","one 【"+tag+" "+i);
}
}
public synchronized static void three(){
for (int i = 0; i < 100; i++) {
Log.v("mmp","three "+i);
}
}
可以看到打印的数据是乱的,这就示例1的不同,上面synchronized修饰两个普通方法,这个是synchronized修饰一个普通方法和一个静态方法。这样就能证明修饰普通方法和修饰静态方法不是作用在同一个对象。
那如果要静态方法和普通方法里面的操作实现同步怎么做?
说实话我好像基本没见过这样的要求,那是不能做吗?当然能做,没什么是不能做的。
静态方法和基本方法保持同步(示例3)
public void one(String tag){
synchronized (Test.class) {
for (int i = 0; i < 100; i++) {
Log.v("mmp", "one 【" + tag + " " + i);
}
}
}
public synchronized static void three(){
for (int i = 0; i < 100; i++) {
Log.v("mmp", "three " + i);
}
}
这样就行了,这样数据就不会乱了。这里用到了同步代码块,把锁的对象设置成类,两个都是对类的锁,就会产生互斥。
4.同步代码块(示例4)
如果我们对某个方法,不想让这个方法的全部代码都同步,只想让部分代码同步,在上面的栗子中加入一个循环打印(循环次数改小方便截图)
public void one(String tag){
for (int i = 0; i < 10; i++) {
Log.v("mmp", "非同步块:" + tag + " " + i);
}
synchronized (Test.class) {
for (int i = 0; i < 10; i++) {
Log.v("mmp", "one 【" + tag + " " + i);
}
}
}
public synchronized static void three(){
for (int i = 0; i < 10; i++) {
Log.v("mmp", "three " + i);
}
}
可以从结果中看出,只有synchronized 同步块里面的代码才会同步。
5.小结
示例1演示了synchronized 作用于类的实例对象。
示例2演示了synchronized 作用于类对象。
示例3和示例4演示了synchronized 使用同步代码块作用于指定的对象。
比如你想作用于当前的类的实例对象,可以这样写
synchronized (this) {
.......
}
如果你想作用于类对象,可以这样写
synchronized (类名.class) {
.......
}
三. 原理
简单来看看synchronized的原理,为什么说简单呢,因为底层的代码我也看不懂,所以大概就只能去看别人总结的,大致了解一下,详细的话以后看得懂代码再来详细说吧。
将synchronized反编译之后
网上找的图
我自己也试着反编译了下,发现找到的不是monitorenter和monitorexit,我这边看到的是monitor-enter和monitor-exit
可以看出,锁的进入和退出都有和monitor有关。
1.对象头
这就要涉及到java的对象头了,那对象头是什么。
Java对象保存在内存中时,由以下三部分组成: 对象头、实例数据、 对齐填充字节。也就是可以把JAVA对象抽象的认为是: 对象头 + 实例数据 + 对象填充
那这个对象头反过来就是Java对象的一部分,那肯定是用来存储某部分的数据。
对象头又由两部分组成,一部分用于存储自身的运行时数据,称之为 Mark Word,另外一部分是类型指针(先了解就好)。
所以 对象头 = Mark Word + 类型指针
Mark Word主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等。当然这个我也是看别人说的。然后可以看看别人画的结构图,会比较清晰。
网上的图
// 32位系统的
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|-------------------------------------------------------|--------------------|
// 64位系统的
|------------------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | Normal |
|------------------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | Biased |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
| | lock:2 | Marked for GC |
|------------------------------------------------------------------------------|--------------------|
网上的图
大概了解下就行。
2.Monitor
Monitor 是一种同步工具,也是一个对象。
Object 类本身就是监视者对象,可以想象成他们本身就带了一把看不见的锁,可以看看Java Monitor 工作原理的图。
网上的图
这里也是简单理解一下,详细的流程就没这么简单了。
3.锁的状态
之前的synchronized是重量级的锁,在JAVA 6之后做了优化,锁的状态有4种,看上面的图就能看出,分为无锁状态、偏向锁、轻量级锁和重量级锁。