synchronized介绍

242 阅读5分钟

一、简单介绍

  • synchronized中文意思是同步,也称之为”同步锁“。
  • synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
  • synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
    在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。

二、作用

synchronized的作用主要有三个:

  1. 原子性:确保线程互斥的访问同步代码,简单来说就是在执行过程中,不会被任何因素打断
  2. 可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。
  3. 有序性:有序性值程序执行的顺序按照代码先后执行。

三、用法

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线程阻塞,响应时间缓慢追求吞吐量
同步块执行速度较长