synchronized 同步

同步问题的引出

需求:多个线程同时卖票

class MyThread implements Runnable {
    private int ticket = 10;
    @Override
    public void run() {
        while (this.ticket > 0)
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黄牛1").start();
        new Thread(myThread,"黄牛2").start();
        new Thread(myThread,"黄牛3").start();
    }
}

Files\Java\jdk1.8.0_181\jre\lib\rt.jar;E:\Java\code\out\production\code" www.bit.java.TestThread
黄牛3还剩下10票
黄牛1还剩下9票
黄牛2还剩下10票
黄牛3还剩下8票
黄牛1还剩下6票
黄牛2还剩下7票
黄牛3还剩下5票
黄牛2还剩下3票
黄牛1还剩下4票
黄牛2还剩下2票
黄牛3还剩下1票
黄牛1还剩下0票
黄牛2还剩下-1票

Process finished with exit code 0

这个时候我们发现,票数竟然出现负数,这种问题我们称之为不同步操作。
不同步的唯一好处是处理速度快(多个线程并发执行)

同步处理

所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个进来。

synchronized处理同步问题

如果要想实现这把"锁"的功能,可以采用关键字synchronized来处理。
使用synchronized关键字处理有两种模式:同步代码块、同步方法

  • 同步代码块:在方法中使用synchronized(对象),一般可以锁定当前对象this。表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。
class MyThread implements Runnable {
    private int ticket = 1000;
    @Override
    public void run() {
        for(int i = 0;i< 1000;i++){
            synchronized (this){
                if(this.ticket > 0)
                {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
                }
            }
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黄牛1").start();
        new Thread(myThread,"黄牛2").start();
        new Thread(myThread,"黄牛3").start();
    }
}
  • 同步方法:在方法上加synchronized,表示此时只有一个线程能够进入同步方法。
class MyThread implements Runnable {
    private int ticket = 1000;
    @Override
    public void run() {
        for(int i = 0;i< 1000;i++){
            SellTicket(this.ticket);
        }
    }
    private synchronized void SellTicket(int ticket){
        if(this.ticket > 0)
        {
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"还剩下"+this.ticket--+"票");
        }
    }
}
public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"黄牛1").start();
        new Thread(myThread,"黄牛2").start();
        new Thread(myThread,"黄牛3").start();
    }
}

关于synchronized的额外说明

先来看一段代码:观察synchronized锁多对象

class Sync {
    public synchronized void test() {
        System.out.println("test方法开始,当前线程为 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法结束,当前线程为 "+Thread.currentThread().getName());
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

通过上述代码以及运行结果我们可以发现,没有看到synchronized起到作用,三个线程同时运行test()方法。

实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。对于非static的synchronized方法,锁的就是对象本身也就是this。
当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
那么,如果真要锁住这段代码,要怎么做?

  • 锁同一个对象
class Sync {
    public synchronized void test() {
        System.out.println("test方法开始,当前线程为 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法结束,当前线程为 "+Thread.currentThread().getName());
    }
}

class MyThread extends Thread{
    private Sync sync;
    public MyThread(Sync sync){
        this.sync = sync;
    }
    @Override
    public void run() {
        this.sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        Sync sync = new Sync();
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread(sync)).start();
        }
    }
}
  • 让synchronized锁这个类对应的Class对象—全局锁
class Sync {
    public synchronized void test() {
        synchronized (Sync.class){
            System.out.println("test方法开始,当前线程为 "+Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("test方法结束,当前线程为 "+Thread.currentThread().getName());
        }
    }
}

class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

上面代码用synchronized(Sync.class)实现了全局锁的效果。因此,如果要想锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this。
static synchronized方法,static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的Class对象,所以,static synchronized方法也相当于全局锁,相当于锁住了代码段。

class Sync {
    public static synchronized void test() {
        System.out.println("test方法开始,当前线程为 "+Thread.currentThread().getName());
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("test方法结束,当前线程为 "+Thread.currentThread().getName());

    }
}
class MyThread extends Thread{
    @Override
    public void run() {
        Sync sync = new Sync();
        sync.test();
    }
}

public class TestThread {
    public static void main(String[] args) {
        for(int i = 0;i < 3;i++){
            new Thread(new MyThread()).start();
        }
    }
}

synchronized实现原理

synchronized 同步_第1张图片

同步代码块底层实现

先来看一段简单的代码:

public class Test{    
	private static Object object = new Object();    
	public static void main(String[] args) {        
		synchronized (object) {            
			System.out.println("hello world");        
		}    
	} 
}

下面我们使用javap反编译后看看生成的部分字节码

 ...
 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                  // Field object:Ljava/lang/Object;
         3: dup
         4: astore_1
         5: monitorenter   // 瞪大眼睛看这里!!! 
         6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc           #4                  // String hello world
        11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: aload_1
        15: monitorexit    // 瞪大眼睛看这里!!! 
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit    // 瞪大眼睛看这里!!! 
        22: aload_2
        23: athrow
        24: return
      Exception table:
         from    to  target type
             6    16    19   any
            19    22    19   any
...

执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用 Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程可以获取到该对象的monitor监视器。

上述字节码中包含一个monitorenter指令以及多个monitorexit指令。这是因为Java虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁。

同步方法底层实现

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

当使用synchronized标记方法时,字节码会出现一个访问标记ACC_SYNCHRONIZED。该标记表示在进入方法时,JVM需要进行monitorenter操作。在退出方法时,无论是正常返回,还是向调用者抛异常,JVM需要进行monitorexit操作。

这里 monitorentermonitorexit 操作所对应的锁对象是隐式的。对于实例方法来说,这两个操作对应的锁对象是 this;对于静态方法来说,这两个操作对应的锁对象则是所在类的 Class 实例。

当JVM执行monitorenter时,如果目标对象monitor的计数器为0,表示此时该对象没有被其他线程所持有。此时JVM会将该锁对象的持有线程设置为当前线程,并且将monitor计数器+1。

在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,JVM可以将计数器再次+1(可重入锁);否则需要等待,直到持有线程是释放线程。

当执行monitorexit时,JVM需将锁对象计数器-1。当计数器减为0时,代表该锁以及被释放掉,唤醒所有正在等待的线程去竞争该锁。

之所以采用这种计数器的方式,是为了允许同一个线程重复获取同一把锁。举个例子,如果一个 Java 类中拥有多个 synchronized 方法,那么这些方法之间的相互调用,不管是直接的还是间接的,都会涉及对同一把锁的重复加锁操作。因此,我们需要设计这么一个可重入的特性,来避免编程里的隐式约束。

证明同一个对象的同步方法再次获得锁时不能获取成功

class MyThread extends Thread{
    public synchronized void A(){
        while(true){}
    }
    public synchronized void B(){
        System.out.println(Thread.currentThread().getName()+",线程B...");
    }

    @Override
    public void run() {
        A();
        B();
    }
}

public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"A").start();
        new Thread(myThread,"B").start();
    }
}

证明同一个线程再次获得锁时可以获取成功,而其他线程获取锁会阻塞(即锁的可重入性)

class MyThread extends Thread{
    public synchronized void A(){
    	 while(true){
    	 	System.out.println(Thread.currentThread().getName()+",线程A...");
        	B();
    	 }
    }
    public synchronized void B(){
        System.out.println(Thread.currentThread().getName()+",线程B...");
    }

    @Override
    public void run() {
        A();
        B();
    }
}

public class TestThread {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        new Thread(myThread,"A").start();
        new Thread(myThread,"B").start();
    }
}

你可能感兴趣的:(Java)