【JavaEE学习日记】----多线程基础(中)

目录

1️⃣:线程的状态

1.1:观察线程所有的状态

NEW:

RUNNABLE:

为什么要这么细分?

1.2线程状态转换简图

 2️⃣:线程安全

这不禁令人思索,count++到底干嘛了呢?原因是什么?

3️⃣:线程不安全产生的原因

3.1修改共享数据

3.2没有原子性

3.3内存可见性

3.4代码的顺序性/指令重排序

4️⃣:synkronized关键字-监视器锁monitor lock

4.1synkronized使用方法

1)直接修饰普通的方法

2)修饰一个代码块

3)修饰一个静态方法

4.2synkronized的主要作用


1️⃣:线程的状态

1.1:观察线程所有的状态

🍕NEW: 把Thread对象创建好,但是还没有调用start
🍔RUNNABLE:就绪状态,处于这个状态的线程随时可以被调度到CPU上,如果代码中没有进行sleep或者其他的阻塞的操作,代码大概率是处于RUNNABLE状态的
🍣BLOCKED: 当前线程在等待锁(synkronized),导致了阻塞
🍥WAITING: 当前线程在等待唤醒,导致了阻塞
🍢TIMED_WAITING: 线程代码中是用来sleep或者是join
🍜TERMINATED: 操作系统中线程已经执行完毕,销毁了,但是Thread对象还在

通过代码来演示前两个:

通过 getState 来获取当前线程的一个状态

NEW:

public class Demo1 {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{

        });
        System.out.println(thread.getState());
        thread.start();
    }
}

RUNNABLE:

public class Demo1 {
    public static void main(String[] args) {
        Thread thread = new Thread(() ->{

        });
        thread.start();
        System.out.println(thread.getState());
    }
}

为什么要这么细分?

这是因为以后我们在日常开发过程中,经常会遇见程序 “卡死” 的情况,这其实代表着一些关键的线程阻塞了,而此时我们就可以通过获取当前线程的一个状态来分析卡死的原因🍭

1.2线程状态转换简图

 2️⃣:线程安全

我们都知道,操作系统在调度线程的时候是随机的,这就会带来很大的问题,例如产生bug,如果一个线程因为线程调度产生bug,我们就可以认为此线程是不安全的,如果操作系统调度线程并没有产生bug,则就认为此线程是安全的

典型案例:使用两个线程对同一个变量自增5w次

class A{
    int count;
    public void Count(){
        count++;
    }
}

public class Demo2 {
    public static A a = new A();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                a.Count();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                a.Count();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

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

这个代码的运行结果并不是我们最开始希望的10w,总是在5w到10w之间

这不禁令人思索,count++到底干嘛了呢?原因是什么?

我们可以站在CPU的角度来想,count++实际上执行了三个指令:

①将count从内存中加载到CPU的寄存器中(load)

②将count++ (add)

③将寄存器加过的值返回到内存中(save)

两个线程分别都执行了这三个指令,而我们知道,CPU的调度是随机的,也就会导致每个线程的是哪个指令并没有先后顺序之分,可能在线程1中执行了load后就直接执行线程2的load了,充满着随机性,这种结果最终导致本该加2的count变成加1了,也就导致输出的结果并不是我们预期的10w

3️⃣:线程不安全产生的原因

3.1修改共享数据

就如上述例子,多个线程同时修改同一份数据

怎么解决?加锁

3.2没有原子性

这里的原子性和之前介绍事务的原子性是本质上是一样的,举个例子,我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证,A进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的

怎么解决?加锁

有时候这个现象也叫 互斥 ,表示操作是互相排斥的,一般来说,如果java语句只有一条,那么这个语句肯定是原子性的,但是实事并不止一条java语句,比如上述例子的count++,他是分为三条语句执行的

如果不能保证原子性会给多线程带来什么样的问题呢,一个线程正在好端端的修改着数据,突然另一个线程突然的闯了进来,是的原本的线程被打断,就会导致这个线程修改的数据就是错误的

3.3内存可见性

举一个简答的例子,针对同一个变量,一个线程进行读操作(循环的读),另一个线程进行写操作(在合适的时候进行修改)

 thread1这个线程在循环的这个变量,根据我们所知,读取内存这个操作相比较于读取寄存器,是一个非常低效的操作,因此thread1频繁的读取这里的内存值,就会非常的低效,而且如果thread2线程一直不修改数据的内容,thread1读取的数据始终是一样的

所以此时thread1就有个大胆的想法💣 直接不从内存中读取数据,而是直接从寄存器里读取数据(哇哦w(゚Д゚)w),正当thread1跑到寄存器去读取数据的时候,thread2这个线程突然修改了count这个变量,但是这个修改之后的变量thread1并没有看到,这就是我们所说的内存不可见问题

如何解决?加锁

3.4代码的顺序性/指令重排序

对于程序员来说,我们写的代码谁在前谁在后都无所谓,但是编译器就不这么认为的,在保证逻辑性不变的前提下,编译器会自动的调整顺序,如果是单线程的代码编译器的调整都是无所谓的,会是的代码更优,但是多线程的代码就不一样了,编译器可能会产生误判

如何解决?加锁

4️⃣:synkronized关键字-监视器锁monitor lock

synchronized是Java多线程中元老级的锁,使用了锁就能能够解决线程不安全问题!

4.1synkronized使用方法

1)直接修饰普通的方法

class A{
    int count;
    //给普通方法加锁
    synchronized public void Count(){
        count++;
    }
}

public class Demo2 {
    public static A a = new A();
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                a.Count();
            }
        });

        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                a.Count();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

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

上述案例,通过synkronized对A中的普通方法加锁,就能够解决多线程修改同一个变量产生的不安全的后果

使用synkronized加锁,本质上是针对某个“对象”进行加锁,而这个对象就是this

 一个对象,在Java中,每个类都继承自object,每个new出来的实例,里面乙方面包含了自己安排的一些属性,一方面包含了“对象头”,对象一些元数据,例如:

 对象头对于我们来说是用不到的,但是JVM会用到,砸门所说的加锁操作,就是在给这个对象头里进行设置一个标志位

2)修饰一个代码块

synchronized(this) {
  //业务代码
}

需要显示制定针对那个对象加锁(Java中的任意对象都可以作为锁对象)

3)修饰一个静态方法

synchronized void staic method() {
  //业务代码
}

相当于针对当前类的类对象进行加锁

4.2synkronized的主要作用

1)原子性所谓原子性就是指一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。synchronized修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放。

2)可见性:可见性是指多个线程访问一个资源时,该资源的状态、值信息等对于其他线程都是可见的。 synchronized和volatile都具有可见性,其中synchronized对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到共享内存当中,保证资源变量的可见性。

3)有序性有序性值程序执行的顺序按照代码先后执行。 synchronized和volatile都具有有序性,Java允许编译器和处理器对指令进行重排,但是指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性。synchronized保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w-ib

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值