美文网首页中北软院创新实验室
Java编程的逻辑 -- 并发章 -- Synchronized

Java编程的逻辑 -- 并发章 -- Synchronized

作者: HikariCP | 来源:发表于2018-06-22 08:22 被阅读80次

    Synchronized

    Synchronized

    共享内存有两个重要问题,一个是竞态条件,一个是内存可见性。其实一种解决方案则是Synchronized

    原理

    我们首先来看一段synchronized修饰方法和代码块的代码:

    public class Main {
        //修饰方法
        public synchronized void test1(){
    
        }
    
        
        public void test2(){
            // 修饰代码块
            synchronized (this){
    
            }
        }
    }
    

    来反编译看一下:


    image

    从上面可以看出,同步代码块是使用monitorenter和monitorexit指令实现的,同步方法(在这看不出来需要看JVM底层实现)依靠的是方法修饰符上的ACC_SYNCHRONIZED实现。

    同步代码块:monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。synchronized底层是通过monitor对象,对象有自己的对象头,存储了很多信息,其中一个信息标示是被哪个线程持有。

    同步方法:synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,在VM字节码层面并没有任何特别的指令来实现被synchronized修饰的方法,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass做为锁对象。(摘自:http://www.cnblogs.com/javaminer/p/3889023.html)

    具体可参考:

    用法

    1. 实例方法

    public class Counter {
        private int count;
        public synchronized void incr(){
            count ++;
        }
        public synchronized int getCount() {
            return count;
        }
    }
    

    Synchronized可以用来修饰实例方法,静态方法,(静态或实例)代码块。

    多个线程可以同时执行一个Synchronized方法,只要他们访问的不是同一个实例对象即可。

    Synchronized实际上保护的是当前实例对象,而不是它仅描述的那个方法。此时this对象有一个锁和一个等待队列,锁只能被一个线程持有, 其他试图获得同样锁的线程需要等待。当前线程不能获得锁的时候,它会加入等待队列,线程的状态会变为BLOCKED


    Thread有一个与其状态对应的枚举类。Thread.State枚举类:

    public enum State {
      NEW,
      RUNNABLE,
      BLOCKED,
      WAITING,
      TIMED_WAITING,
      TERMINATED;
    }
    

    关于这些状态.我们简单解释下:

    1. NEW:没有调用start的线程状态为NEW。
    2. TERMINATED:线程运行结朿后状态为TERMINATED。
    3. RUNNABLE:调用start后线程在执行run方法且没有阻寒时状态为RUNNABLE,不过,
      RUNNABLE不代表CPU—定在执行该线程的代码,可能正在执行也可能在等待操作系统分配时间片,只
      是它没有在等待其他条件。
    4. BLOCKED、WAITING、TIMED_WAmNG:都表示线程被阻塞了,在等待一些条件。其中BLOCKED表示当前线程在等待的是CPU的时间片而WAITING、TIMED_WAmNG则等待的是外部条件。

    Synchronized保护的是对象而非方法块,只要访问的是同一个对象的synchronized方法,即使是不同的方法块,也会被同步顺序访问。比如,对于Counter类中的两个同步实例方法get和incr,对同一个Counter对象,一个线程执行get,另一个执行incr,它们是不能同时执行的,会被synchronized同步顺序执行。

    此外,需要说明的是,****synchronized方法不能防止非synchronized方法被同时执行****。比如,如果给Counler类增加一个非synchronized方法:

    public void decr(){
        count --;
    }
    

    则该方法可以和synchronized的incr方法同时执行,这通常会出现非期望的结果,所以,一般在保护变量时,需要在所有访问该变量的方法前加shsynchronizcd(不一定)

    2. 静态方法

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

    对实例方法,synchronized保护的是当前实例对象this,对静态方法,保护的是类对象,这里是StaticCounter.class。实际上,每个对象都有一个锁和一个等待队列,Class类对象也不例外。

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

    3. 代码块

    public class Counter {
        private int count;
        public void incr(){
        
            synchronized(this){
                count ++;
            }
            
        }
        public int getCount() {
            synchronized(this){
                return count;
            }
        }
    }
    

    synchronized括号里即保护的对象,对于实例方法而言则是this{}是执行同步的代码。对于上例中的StaticCounter类,等价的代码则是:

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

    synchronized同步的对象可以是任意对象,任意对象都有一个锁和等待队列,或者说,任何对象都可以作为锁对象。如下的类成员lock。所以在如下这种情况中,所有的线程中所跑的涉及域lock的方法都会在一个等待队列中排队执行。

    public class Counter {
        private int count;
        private Object lock = new Object();
        public void incr(){
            synchronized(lock){
                count ++;
            }
        }
        public int getCount() {
            synchronized(lock){
                return count;
            }
        }
    }
    

    特性

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

    可重入性

    Synchronized是一种可重入锁。

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

    内存可见性

    Synchronized除了可以保证原子性,同时还可以保证内存可见性。在释放锁时,所有的写入都会从寄存器或缓存(工作内存)写回内存,而获得锁后都会从内存中获得最新数据。

    但如果只是为了保证内存可见性,使用synchronized关键字的成本则要高于volatile修饰符的使用。

    如下代码:

    public class Switcher {
        private boolean on;
        public boolean isOn() {
            return on;
        }
        public void setOn(boolean on) {
            this.on = on;
        }
    }
    

    该代码在并发执行的时候不涉及非原子操作,仅修改变量on的状态,是一个原子操作。所以这里完全不必要用synchronized修饰符来附加在setOn方法上来锁住整个对象。它并不面临原子性问题,而是面临的内存可见性问题,只需要对on变量用volatile来修饰即可。

    public class Switcher {
        private volatile boolean on;
        public boolean isOn() {
            return on;
        }
        public void setOn(boolean on) {
            this.on = on;
        }
    }
    

    死锁

    经典示例:

    public class DeadLockDemo {
        private static Object lockA = new Object();
        private static Object lockB = new Object();
        private static void startThreadA() {
            Thread aThread = new Thread() {
                @Override
                public void run() {
                    synchronized (lockA) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                        }
                        synchronized (lockB) {
                        }
                    }
                }
            };
            aThread.start();
        }
        private static void startThreadB() {
            Thread bThread = new Thread() {
                @Override
                public void run() {
                    synchronized (lockB) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                        }
                        synchronized (lockA) {
                        }
                    }
                }
            };
            bThread.start();
        }
        public static void main(String[] args) {
            startThreadA();
            startThreadB();
        }
    }
    

    运行后aThread和bThread陷入了相互等待。怎么解决呢?首先,座该尽量避免在持有一个锁的同时去中请另一个锁,如果确实需要多个锁,所有代码都砹该按照相同的顺序去申请锁。比如,对于上面的例子,可以约定都先申请lockA,再申请lockB,而不是像代码中那样分开申请。

    不过,在复杂的项目代码中,这种约定可能难以做到。还有一种方法是显示锁接口Lock,它支持尝试获取锁(tryLock)和带时间限制的获取锁方法,使用这些方法可以在获取不到锁的时候释放已经持有的锁,然后再次尝试获取锁或干脆放弃,以避免死锁。
    如采还是出现了死锁,Java不会主动处理,不过借助一些工貝,我们可以发现运行中的死锁,比如,Java自带的jsiadc命令会报告发现的死锁。

    同步容器

    类Collections中提供了一些方法,返回线程安全的同步容器。

    image

    它们是給所有容器方法都加上synchronized来实现线程安全的。

    static class SynchronizedCollection<E> implements Collection<E> {
        final Collection<E> c;  //Backing Collection
        final Object mutex;     //Object on which to synchronize
        SynchronizedCollection(Collection<E> c) {
            if(c==null)
                throw new NullPointerException();
            this.c = c;
            mutex = this;
        }
        public int size() {
            synchronized (mutex) {return c.size();}
        }
        public boolean add(E e) {
            synchronized (mutex) {return c.add(e);}
        }
        public boolean remove(Object o) {
            synchronized (mutex) {return c.remove(o);}
        }
       //…
    }
    

    这里的线程安全指的是容器对象,即当多个线程并发访问同一个容器对象时不需要额外的同步操作,也不会出现错误的结果。

    加了synchronized之后所有操作都变成了原子操作,但并不意味着客户端在调用的时候就绝对安全了。以下情况还需注意:

    • 复合操作,比如先检查后更新
    • 伪同步
    • 迭代

    1. 复合操作

    public class EnhancedMap <K, V> {
        Map<K, V> map;
        public EnhancedMap(Map<K,V> map){
            this.map = Collections.synchronizedMap(map);
        }
        public V putIfAbsent(K key, V value){
             V old = map.get(key);
             if(old!=null){
                 return old;
             }
             return map.put(key, value);
         }
        public V put(K key, V value){
            return map.put(key, value);
        }
        //…
    }
    

    代码中的putIfAbsent方法在多线程下显然是不安全的。如果多个线程都执行这一步则必然会出现竞态条件。

    2. 伪同步

    public synchronized V putIfAbsent(K key, V value){
        V old = map.get(key);
        if(old!=null){
            return old;
        }
        return map.put(key, value);
    }
    

    如上代码即便加上synchronized关键字修饰也任然是不安全的。因为我们同步错了对象,putlfAbsent同步使用的是EnhancedMap对象,而其他方法(如代码中的put方法)使用的是Collections.synchronizedMap返回的对象map、两者是不同的对象。要解决这个问题,所有方法必须使用相同的锁,可以使用EnhancedMap的对象锁,也可以使用map对象锁。使用EnhancedMap对象作为锁,则Enhanced-Map中的所有方法都需耍加上synchronized。使用map作为锁,putlfAbsent方法可以改为:

    public V putIfAbsent(K key, V value){
        synchronized(map){
             V old = map.get(key);
             if(old!=null){
                 return old;
             }
             return map.put(key, value);
        }
    }
    

    3. 迭代

    对于同步容器虽然单个操作是安全的,但是迭代却不是。如下代码截取自Collections.SynchronizedList类。可以看到函数并不是同步实现的。

    public ListIterator<E> listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }
    
    public ListIterator<E> listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }
    

    我们通过两个线程来并发的修改和迭代该同步容器则会出现List迭代时规定的并发修改异常。

    private static void startModifyThread(final List<String> list) {
        Thread modifyThread = new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 100; i++) {
                    list.add("item " + i);
                    try {
                        Thread.sleep((int) (Math.random() * 10));
                    } catch (InterruptedException e) {
                    }
                }
            }
        });
        modifyThread.start();
    }
    private static void startIteratorThread(final List<String> list) {
        Thread iteratorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    for(String str : list) {
                    }
                }
            }
        });
        iteratorThread.start();
    }
    public static void main(String[] args) {
        final List<String> list = Collections
                .synchronizedList(new ArrayList<String>());
        startIteratorThread(list);
        startModifyThread(list);
    }
    

    运行结果:

    Exception in thread "Thread-0" java.util.ConcurrentModificationException
        at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
        at java.util.ArrayList$Itr.next(ArrayList.java:831)
    

    我们知道对于遍历操作而言,如果迭代时容器发生了结构性变化。就会抛出该异常。很显然同步容器并没有解决这个问题,要想避免这个问题则需要在遍历的时候给整个容器对象加锁。 即谁无法保证线程安全,方法体中的代码块就锁谁。

    private static void startIteratorThread(final List<String> list) {
        Thread iteratorThread = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true) {
                    synchronized(list){
                        for(String str : list) {
                        }
                    }
                }
            }
        });
    }
    

    4. 并发容器

    在使用synchronized的时候除了需要注意以上注意事项,同时同步容器的性能也是比较低的,当并发访问量比较大的时候性能比较差。但Java还为我们提供了很多专为并发设计的容器类。比如:

    • CopyOnWriteArrayList
    • ConcurrentHashMap
    • ConcurrentLinkedQueue
    • ConcurrentSkipListSet

    这些容器类都是线程安全的,仍但都没有使用synchronized,没存迭代问题,直接支持一些复合操作,
    性能也搞得多。

    释放锁的时机

    • 当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
    • 当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    不会由于异常导致出现死锁现象。

    相关文章

      网友评论

        本文标题:Java编程的逻辑 -- 并发章 -- Synchronized

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