并发系列--Synchronized使用与原理

前言

Synchronized在多线程中使用得比较多的,这两天看了下慕课网相关课程,在此总结下其使用和原理

作用:

Synchronized据有可重入,不可中断性,能够保证在同一时刻最多只有一个线程执行该段代码,以达到保证并发安全的效果,内部是通过monitor来加锁和解锁的。

地位:

1.Synchronized是java的关键字,被java语言原生支持。
2.是最基本的互斥同步手段。
3.是并发编程中的元老级角色,是并发编程的必学内容。

不使用并发的后果

两个线程同时a++,如果不加锁,最后结果会比预计的少

public class DisappearRequest {

    int i = 0;

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            for (int j = 0; j < 100000; j++) {
                i++;
            }
        }
    };

    @Test
    public void test01() throws InterruptedException {

        Thread thread = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread.start();
        thread2.start();
        thread.join();
        thread2.join();

        System.out.println(i);

    }
}

运行上面代码,期望结果是200000,但是每次运行结果都是小于这个数的。

原因:
i++不是一个原子操作,实际上包含三个动作
1.读取i;
2.将i加1;
3.将i的值写入到内存中。

在多线程环境下这样是不安全的。在解决问题之前,还是先来学习下Synchronized的用法。

Synchronized的两个用法

对象锁

包括方法锁(默认锁对象为this当前实例对象)和同步代码块锁(自己指定的锁对象)

类锁

指synchronized修饰静态的方法或指定锁对象为class对象

一、对象锁

1、代码块形式
public Object lock = new Object();

//代码块锁
public Runnable runnable = new Runnable() {
    @Override
    public void run() {
        synchronized (lock) { //this或者自建对象
            System.out.println("我是对象锁的代码块形式,我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":运行结束");
        }
    }
};

@Test
public void test01() {

    Thread t1 = new Thread(runnable);
    Thread t2 = new Thread(runnable);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

对象锁只对同一个对象的方法起到作用,如果是不同对象同一个方法,锁会失效

2、方法锁形式
//方法锁
public Runnable runnable2 = new Runnable() {
    @Override
    public synchronized void run() {
        System.out.println("我是对象锁的方法修饰符形式,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }
};
@Test
public void test01() {

    Thread t1 = new Thread(runnable2);
    Thread t2 = new Thread(runnable2);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

方法锁形式,锁对象默认为this

二、类锁

概念:java类可能有很多个对象,但只有一个class对象
本质:所以所谓的类锁,不过是Class对象而已。

1、静态方法锁
class RunnableTest implements Runnable{

    @Override
    public void run() {
        method();
    }

    public static synchronized void method(){
        System.out.println("我是对象锁的静态方法修饰符形式,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

}

@Test
public void test01() {

    Thread t1 = new Thread(new RunnableTest());
    Thread t2 = new Thread(new RunnableTest());
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

如果不加static,RunnableTest里面的method方法可以同时运行

2、class对象代码块
//class对象锁
public Runnable runnable5 = new Runnable() {
    @Override
    public void run() {
        synchronized (SynchronizedTest.class){
            System.out.println("我是对象锁的class对象修饰符形式,我叫:" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":运行结束");
        }
    }
};
@Test
public void test01() {

    Thread t1 = new Thread(runnable5);
    Thread t2 = new Thread(runnable5);
    t1.start();
    t2.start();
    while (t1.isAlive() || t2.isAlive()) {

    }
    System.out.println("finished");
}

对象锁和类锁学完了,前面的例子就可以很容易的解决了,这里就不多说了。

多线程访问同步方法的7种情况

一、两个线程同时访问一个对象的同步方法

前面的对象锁已经学习过

二、两个线程访问的是两个对象的同步方法

前面的类锁的静态方法同步锁已经学过,如果不加静态的,那么两个线程运行毫无影响,不受干扰

三、两个线程访问的是synchronized的静态方法

加了静态方法就变成类锁,会一个一个执行

四、同时访问同步方法与非同步方法

public class SynchronizedYesAndNoTest {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是加锁的方法,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    public void method2(){
        System.out.println("我是没加锁的方法,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

非同步方法不受影响

五、访问同一个对象的不同的普通同步方法

public class SynchronizedDifferentMethod {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是加锁的方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是加锁的方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

一个一个执行

六、同时访问静态synchronized和非静态synchronized方法

public class SynchronizedStaticAndNormal {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public static synchronized void method(){
        System.out.println("我是静态加锁的方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是非静态加锁的方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }

}

不受影响,同时进行

七、方法抛出异常后,会释放锁

public class SynchronizedException {

    private Runnable runnable = new Runnable(){
        @Override
        public void run() {
            if(Thread.currentThread().getName().equals("Thread-0")){
                method();
            }else {
                method2();
            }
        }
    };

    public synchronized void method(){
        System.out.println("我是方法method,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        throw new RuntimeException();

//        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    public synchronized void method2(){
        System.out.println("我是方法method2,我叫:" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":运行结束");
    }

    @Test
    public void test01() {

        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        while (t1.isAlive() || t2.isAlive()) {

        }
        System.out.println("finished");
    }
}

抛出异常后,jvm会自动释放锁

八、七种情况总结

1.一把锁只能同时被一个线程获取,没有拿到锁的线程必须等待(对应第1,5种情况)
2.每个实例都对应有自己的一把锁,不同实例之前互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象共用同一把类锁(对应第2,3,4,6种情况)
3.无论是正常执行完毕或者抛出异常,都会释放锁。

Synchronized的性质

一、可重入性

定义:指的是同一个线程的外层函数获得锁之后,内层函数可以直接再次获取到锁。(Synchronized,Lock)

好处:避免死锁、提升封装性

粒度:线程而非调用(用3种情况来说明和pthread的区别)

1、情况1:证明同一个方法是可重入的
public class SynchronizedRecursion {

    private int a = 0;

    private synchronized void method(){
        System.out.println("这是method1,a = " + a);
        if(a==0){
            a ++;
            method();
        }
    }

    @Test
    public void test01(){
        method();
    }

}

递归调用自己可以成功

2、情况2:证明可重入不要求是同一个方法
public class SynchronizedOtherMethod {

    private synchronized void method(){
        System.out.println("这是method1");
        method2();
    }

    private synchronized void method2(){
        System.out.println("这是method2");
    }

    @Test
    public void test01(){
        method();
    }
}
3、情况3:证明可重入不要是同一个类种的
public class SynchronizedSuperClass {

    @Test
    public void test01(){
        TestClass testClass = new TestClass();
        testClass.method();
    }

}

class SuperClass{

    public synchronized void method(){
        System.out.println("我是父类的方法");
    }

}

class TestClass extends SuperClass{

    @Override
    public synchronized void method() {
        super.method();
        System.out.println("我是子类的方法");
    }
}

二、不可中断性

一旦这个锁已经被别人获取到了,如果我还想获得,我只能选择等待或者阻塞,直到别的线程释放这个锁。如果别人永远不释放锁,那么我就只能永远等待下去。

相比之下,Lock可以拥有中断的能力,第一点,如果我觉得我等的时间太长,有权中断现在已经获取到锁的线程的执行;第二点,如果我等待的时间太长了不想再等了,也可以退出。

原理

一、加锁和释放锁的原理

获取和释放的时机:内置锁,每个java对象可以用做实现同步的锁,这个锁就是内置锁,或者称做监视锁。

表达成lock的形式

public class SynchronizedToLock {

    Lock lock = new ReentrantLock();

    public synchronized void method1(){
        System.out.println("我是Synchronized形式的锁");
    }

    public void method2(){
        lock.lock();

        try{
            System.out.println("我是lock形式的锁");
        }finally {
            lock.unlock();
        }
    }

    @Test
    public void test01(){
        method1();
        method2();
    }

}

深入JVM看字节码
javac 编译成class文件
javap [-verbose] 反编译

//java文件
public class Decompilation {

    private Object object = new Object();

    public void insert(Thread thread){
        synchronized (object){
            
        }
    }

}

//反编译之后的文件
  Last modified 2020-8-9; size 503 bytes
  MD5 checksum 1832d1176898be312930160e30a29bf2
  Compiled from "Decompilation.java"
public class com.example.threadlocaldemo.Decompilation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // java/lang/Object
   #3 = Fieldref           #4.#22         // com/example/threadlocaldemo/Decompilation.object:Ljava/lang/Object;
   #4 = Class              #23            // com/example/threadlocaldemo/Decompilation
   #5 = Utf8               object
   #6 = Utf8               Ljava/lang/Object;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               insert
  #12 = Utf8               (Ljava/lang/Thread;)V
  #13 = Utf8               StackMapTable
  #14 = Class              #23            // com/example/threadlocaldemo/Decompilation
  #15 = Class              #24            // java/lang/Thread
  #16 = Class              #21            // java/lang/Object
  #17 = Class              #25            // java/lang/Throwable
  #18 = Utf8               SourceFile
  #19 = Utf8               Decompilation.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Utf8               java/lang/Object
  #22 = NameAndType        #5:#6          // object:Ljava/lang/Object;
  #23 = Utf8               com/example/threadlocaldemo/Decompilation
  #24 = Utf8               java/lang/Thread
  #25 = Utf8               java/lang/Throwable
{
  public com.example.threadlocaldemo.Decompilation();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: new           #2                  // class java/lang/Object
         8: dup
         9: invokespecial #1                  // Method java/lang/Object."<init>":()V
        12: putfield      #3                  // Field object:Ljava/lang/Object;
        15: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public void insert(java.lang.Thread);
    descriptor: (Ljava/lang/Thread;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #3                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter  //monitorenter和monitorexit两个指令实现加锁和释放锁
         7: aload_2
         8: monitorexit
         9: goto          17
        12: astore_3
        13: aload_2
        14: monitorexit
        15: aload_3
        16: athrow
        17: return
      Exception table:
         from    to  target type
             7     9    12   any
            12    15    12   any
      LineNumberTable:
        line 8: 0
        line 10: 7
        line 11: 17
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class com/example/threadlocaldemo/Decompilation, class java/lang/Thread, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

monitorenter和monitorexit两个指令实现加锁和释放锁

二、可重入原理:加锁次数计数器

1.jvm负责跟踪对象被加锁的次数
2.线程第一次给对象加锁的时候,计数变为1。每当这个相同的线程在此对象上再次获得锁时,计数会递增(可重入)。
3.每当任务离开时,计数递减,当计数为0的时候,锁会被完全释放。

三、保存可见性的原理:内存模型

在这里插入图片描述
在这里插入图片描述
Synchonized对象释放锁前,任何对象的修改都会被写入到主内存,保证每次执行都是可靠的,保证可见性

缺陷

效率低:锁的释放情况少、试图获得锁时不能设定超时、不能中断一个正在试图获得锁的线程
不够灵活(读写锁更灵活):加锁和释放的时机单一,每个锁仅有单一的条件(某个对象),可能是不够的
无法知道是否成功获取到锁

常见问题

1.使用注意点:锁对象不能为空(锁信息是保存在对象头中,锁为空,那么锁信息就没有了)、作用域不宜过大、避免死锁
2.如何选择Lock和Synchronized关键字

如果可以两个都不要用,而是选择java.util.courrent包中的类,优先Synchronized,避免出错的发生

3.多线程访问同步方法的各种情况

思考

1.多个线程等待同一个

参考

Java高并发之魂:synchronized深度解析

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值