中北软院创新实验室

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

2018-06-22  本文已影响80人  HikariCP

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还为我们提供了很多专为并发设计的容器类。比如:

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

释放锁的时机

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

上一篇下一篇

猜你喜欢

热点阅读