Java 多线程 - 初识 Synchronized

Synchronized 简介

本文出自汪文君老师的《Java 并发编程》课程,如需转载,请注明源出处!

先来看一个例子,这个例子是模拟银行叫号的,使用三个线程模拟三个柜台一起叫号,总共50个号。在不加 synchronized 的关键字的情况下,很容易就会出现并发问题。

public class BankRunnable {
    public static void main(String[] args) {
        // 一个runnable实例被多个线程共享
        TicketWindowRunnable ticketWindow = new TicketWindowRunnable();

        Thread windowThread1 = new Thread(ticketWindow, "一号窗口");
        Thread windowThread2 = new Thread(ticketWindow, "二号窗口");
        Thread windowThread3 = new Thread(ticketWindow, "三号窗口");
        windowThread1.start();
        windowThread2.start();
        windowThread3.start();
    }
}

public class TicketWindowRunnable implements Runnable {
    private int index = 1;
    private static final int MAX = 50;

    @Override
    public void run() {
        while (true) {
            if (index > MAX) {//1
                break;
            }
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName()+" 的号码是:"+(index++));//2
        }
    }
}

多运行几遍程序,就会出现下面这个问题:

image-20200603192220517

在一号窗口拿完最后一个号码之后,二号窗口和三号窗口又后续拿到了 52 和 51 号。为什么会出现这种现象呢?

首先当 index=499 的时候,三个线程均不满足 index > MAX,都会向下执行。三个线程都可以向下执行,将 index 加 1。

为了解决这个问题,这里引入了 synchronized 。Java通过 synchronized 对共享数据的线程访问提供了一种避免竞争条件的机制。synchronized 可以修饰方法或者代码块,被修饰的方法或者代码块同一时间只会允许一个线程执行,这条执行的线程持有同步部分的锁。

synchronized 关键字可以修饰方法或者代码块,那么这两者有什么区别呢?

// 同步代码块
public class TicketWindowRunnable implements Runnable {
    private int index = 1;
    private static final int MAX = 500;

    private final Object MONITOR = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (MONITOR) {
                if (index > MAX) {
                    break;
                }
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
            }
        }
    }
}

synchronized 方法修饰代码块的时候,使用的是 LOCK 锁。再来用 synchronized 修饰一下同步方法:

@Override
public synchronized void run() {
    while (true) {
        if (index > MAX) {
            break;
        }
        try {
            Thread.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
    }
}

运行之后发现都是同一个线程在跑,另外两个线程无法执行。这是因为 synchronized 在修饰方法的时候使用的是 this 锁,当其中一个线程拿到锁进到 while 循环之后,就一直去做事情,直到满足条件退出为止。将 while 里面的代码抽出来放到一个方法里,用 synchronized 来修饰该方法就可以解决这个问题。

@Override
public void run() {
    while (true) {
        if (ticket()) {
            break;
        }
    }
}

private synchronized boolean ticket() {
    if (index > MAX) {
        return true;
    }
    try {
        Thread.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + " 的号码是:" + (index++));
    return false;
}

synchronized 修饰方法时默认是使用的 this 锁,修饰代码块时使用的是对象锁。synchronized 关键字还可以用来修饰静态方法和静态代码块。

public class SynchronizedStatic {

    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public synchronized static void m2() {
        System.out.println("m2 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SynchronizedStaticTest {
    public static void main(String[] args) {
        new Thread("T1") {
            @Override
            public void run() {
                SynchronizedStatic.m1();
            }
        }.start();

        new Thread("T2") {
            @Override
            public void run() {
                SynchronizedStatic.m2();
            }
        }.start();
    }
}

// output
m1 T1
m2 T2

静态方法 m1 和 m2 同时被 synchronized 修饰,这个时候线程 T2 会等到线程 T1 执行完再执行,说明这两个方法使用的是同一把锁,这就是 Class 锁。我们把 sleep 的时间变长一点来观察一下是不是 Class 锁。

image-20200609191557455

image-20200609191629867

可以看到,线程 T1 执行的时候,持有的是 Class 锁,此时线程 T2 在等待 T1 执行完释放锁,当 T1 执行完之后,T2 拿到 Class 锁执行代码。

image-20200609191812086

了解了 synchronized 修饰静态方法使用的是 Class 锁之后,我们再来验证一下当它修饰静态方法的时候是不是也是使用 Class 锁?

public class SynchronizedStatic {
    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(100_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void m3() {
        System.out.println("m3 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class SynchronizedStaticTest {
    public static void main(String[] args) {
        new Thread("T1") {
            @Override
            public void run() {
                SynchronizedStatic.m1();
            }
        }.start();

        new Thread("T3") {
            @Override
            public void run() {
                SynchronizedStatic.m3();
            }
        }.start();
    }
}

这里加了一个没有 synchronized 修饰的静态方法 m3,运行之后很容易知道,这两个线程是同时运行的。我们在 SynchronizedStatic 开始的地方加一个静态代码块,静态代码块内部使用 synchronized 锁。

public class SynchronizedStatic {
    static {
        synchronized (SynchronizedStatic.class) {
            System.out.println("static " + Thread.currentThread().getName());
            try {
                Thread.sleep(10_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized static void m1() {
        System.out.println("m1 " + Thread.currentThread().getName());
        try {
            Thread.sleep(100_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void m3() {
        System.out.println("m3 " + Thread.currentThread().getName());
        try {
            Thread.sleep(10_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

//output
static T1
m1 T1
m3 T3

可以发现,T1 线程要先执行静态代码块才能往下走,说明静态代码块使用的锁和静态方法是一样的,另外这个时候没有用 synchronized 修饰的 m3 也要等静态代码块执行实例化才行。

总结一下,synchronized 关键字能够避免多线程竞争导致的数据不一致,被 synchronized 修饰的方法或者代码块同一时间只会允许一个线程执行,这条执行的线程持有同步部分的锁。synchronized 关键字修饰普通方法时,使用的是 this 锁,修饰静态方法和静态代码块时,使用 Class 锁,修饰代码块时,使用 LOCK 锁。

posted @ 2020-09-17 14:46  chenxueqiang  阅读(147)  评论(0编辑  收藏  举报