Synchronized关键字-监视器锁-monitor-lock,voliatile关键字,wait()和notify方法(详解)

在上一章的学习中,我们认识到了线程安全问题的风险和特征,而我们在处理线程安全问题的时候首先就会观察他是不是原子性问题和可见性问题导致的,这一章我们就会从原子性和可见性的两方面来着手解决线程安全问题。

目录

认识synchronized关键字

1.1.synchronized在这起到了二个效果:

1.2.提出问题:各位铁汁们,有没有发现我们的标题叫监视器锁-monitor-lock,而我们讲的是synchronized关键字???(末尾咱们揭晓答案哦!!!!)。

1.3.synchronized具体使用方法

理解synchronized可重入(重点哦)

2.1.分析以下代码

2.2.提问:我们应该如何理解,锁对象再加多次锁的时候,它时如何知道我们加的是同一个线程的锁的?

2.3.提问:加锁之后代码执行速度变慢了,我们有没有解决的办法?

简单认识java标准库中线程安全类

死锁(重点)

4.1.理解死锁

4.2.分析死锁的三个典型情况

再谈可重入不可重入问题

5.1.理解重入不可重入

死锁的四个必要条件

6.1.互斥使用:

6.2.不可抢占:

6.3.请求与保持:

6.4.循环等待:

如何破除死锁(重点)

Volatile关键字 (防止内存可见性问题,禁止编译器优化)

7.1.volatile的作用是基于内存可见性问题展开的

7.2.理解为什么没有被读到的过程

7.3.再谈可见性问题

7.4.拓展解释java中的工作内存和这里的工作内存

7.5.volatile关键字不保证原子性

认识wait和notify

8.1.wait() --- wait(),notify,notifyAll 都是object里面的方法哦

8.2.wait() 的执行机制

8.3.提出问题:join和sleep在上述场景中行不行呢???

8.4.再次分析在wait()情况下的线程随机调度和抢占式执行问题

8.5.notify和notifyAll

8.6.wait 和 sleep的对比

各位老铁有没有忘记我们之前提出的一个问题,为什么标题叫监视器锁-monitor-lock?


认识synchronized关键字

在前面的学习中,我们都知道,要想确保一段代码的线程安全性,是需要同时满足原子性和可见性以及防止指令重排序的问题的。而我们的synchronized关键字他就能处理这样的问题,那么在下面这段代码让我们来感受一下synchronized的神奇之处

package TestDemo;


class Counter{
    public int count = 0;

    synchronized public void add(){
        count++;
    }
}
public class TestDemo4 {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread thread1 = new Thread(()->{

            for(int i = 0; i < 50000;i++){
                counter.add();
            }
        },"thread1");
        Thread thread2 = new Thread(()->{

            for(int i = 0 ; i < 50000; i++){
                counter.add();
            }
        },"thread2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(counter.count);
    }
}

在上面的代码中预期的结果和实际结果相等!而synchronized关键字到底在这干了什么,让代码能够正确执行!!

1.1.synchronized在这起到了二个效果:

1.互斥性 -->某个线程在执行到被sychronized修饰的对象的时候,其他线程如果也想执行这个对象,此时此刻因为synchronized(晒q来自的)的修饰就会阻塞等待。

简单理解就是进入了synchronized修饰的代码块,就会对代码进行加锁处理。而执行完这段被sychornized修饰的代码块,就会对代码进行解锁处理,从而下一个线程才能继续调用这段代码。

2.保证了内存的可见性,以及防止了指令重排序(这两个都是提醒编辑器优化 --- 必须谨慎一点)

注意:synchronized用的锁是存在java对象头里的。

理解:什么叫在java对象头里了,就是每个对象在内存存储的时候,都存有一块表示当前"锁定的一个状态",好比厕所有一个有人/无人状态的设定,如果当前无人就可以使用,如果当前有人就需要排队。

阻塞等待:其实阻塞等待就跟上面上厕所的情况类似,在操作系统中,每一把锁都被系统内部维护了一个等待队列,这个锁被某个线程占用的时候,其他线程再尝试去加锁,不好意思,加不上,这个时候就会产生阻塞等待,一直等到正在调用的这个线程解锁之后,操作系统再去唤醒一个新线程,再次才能获取到这个锁.

我们在这个地方要特别注意的是,在上一个线程解锁之后,下一个线程不是立即就被唤醒了,它是需要我们操作系统来去“唤醒”,可以认为是系统调度过程的一部分工作。当然咯我们一定不能忘记,线程是随机调度,抢占式执行的,如果有A,B,C三个不同线程,去获取锁,A先获取到锁,B要想获取锁,就需要进入等待队列,等A解锁了才能再次获取,这个时候C他也是在等待获取的一个过程,所以当我们的A被解锁之后,不是B就能立即去获取锁的,这个地方他不分先后,就必须还要跟C竞争一下(因为A解锁了所以A线程还可能继续调度这个锁,所以他也会参与竞争),谁先获取到锁谁先用,这就是我们的随机调度和抢占式的特点,一定不能忘记哦各位铁汁们。

1.2.提出问题:各位铁汁们,有没有发现我们的标题叫监视器锁-monitor-lock,而我们讲的是synchronized关键字???(末尾咱们揭晓答案哦!!!!)。

1.3.synchronized具体使用方法

1.直接修饰普通方法,进入方法就是加锁,出方法就是解锁,锁对象相当于this.

package TestDemo;

class Student{
    public int count;
    
    public synchronized void Add(){
        count++;
    }
}
public class TestDemo5 {
    public static void main(String[] args) throws  InterruptedException{
        Student student = new Student();
        Thread thread = new Thread(()->{
            for(int i = 0; i < 10; i++){
                student.Add();
            }
        });
        thread.start();
        thread.join();
        System.out.println(student.count);
    }
}

2.修饰一个static方法,此时相当于修饰当前的类对象,为锁对象.

class Student{
    public static int count;

    public static synchronized void Add(){
        count++;
    }
}

3.修饰一个代码块,需要我们手动指定一个"锁对象".

class Student{
    public static int count;

    public void Add(){
        synchronized (this){
            count++;
        }
    }
}

还有一个也是表示锁类对象,跟我们的修饰静态方法一样也是锁类对象,不过他是在修饰一个代码块的时候进行手动指定的

class Student{
    public static int count;

    public void Add(){
        synchronized (Student.class){
            count++;
        }
    }
}

4.重点理解锁对象

咱们在代码中锁的到底是个什么,我们得明确我们针对的是哪个对象加的锁!!!(这是两个问题,不要混为一谈)

我们此处分三种情况讨论:

1.如果两个线程对同一个对象加锁,那么就会出现锁竞争/锁冲突,因为线程它是抢占式执行,随机性调度的,那个线程先获取到锁那个线程就先用,另一个线程就会出现在等待序列中阻塞等待,等到上一个线程解锁之后,才能再次调度。

public class TestDemo5 {
    private static Object loker1 = new Object();
    private static Object loker2 = new Object();

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 =  new Thread(()->{
            int i = 6;
           while(i > 0){
               synchronized (loker1){
                   System.out.println("hello thread1");
                   i--;
               }
           }
        });
        thread1.start();
        //这个地方加一个sleep,是为了更加方便的理解,两线程的竞争关系
        //让thread1先执行完,再次执行thread2
        Thread.sleep(3000);
        Thread thread2 = new Thread(()->{
            int i = 6;
           while(i > 0){
               synchronized (loker1){
                   System.out.println("hello thread2");
                   i--;
               }
           }
        });
        thread2.start();
        thread2.join();
    }
 }

上面结果的执行顺序看起来是一个固定的顺序,一定不要被迷惑,因为在我们调用thread2之前加了一个sleep()睡眠机制,只是为了让我们更好的观察锁竞争的关系,大家可以把sleep去掉,再去运行一下,看看是啥结果!!!(记住哦,线程是随机调度的,抢占式执行的)

2.两个线程对不同对象加锁,此时就不会发生锁竞争/锁冲突,各自获取各自的锁,也就不会有阻塞等待了。

public class TestDemo5 { private static Object loker1 = new Object(); private static Object loker2 = new Object(); public static void main(String[] args) throws InterruptedException{ Thread thread1 = new Thread(()->{ int i = 6; while(i > 0){ synchronized (loker1){ System.out.println("hello thread1"); i--; } } }); thread1.start(); //这个地方加一个sleep,是为了更加方便的理解,两线程的竞争关系 //让thread1先执行完,再次执行thread2 //Thread.sleep(3000); Thread thread2 = new Thread(()->{ int i = 6; while(i > 0){ synchronized (loker2){ System.out.println("hello thread2"); i--; } } }); thread2.start(); thread2.join(); } }

此时就不会出现锁竞争/锁冲突问题。

3.最后一种情况,还是两个线程,一个线程加锁,一个线程不加锁,那么这个时候是否还会出现锁竞争呢!!!答案是不会的

class Student{
    public static int count;

    public void Add(){
        synchronized (Student.class){
            count++;
        }
    }
    public void Add2(){
        count++;
    }
}
public class TestDemo5 {


    public static void main(String[] args) throws InterruptedException{
        Student student = new Student();
        Thread thread1 = new Thread(()->{
           for(int i = 0; i < 5000; i++){
               student.Add();
           }
        });
        thread1.start();
        Thread thread2 = new Thread(()->{
            for(int i = 0; i < 5000; i++){
                student.Add2();
            }
        });
        thread2.start();

        thread1.join();
        thread2.join();

        System.out.println(Student.count);
    }
}

当我运行两次(甚至多次)后,他会出现两种不同的结果,不是有一个加锁了嘛,怎么还会出现这种结果呢!!!这个地方就得注意,其实这样的锁,加了等于没加,因为它不能控制另一个线程在调度没加锁的对象的时候保持在阻塞等待状态,所以上一个线程在对对象在进行运算的时候,这一个线程也在对对象进行运算,导致两个线程对象不知道对方算到哪一步了,就会把自己算出的结果保存到主内存中,最后就会出现不同执行结果。(注意这个线程他就是一个不安全的线程,在实践工作中,尽量避免写这类代码).

注意:我们在理解关于锁对象的规则的时候,其实非常简单的,就是上面这三种情况(如果还有就是这三种情况的延伸),不要多想,不要多想,不要多想!!!

理解synchronized可重入(重点哦)

2.1.分析以下代码

class Counter1{
    public int count;

    public synchronized void add(){
        synchronized (this){
            count++;
        }
    }
}

当我们在写代码的时候,有时候就会出现上面这样一段代码,对同一个对象,加锁两次!!!就会出现一个疑虑,这段代码会不会出现问题?

此时此刻,我们的锁对象是this,只要有线程调用我们的add()方法,当进入add方法的时候,我们就会加一次锁,紧接着我们在进入代码块的时候,又会尝试去加锁,第一次加锁是能够成功的,那么第二次能不能成功呢?我们从this的角度出发,他可能会认为我已经被其他线程占用了,被其他线程加锁了,那么这第二次加锁可能就不会成功,可能会出现阻塞等待!但是在我们的synchronized中有这样一个设定,就是当我们在对同一个线程加锁两次甚至是多次的时候,不会出现阻塞等待情况,依然能加锁成功,在java中这就叫可重入!如果不允许上述操作的话,就叫不可重入,这个时候我们就会出现另外一个问题 死锁 就是线程僵住了,动弹不了了。(放心咱们后面马上就会讲解死锁问题,知道你们急,但你们先别急,哈哈哈!!!)。

2.2.提问:我们应该如何理解,锁对象再加多次锁的时候,它时如何知道我们加的是同一个线程的锁的?

答:此时此刻,在锁对象中在第一次加锁的时候,我们的锁会记录一下,当前的锁是哪个线程持有的。如果加锁线程和持有线程是同一个,此时就直接进去,否则进不去,给我在外面排队 (阻塞等待)。

2.3.提问:加锁之后代码执行速度变慢了,我们有没有解决的办法?

答:没错,一旦加锁之后,代码速度一定是大打折扣的,有没有解决办法,是没有的,但是还是有意义的,我们加锁是为了保证代码的安全性和执行结果的准确性,虽然会有一定的耗时开销,但是还是比我们的单线程快,我们在写代码的时候,我们一定要让他准确和安全,如果结果不准确代码也没有安全性,算得快,执行快哪又有什么用了,对叭!!!

简单认识java标准库中线程安全类

java标准库中有很多线程是不安全的,简单的讲就是有很多集合类他是不安全的,当我们多线程操作同一个集合类的时候,我们就需要考虑到线程安全问题,我们在使用以下代码的时候就需要个格外注意:

ArrayList

linkedList

HashMap

TreeMap

HashSet

TreeSet

StringBuilder

这些代码是不具备安全性的,所以我们在使用的时候一定要格外小心,特别是在多线程的使用情况下。

当然咯,有不安全的集合类,一定就会有安全的集合类:

Vector(不推荐使用)

HashTable(不推荐使用)

ConcurrentHashMap

StringBuffer

这些集合类就是相对安全的,因为在集合类中内置了synchronized锁。

死锁(重点)

哈哈!铁汁们别急,咱这就来讲讲啥是死锁。

4.1.理解死锁

在线程中,当有两个或多个的线程在等待对方释放资源时 ,这个时候大概率就会发生死锁状态,线程会一直处于阻塞等待这个状态,程序无法执行下去。而导致死锁通常是因为线程与线程之间的的资源竞争和同步问题导致的,就是多个线程竞争同一个锁或处于一种循环请求的状态中导致的,要想避免死锁状态的产生,就需要我们合理设计线程间的同步机制和资源分配。

4.2.分析死锁的三个典型情况

1.持有并等待死锁情况:一个线程一把锁,连续加锁两次

class Student1{
    public  int StuId;

    public synchronized void add(){
        synchronized (this){
            StuId++;
        }
    }
}

public class TestDemo6 {
    public static void main(String[] args) throws InterruptedException {
       Student1 student1 = new Student1();
        Thread thread1 = new Thread(()->{
           for(int i = 0; i < 5000; i++){
               student1.StuId++;
           }
        });
        thread1.start();
        thread1.join();
        Thread thread2 = new Thread(()->{
            for(int i = 0; i < 5000; i++){
                student1.StuId++;
            }
        });

        thread2.start();
        thread2.join();
        System.out.println(student1.StuId);
    }

上面这种情况一个线程持有一个锁,再次获取同一个锁的时候,此时就会发生加锁两次的情况,这种情况一般称为可重入锁,可重入锁的设定有效的避免了死锁状态的产生。

2.循环等待死锁的情况:两个线程两把锁分别为 thread1和thread2,thread1和thread2各自针对锁A和锁B,再尝试获取对方的锁。

如何理解呢!!就好比吃饺子,朋友A吃饺子的时候就喜欢蘸酱油,朋友B吃饺子就喜欢蘸醋吃,此时呢,朋友A对朋友B说你把醋给我,我用完了给你!此时,朋友B也对朋友A说,你把酱油给我,我用完了给你!!!如果两人都不礼让,此时俩哥们就僵住了,我们的上述的循环等待死锁情况就跟这情况类似,谁也不让谁,谁都得不到。

package TestDemo;

public class TestDemo7 {
    public static void main(String[] args) {
        Object jiangyou = new Object();
        Object cu = new Object();

        Thread A = new Thread(()->{
            synchronized (jiangyou){
                try{
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (cu){
                    System.out.println("朋友A把酱油和醋都拿到了");
                }
            }
        });
        Thread B = new Thread(()->{
           synchronized (cu){
               try{
                   Thread.sleep(1000);
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
               synchronized (jiangyou){
                   System.out.println("朋友B把酱油和醋都拿到了");
               }
           }
        });
        A.start();
        B.start();
    }
}

上面这段代码就很好的反应了,循环等待死锁的情况,大家看,两个线程都在等待对方释放资源,两边都没有等到对方释放,导致两个人都没有拿到酱油和醋。如何解决这个代码问题了,我们可以改变一下两个线程获取锁的顺序,如一下优化代码:

package TestDemo;

public class TestDemo7 {
    public static void main(String[] args) {
        Object jiangyou = new Object();
        Object cu = new Object();

        Thread A = new Thread(()->{
            synchronized (jiangyou){
                try{
                    //这个地方加sleep是为了确保两个线程先把第一个锁拿到
                    //否则不容易构造出来。因为线程是抢占式执行的。
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized (cu){
                    System.out.println("朋友A把酱油和醋都拿到了");
                }
            }
        });
        Thread B = new Thread(()->{
           synchronized (jiangyou){
               try{
                   Thread.sleep(1000);
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
               synchronized (cu){
                   System.out.println("朋友B把酱油和醋都拿到了");
               }
           }
        });
        A.start();
        B.start();
    }
}

这个时候,两人就都能获取到对方的需求.

3.资源互斥锁的情况:多个线程,多把锁的情况。

好比有几个人同时要穿鞋:

出现这种情况,咋办,老铁们是不是懵住了,放心,咱们是有解决的办法的,我们先让他们都穿右边的鞋子,然后再去穿左边的鞋子,这样就能避免死锁的问题。咱们还是通过代码感受一下:

package TestDemo;

public class TestDemo8 {
    public static void main(String[] args) {
        Object RihgtShoes = new Object();
        Object LeftShoes = new Object();

        Thread thread1 = new Thread(()->{
            synchronized (RihgtShoes){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(LeftShoes){
                    System.out.println("No1.穿上了左脚也穿上了右脚");
                }
            }
        });
        Thread thread2 = new Thread(()->{
            synchronized (RihgtShoes){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(LeftShoes){
                    System.out.println("No2.穿上了左脚也穿上了右脚");
                }
            }
        });
        Thread thread3 = new Thread(()->{
            synchronized (RihgtShoes){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(LeftShoes){
                    System.out.println("No3.穿上了左脚也穿上了右脚");
                }
            }
        });
        Thread thread4 = new Thread(()->{
            synchronized (RihgtShoes){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(LeftShoes){
                    System.out.println("No4.穿上了左脚也穿上了右脚");
                }
            }
        });
        Thread thread5 = new Thread(()->{
            synchronized (RihgtShoes){
                try {
                    Thread.sleep(1000);
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
                synchronized(LeftShoes){
                    System.out.println("No5.穿上了左脚也穿上了右脚");
                }
            }
        });
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
        thread5.start();

    }
}

此时这五个人就都能穿上鞋子了,是不是看着结果很别扭,我就知道各位老铁会这么想,咱们得理解线程是抢占式执行的,随机性调度的!!!!

再谈可重入不可重入问题

5.1.理解重入不可重入

我们在上面的学习中知道了死锁是指在多线程并发执行时,两个或多个线程相互持有对方需要的资源,导致彼此等待,无法继续执行下去的一种情况。

在死锁中,其实可重入和不可重入的问题主要涉及到线程中所持有的锁的类型。

可重入锁是指一个线程在持有锁的同时,可以再次请求该锁,而不会被自己所持有的锁阻塞。并且可重入锁通常是基于计数器实现的,每当一个线程获得锁时,计数器加1,当线程释放锁时,计数器减1.如果计数器为0,锁就释放。

不可重入锁是指一个线程在持有锁的时候,如果再次请求该锁,就会被自己所持有的锁所阻塞。这种锁通常是基于标志位实现的,如果标志位为true,就表示锁已经被持有,其他线程无法获取锁了。

在死锁中,如果线程持有的是可重入锁,那么即使它在等待其他锁的时候再次请求自己所持有的锁,也不会阻塞,这样就能避免死锁的发生。但是如果线程持有的是不可重入锁,那么在等待其他锁的时候再次请求自己所持有的锁就会被阻塞,从而就会导致死锁的发生。

因此,在多线程并发执行的时候,为了避免死锁的产生,应该尽量使用可重入锁,避免使用不可重入锁。同时,应该尽量减少锁持有的时间,避免出现长时间等待的情况。

死锁的四个必要条件

6.1.互斥使用:

指一个线程获得了一个锁,另一个线程要想再获得这个锁,就得给我等着,等上一个锁释放完了再来使用。

6.2.不可抢占:

一个线程获得了一个锁,在未使用完之前,不能被其他线程所调用执行,只能由该线程自己释放。

6.3.请求与保持:

一个线程本身持有一个锁,但又提出了获得一个新锁的请求,而这个新锁正好被另外一个线程所占用了,请求线程就会被阻塞,但是我们本身所持有的那个锁是保持不放的一个状态。

6.4.循环等待:

比如一个线程1获取到一个锁A和锁B,线程2又尝试获取锁B和锁A,那么在线程1在获取锁B的时候,需要等待线程2释放B,线程2在获取锁A的时候,需要等待线程1释放A,这个时候每一个线程都在等待下一个线程所占用的资源,此时就会导致系统资源无法释放。

如何破除死锁(重点)

我们在上面的学习中,了解到了死锁的四个必要条件,其实我们会发现,前三个条件都是锁的基本特性,这个咱们也是改变不了的,所以,我们要想避免死锁的发生,我们的突破口就是循环等待这第四个条件。

那么我们该如何在第四个条件上加以修改了,才能避免死锁的发生???

嘿嘿嘿,相信大家在上面的学习中,都了解到了,让两个不同线程在调用共享资源的时候,给他们固定一个顺序(或者来一个编号)让他们从小到大进行加锁。当我们不管有多少个线程的加多少个锁的时候,都让线程遵守上述顺序,此时我们的循环等待自然就破除了,死锁就可以有效避免了,哈哈哈哈各位铁汁们是不是惊讶到了,就这就行了??没错,这就是我们解决死锁,最简单可靠的办法,所谓大道至简,说的就是这个理。

Volatile关键字 (防止内存可见性问题,禁止编译器优化)

7.1.volatile的作用是基于内存可见性问题展开的

什么意思,在一个循环中,如果一个线程正在读一个变量,而另一个线程正在写这个变量,此时很可能就会发生,写的这个变量,没有被读到,从而读的这个变量很可能一直都是之前的那个原始数据。而volatile关键字就是为了保证,我在写完这个变量的时候,你要读这个变量。

import java.util.Scanner;

class Book{
    public   int count = 0;
   // public volatile int count = 0;
}
public class TestDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Book book = new Book();
        Thread thread1 = new Thread(()->{
           while(book.count == 0){

           }
            System.out.println("循环结束!");
        });
        Thread thread2 = new Thread(()->{
           Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            book.count = scanner.nextInt();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
 }

在上面的一段代码中,我们应该可以了解到Voliatle关键字是和内存的可见性问题息息相关的,在前面的章节中我们也认识到了voliatile关键字的特点,那么我们在这一块内容就从内存出发,再次解析解析内存可见性问题和voliatile关键字在内存中发生了那些故事。

我还是拿上面的一段代码进行分析:

7.2.理解为什么没有被读到的过程

这里咱们要用汇编语言来理解

1.首先通过load,把内存中的变量值,读取到寄存器上。

2.第二步,把寄存器的值,和0进行比较。根据比较结果,决定下一步往那个地方执行。

因为我们上述执行的是一个循环,这个循环执行的非常快,一秒大概能执行百万次以上....而问题就会出现在这个地方,循环这么多次,而当再写这个变量时,load获取到的结果都是一样的(load执行速度是非常慢的),此时我们的jvm就做出了一个非常大胆的决定 ---不在重复执行load这个操作了,就是不在去重新加载这个变量了,就直接默认是之前的那个原始值。所以在后来的不管写入多少次新的值变量的时候,我们的线程执行的时候是没有读到的,而volatile关键字的出现就是来解决这个问题的,就让load必须每次给我执行一次,也是为了保证线程的安全性。

所以我们在编写代码的时候一定要注意这个点,当然咯,编译器优化问题也不是始终会出现的比如下面代码中加一个sleep,就避免编译器的错误优化

import java.util.Scanner;

class Book{
    public   int count = 0;
   
}
public class TestDemo14 {
    public static void main(String[] args) throws InterruptedException {
        Book book = new Book();
        Thread thread1 = new Thread(()->{
           while(book.count == 0){

               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
            System.out.println("循环结束!");
        });
        Thread thread2 = new Thread(()->{
           Scanner scanner = new Scanner(System.in);
            System.out.println("输入一个整数:");
            book.count = scanner.nextInt();
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
    }
 }

这个时候代码就正确运行,那是不是以后我们就可以不用volatile关键字了呢!就直接用这样的方式去避免编译器优化问题了,首先编译器优化问题本来就是个“玄学问题”,就是无法预知,咱们看不到它是如何工作的,所以比较稳妥的办法,还是加上volatile关键字比较好.

7.3.再谈可见性问题

从JMM的角度重新看待可见性问题的产生:Java程序里有一个主内存,和一个工作内存(这个地方不要误会,我们线程的工作内存和这个地方的工作内存不是一个东西),我们上面代码的thread1线程在进行读取的时候,只是读取了工作内存的值,而thread2线程在进行修改的时候,先修改了工作内存的值,然后再把工作内存中的值同步保存到主内存中。由于编译器的优化,这个时候thread1根本就没有重新读主内存中的变量值,读到的还是之前未修改的值。哦豁,这个时候内存可见性问题就被触发咯!!!!

7.4.拓展解释java中的工作内存和这里的工作内存

主内存 ---- 就是我们平时说的内存

工作内存 ----- 就是工作存储区 (你可以认为是某个变量的暂时存储区)

我们重点理解工作内存,工作内存他不是说的是内存,而是cpu上存储数据的单元也就是寄存器。

此时此刻问题就来了,工作内存不就是一个cpu寄存器嘛,为什么在Java中就非要说成是工作内存这个说法,哈哈,其实不然,在工作内存中,也就是在cpu中不一定只有寄存器还有一个东西叫做cache(高速缓存器),这个玩意是干什么的,我们都知道寄存器的出现是为提升我们cpu的执行效率,但是寄存器存储空间太小了,并且很贵,所以为了平航这个寄存器,我们伟大的科学家就研究出了高速缓存器这个玩意,存储空间居中,读写速度也居中,当然最重要的就是成本也居中,所以在cpu中进行读和写操作的时候也可能是在cache上面,我们编写和优化Java各位大佬,为了不让我们搞混淆这个地方,所以在Java中,就统称为工作内存.嘿嘿嘿,各位老铁理解了嘛!!!!

7.5.volatile关键字不保证原子性

voliatile它只是针对内存中可见性问题的一个解决方法,但是它不保证原子性,咱们还是从下一面一段代码好好感受一下:

package TestDemo;

class Cur{
    public volatile int count;//记录有多少个被子
}
public class TestDemo15 {
    public static void main(String[] args) throws InterruptedException {
        final Cur cur = new Cur();
        Thread thread1 = new Thread(()->{
           for(int i = 0; i < 50000; i++){
               cur.count++;
           }
        });
        Thread thread2 = new Thread(()->{
            for(int i = 0; i < 50000; i++){
                cur.count++;
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(cur.count);
    }
}

此时此刻咱们就会发现,哎,这个结果他怎么就对不上号了,哈哈哈,本质说明voliatile不保证原子性的。所以保证原子性问题还是需要请出我们的synchronized来上锁。

voliatile他只是针对于,一个线程读操作,一个线程写操作这个场景上保证他的可见性,防止编译器优化,而synchronized不仅是可以保证原子性还能保证可见性,防止指令重排序。

我们来感受一下synchronized保证内存可见性的案例:

package TestDemo;

import java.util.Scanner;

class Mouse{
    public int count = 0;//记录有几个鼠标
}
public class TestDemo16 {
    public static void main(String[] args) {
        Mouse mouse = new Mouse();
        Thread thread1 = new Thread(()->{
           while(true){
               synchronized (mouse){
                   if(mouse.count != 0){
                       break;
                   }
               }
           }
            System.out.println("循环结束!!");
        });

        Thread thread2 = new Thread(()->{
            Scanner sc = new Scanner(System.in);
            System.out.println("输入一个整数:");
            mouse.count = sc.nextInt();
        });

        thread1.start();
        thread2.start();
    }
}

是不是震惊到了,synchronized也太牛波了叭,还能这样,其实我们在使用synchronized的时候也不一定什么场景下都能保证可见性,还是要具体情况具体分析,最好就是在不确保原子性和可见性问题上,把voliatile和synchronized都加上。

认识wait和notify

8.1.wait() --- wait(),notify,notifyAll 都是object里面的方法哦

随着咱们多线程的出现,抢占式执行和随机性调度的风险安全也随之而来,不知道各位老铁有没有这样的感受,咱们在编写代码的时候总喜欢控制这个代码的执行顺序,不喜欢随机的,不确定的东西,反正我是有的哈哈哈哈!!!!

那么在java中,有这样一批程序猿大佬他还真就为我们设计了一个这样的功能方法,来控制线程之间的执行顺序,当然线程在内核里的调度还是随机的,我们只是通过一些api来主动阻塞线程,就是主动放弃cpu的调度(给别的线程让路),就像我们火车,同是火车,有的他就会给我们的高铁让路,自己一边等着,一样的道理。这个方法就是我们的wait()方法。

所以 他到底如何去控制我们的执行顺序的,通过接下来的这个例子,让我们来感受一下wait()方法叭:

比如:我们有两个线程分别为thread1和thread2,当前需要让thread1先开始执行,等执行到差不多了,再让thread2执行,我们都知道线程是随机性调度的,抢占式执行的,想让我们的thread1先执行是不确定的, 此时,我们就可以在thread2中调用wait()方法,让他阻塞,让它主动放弃cpu的调用,等我们的thread1干的差不多了,在通过另一个方法notify通知thread2,把thread2唤醒,再让thread2接着干。

public class ThreadDemo{


    public static void main(String[] args) throws InterruptedException{
        Object object = new Object();
      
            //这里面不写参数,就是死等
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");

    }
}

我们会发现这段代码,报出了非法锁状态异常提醒,啥原因导致的,这个地方我们就得了解一下wait在这个地方做了什么工作:

8.2.wait() 的执行机制

首先,我会先释放锁,其次,让某个线程进行阻塞等待,最后,等到通知之后,重新尝试获取锁,获取到锁之后,继续往下执行

那么这个地方就有一个问题,我都没有锁,如何去获取锁!!在Java中规定,wait操作需要搭配synchronized来使用:

public class ThreadDemo{


    public static void main(String[] args) throws InterruptedException{
        Object object = new Object();
        synchronized (object){
            //这里面不写参数,就是死等
            System.out.println("wait 之前");
            object.wait();
            System.out.println("wait 之后");
        }

    }
}

这个时候,咱们就能看到程序执行完“wait()之前”就会一直处于一个阻塞的状态,后面的代码咱不跑了,只有等到notify来通知了,来唤醒了这个wait状态,我们后面的代码才会继续执行.

8.3.提出问题:join和sleep在上述场景中行不行呢???

首先使用join,那么必须要等thread1全部执行完,thread2才能执行,那如果说,我只希望thread1执行到一半,我thread2就开始执行,怎么办,此时join()是没办法做到的,只有使用我们的wait()

其次,我们的sleep它是一个指定休眠时间的操作。如果在这个两个线程中调用sleep,时间是不可控的,就是thread1什么时候执行完,咱是不知道的,所以时间是不可控的

那么在以上场景使用wait()和notify()可以更好的解决以上问题.

8.4.再次分析在wait()情况下的线程随机调度和抢占式执行问题

我们写三个线程,分别按顺序输出ABC这三个字符,循环输出10次这样的操作

public class TestDemo{

        public static void main(String[] args) throws InterruptedException {
            Object locker1 = new Object();
            Object locker2 = new Object();
            for(int i = 0; i < 10; i++){
                Thread thread1 = new Thread(()->{
                    System.out.print("A");
                    synchronized (locker1){
                        locker1.notify();
                    }
                });
                Thread thread2 = new Thread(()->{
                    synchronized(locker1){
                        try {
                            locker1.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print("B");
                    synchronized(locker2){
                        locker2.notify();
                    }
                });
                Thread thread3 = new Thread(()->{
                    synchronized (locker2){
                        try {
                            locker2.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("C");
                });


                thread2.start();
                thread3.start();
               
                thread1.start();
            }

        }
    }

这个时候咱们就会发现,咦,为什么执行结果不是按照ABC这个顺序来执行的,不是说我们的wait()可以让线程阻塞等待嘛,来控制线程执行顺序吗!!!各位老铁,小小的脑袋是不是充满了大大的疑惑???到底为什么呢!!!!

虽然wait()它能控制线程的执行顺序,但是它控制不了线程的随机性调度特点,就是说这个地方,不能保证先执行的是wait()还是notify,如果先调用notify,此时没有线程去wait,那么此处的wait是无法被唤醒的,那么就会像上面那样一直处于阻塞状态,那么这个时候的通知就是无效的,放心虽然无效但也不会起什么副作用,只是一直等着罢了。

那我们该如何解决这个问题了?我们先让拥有wait的线程先执行就好了,优化代码如下:

public class TestDemo{

        public static void main(String[] args) throws InterruptedException {
            Object locker1 = new Object();
            Object locker2 = new Object();
            for(int i = 0; i < 10; i++){
                Thread thread1 = new Thread(()->{
                    System.out.print("A");
                    synchronized (locker1){
                        locker1.notify();
                    }
                });
                Thread thread2 = new Thread(()->{
                    synchronized(locker1){
                        try {
                            locker1.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.print("B");
                    synchronized(locker2){
                        locker2.notify();
                    }
                });
                Thread thread3 = new Thread(()->{
                    synchronized (locker2){
                        try {
                            locker2.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("C");
                });


                thread2.start();
                thread3.start();
                //为什么加上sleep,还把start放在后面写,主要是怕线程调度的时候
                //先把locker1.notify给执行了,但是后面的线程2还没开始执行,导致进入线程2的
                //时候,再执行locker1.wait()的时候,就僵住了,因为前面的唤醒功能已经执行完了
                //现在这个还没有收到唤醒机制,此时就会出现死等情况,后面的代码就执行不了了。
                //加上这个就是为保证后面的代码能够先执行到。
                Thread.sleep(1000);
                thread1.start();
            }

        }
    }

这个时候他就能正确运行了!!!所以我们在多线程代码的时候啊,一定要注意多线程的随机性调度,抢占式执行的特点(一定要切记啊,兄弟们)。

8.5.notify和notifyAll

notify()方法就是唤醒线程等待

在上面的代码中,我们都知道了一个wait就对应一个notify,当notify被调用后,对应的某个因为wait()正在阻塞的线程,就会唤醒这个线程,执行后面的代码,当执行完毕后,退出同步代码块释放锁,进行下一个线程的调度,下面这段代码就很好的诠释了notify的作用:

public class ThreadDemo { public static void main(String[] args) { Object object = new Object(); Thread thread1 = new Thread(()->{ //这个线程负责进行等待 System.out.println("t1: wait 之前"); try{ synchronized (object){ object.wait(); } }catch (InterruptedException e){ e.printStackTrace(); } System.out.println("t2: wait 之后"); }); Thread thread2 = new Thread(()->{ System.out.println("t2 notify 之前"); synchronized (object){ //notify 务必要获取到锁,才能进行通知 object.notify(); } System.out.println("t2 notify 之后"); }); thread1.start(); try { Thread.sleep(1000); thread2.start(); } catch (InterruptedException e) { e.printStackTrace(); } } }

而我们的notifyAll方法其实跟我们的notify方法类似,只不过notifyAll方法是同时唤醒同个线程,线程之间的顺序不是同时执行,还是要根据线程的抢占式和随机性来执行,并且在业务场景中,我们还是使用notify比较多,notifyAll只了解就好了....

8.6.wait 和 sleep的对比

讲实话两者其实没啥好说的,但挡不住这玩意有时候会考到,所以咱还是在这总结一下把

wait()用于多线程之间进行同步操作,通常在多个线程中共享资源使用时。wait()方法会使当前的线程暂停执行,并释放线程所持有的锁,直到被其他线程notify()或notifyAll()所唤醒.

sleep()用于让当前线程休眠指定的时间,让CPU交出执行权给其他线程。sleep()方法不会释放线程持有的锁(跟锁没关系),并且在指定时间结束后会自动唤醒线程继续执行。

总之,wait()用于线程间协调、同步和通信,而sleep()用于限制线程执行时间,控制程序流程。此外,wait()只能在同步代码块或同步方法中使用,需要手动释放锁,而sleep()可以在任何地方调用,并不能释放锁,跟锁就没关系,记住吼!!!

各位老铁有没有忘记我们之前提出的一个问题,为什么标题叫监视器锁-monitor-lock?

monitor-lock其实这是jvm给咱们的synchronized起的这个名字,因此代码中出现异常的时候可能会看到这个说法(哈哈哈就是一个东西指的~~~).


到这里,又要跟各位老铁说再见咯!我们这一章的学习到这就结束了,这一章是有点难理解的,如果学习时候感到吃力是很正常的,需要各位老铁反复学习观看,如果觉得对各位老铁有帮助的话,给个小星星咯!下次再见咯!!!!拜拜

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值