多线程之synchronized关键字

synchronized关键字

1、为什么需要同步器

多线程编程中,有可能会出现多个线程同时访问同一个共享、可变资源的情况,这个资源我们称之其为临界资源;这种资源可能是:

对象、变量、文件等。

共享:资源可以由多个线程同时访问 

可变:资源可以在其生命周期内被修改 

引出的问题:

由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问!

2、如何解决线程并发安全问题?

实际上,所有的并发模式在解决线程安全问题时,采用的方案都是序列化访问临界资源。即在同一时刻,只能有一个线程访问临

界资源,也称作同步互斥访问

Java 中,提供了两种方式来实现同步互斥访问:****synchronized Lock

同步器的本质就是加锁

加锁目的:序列化访问临界资源,即同一时刻只能有一个线程访问临界资源(同步互斥访问)

不过有一点需要区别的是:当多个线程执行一个方法时,该方法内部的局部变量并不是临界资源,因为这些局部变量是在每个线程的

私有栈中,因此不具有共享性,不会导致线程安全问题。

synchronized内置锁是一种对象锁(锁的是对象而非引用),作用粒度是对象,可以用来实现对临界资源的同步互斥访问,是可

重入的。

加锁的方式:

1、同步实例方法,锁是当前实例对象

2、同步类方法,锁是当前类对象

3、同步代码块,锁是括号里面的对象

synchronized是基于JVM内置锁实现,通过内部对象Monitor(监视器锁)实现,基于进入与退出Monitor对象实现方法与代码

块同步,监视器锁的实现依赖底层操作系统的Mutex lock(互斥锁)实现,它是一个重量级锁性能较低。当然,JVM内置锁在1.5

之后版本做了重大的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销,,内置锁的并发性能已经基本与 Lock持平。

synchronized关键字被编译成字节码后会被翻译成monitorenter 和 monitorexit 两条指令分别在同步块逻辑代码的起始位置

与结束位置。

每个同步对象都有一个自己的Monitor(监视器锁),加锁过程如下图所示:

从这里我们可以得到一个信息,也就是说这些线层也在进行操作,此时在多核操作系统下,可能一直在CPU上来进行执行。

Monitor监视器锁

任何一个对象都有一个Monitor与之关联,当且一个Monitor被持有后,它将处于锁定状态。Synchronized在JVM里的实现都是

基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和

MonitorExit指令来实现。

monitorenter:每个对象都是一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行

monitorenter指令时尝试获取monitor的所有权,过程如下:

a. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor

的所有者;

b. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1;

c. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝

试获取monitor的所有权;

monitorexit:执行monitorexit的线程必须是objectref所对应的monitor的所有者。指令执行时,monitor的进入数减

1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去

获取这个 monitor 的所有权。

monitorexit,指令出现了两次,第1次为同步正常退出释放锁;第2次为发生异步退出释放锁

通过上面两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来

完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则

会抛出java.lang.IllegalMonitorStateException的异常的原因

看一个同步方法

public class SynchronizedMethod { 
     public synchronized void method() { 
    System.out.println("Hello World!"); 
   } 
 }

从编译的结果来看,方法的同步并没有通过指令 monitorentermonitorexit 来完成(理论上其实也可以通过这两条指令来

实现),不过相对于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根据该标示符来实现方法的同步的

当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取

monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个

monitor对象。

两种同步方式本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。两个指令的执行是JVM通

过调用操作系统的互斥原语mutex来实现,被阻塞的线程会被挂起、等待重新调度,会导致“用户态和内核态”两个态之间来回切

换,对性能有较大影响。

什么是monitor?

可以把它理解为 一个同步工具,也可以描述为 一种同步机制,它通常被 描述为一个对象。与一切皆对象一样,所有的Java对象

是天生的Monitor,每一个Java对象都有成为Monitor的潜质,因为在Java的设计中 ,每一个Java对象自打娘胎里出来就带了一把

看不见的锁,它叫做内部锁或者Monitor锁也就是通常说Synchronized的对象锁,MarkWord锁标识位为10,其中指针指向的

是Monitor对象的起始地址。在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的,其主要数据结构如下(位于

HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的):

1 ObjectMonitor() { 
    2 _header = NULL; 
    3 _count = 0; // 记录个数 
    4 _waiters = 0, 
    5 _recursions = 0; 
    6 _object = NULL; 
    7 _owner = NULL; 
    8 _WaitSet = NULL; // 处于wait状态的线程,会被加入到_WaitSet 
    9 _WaitSetLock = 0 ; 
    10 _Responsible = NULL ; 
    11 _succ = NULL ; 
    12 _cxq = NULL ; 
    13 FreeNext = NULL ; 
    14 _EntryList = NULL ; // 处于等待锁block状态的线程,会被加入到该列表 
    15 _SpinFreq = 0 ; 
    16 _SpinClock = 0 ; 
    17 OwnerIsThread = 0 ; 
    18 }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成

ObjectWaiter对象 ),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:

  1. 首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当

前线程,同时monitor中的计数器count加1

  1. 若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet

集合中等待被唤醒

  1. 若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁)

同时,Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式

获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须

在同步代码块中使用。监视器Monitor有两种同步方式:互斥与协作。多线程环境下线程之间如果需要共享数据,需要解决互斥访问

数据的问题,监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问

3、练习

3.0 案例一

想要实现的功能是能够达到交替执行1-2-3这种效果,于是乎开始写出来这样的代码:

/**
 * 验证交替打印
 * 三个线程交替打印
 */
public class MyThread extends Thread {
    private static volatile int i = 1;
    public static void main(String[] args) {
        MyThread t = new MyThread();
        new Thread(()->{
            while (true) {
                synchronized (t) {
                    if (i != 1) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=2;
                    System.out.println("11111");
                }
            }
        }).start();

        new Thread(()->{
            while (true) {
                synchronized (t) {
                    if (i != 2) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=3;
                    System.out.println("22222");
                }
            }
        }).start();


        new Thread(()->{
            while (true) {
                synchronized (t) {
                    if (i != 3) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=1;
                    System.out.println("33333");
                }
            }
        }).start();
    }
}

但是通过控制台打印,可以发现如下结果:

11111
22222
11111
22222
11111
22222

这种并没有达到我们想要的效果。

那么来分析一下:

当i !=1 的时候,那么当前i为2和3的时候就会进入到wait状态;三个线程又开始同时开始抢。但是要记住的一点是wait的特性,那就是第二次进来的时候,哪里睡得就从哪里开始来进行执行。如果线程抢到了,但是不是1,而是2,又因为wait是在哪里睡的,又会从哪里醒来,那么就会失去原来的判断,那么也就会导致没有顺序的出现。所以为了保证顺序,应该将上面的if都修改成while,代码如下所示:

public class MyThread extends Thread {
    private static volatile int i = 1;
    public static void main(String[] args) {
        MyThread t = new MyThread();
        new Thread(()->{
            while (true) {
                synchronized (t) {
                    while (i != 1) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=2;
                    System.out.println("11111");
                }
            }
        }).start();

        new Thread(()->{
            while (true) {
                synchronized (t) {
                    while (i != 2) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=3;
                    System.out.println("22222");
                }
            }
        }).start();


        new Thread(()->{
            while (true) {
                synchronized (t) {
                    while (i != 3) {
                        try {
                            t.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 程序执行到了这里,那么说明i一定为1
                    t.notifyAll();
                    i=1;
                    System.out.println("33333");
                }
            }
        }).start();
    }
}

即使线程从哪里睡得,当再次醒来的时候,执行完成之后又会来进行判断,如果判断不成立,那么依然会陷入到睡眠中去。如果判断成功,那么就会执行。

3.0、小结

1、需要注意这里的while的巧妙用法!

2、这里的notifyAll()和notify()方法这里是唤醒其他线程,但是在当前的同步块中如果没有执行完成,那么是不会释放掉锁的,等到notify或者是notifyAll在synchronized同步代码块中的代码执行完成之后,才会真正的释放掉锁;

3、wait方法可以查看之前讲解的。具体的可以分成为第一次和第二次(及其之后的特性),如果是第一次,多个线程在处于争抢监视器锁的时候,如果抢到了,那么将会进入到同步代码块中来执行;而没有抢到监视器的线程,将会进入到阻塞状态中,如果说抢到监视器的线程在执行代码的之后遇到了wait的执行,那么让出当前的监视器,然后大家一起进入到争抢状态。假设让出的监视器的线程又一次拿到了监视器,那么就会在原来阻塞的地方继续执行。

3.1、案例二

场景:三个线程,两个消费者,一个投食者。当消费者消费完的时候,投食者来进行投食

/**
 * 三个线程,两个消费者,一个投食者。当消费者消费完的时候,投食者来进行投食
 */
public class MyThreadOne {


    public static void main(String[] args) {

        Food food = new Food();

        // 两个动物线程
        Thread t1 = new Thread(() -> {
            synchronized (food) {
                while (food.meat < 1) {
                    try {
                        food.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序执行到了这里,那么说明了food.meat一定是>=1的,因为有了一个while循环在这里来进行判断
                System.out.println("大熊吃了一斤肉,还剩:   " + food.meat + "     斤肉");
                food.meat--;
                System.out.println("大熊吃了一斤肉,还剩:   " + food.meat + "     斤肉");
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (food) {
                while (food.meat < 1) {
                    try {
                        food.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 程序执行到了这里,那么说明了food.meat一定是>=1的,因为有了一个while循环在这里来进行判断
                System.out.println("小熊吃了一斤肉,还剩:   " + food.meat + "     斤肉");
                food.meat--;
                System.out.println("小熊吃了一斤肉,还剩:   " + food.meat + "     斤肉");
            }
        });

        // 饲养员线程
        Thread t3 = new Thread(() -> {
            synchronized (food) {
                while (food.meat < 1) {
                    food.meat++;
                }
                food.notifyAll();
                System.out.println("添加了一斤肉片进去");
            }
        });
        t1.start();
        t2.start();
        t3.start();
    }
}

class Food  {
   volatile int meat = 1;
}

输出控制台打印:

大熊吃了一斤肉,还剩:   1     斤肉
大熊吃了一斤肉,还剩:   0     斤肉
添加了一斤肉片进去
小熊吃了一斤肉,还剩:   1     斤肉
小熊吃了一斤肉,还剩:   0     斤肉

从上面可以看到,我们可以自定义一个消费者和生产者来进行操作。那么看到了这里,一切似乎开始变得明朗起来。

案例三

/**
 * @author lg
 * @Description 三条线程交互打印1-100
 * @date 2021/10/19 10:38
 */
public class DemoTwo {
    /**
     * 每个线程需要来进行打印的
     */
    volatile int i;
    /**
     * 用来表示线程
     * 1表示线程1
     * 2表示线程2
     * 3表示线程3
     */
    volatile int j;

    public static void main(String[] args) {
        DemoTwo demoTwo = new DemoTwo();
        // 需要注意的是这里的值需要和下面的if判断(demoTwo.i % 3 == 1)结合在一起来进行操作
        demoTwo.i = 1;
        demoTwo.j = 1;
        new Thread(() -> {
            while (true) {
                synchronized (demoTwo) {
                    while (demoTwo.j != 1) {
                        try {
                            demoTwo.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (demoTwo.i % 3 == 1) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 这里就是demoTwo.i=1的情况
                    demoTwo.notifyAll();
                    System.out.println("当前的线程是:" + Thread.currentThread().getName() + "   ,当前的i的值是______" + demoTwo.i);
                    demoTwo.i++;
                    demoTwo.j = 2;
                }
            }
        }).start();


        new Thread(() -> {
            while (true) {
                synchronized (demoTwo) {
                    while (demoTwo.j != 2) {
                        try {
                            demoTwo.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (demoTwo.i % 3 == 2) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 这里就是demoTwo.i=1的情况
                    demoTwo.notifyAll();
                    System.out.println("当前的线程是:" + Thread.currentThread().getName() + "   ,当前的i的值是______" + demoTwo.i);
                    demoTwo.i++;
                    demoTwo.j = 3;
                }
            }
        }).start();


        new Thread(() -> {
            while (true) {
                synchronized (demoTwo) {
                    while (demoTwo.j != 3) {
                        try {
                            demoTwo.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    if (demoTwo.i % 3 == 0) {
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    // 这里就是demoTwo.i=1的情况
                    demoTwo.notifyAll();
                    System.out.println("当前的线程是:" + Thread.currentThread().getName() + "   ,当前的i的值是______" + demoTwo.i);
                    demoTwo.i++;
                    demoTwo.j = 1;
                }
            }
        }).start();
    }
}

3.1总结

从上面两个案例可以看出来,涉及到syncronized关键字的时候,一般涉及到while来进行操作。

posted @ 2021-10-17 18:31  雩娄的木子  阅读(89)  评论(0编辑  收藏  举报