多线程之synchronized

191 阅读8分钟

如果多个线程访问同一个对象的实例变量的时候,则有可能会出现非线程安全的问题。

比如我们有一个ThreadTest类,这个类主要是给count赋值,当然我们把它加工了一下,当B线程进入的时候让他睡眠2s。

public class ThreadTest {
    private  int  count;
    
    public void setCount(int cin) throws InterruptedException {
        count = cin;
        if (Thread.currentThread().getName().equals("B")) {
            Thread.sleep(2000);
        }
        System.out.println(Thread.currentThread().getName()+"cin = " + count);
    }
}

然后我们分别有ThreadB和ThreadC两个类都继承了Thread

public class ThreadB extends Thread {

    private ThreadTest test;

    public ThreadB(String name, ThreadTest test){
        this.setName(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.setCount(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadC extends Thread {
    private ThreadTest test;

    public ThreadC(String name, ThreadTest test){
        this.setName(name);
        this.test = test;
    }

    @Override
    public void run() {
        try {
            test.setCount(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

之后我们在主线线程中启动这两个线程

public class Run {
    public static void main(String[] args){
        ThreadTest test = new ThreadTest();
        ThreadB threadB = new ThreadB("B",test);
        threadB.start();
        ThreadC threadC = new ThreadC("C",test);
        threadC.start();
    }
}

很出乎意料的是,此时两个线程打印出来的值都是200,并不是我们期待的Bcin = 100 Ccin=200

其实这就是多线程在抢占资源的时候产生的数据的脏读和脏写,如果我们对多线程内的同一个对象不加限制,那么很容易就会产生非线程安全问题。那么我们怎么解决呢?

  • synchronized关键字

    关键字synchronized可以用来保障原子性,可见性和有序性。

    原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

    可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

    有序性:即程序执行的顺序按照代码的先后顺序执行,防止jvm进行代码重排。(可以理解为代码的同步执行)

    如上面的问题我们就可以在方法setCount上添加关键字synchronized,使其保持原子性操作,这样就不会出现脏写的问题了。

    public synchronized void setCount(int cin) throws InterruptedException {
            count = cin;
            if (Thread.currentThread().getName().equals("B")) {
                Thread.sleep(2000);
            }
            System.out.println(Thread.currentThread().getName()+"cin = " + count);
        }
    

    所以只有共享资源的读写访问才需要同步化,如果是不共享的则不需要同步,比如将count写在setCount方法内那么就不需要同步了。

    • 在java中只能锁对象

      在Java中只有将对象作为锁,并没有锁方法或者代码块的说法。虽然synchronized存在三种形式(加在普通方法上,加在代码块上,加在静态方法上或者上锁的对象为Class对象)但是这三种形式都是对对象上锁。

      • synchronized加在普通的方法上

        我们在setCount方法上加上synchronized关键字,可以使setCount方法同步但是我们需要注意的是,因为在对象上加锁导致我们访问的业务对象只有是同一个的时候才能保持方法的同步。

        我们将ThreadTest类和main方法稍加改造

        public class ThreadTest {
            private  int  count;
            public synchronized void setCount(int cin) throws InterruptedException {
                System.out.println("线程"+Thread.currentThread().getName()+"开始");
                count = cin;
                if (Thread.currentThread().getName().equals("B")) {
                    Thread.sleep(2000);
                }
                System.out.println(Thread.currentThread().getName()+"cin = " + count);
                System.out.println("end");
            }
        }
        
        public class Run {
            public static void main(String[] args){
                // 两个不同的业务对象
                ThreadTest test = new ThreadTest();
                ThreadTest test1 = new ThreadTest();
                ThreadB threadB = new ThreadB("B",test);
                threadB.start();
                ThreadC threadC = new ThreadC("C",test1);
                threadC.start();
            }
        }
        

        我们得到的运行结果是

      由此可见线程和业务对象是一对一关系,而且关键字synchronized取得的锁都是对象锁

      虽然这样解决了非线程安全问题,但是如果这个方法很大而且我们又把synchronized加在方法上面那么就会造成我们程序的执行时间要被拉长,因为对于多个线程来说这个方法是同步的只有一个线程执行完毕并释放资源之后另外一个线程才能去执行。那么我们如何解决这个问题呢?

      • synchronized加在代码块上

        如果我们将synchronized加在不能保持原子性的那几段代码块上,不就能很好的去掉多余的不需要保持同步代码了吗。但是我们之前说到锁是加在对象上的,所以synchronized并不能像静态代码块那样使用,他必须指定一个对象。

        synchronized加在代码块上又两种实现方式

        1. synchronized(this){}

          这里面的this就是指定的当前对象,我们改造一下ThreadTest类

          public class ThreadTest {
              private  int  count;
          
              public  void setCount(int cin) throws InterruptedException {
                  System.out.println("线程"+Thread.currentThread().getName()+"开始");
                  // 将此部分代码块同步
                  synchronized (this){
                      // 为了验证this是同一个对象且为当前对象我们打印this
                      System.out.println("this = " + this);
                      count = cin;
                      if (Thread.currentThread().getName().equals("B")) {
                          Thread.sleep(2000);
                      }
                      System.out.println(Thread.currentThread().getName()+"cin = " + count);
                  }
                  System.out.println("end");
              }
          }
          

          main方法

          public class Run {
              public static void main(String[] args){
                  ThreadTest test = new ThreadTest();
                  System.out.println("test = " + test);
                  ThreadB threadB = new ThreadB("B",test);
                  threadB.start();
                  ThreadC threadC = new ThreadC("C",test);
                  threadC.start();
              }
          }
          

          我们可以看到this对象为我们当前创建的业务对象,且唯一。

        2. synchronized(非this对象){}

          只要我们遵循着对象就是锁,那么我们可以将synchronized(this){}中的this对象改为非this对象。但是我们需要注意的是,这里的锁必须是同一个,即非this对象必须是同一个否则运行结果就是异步调用。

          注意:非this对象调用中不能使用String和基本数据类型,因为String是存在常量池的,每一个持有此常量的String方法都会持有此锁,其他的基本数据类型也会有存在常量池或者重新new一个对象的情况,如Integer,当Integer在[-128,127]之间时,Integer会在常量池中取值,如果不在这之间时,此时会生成新的Integer对象,所以这个时候锁就失效了。

        以上两种方式我们发现锁的都是一个业务对象,但是这个业务对象中并不是一个方法,那么如果我们调用其他的方法也会是同步的吗?

        我们在ThreadTest类中添加一个新方法

        public  void setCount1(int cin) throws InterruptedException {
            System.out.println("线程"+Thread.currentThread().getName()+"开始运行setCount1方法");
            Thread.sleep(3000);
            System.out.println("setCount1end");
        }
        

        然后我们让ThreadB调用这个新的方法

        我们发现他并不是同步执行的,所以我们可以得出结论:不使用synchronized关键字的方法是异步的即使在同一个类里面有一个使用synchronized关键字的方法。但是这要这个方法加synchronized关键字那么他就会变成同步的。

      • syn static和syn(Class)

        这两种我们为什么放在一起说呢,因为这两种方式锁的对象并不是业务对象,而是Class类的对象。在jvm加载的时候会将所有的Class类都生成一个单例对象,而syn static和syn(Class)都是将锁加在这个对象上的。而且他对于这个类的所有实例对象都起作用。

        我们改造一下setCount方法,让他将锁加在Class类的对象上

         public   void setCount(int cin) throws InterruptedException {
                synchronized (ThreadTest.class){
                    System.out.println("线程"+Thread.currentThread().getName()+"开始");
                    count = cin;
                    if (Thread.currentThread().getName().equals("B")) {
                        Thread.sleep(2000);
                    }
                    System.out.println(Thread.currentThread().getName()+"cin = " + count);
                    System.out.println("end");
                }
            }
        

        然后产生两个业务对象,让不同的线程去执行

        public class Run {
            public static void main(String[] args){
                ThreadTest test = new ThreadTest();
                ThreadTest test1 = new ThreadTest();
                ThreadB threadB = new ThreadB("B",test);
                threadB.start();
                ThreadC threadC = new ThreadC("C",test1);
                threadC.start();
            }
        }
        

        我们发现他是同步的,并没有异步执行。

        syn static 也可以如此验证。

        那么他会不会和上面两种方式一样存在不使用synchronized关键字的方法是异步的呢?

        我们可以将setCount1放入ThreadTest中。最后我们得出的结果发现他入上面两个方式一样

        setCount1方法是异步的。

    • 锁的重入和其他

      当一个线程得到一个对象的锁之后是可以再次请求此对象锁,这种情况叫做锁的重入。

      为什么会有锁的重入呢,因为当一个上锁的方法调用本类类外一个上锁的方法,如果锁不能重入,那么就会导致此线程一直拿不到资源而死锁。

      除了本类的锁重入之外,还支持继承的环境的锁重入,即支持子类访问父类的方法。

      另外,当加锁的方法出现异常的时候,正在使用此锁的线程会自动释放锁,如果不释放会导致死锁的产生

      重写的方法不加synchronized是非同步的

    • synchronized的原理

      在方法中使用synchronized关键字实现同步的原因是使用了flag标记了ACC_SYNCHRONIZED,当调用方法的时候JVM会检查此方法是否设置了这个属性,如果设置了,线程先持有同步锁然后执行方法,方法执行完毕释放锁。 我们可以使用javap -c -v 对.class文件进行反编译可以看到字节码的具体设置

       public static synchronized void test();
      descriptor: ()V
      flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
      Code:
        stack=2, locals=0, args_size=0
           0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           3: ldc           #3                  // String 娴嬭瘯
           5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
           8: return
        LineNumberTable:
          line 6: 0
          line 7: 8
      

      如果使用的是synchronized代码块则会在字节码中将这块代码加上monitorenter和monitorexit指令

       public void test2();
      descriptor: ()V
      flags: ACC_PUBLIC
      Code:
        stack=2, locals=3, args_size=1
           0: aload_0
           1: dup
           2: astore_1
           3: monitorenter
           4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
           7: ldc           #5                  // String 娴嬭瘯2
           9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          12: aload_1
          13: monitorexit
          14: goto          22
          17: astore_2
          18: aload_1
          19: monitorexit
          20: aload_2
          21: athrow
          22: return
        Exception table:
           from    to  target type
               4    14    17   any
              17    20    17   any
        LineNumberTable:
          line 10: 0
          line 11: 4
          line 12: 12
          line 13: 22
        StackMapTable: number_of_entries = 2
          frame_type = 255 /* full_frame */
            offset_delta = 17
            locals = [ class com/example/javacode/thread/ThreadTest, class java/lang/Object ]
            stack = [ class java/lang/Throwable ]
          frame_type = 250 /* chop */
            offset_delta = 4