Java中的多线程和线程安全问题

线程

线程是操作系统进行调度的最小单位。一个进程至少包含一个主线程,而一个线程可以启动多个子线程。线程之间共享进程的资源,但也有自己的局部变量。多线程程序和普通程序的区别:每个线程都是一个独立的执行流;多个线程之间是并发执行的。

在这里插入图片描述

多线程的实现方法

继承Thread类

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }

    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

public class Text {
    public static void main(String[] args) {
        //实例化对象  创建线程
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread("线程t2");
        t1.start();
        t2.start();  //启动线程

        for (int i = 0; i < 5; i++) {
            System.out.println("hello main");
        }
    }
}

使用lambda表达式创建线程

public class Demo1 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (true) {
                System.out.println("t1线程");
            }
        });

        Thread t2 = new Thread(() -> {
            while (true) {
                System.out.println("t2线程");
            }
        });
        t1.start();
        t2.start();

        while (true) {
            System.out.println("hello main");
        }
    }
}

start()和run()的区别

start()是启动一个分支线程 是一个专门用来启动线程的方法 而run()是一个普通方法和main方法是同级别的 单纯调用run方法是不会启动多线程并发执行的.

public class Demo2 {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName()+ ":我还存活");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(Thread.currentThread().getName() + ":我即将死去");
        });

        System.out.println(Thread.currentThread().getName() + ": 状态" + thread.getState());

        //启动线程
        thread.start();
        boolean flg = thread.isAlive();  //是否存活
        System.out.println(Thread.currentThread().getName() + ": 状态:" + thread.getState());
    }
}

上面的代码涉及到的一些线程的方法
在这里插入图片描述
还有获取线程状态的方法是getState()

sleep()方法

该方法就是让线程休眠 括号里面写休眠的时间 单位是毫秒级别 下面通过代码来描述

public class Demo3 {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("hello t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5;i++) {
                System.out.println("hello t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        
        t1.start();
        t2.start();
    }
}

通过上面的代码 就可以控制两个线程没执行一遍就休眠一秒钟 再继续执行下一遍。

join方法

多线程的join方法就是用来等待其他线程的方法 就是谁调用该方法谁就等待 join()这种是死等 ,当然也可以有时间的等 过了这个时间就不等了
就类似舔狗 有写舔狗 舔自己的女神 可能 会一直舔 舔到死 那种 有些就是有原则的舔 ,可能就在固定的时间内舔 ,过了这个时间段就坚决不舔。

代码实现如下:

public class Demo4 {
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        Thread t1 = new Thread(() -> {
            //t1等t2
            try {
                Thread.sleep(500);
                t2.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

            for (int i = 0; i < 5; i++) {
                System.out.println("t1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        System.out.println("main end");
    }
}

import java.time.Year;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("t1线程在执行");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t1.join(3000);
        System.out.println("main线程over");
    }
}

运行结果:
在这里插入图片描述
通过代码和运行截图可以看出 main线程在等了3秒之后就结束了 而t1线程还没执行完。
join方法的底层原理:
join方法的工作原理基于Java的内置锁机制。当你调用join方法时,当前线程会尝试获取目标线程对象的锁,并在目标线程执行完毕后释放锁。在这个过程中 ,当前调用的线程就会被阻塞等待,直到目标线程结束执行并释放锁,当前线程才能执行。

线程的状态

在Java官方的线程状态分类中 ,一共给出6种线程状态。
在任意一个时间点 ,一个线程就有且仅有一种状态

6种状态如下

NEW:创建好线程但是还没启动

RUNNABLE该状态是已经调用了start()方法 是可以工作的状态 但这种状态的线程有两种情况 :一种是正在执行 还要一种就是在等待CPU分配执行时间。

BLOCKED: 该状态就是线程被阻塞了,在等待别的线程的锁

WAITING:这种状态就是无限期等待 CPU不会给他分配执行时间 这种要等别的线程来唤醒。

TINED_WAITING: 这种就是有限期德等待,在一定时间之后就会被系统自动唤醒。

TERMINATED:工作完成了 线程已经执行结束了 。

线程状态的转换

在这里插入图片描述

线程的安全问题

那什么叫做线程安全呢
就是多线程环境下代码运行的结果是符合问你预期的,即在单线程环境应该的结果,则表示该线程是安全的 ,否则该线程就是不安全的。

线程不安全的原因

线程的调度是随机的 这是线程不安全的罪魁祸首
随机调度使一个程序在多线程环境下,执行顺序存在很多变数
多线程是一个神奇的东西

下面我们通过一个代码来看看什么是线程的不安全 也就是有bug 和预期效果不符。

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行结果:
在这里插入图片描述
在这里插入图片描述
结果不符合预期 预期结果是100000 但是却输出小于10万的数,且每次运行的结果都不一样 为什么呢?
1、count++这个操作,站在cpu指令的角度来说,其实是三个指令。
load :把内存中的数据,加载到寄存器中
add:把寄存器中的值 + 1
save :把寄存器中的值写回到内存中

2、两个线程并发执行的进行count++
因为多线程的执行是随机调度,抢占式执行的模式

相当于某个线程执行指令的过程中,当她执行到任何一个指令的时候都有可能被其他线程把他的cpu资源抢占走。

综上所述,实际并行执行的时候,两个线程执行指令的相对顺序就可能存在无数种可能。

在这里插入图片描述
除了上面的两种可能还有无数种可能。

出现线程不安全的原因:
还是那句话:1、线程在系统中是随机调度的

2、在上面那个代码中 ,多个线程同时修改同一个变量就会出现这种线程不安全的问题

3、线程针对变量的修改操作,不是“原子”的
就像上面count++这种代码 就不是原子操作 因为该操作涉及到三个指令。
但有些操作,虽然也是修改操作 ,但是只有一个指令,是原子的。
比如直接针对 int / double进行赋值操作(在cpu上就只有一个move操作)
相当于来说 ,就是某个代码操作对应到一个cpu指令就是原子的 如果是多个那就是原子的。

那如何解决 该问题呢
那必须得从原因入手

线程调度是随机的这个我们无法干预
我们可以通过一些操作 把上诉非原子操作,打包成一个原子操作
那就是给线程加锁 下面我们举个例子:
就比如上厕所 现在厕所里面就一个坑位 现在来了两个人A和B 现在A先进去上厕所了 结果 A还没上完 B就冲了进去 这显然是不科学的 。在现实生活中,一般我们上厕所都会锁上门 。A进去上厕所把门给锁上,这时B要是也想进去上厕所就得等待A上完厕所解锁出来 这时B才能进去接着上厕所。

:本质上也是操作系统提供的功能 通过api给到应用程序 ;在Java中JVM对于这样的操作系统又进行了封装。

synchronized对象锁(可重入锁)

在Java中 我们引入synchronized 关键字
synchronized()括号里面就是写锁的对象

锁对象的用途,有且仅有一个 ,就是用来区分 两个线程是否针对同一个对象加锁
如果是 那就会出现锁竞争 /互斥 就会引起阻塞等待
如果不是,就不会出现锁竞争 ,也就不会出现阻塞。

下面给你们看看加上锁之后的代码

public class Demo6 {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Object locker = new Object();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                synchronized (locker) {
                    count++;
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

运行的结果:
在这里插入图片描述
加锁之后 明显线程就没有bug了变安全了

加上锁之后 当t1进入count操作的时候 ,如果t2想进去执行 就会阻塞等待 因为 现在锁在t1的手里

还有 一种嵌套加锁 就是在第一个锁的基础上再加一个锁 就相当于 你要获取第二个锁得先执行完第一个锁 要想执行完第一个锁 ,得获取到第二个锁 ,这就相互矛盾了 就产生死锁看了

但是实际上 对于synchronized是不适用的 这个锁在上面这种情况下不会出现死锁 但是这种情况在C++和Python中就会出现死锁。

synchronized没有出现上诉情况是因为自己内部进行了特殊的处理(JVM)
每个锁对象里,会记录当前是哪个线程持有这个锁。
当针对这个对象加锁操作时,就会先判定一下,当前尝试加锁线程是否是持有锁的线程
如果不是就阻塞 否则就直接放行 不会阻塞。

场景二: ;两个线程 两把锁
现在有线程t1 和t2 以及锁A和锁B 现在这两个线程都需要获取这两把锁 ‘拿到锁A后 不释放锁A ,继续去获取锁B 就相当于 先让两个线程分别拿到一把锁,然后去尝试获取对方的锁。

举个例子: 疫情期间,现在广东的健康吗崩了 程序猿赶紧来到公司准备修bug 被保安拦住了
保安: 请出示健康吗 才能上楼
程序猿:我得上楼修复bug才能出示健康码

就这样 如果两个人互不相让 就会出现僵住的局面。
类似的 还有 钥匙锁在车里 ,而车钥匙锁屋子里了 这样也是僵住了

下面我们通过代码来实现一下 该情况:

public class Demo8 {
    public static void main(String[] args) throws InterruptedException {
        Object locker1 = new Object();
        Object locker2 = new Object();
        Thread t1 = new Thread(() -> {
            synchronized (locker1) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker2) {
                    System.out.println("t1获取到两把锁");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                synchronized (locker1) {
                    System.out.println("t2获取到两把锁");
                }
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

运行结果:
在这里插入图片描述
你会发现现在运行起来什么都没有
t1尝试针对locker2加锁 就会阻塞等待 等待t2释放locker2 ,而t2尝试针对locker1加锁 也会阻塞等待等待t1 释放locker1.

这就相当于两个互相暗恋的人 你喜欢我 我也喜欢你 谁都在等对方 但是没人主动 说出来 终究是会错过。

针对这个问题 我们可以不用使用嵌套锁 ,但是也可以规定获取锁的顺序 比如说 t1和t2
线程规定好 先对locker1 加锁 再对locker2加锁 。这样就不会出现死锁的情况了。

产生死锁的四个必要条件

1.互斥条件
每个资源不能同时被两个或更多个线程使用。
2、不可剥夺性
一旦一个线程获得了 资源,除非该线程自己释放 ,否则其他线程不难强行剥夺这些资源。
3、请求和保持条件
一个线程因请求资源而阻塞时,必须保持自己的资源不放。如果一个线程在请求资源之前就释放了已获得的资源,那么就不会发生死锁现象。
4循环等待条件
如果存在一个资源等待链 ,即P1正在等待P2释放的资源 ,P2正在等待P3释放的资源 ,以此类推,最后Pn又在等待P1释放的资源。

以上四个条件必须同时满足,才能产生死锁现象 在实际开发中我们应该合理设计代码 避免死锁的发生。

在上面这四种产生死锁的条件中 前面两个是线程的基本特征 ,我们无法干预 ,最好解决死锁的方法就是破除条件3或者条件4 条件3 要破解 就需要 不要写锁嵌套 ,那如果非要写成锁嵌套怎么办 ,那就是破解第四个条件 当代码中有多个线程获取多把锁的情况 ,我们就需要统一规定好加锁的顺序 ,这样就能有效的避开死锁的现象。

内存可见性 引起的线程安全问题

下面我们通过一个代码来展示这个线程安全问题:

import java.util.Scanner;

public class Demo1 {
    private  static int count = 0;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (count == 0) {
                ;
            }
            System.out.println("t1 执行结束");
        });

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

        t1.start();
        t2.start();
    }
}

运行结果:
在这里插入图片描述
你会发现虽然修改了count的值 但是程序并没有结束 这就和我们预期的出现了差错 出现bug了 那是为什么呢?

在那个while循环里面 会执行 load和cmp这两个指令
**load 😗*从内存读取数据到寄存器
cmp:(比较,同时产生跳转) 条件成立,就继续执行程序 条件不成立,就会跳到另一个地址来执行

当前的循环旋转速度很快 短时间内会有大量的load 和 cmp 反复执行的效果 load执行消耗的时间 会比 cmp 多很多
这样JVM 就会因为load执行速度慢 而每次load的结果都是一样的 JVM 就会干脆 把上面的load操作给优化了 只\有第一次执行load才是真的在进行load 后续再执行到相对应的代码,就不再真正的load了,而是直接读取已经load过的寄存器的值了
当我们在while循环体里面加入一些IO操作 程序运行就要正确了
这又是为什么呢
因为如果循环体里面有IO操作 就会使循环体的旋转速度大幅度降低 ,因为IO操作比load操作要慢得多 所以JVM也就不会再去优化load操作 ,而IO操作是不会被优化的.
内存可见性问题说到底是由于编译器优化引起的,优化掉load操作之后 ,使得t2线程的修改没有被t1线程感知到.
JVM在什么时候优化 什么时候不优化 这也是不确定的
那我们该怎么解决该内存可见性问题呢?

volatile

我们会引入volatile关键字
这个关键字的作用就是告诉编译器不要触发上述优化 volatile关键字是专门针对内存可见性的场景来解决问题的.

关于线程安全问题还有一些内容 我们下篇内容讲解 本篇内容就到此结束了 谢谢大家的浏览 !!!

  • 28
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值