美文网首页程序员技术干货
并发基础知识之synchronized关键字

并发基础知识之synchronized关键字

作者: 秃头哥编程 | 来源:发表于2018-05-17 19:50 被阅读5次

    上一篇文章我总结了一下线程的创建方法以及线程的一些属性,同时还讲了线程的共享以及带来的原子性和内存可见性的问题。这篇文章就讲讲怎么用synchronized关键字解决那两个问题。


    1.synchronized的用法和基本原理

    synchronized可以修饰实例方法,静态方法和代码块。

    上篇我们讲了一个counter计数器的问题,由于counter++不是一个原子操作,所以在多线程中,输出的结果往往不是我们所预期的,现在我们看看怎么分别用着三种方式解决这个问题。

    (1)修饰实例方法

    public class Counter {
        private int counter = 0;
        public synchronized void incr() {
            counter++;
        }
        
        public synchronized int getCounter() {
            return counter;
        }
    }
    

    Counter类是一个简单的计数器类,里面有两个方法,一个让计数加1,一个返回计数的值,都加了synchronized 修饰,这样方法内的代码就是原子操作,当多个线程更新同一个Counter对象的时候,也不会有问题。

    public class CounterThread extends Thread {
        private Counter counter;
        public CounterThread(Counter counter) {
            this.counter = counter;
        }
        @Override
        public void run() {
            for(int i = 0; i < 1000; i++) {
                counter.incr();
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            int num = 1000;
            Counter counter = new Counter();
            Thread[] threads = new Thread[num];
            for(int i = 0; i < num; i++) {
                threads[i] = new CounterThread(counter);
                threads[i].start();
            }
            for(int i = 0; i < num; i++) {
                threads[i].join();
            }
            System.out.println(counter.getCounter());
        }
    }
    

    不论运行多少次,都是输出1000*1000。

    那么这里的synchronized到底起了什么作用呢?表面上看,是让同时只能有一个线程执行实例方法,但其实这是有条件的,那就是同一个对象。是的,如果多个线程访问同一个对象的实例方法,那么synchronized就会让线程按顺序来执行,如果是不同对象,那么多个线程时可以同时访问同一个synchronized方法的,只要它们访问的对象是不同的即可。

    比如

    Counter c1 = new Counter();
    Counter c2 = new Counter();
    Thread t1 = new CounterThread(c1);
    Thread t2 = new CounterThread(c2);
    t1.start();
    t2.start();
    

    这里,t1和t2两个线程时可以同时执行Counter的incr方法的,因为它们访问的是不同的Counter对象。

    相反,如果访问的是同一个对象的synchronized方法,那么即使是不同的synchronized方法,也需要等待的。比如Counter类中的getCounter和incr,对同一个Counter对象,一个线程执行getCounter方法,一个线程执行incr方法,虽然是不同的方法,但它们还是不能同时执行,会被synchronized同步顺序执行。

    所以,synchronized实际保护的是同一个对象的方法调用,确保同时只要一个线程执行。再具体来说,synchronized保护的是当前的实例对象,即this,this对象有一个锁和一个等待队列,锁只能被一个线程拥有,其他线程要获得同样的锁需要等待。执行synchronized修饰的实例方法的大致过程如下:

    1.尝试获得锁,如果能获得,执行下一步,否则加入等待队列,阻塞并等待唤醒,线程状态变成BLOCKED。
    2.执行实例方法内的代码。
    3.释放锁,如果等待队列里有等待的线程,则取一个唤醒,如果有多个,则随机,不保证公平性。

    synchronized实际的执行过程比这复杂得多,但我们可以这样简单的理解。

    此外还要说明的是,synchronized方法不能防止非synchronized方法被同时执行,比如给Counter类加一个非synchronized方法,则该方法可以和incr方法一起执行,这通常会出现意想不到的结果,所以,对于一个变量来说,一般给该访问该变量的所有方法加上synchronized

    (2)修饰静态方法

    public class StaticCounter {
        private static int counter = 0;
        public static synchronized void incr() {
            counter++;
        }
        
        public static synchronized int getCounter() {
            return counter;
        }
    }
    

    前面我们说,synchronized修饰实例方法,保护的是当前实例对象this,那么修饰静态方法,保护的是那个对象呢?是类对象。对上面的例子也就是StaticCounter.class,每个对象都有一个锁和一个等待队列,类对象也不例外。

    因为synchronized静态方法和synchronized实例方法保护的是不同的对象,所以不同的两个线程,可以一个执行synchronized静态方法,一个执行synchronized实例方法。

    (3)修饰代码块

    public class Counter {
        private int counter = 0;
        public void incr() {
            synchronized(this) {
                counter++;
            }
        }
        
        public int getCounter() {
            synchronized(this) {
                return counter;
            }
        }
    }
    

    synchronized括号里面就是保护的对象。对于实例方法,就是this。对于前面的StaticCounter类,等价代码如下

    public class StaticCounter {
        private static int counter = 0;
        public static void incr() {
                    synchronized(StaticCounter .class) {
                            counter++;
                    }
        }
        
        public static int getCounter() {
            synchronized(StaticCounter .class) {
                           return counter;
                    }
        }
    }
    

    synchronized同步的对象可以是任意对象,任意对象都有一个锁和一个等待队列,或者说,任何对象都可以成为锁对象

    比如Counter的等价代码还可以如下

    public class Counter {
        private int counter = 0;
        private Object lock = new Object();
        public void incr() {
            synchronized(lock) {
                counter++;
            }
        }
        
        public int getCounter() {
            synchronized(lock) {
                return counter;
            }
        }
    }
    

    2.进一步了解synchronized

    介绍了synchronized的基本用法和原理之后,现在从以下三个方面进一步介绍

    • 可重入性
    • 内存可见性
    • 死锁

    (1)可重入性
    可重入性是指如果一个线程获得一个锁之后,在调用其他需要同样锁的代码时,可以直接调用。比如在一个synchronized实例方法内,可以直接调用其他synchronized实例方法。

    可重入是通过记录锁的持有线程和持有数量来实现的。当调用synchronized保护的代码时,检查对象是否被锁,如果是,再检查是否是被当前线程持有,如果是,增加持有数量,如果不是,则线程加入等待队列,当释放锁时,减少持有数量,当持有数量变为0的时候,才释放整个锁。

    (2)内存可见性

    synchronized除了可以保证原子性之外,还能保证内存可见性。在释放锁的时候,所有写入都会写入内存,而获得锁后,都会从内存中读最新数据。

    但如果只是为了保证内存可见性,使用synchronized成本有点高,我们可以使用volatile关键字修饰变量,比如上篇文章中的内存可见性问题,代码可以该成如下,就可以解决内存可见性问题。

    public class VisibilityDemo {
        private static volatile boolean shutdown = false;
        static class HelloThread extends Thread {
            @Override
            public void run() {
                while(!shutdown) {
                    System.out.println("1");
                }
                System.out.println("exit hello");
            }
        }
        
        public static void main(String[] args) throws InterruptedException {
            new HelloThread().start();
            Thread.sleep(1000);
            shutdown = true;
            System.out.println("exit main");
        }
    }
    

    可以看到使用volatile修饰了shutdown变量。加入volatile后,java会在操作对应变量时插入特殊的指令,保证读写到内存最新值,而非缓存的值。

    (3)死锁
    使用synchronized或者其他锁,可以回产生死锁,比如,有a,b两线程,a线程持有锁A,等待锁B,b线程持有锁B,等待锁A,这样a,b就互相等待,永远不会执行。

    public class DeadLockDemo {
        private static Object lock1 = new Object();
        private static Object lock2 = new Object();
        private static void startThreadA() {
            Thread thread1 = new Thread() {
                @Override
                public void run() {
                    synchronized (lock1) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (lock2) {
                        }
                    }
                }
            };
            thread1.start();
        }
        
        private static void startThreadB() {
            Thread thread2 = new Thread() {
                @Override
                public void run() {
                    synchronized (lock2) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (lock1) {
                        }
                    }
                }
            };
            thread2.start();
        }
        
        public static void main(String[] args) {
            startThreadA();
            startThreadB();
        }
    }
    

    应该尽量避免在持有一个锁的同时去申请另外一个锁,如果确实需要多个锁,所有代码应该按照相同的顺序去申请锁。对于上面的例子,可以约定都先申请lock1,再申请lock2。

    相关文章

      网友评论

        本文标题:并发基础知识之synchronized关键字

        本文链接:https://www.haomeiwen.com/subject/ensbdftx.html