文章目录
线程同步
这种操作的核心问题在于:每一个线程对象轮番抢占资源带来的问题。
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 | 实例对象Object | String 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重量级锁,线程的阻塞以及唤醒均需要操作系统由用户态切换到内核态,开销非常之大,因此效率很低。