Java 之 synchronized 讲解-青铜段

免责声明: 这是本人第一个原创系列,希望以轻松幽默的风格讲解代码的实现原理,更因为本人水平有限,难免有疏漏的地方。如果读者遇到文章中需要改进或者看不懂,甚至是觉得错误的地方,可以给我留言。

文章中重要名词会以高亮形式展示,如synchonized, 如果文章中以粗体展示,如 synchronized,表示这一段比较重要。 如果文章中以细斜体展示,如synchronized,表示这一段的叙述我并没有十分把握,仅以参考为主。

本实验平台主要是基于本人的 MacbookPro 基于 jdk1.8,HotSpot macOS Catalina 10.15 内存 8 GB 2133 MHz LPDDR3

$ uname -a
Darwin MacBook-Pro.local 19.0.0 Darwin Kernel Version 19.0.0: Wed Sep 25 20:18:50 PDT 2019; root:xnu-6153.11.26~2/RELEASE_X86_64 x86_64
$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

openjdk 版本为 8u60 下载地址为 openjdk-8u60


helloworld

众所周知,synchronizedjava中的关键字。 既然从青铜段位开始,我们就从synchronized的用法开始说起。synchronized主要有代码块和修饰方法两种使用方法。

代码块

代码块又可以分为对象

Object o = new Object();
synchronized (o) {
    ...
}
synchronized (Object.class) {
    ...
}

方法

方法又可以分为修饰实例方法静态方法

public synchronized String method() {
    return "实例方法";
}
public static synchronized String method() {
    return "静态方法";
}

优缺点

修饰方法的话使用起来是最简单的,但是最小颗粒就是方法级别的了,而且只能以当前持有方法的对象或者是类为锁。 代码块的话,颗粒度可以自己控制,并且还能以任意对象作为锁对象,更自由。 我个人还是更倾向于代码块。

jdk的举例

jdk中已经有很多地方用到了该关键字,让我们来看看吧

代码块

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {
    ...
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        for (Node<K,V>[] tab = table;;) {
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else  if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else  if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                synchronized (f) {                    
                    ...
                }
            }
        }
        ...
    }
    ...
}
public class Observable {
    ...
    public void notifyObservers(Object arg) {
        ...
        synchronized (this) {            
            if (!changed)
                return;
            arrLocal = obs.toArray();
            clearChanged();
        }
        ...
    }
}

方法

直接修饰方法,的确可以做到线程安全,但是过于粗暴的加锁也极大的降低了并发时候的性能。

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    ...
    @Override
    public synchronized StringBuffer append(CharSequence s) {
        ...
    }
    @Override
    public synchronized StringBuffer append(float f) {
        ...
    }

    @Override
    public synchronized StringBuffer append(double d) {
        ...
    }
    ...
}
public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {
    ...
    @Override
    public synchronized V put(K key, V value) {
        ...
    }
    @Override
    public synchronized V remove(Object key) {
        ...
    }

    @Override
    public synchronized V get(Object key) {
        ...
    }
    ...
}

字节码指令

举了这些使用示例,synchronized究竟是如何实现的呢?还是先从最简单的字节码指令说起。使用javap命令可以把class文件转成字节码指令

代码块

假设我有这样一个class

public class SynchronizedTest {

    public static void main(String[] args) {
        int i = 1;
        synchronized (SynchronizedTest.class) {
            i++;
        }
        System.out.println(i);
    }
}

通过javap -v可以查看class文件

$ javap -v SynchronizedTest
0: iconst_1
1: istore_1
2: ldc           #2                  // class jvmtest/SynchronizedTest
4: dup
5: astore_2
6: monitorenter
7: iinc          1, 1
10: aload_2
11: monitorexit
12: goto          20
15: astore_3
16: aload_2
17: monitorexit
18: aload_3
19: athrow
20: iload_1
21: invokestatic  #3                  // Method syncMethod:(I)I
24: istore_1
25: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
28: iload_1
29: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
32: return

重点看下6 monitorenter,11 monitorexit,17 monitorexit,而11,17的区别应该就在于抛不抛异常,两条monitorexit确保了synchronized能在程序抛出异常后能正常解锁。

方法

同样假设我有这样一个 class

public  class SynchronizedTest {

    public static void main(String[] args) {
        method("laoxun");
    }

    public static synchronized String method(String name) {
        System.out.println(name);
        return name;
    }
}

通过javap -v可以查看class文件

...
public static synchronized java.lang.String method(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
    stack=2, locals=1, args_size=1
        0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        3: aload_0
        4: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        7: aload_0
        8: areturn
    LineNumberTable:
    line 11: 0
    line 12: 7
    LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       9     0  name   Ljava/lang/String; 

看到生成的class文件中的method方法有ACC_SYNCHRONIZED标识符,JVM 在执行该方法时会隐式的在方法前后加上monitorentermonitorexit,并且根据该方法是实例方法还是静态方法决定锁对象是对象还是

代码示例

使用同一个锁对象

public  class SynchronizedTest {

    private  static class Task extends Thread {
        private SynchronizedTest synchronizedTest;
        public Task(SynchronizedTest synchronizedTest, String name) {
            this.synchronizedTest = synchronizedTest;
            this.setName(name);
        }
        @Override
        public void run() {
            synchronized (this.synchronizedTest) {
                try {
                    System.out.println(String.format("%s  %s start", this.getName(), System.currentTimeMillis()));
                    Thread.sleep(5000L);
                    System.out.println(String.format("%s  %s end", this.getName(), System.currentTimeMillis()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Task t1 = new Task(synchronizedTest, "t1");
        Task t2 = new Task(synchronizedTest, "t2");
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("main end");
    }
}

控制台输出

t1  1589450854584 start
t1  1589450859607 end
t2  1589450859608 start
t2  1589450864608 end
main end

可以看到两个线程直接是同步的

使用不同的锁对象

把 t2 修改下,t1使用对象作为锁,t2 使用该对象的类作为锁

public  class SynchronizedTest {

    private  static class Task extends Thread {
        private SynchronizedTest synchronizedTest;
        public Task(SynchronizedTest synchronizedTest, String name) {
            this.synchronizedTest = synchronizedTest;
            this.setName(name);
        }
        @Override
        public void run() {
            synchronized (this.synchronizedTest) {
                try {
                    System.out.println(String.format("%s  %s start", this.getName(), System.currentTimeMillis()));
                    Thread.sleep(5000L);
                    System.out.println(String.format("%s  %s end", this.getName(), System.currentTimeMillis()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private  static class TaskClass extends Thread {
        public TaskClass(String name) {
            this.setName(name);
        }
        @Override
        public void run() {
            synchronized (SynchronizedTest.class) {
                try {
                    System.out.println(String.format("%s  %s start", this.getName(), System.currentTimeMillis()));
                    Thread.sleep(5000L);
                    System.out.println(String.format("%s  %s end", this.getName(), System.currentTimeMillis()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        SynchronizedTest synchronizedTest = new SynchronizedTest();
        Task t1 = new Task(synchronizedTest, "t1");
        TaskClass t2 = new TaskClass("t2");
        t1.start();
        t2.start();

        t1.join();
        t2.join();
        System.out.println("main end");
    }
}

控制台输出

t2  1589451037997 start
t1  1589451037997 start
t2  1589451043022 end
t1  1589451043022 end
main end

两个线程是同时执行的

总结

青铜篇就介绍到这里,向大家介绍了synchronized两种基本用法,当锁对象相同的所有线程执行是同步的,不同的锁对象之间的线程是异步的。 别急,毕竟只是第一篇,循序渐进慢慢来,如果你这一篇读完了,恭喜你可能实习的岗位也面不上。哈哈哈

扫一扫,关注它

猜你喜欢

转载自blog.csdn.net/a419240016/article/details/107171746