【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

导读:本篇文章讲解 【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify),希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

目录

1. 线程安全

1.1  出现线程不安全

 1.2 线程不安全的原因

1.3解决线程不安全(加锁🔒)

2.加锁使用synchronized🔒

2.1  修饰方法

2.2 修饰代码块

2.3 synchronized的特性

 2.4 锁竞争

3.volatile关键字(保证内存可见性)

3.1 volatile能保证内存可见性

3.2volatile不保证原子性   

3.3JMM(Java内存模型)

4.wait和notify(协调多个线程的执行顺序)

4.1 wait和notify方法

4.2 notifyAll方法

4.3 wait和sleep对比


1. 线程安全

1.1  出现线程不安全

两个线程,每个线程都针对counter进行5w次自增,预期结果是10w


class  Counter  {
    public int counter =0;
    public void increase() {
        counter++;
    }
}
public class Demo {
    private static Counter  counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread  t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter.counter);
    }
}

多次运行程序后,发现每次结果都不同,且不为10w

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 进程counter++操作,底层是三条指令在CPU上完成的

(1)把内存中的数据读取到CPU寄存器中   load

(2)把CPU的寄存器的值进行+1操作          add

(3)把寄存器的值,写回到内存中              save

由于当前是两个线程修改一个变量,并且每次修改是三个步骤(不是原子操作),而且线程之间的调度顺序是不确定的,最终导致两个线程真正执行这些操作时,可能会有多种执行的排列顺序

 1.2 线程不安全的原因

(1)操作系统调度的随机性,抢占式执行(内核实现的 没办法避免)

多个线程的调度执行过程,可以视为是“全随机”的

(在写 多线程 代码时,需要考虑到,任意一种调度的情况下,都可以运行出正确结果的)

(2)多个线程修改一个变量

String是不可变对象(不能修改String对象的内容)

不可变对象,不是指final修饰,而是set系列方法隐藏了(private)

这样的好处就是其中一个是“线程安全”的。

(3)修改操作不是原子的(解决线程安全最常见的方法)

比如前面的counter++操作,本质就是三个CPU指令

load+add+save (CPU执行指令,都是以“一个指令”为单位进行执行,一个指令相当于CPU上“最小单位了”,不能说指令执行一半就把线程调度走)

但是像有些操作,比如int赋值,就是单个CPU指令,这个时候更加安全一些

(4)内存可见性

内存可见性属于是JVM的代码优化引入的bug

编译器优化:因为程序猿写代码的能力高低不同,所以想让编译器把写代码等价转化成另一种执行逻辑,使逻辑不变,效率提高

虽然这样的优化,能够使效率提高,非常优秀,但是多线程代码下容易出现误判

(5)指令重排序

1.3解决线程不安全(加锁🔒)

加锁:

就拿刚开始的例子来,在counter++之前加锁,在counter++后解锁,(这两个操作之间,就是独占线程的,别的线程用不了,独占就是 互斥)在加锁和解锁之间,可以进行修改,这个时候别的线程想要修改,是修改不了的(别的线程只能阻塞等待,阻塞等待的线程,BLOCKED状态


2.加锁使用synchronized🔒

synchronized的几种写法

(1)修饰普通方法,锁就相当于 this

(2)修饰代码块,锁对象在()指定

(3)修饰静态方法,锁对象相当于类对象(不是锁整个类)

2.1  修饰方法

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

使用synchronized关键字,来修饰一个普通方法

当进入方法的时候,就会加锁,方法执行完毕就会解锁 


class  Counter  {
    public int counter =0;
    public synchronized void increase()  {
        counter++;
    }
//    public void increase() {
//        counter++;
//    }
}
public class Demo {
    private static Counter  counter = new Counter();

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread  t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("counter="+counter.counter);
    }
}

🔒锁,具有独占的特性,如果当前锁没人加,加锁操作就能成功

如果当前锁已经被加上,加锁操作就会阻塞等待

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

这个操作相当于把“并发” 变成了“串行”,所以会减慢执行效率

加锁并不是说,CPU一次性执行完,之间也是有可能调度切换的

即使t1切走了,t2仍然是BLOCKED状态

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

increase里面涉及到 锁竞争  ,这里的代码时串行执行的,但是for循环在加锁的外面,两个for仍然是并发的,所以这个代码仍然要比两个循环串行执行要快,但是肯定比不加锁要慢 

效率  (完全串行 <  加锁 <  完全并发)

如果把for写到加锁的代码中,此时就和完全串行一样了

加锁需要考虑锁哪些代码,锁的范围不一样,代码执行效果会影响很大

加锁的代码越多,就说“锁的粒度越大/越粗”

加锁的代码越少,就说“锁的粒度越小/越细”

线程安全,不是加锁了就一定安全,而是通过锁,让并发修改同一个变量,变成串行修改同一个变量,才安全

 不正确的加锁操作,不一定能够解决线程安全问题

比如,一个线程加锁,一个线程不加锁,就不涉及到锁竞争,也就不会阻塞等待,也不会将并发修改变成串行修改

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

2.2 修饰代码块

可以把要进行加锁的逻辑放到 synchronized 修饰的代码块中,也能起到加锁的效果

在使用锁的时候,一定要明确,当前针对那个对象进行加锁,这就直接影响到了后面的操作是否会触发阻塞

()中要填的就是针对那个对象进行加锁(被用来加锁的对象,就叫“锁对象”)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

任意对象都可以在 synchronized 里面作为锁对象,

所以我们写多线程代码时,不用关心这个锁对象是谁,是那种形态,

只要注意,两个线程是否锁同一个对象,如果锁同一个对象就会有“锁竞争” 

(1)针对当前对象加锁

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

谁调用increase2方法,谁就是this 

 (2)不用counter本事,而是用counter内部持有的另外一个对象

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

针对locker对象进行加锁,locker是Counter的一个普通成员,每个Counter实例中,都有自己的locker实例

(3)可以使用外部类的实例

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

2.3 synchronized的特性

(1) 互斥

synchronized里面的锁对象是this,这两个线程即使针对counter对象进行加锁,两个线程在执行过程中就会出现互斥的情况

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

(2)可重入

不会产生死锁,这样的锁叫“可重入锁
会产生死锁,这样的锁叫“不可重入锁
可重入锁底层实现,是比较简单的
只要让锁里面记录好,是哪个线程持有的这把锁
当第二次加锁时,锁一看发现还是那个加了锁的线程,就直接通过了,不会阻塞等待

 

可重入锁实现要点:
(1)让锁里持有线程对象,记录是谁加了锁
(2)维护一个计数器,用来判断什么时候是真加锁,什么时候是真解锁,什么时候直接放行

 

一个线程针对一把锁,连续加锁两次,
第一次加锁,能够加锁成功
第二次加锁,就会加锁失败(锁已经被占用)
导致在第二次加锁这里阻塞等待,等到第一把锁被解锁,第二把锁才能加锁成功
(第一把锁解锁,要求执行完synchronized代码块,也就是要求第二把锁加锁成功才可以)

 

 【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 两次加锁,第一次加锁成功,第二次加锁看这个锁加锁了没,如果锁了就直接放行,但需要考虑的是直接放行后,要不要真解锁,如何来判断
方法是,引入一个计数器,每次加锁,计数器++,每次解锁计数器–,如果计数器为0,此时的加锁操作才能真加锁,同样计数器为0,此时的加锁操作才能真解锁

 2.4 锁竞争

锁竞争的核心是:无论锁对象,是什么状态,什么类型,只要两个线程争一个锁对象,就会产生锁竞争

锁竞争的目的:保证线程安全

下面看几种情况,理解一下锁竞争

(1)此时的locker是一个静态成员(类属性),类属性是唯一的(一个进程中,类对象只有一个,类属性也只有一个)
虽然counter和counter2是两个实例,但是这两个里面的locker实际是同一个locker
也就会产生锁竞争

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 (2)第一个线程是针对locker对象进行加锁,第二个针对counter本身加锁
这两个线程针对不同对象加锁,不会产生锁竞争

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 (3)类对象,在JVM进程中只有一个,如果多个线程来针对类对象加锁,就会锁竞争
所以下面这两个对象都是针对,同一个Counter.class加锁

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)


3.volatile关键字(保证内存可见性)

3.1 volatile能保证内存可见性

什么叫做内存可见性

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 但是前面的修改,对于t2的读内存操作不会有影响。
因为t2已经被优化成不再循环读内存了(读一次就完了)
t.把内存改了,t2没发现,这就是内存可见性问题,是由编译器优化出现的问题
(前面说过,编译器优化的前提是保证逻辑不变,让效率提高,但是在多线程情况下编译器就可能出现误判)
解决方案:为了解决编译器把不该优化的进行优化,就可以在代码中进行显示提醒编译器,这段代码不要进行优化,这也是volatile的作用

下面来看volatile的作用
volatile作用:可以使用这个关键字来修改一个变量
此时被修改的变量,编译器就强制不进行优化(不优化就可以,读取到内存了)

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

3.2volatile不保证原子性   

可以看到加了volatile的count,运行程序后,不是10w,说明加了volatile,针对两个线程这样的情况,只能保证内存可见性,不能保证“原子性”。

public class Demo01 {
    static class Counter {
        volatile public int count = 0;
 
        public void increase() {
            count++;
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
 
        System.out.println(counter.count);
    }
}

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

3.3JMM(Java内存模型)

说到volatile,大概就要联系到JMM了
JMM = Java Memory Model(java内存模型)
更专业术语进行描述
【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)


4.wait和notify(协调多个线程的执行顺序)

4.1 wait和notify方法

wait notify 就是用来调配线程执行顺序的

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

 wait操作本质上三步走
(1)释放当前锁,保证其他线程能够正常往下进行(前提是得加了锁。才能释放)
(2)进行等待通知(前提是先要释放锁)
(3)满足一定条件的时候(别的线程调用notify),被唤醒,然后尝试重新获取锁

notify是包含在synchronized里面的
线程1没有释放锁的话,线程2也就无法调用到notify(因为锁阻塞等待)
线程1调用wait,在wait里面就释放了锁,这个时候虽然线程1代码阻塞在synchronized里面
但是此时锁还是释放状态,线程2能拿到锁

【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

要确定加锁的对象,和调用wait的对象是同一个对象,并且也要确定调用wait的对象和调用notify的对象,也是同一个对象 
 下面看一下代码,理解wait和notify的执行顺序

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        //准备一个对象,保证等待和通知是一个对象
        Object object = new Object();
 
        //第一个线程,进行 wait 操作
        Thread t1 = new Thread(() -> {
            while(true) {
                synchronized (object) {
                    System.out.println("wait 之前");
                    try {
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //这里写的代码,实在notify之后执行的
                    System.out.println("wait 之后");
                }
            }
        });
        t1.start();
 
        Thread.sleep(500);
        
        //第二个线程,进行notify
        Thread t2 = new Thread(() -> {
            while (true) {
                synchronized (object) {
                    System.out.println("notify 之前");
                    //这里写的代码,是在wait唤醒之前执行的
                    object.notify();
                    System.out.println("notify 之后");
                }
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t2.start();
    }
}

 【JavaEE】多线程笔记第二天(【线程安全】synchronized + volatile + wait + notify)

4.2 notifyAll方法

多个线程都在wait
notify是随机唤醒一个(用的更多)
notifyAll是全部唤醒(即使全部唤醒了所有wait,这些wait又需要重新竞争锁,重新竞争锁的过程仍然是串行的)

4.3 wait和sleep对比

理论上wait和sleep是完全没有可比性的,
唯一相同的是都可以让线程进入阻塞等待的
不同点:
(1)wait是Object类的成员本地方法,sleep是Thread类的静态本地方法
(2)wait必须在synchroized修饰的代码块或方法中和使用,而sleep方法可以在任何位置使用
(3)wait被调用后当前线程进入BLock状态并释放锁,需要通过notify或notifyAll进行唤醒也就是被动唤醒,sleep被调用后当前线程进入TIMED_WAIT状态,不涉及锁相关操作,能主动唤醒。
(4)sleep必须进行异常捕获,而wait,notify和notifyAll不需要异常捕获
sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/119549.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!