synchronized处理同步问题、synchronized的底层实现

线程同步

  这种操作的核心问题在于:每一个线程对象轮番抢占资源带来的问题。

1.1 同步问题的引出

需求:多个线程同时卖票

class MyThread implements Runnable{
    //票数是多个线程的共享资源
    private int ticket = 10;
    @Override
    public void run() {
        while(ticket > 0)
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"还剩下"+ticket--+"张票");
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        new Thread(thread1,"黄牛A").start();
        new Thread(thread1,"黄牛B").start();
        new Thread(thread1,"黄牛C").start();
    }
}

在这里插入图片描述
这个时候我们发现,剩余票数竟然出现了0和负数,为什么呢?
在这里插入图片描述
  对于此类问题票数是共享资源,同一时刻只允许一个线程对于票数进行操作,以上这种情况我们称之为不同步操作。不同步的唯一好处就是处理速度快(多个线程并发执行,及多个黄牛一起卖票,当然速度快)

1.2 同步问题

  所谓的同步指的是所有的线程不是一起进入到方法中执行,而是按照顺序一个一个来。如果想要实现这种“锁”的功能,可以采用关键字synchronized来处理。
  使用synchronized关键字处理有两种模式:同步代码块同步方法

1.2.1 同步代码块

  在方法中使用synchronized(对象)关键字,一般可以锁定当前对象this,表示同一时刻只有一个线程能够进入同步代码块,但是多个线程可以同时进入方法。

class MyThread implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(ticket > 0)
        {
        	 //以下代码在同一时刻,只允许一个线程进入代码块
        	 //------------------------------------------
            synchronized(this) {  //表示为程序逻辑上锁
                if(ticket > 0)
                {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "还剩下" + ticket-- + "张票");
                }
            }
             //------------------------------------------
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        new Thread(thread1,"黄牛A").start();
        new Thread(thread1,"黄牛B").start();
        new Thread(thread1,"黄牛C").start();
    }
}

  这种方式是在方法里面拦截,也就是说同一时刻进入到run()方法的同步代码块有多个,但仅仅只有一个线程进入到了同步代码块(被synchronized修饰的代码块)中。
  对于上述例子,在同一时刻线程黄牛A、、黄牛B、黄牛C同时进入run方法,而此时剩余票数为1张,假如线程A获得了锁,进入代码块上锁不让其他线程进入,线程黄牛A卖掉了最后一张票,ticket变为0,释放锁,线程黄牛B,黄牛C进入同步代码块后if条件不成立,则不会卖票,票数也就不会出现负数。

1.2.2 同步方法

在方法声明上加synchronized关键字,表示此时只有一个线程能够进入同步方法

class MyThread implements Runnable{
    private int ticket = 100;
    @Override
    public void run() {
        while(ticket > 0)
        {
            this.sale();
        }
    }
    public synchronized void sale()
    {
        if(ticket > 0)
        {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+
                    "还剩下"+ticket--+"张票");
        }
    }
}
public class Test {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        new Thread(thread1,"黄牛A").start();
        new Thread(thread1,"黄牛B").start();
        new Thread(thread1,"黄牛C").start();
    }
}

  同步虽然可以保证数据的完整性(线程安全操作),但是其运行速度很慢。

1.3 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 Test {
    public static void main(String[] args) {
       for(int i= 0;i< 3;i++)
       {
           MyThread mt = new MyThread();
           mt.start();
       }
    }
}

在这里插入图片描述
  通过上述代码以及运行结果我们可以发现,synchronized并没有起到作用
  实际上,synchronized(this)以及非static的synchronized方法,只能防止多个线程同时执行同一个对象的同步代码段。即synchronized锁住的是括号里的对象,而不是代码。
  当synchronized锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才能达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。
那么,如果真的锁住这段代码要怎么做?

1.3.1 锁住同一个对象

package 同步问题;
class Sync{
    public void test()
    {
        synchronized(this)
        {
            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 Test {
    public static void main(String[] args) {
        Sync sync = new Sync();
        for(int i = 0 ; i < 3 ; i++)
        {
            MyThread thread1 = new MyThread(sync);
            thread1.start();
        }
    }
}

在这里插入图片描述

1.3.2 锁住这个类对应的Class对象

class Sync{
    public  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 Test {
    public static void main(String[] args) {
        for(int i = 0 ; i < 3 ; i++)
        {
            MyThread thread1 = new MyThread();
            thread1.start();
        }
    }
}

在这里插入图片描述
  上面代码用synchronized(Sync.class)实现了全局锁的效果。因此,如果想要锁的是代码段,锁住多个对象的同一方法,使用这种全局锁,锁的是类而不是this。

1.3.3 静态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.test();
    }
}
public class Test {
    public static void main(String[] args) {
        for(int i = 0 ; i < 3 ; i++)
        {
            MyThread thread1 = new MyThread();
            thread1.start();
        }
    }
}

在这里插入图片描述
  static方法可以直接类名加方法名调用,方法中无法使用this,所以它锁的不是this,而是类的class对象,所以 static synchronized方法也相当于全局锁,相当于锁住了代码段。

分类具体分类被锁的对象伪代码
方法实例方法类的实例对象public synchronized void test(){…………}
方法静态方法类对象public static synchronized void test(){…………}
代码块实例对象类的实例对象synchronized(this){…………}
代码块class对象类对象synchronized(类.class){…………}
代码块任意实例对象Object实例对象ObjectString lock = “”; synchronized(lock){…………}

1.4 synchronized的底层实现

1.4.1 同步代码块的底层实现

先看一段简单的代码:

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

反编译(javap -v 类名)后生成的部分字节码:

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 an

  可见执行同步代码块后首先要执行monitorenter指令,退出的时候执行monitorexit指令。
  使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor之后才能继续往下执行,否则就只能等待,然而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor
  但是我们可以发现,上述字节码包含一个monitorenter指令以及多个monitorexit指令,这是因为Java虚拟机需要确保所获得的锁在正常执行路径,以及异常执行路径上都能够被解锁

1.4.2 同步方法的底层实现

public synchronized void foo() {
	System.out.println("hello world");
}
public synchronized void foo();
	descriptor: ()V
	flags: ACC_PUBLIC, ACC_SYNCHRONIZED // *******
	Code:
		stack=2, locals=1, args_size=1
			0: getstatic    #5                                           // Field
java/lang/System.out:Ljava/io/PrintStream;
			3: ldc            #6                                          // String hello world
			5: invokevirtual #7                                      // Method java/io/PrintStream.println:
(Ljava/lang/String;)V
			8: return
			LineNumberTable:
			line 9: 0
			line 10: 8
}

  当用synchroniezed标记方法时,会看到字节码中方法的访问标记包括ACC_SYNCHRONIZED,该标记表示在进入该方法时,Java虚拟机需要进行monitorenter操作。而在退出该方法时,不管是正常返回,还是向调用者抛出异常,Java虚拟机都会进行monitorexit操作。
monitorenter指令:
  当JVM执行monitorenter时,如果目标锁对象monitor的计数器为0,那么说明它没有被其他线程所持有,在这个情况下,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将计数器加1,在目标锁对象的计数器不为0的情况下,如果锁对象的持有线程是当前线程,那么Java虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放锁;
monitorexit指令:
  当执行monitorexit时,Java虚拟机则需将该锁对象的计数器减1。当计数器减为0时,那代表该锁已经被释放掉了。
  对象锁(monitor)机制是JDK1.6之前synchronized底层原理,又称为JDK1.6重量级锁,线程的阻塞以及唤醒均需要操作系统由用户态切换到内核态,开销非常之大,因此效率很低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值