Java 中的 synchronized 关键字

张贤 2020年03月14日 79次浏览

synchronized 关键字是为了解决线程安全问题的。我们先来了解下解决线程安全问题的主要诱因。线程安全问题的主要诱因有两点:

  • 存在共享数据(也称为临界资源)
  • 存在多条线程共同操作这些共享数据

要解决线程安全问题,只有一个办法:保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等待该线程处理完数据后再对共享数据进行操作。

因此引出了互斥锁。互斥锁有两个特性:

  • 互斥性:在同一时间只允许一个线程持有某个对象锁,通过这种特性来实现多线程的协调机制,这样在同一时间只有一个线程对需要同步的代码块进行访问。互斥性也称为操作的原子性
  • 可见性:必须确保在锁被释放之前,对共享变量所做的修改,对于随后获得该锁的另一个线程是可见的(即在获得锁是应该获得最新共享变量的值),否则另一个线程可能实在本地缓存的某个副本上继续操作,从而引起不一致
    synchronized 就是满足以上两点的互斥锁,synchronized 锁的不是代码,锁的都是对象。根据获取锁的方式可以分为两种:
  • 获取对象锁。有两种写法:
    • 同步代码块synchronized(this),synchronized(类实例对象)。锁的都是小括号()中的对象
    • 同步非静态方法(synchronized method),锁的是当前类的实例对象
  • 获取类琐。有两种写法:
    • 同步代码块 synchronized(类名.class),锁的是小括号()中的类对象(Class 对象)
    • 同步静态方法 synchronized static method,锁的是当前类的类对象(Class 对象)
      可以看出类琐实际上也是通过对象锁(Class 对象)实现的。但是一个类只有一个 Class 对象,所以同一个类的不同对象使用类琐将是同步的
      类琐和对象实例的锁是互不干扰的。
      同一个类的不同对象的锁互不干扰。

下面是一个例子

public class SyncThread implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        if (threadName.startsWith("A")) {
            async();
        } else if (threadName.startsWith("B")) {
            syncObjectBlock1();
        } else if (threadName.startsWith("C")) {
            syncObjectMethod1();
        }
    }

    //异步方法
    private void async() {
        try {
            System.out.println(Thread.currentThread().getName() + "- Async Start");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "- Async End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 方法中有 synchronized(this|object){} 同步代码块
     */
    private void syncObjectBlock1() {
        System.out.println(Thread.currentThread().getName() + "- syncObjectBlock1");
        synchronized (this) {
            try {
                System.out.println(Thread.currentThread().getName() + "- syncObjectBlock1 Start");
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + "- syncObjectBlock1 End");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * synchronized 修饰非静态方法
     */
    private synchronized void syncObjectMethod1() {
        System.out.println(Thread.currentThread().getName() + "- syncObjectMethod1");
        try {
            System.out.println(Thread.currentThread().getName() + "- syncObjectMethod1 Start");
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "- syncObjectMethod1 End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SyncDemo {
    public static void main(String[] args) {
        SyncThread  syncThread=new SyncThread();
        Thread A_thread1=new Thread(new SyncThread(),"A_thread1");
        Thread A_thread2=new Thread(new SyncThread(),"A_thread2");

        Thread B_thread1=new Thread(new SyncThread(),"B_thread1");
        Thread B_thread2=new Thread(new SyncThread(),"B_thread2");

        Thread C_thread1=new Thread(new SyncThread(),"C_thread1");
        Thread C_thread2=new Thread(new SyncThread(),"C_thread2");

        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}

上述 6 个线程虽然调用了synchronized代码块或者方法,但是它们之间并没有互斥关系,原因是每个线程锁的不是同一个对象。
SyncDemo类做如下修改后,就可以实现线程互斥

public class SyncDemo {
    public static void main(String[] args) {
        SyncThread  syncThread=new SyncThread();
        Thread A_thread1=new Thread(syncThread,"A_thread1");
        Thread A_thread2=new Thread(syncThread,"A_thread2");

        Thread B_thread1=new Thread(syncThread,"B_thread1");
        Thread B_thread2=new Thread(syncThread,"B_thread2");

        Thread C_thread1=new Thread(syncThread,"C_thread1");
        Thread C_thread2=new Thread(syncThread,"C_thread2");

        A_thread1.start();
        A_thread2.start();
        B_thread1.start();
        B_thread2.start();
        C_thread1.start();
        C_thread2.start();
    }
}

每个线程使用的都是同一个 Runnable 对象,所以锁的也是同一个对象。
如果synchronized修饰的是静态方法或者SyncThread.class,那么无论传给ThreadRunnable的同一个对象还是不同的对象,线程执行到synchronized代码块或者方法中都会互斥,也就是只有一个线程能够进入synchronized代码块或者方法中。