【多线程】(2) (观察线程状态 线程的状态转移 观察线程的状态和转移 案例:单线程-串行执行 多线程-并发执行 观察线程中不安全的情况 线程安全概念 线程不安全的原因 解决线程不安全问题)

文章详细介绍了Java中线程的不同状态,包括NEW,TERMINATED,RUNNABLE,WAITING,TIMED_WAITING和BLOCKED,并通过示例展示了线程状态的转移。此外,文章还探讨了线程安全的概念,分析了线程不安全的原因,如抢占式执行、内存可见性和指令重排序,并提供了解决线程安全问题的方法,如使用`synchronized`关键字确保原子性。
摘要由CSDN通过智能技术生成


线程的状态

状态是针对当前线程调度的情况来描述的.现在我们认为线程是调度的基本单位.所以状态更应该是线程的属性.(后面我们谈到的状态都是线程的状态)

观察线程状态

public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}

在Java对于线程的状态,进行了细化.

  1. NEW 表示创建了Thread对象,但是还没调用start(内核中还没创建对应的PCB)

  2. TERMINATED 表示内核中的pcb已经执行完毕了,但是Thread对象还在.

  3. RUNNABLE 表示可运行的.(a.正在CPU上执行的,b.在就绪队列里,随时可以去CPU上执行.)

  4. WAITING 阻塞

  5. TIMED_WAITING 阻塞

  6. BLOCKED 阻塞

线程的状态转移

线程状态图转换简单清晰:
在这里插入图片描述

观察线程的状态和转移

public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 100000000; i++) {
                // 循环体中什么都不干 也不sleep
            }
        });
        //启动之前,获取t的状态 即NEW状态
        System.out.println("start 之前: " + t.getState());
        t.start();
        t.join();
        //线程执行完毕之后,就是 TERMINATED
        System.out.println("t 结束之后: " + t.getState());
    }
}

在这里插入图片描述

加上个查看执行中的状态.

在这里插入图片描述

现在我们加上sleep:

在这里插入图片描述

我们看不到TERMINATED是因为 t 线程sleep时间太长了.把for循环的次数改小我们就可以看到了.

public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //启动之前,获取t的状态 即NEW状态
        System.out.println("start 之前: " + t.getState());
        t.start();
        System.out.println("t 执行中的状态: " + t.getState());
        t.join();
        //线程执行完毕之后,就是 TERMINATED
        System.out.println("t 结束之后: " + t.getState());
    }
}

在这里插入图片描述

若我们想看到TIMED_WAITING状态只需要加个for循环即可.
在这里插入图片描述

public class ThreadDemo11 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        //启动之前,获取t的状态 即NEW状态
        System.out.println("start 之前: " + t.getState());
        t.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("t 执行中的状态: " + t.getState());
        }
        t.join();
        //线程执行完毕之后,就是 TERMINATED
        System.out.println("t 结束之后: " + t.getState());
    }
}

在这里插入图片描述

通过循环获取就能看到这里的交替状态了.当前获取到的状态完全取决于系统里的调度操作.

案例

写个代码来感受下,单个线程和多个线程之间,执行速度的差别。

程序分成:

  1. CPU密集,包含了大量的算数运算。

  2. IO密集,涉及到读写文件,读写控制台,读写网络。

单线程-串行执行

    //串行执行,一个线程完成
    public static void serial(){
        //为了衡量代码执行速度 加上个计时操作
        //获取当前系统的ms级时间戳
        long beg =System.currentTimeMillis();
        long a = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {
            a++;
        }
        long b = 0;
        for (long i = 0; i < 100_0000_0000L; i++) {
            b++;
        }
        long end = System.currentTimeMillis();
        System.out.println("单线程执行时间: " + (end - beg) + " ms");
    }

在这里插入图片描述

执行结果时间误差在20s左右,可以接受.

多线程-并发执行

    public static void concurrency(){
        // 使用两个线程 分别完成自增
        Thread t1 = new Thread(()->{
            long a = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {
                a++;
            }
        });
        Thread t2 = new Thread(()->{
            long b = 0;
            for (long i = 0; i < 100_0000_0000L; i++) {
                b++;
            }
        });
        long beg = System.currentTimeMillis();
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("并发执行时间: " + (end - beg) + " ms");
    }

在这里插入图片描述
在这里插入图片描述
我们也要考虑清楚当前的join是哪个线程调用的:main调用t1.join就是main来等待t1,如果t2调用t1.join,就是t2等待t1.

此处使用两个线程并发执行,时间缩短很明显,使用多线程快是因为多线程可以充分利用到多核心cpu资源.实际上t1和t2在执行过程中,会经历很多次调度,这些次调度,有些是并发执行的(在一个核心上),有些是并行执行的(正好在两个核心上),另一方面线程调度自身也是有时间消耗的.所以缩短的时间可能是50%,也有可能不是50%.但是它缩短的时间是具有很大的意义的.

线程安全

线程安全是多线程编程中最难的地方,也是最重要的地方.

多线程的安全问题出现是多线程抢占式执行的随机性带来的.

如果没有多线程,此时程序代码执行顺序就是固定的(只有一条路),代码顺序固定,程序的结果就是固定的.如果有了多线程,此时抢占式执行下,代码的执行的顺序会出现更多的变数.此时代码执行顺序的可能性就从一种情况变成了无数种情况.所以就需要保证这种无数种线程调度顺序的情况下,代码的执行结果都是正确的.

只要有一种情况下,代码结果不正确,就都视为是有bug,线程不安全.

能否消除这种随机性?
调度的源头来自于操作系统的内核实现,想改变不太现实.

观察线程中不安全的情况

class Counter{
    public int count = 0;

    public void add(){
        count++;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //搞两个线程 分别针对counter 来调用5w次add方法
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        //等待两个线程结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印最终的count值
        System.out.println("count = " + counter.count);
    }
}

程序不符合需求就是bug,我们的需求是两个线程各自自增5w次,一共自增10w次,预期结果是10w,实际结果不是10w而且每次都不一样.

这个现象就是典型的线程安全问题.为什么程序出现了上面的情况?

在这里插入图片描述
如果是两个线程并发的执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异.

在这里插入图片描述
上面的调度随机性是无穷无尽的.下面的各种情况都会出现线程安全问题:
在这里插入图片描述
下面各自分析一下:
在这里插入图片描述
在这里插入图片描述

线程安全概念

如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

线程不安全的原因

  1. [根本原因]抢占式执行,随机调度.
  2. 代码结构:多个线程同时修改同一个变量.
  3. 原子性:如果修改操作是非原子的,出现的概率就非常高.(原子:不可拆分的基本单位)
  4. 内存可见性,如果一个线程读,一个线程改可能就出现此处读的结果不太符合预期.
  5. 指令重排序.本质上是编译器优化出bug了.

解决线程不安全问题

针对线程安全问题,最主要的手段就是从原子性入手,把非原子的操作变成原子的,即加锁.我们会用到synchronized关键字.

在这里插入图片描述
这个关键字表示加锁,方法前面加上,表示进入方法就会加锁,出了方法就会解锁.如果两个线程同时尝试加锁,此时一个能获取成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功.
在这里插入图片描述
加锁说是保证原子性,其实不是说让这里的三个操作一次完成,也不是让这三步操作过程中不进行调度,而是让其它也想操作的线程阻塞等待了.所以加锁本质就是把并发变成了串行.

class Counter{
    public int count = 0;

    synchronized public void add(){
        count++;
    }
}
public class ThreadDemo13 {
    public static void main(String[] args) {
        Counter counter = new Counter();
        //搞两个线程 分别针对counter 来调用5w次add方法
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        //等待两个线程结束
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //打印最终的count值
        System.out.println("count = " + counter.count);
    }
}

在这里插入图片描述
此时我们看到代码始终是100000,加锁后,线程安全问题得到了改善.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

马尔科686

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

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

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

打赏作者

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

抵扣说明:

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

余额充值