一、简单介绍
- synchronized中文意思是同步,也称之为”同步锁“。
- synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
- synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
二、作用
synchronized的作用主要有三个:
- 原子性:确保线程互斥的访问同步代码,简单来说就是在执行过程中,不会被任何因素打断
- 可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
- 有序性:有序性值程序执行的顺序按照代码先后执行。
三、用法
synchronized主要有三种用法:
- 修饰实例方法:作用于当前实例加锁
- 修饰静态方法:作用于当前类对象加锁
- 修饰代码块:指定加锁对象,对给定对象加锁
修饰方法
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized
public synchronized void method()
{
// todo
}
注意: synchronized关键字不能继承。当父类中某个方法使用了synchronized修饰时,子类覆盖这个方法,子类方法默认是不同步的,需要在子类方法中加上synchronized,或者使用super.父类方法
修饰代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。
class SyncThread implements Runnable{
private static int count;
public SyncThread() {
count = 0;
}
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + ":" + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public class Demo01 {
public static void main(String[] args) {
// 调用方式一
// SyncThread s1 = new SyncThread();
// SyncThread s2 = new SyncThread();
// Thread t1 = new Thread(s1);
// Thread t2 = new Thread(s2);
// 输出:
// Thread-1:0
// Thread-0:1
// Thread-1:2
// Thread-0:3
// Thread-0:4
// Thread-1:5
// Thread-0:6
// Thread-1:7
// Thread-0:8
// Thread-1:9
// 调用方式二
SyncThread s = new SyncThread();
Thread t1 = new Thread(s);
Thread t2 = new Thread(s);
// 输出:
// Thread-0:0
// Thread-0:1
// Thread-0:2
// Thread-0:3
// Thread-0:4
// Thread-1:5
// Thread-1:6
// Thread-1:7
// Thread-1:8
// Thread-1:9
t1.start();
t2.start();
}
}
调用方式一中,thread1和thread2同时在执行。这是因为synchronized只锁定对象,每个对象只有一个锁(lock)与之相关联。
调用方式二中,当两个并发线程(thread1和thread2)访问同一个对象(syncThread)中的synchronized代码块时,在同一时刻只能有一个线程得到执行,另一个线程受阻塞,必须等待当前线程执行完这个代码块以后才能执行该代码块。Thread1和thread2是互斥的,因为在执行synchronized代码块时会锁定当前的对象,只有执行完该代码块才能释放该对象锁,下一个线程才能执行并锁定该对象。
总结:
- 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
四、锁的升级过程
各种锁介绍
synchronized锁有如下4种状态:
- 无锁: 不锁住资源,多个线程只有一个能修改资源成功,其他线程会重试
- 偏向锁: 只有一个线程抢锁资源的时候,将线程拥有者标记为当前线程
- 轻量级锁: 多个线程抢夺同步资源时,没有获得锁的线程使用CAS自旋等待锁的释放
- 重量级锁: 多个线程抢夺同步资源时,向内核申请锁资源,未争抢成功的锁的进入线程阻塞等待唤醒
锁的升级过程: 无锁->偏向锁->轻量级锁->重量级锁
注意: 锁升级并不一定是一级级升的,有可能跨级别
偏向锁
在锁对象的对象头中记录下当前获取到该锁线程的id,该线程在下次获取该锁时就可以直接获取到
轻量级锁
由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁。轻量级锁的底层是通过自旋来实现的,并不会阻塞线程
重量级锁
如果自旋次数过多仍然无法获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
各种锁优缺点比较
| 锁 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法仅有纳米级的差距 | 如果线程间存在锁的竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步块场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间 同步响应非常快 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量 同步块执行速度较长 |