javaEE基础 —— 线程的状态和安全

本文深入探讨了Java线程的六种状态,包括new、terminated、runnable、timed_waiting、blocked和waiting,并通过示例代码解释了状态转换。同时,文章阐述了线程安全的概念,分析了线程不安全的原因,如非原子性操作和内存可见性问题,并给出了使用synchronized关键字和volatile修饰符解决线程安全问题的示例。
摘要由CSDN通过智能技术生成

 

目录

一、线程的状态

1.new

2.terminated

3.runnable 

 4.timed_waiting

5.blocked 

 6.waiting

 二、线程安全

1.线程安全的概念

2.线程不安全的原因


一、线程的状态

先前,我们大概介绍了一下线程的两个状态:阻塞和就绪。严格来说线程并不只有这两种状态,上述的两种状态是在系统层面上的线程状态,在Java中,尤其是Thread类之中一共将线程的状态表分成了六种。

1.new

当Thread对象创建好了,但线程并未执行 

示例代码:

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

        });
        System.out.println(t.getState());
    }

运行结果:

 

2.terminated

线程已经运行结束,但是Thread对象还在

示例代码:

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

        });
        t.start();
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(t.getState());
    }

运行结果: 

 

 

 需要注意的是,上述两个状态是Java自身定义的,和操作系统中的PCB状态没有关系。

3.runnable 

这就是我们时常说的就绪状态,这时线程有两种情况:

1.正在被执行

2.没有被调度执行,但随时可以去调度它

示例代码:

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

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

 运行结果:

 4.timed_waiting

代码中因调用了sleep()或是join(等待时间),就会进入这个状态,即阻塞状态。

代码示例:

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        t.start();
        Thread.sleep(500);
        System.out.println(t.getState());
    }

 运行结果:

5.blocked 

当前线程在等待锁而进入了阻塞状态,也是阻塞状态的一种。

 6.waiting

当前线程在阻塞当中且被等待唤醒。一般我们使用wait使某个线程处于阻塞状态就会触发这种状态。

小结 

上述三种状态:TIMED_WAITING、BLOCKED、WAITING,它们都是阻塞状态。
在系统里阻塞状态只有一种,但在Java又进行了进一步的细分:根据不同的原因,分成了不同的状态。
这样子做是有利于程序员理解代码是怎么执行的。

我们可以将这几种状态转移的关系简单地画下面这个图:

 

 

 二、线程安全

1.线程安全的概念

想给出一个线程安全的确切定义是复杂的,但我们可以这样认为:
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。

2.线程不安全的原因

说到线程不安全的原因就不得不提原子性操作,我们先来了解一下什么是原子性操作:

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

 我们来看一段代码:

class count {
    public static int num;

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

我们定义了两个线程,每一个线程都会使num自增50000次,那么按照代码的逻辑最后num的值应该是100000,那么实际结果真的是这样子吗?

执行结果:

我们会发现 结果跟我们认为的是有很大差距的,那么这种结果的原因是什么呢?我们来分析一下。

我们在前面提到了原子性操作,而引发这个问题的恰恰也是原子性的问题。

num++;看起来是只有一个命令,实际上计算机在执行的时候是分成下面3个命令来执行的:

1.从寄存器中读取num当前值

2.在当前基础上加一

3.将++后的值存回寄存器

正因为这样,多个线程在读取储存和修改自增的时候,有极大的概率是在无效自增,比如A线程读取值为3,这时B线程也进来读取数据,读到的也是3(这个时候A线程还没有自增完成),当A加一结束将4存回寄存器后,B线程也将自增后的4存回去,但这个时候,按我们之前约定的应当是两个线程结束之后增加了2才对,但现在只有增加了1

除了  非原子性操作带来的线程不安全之外,线程的抢占式执行才是线程不安全的万恶之源。

而在上述代码之中,我们针对自增的是同一个对象,如果我们可以使两个线程去执行不同变量的自增最后将结果加起来,一样可以避免出现线程不安全的问题。

当然,我们也可以使用synchronized关键字,对代码的某些关键操作上锁,将非原子性操作打包成一个原子性的操作

优化后的代码:

class count {
    public static int num;

    synchronized public void increase() {//加锁
        num++;
    }
}
class Main2 {
    public static void main(String[] args) throws InterruptedException {
        count c = new count();
        Thread t1 = new Thread(() -> {
            Object o = new Object();
            for(int i = 0; i < 50000; i++) {
                c.increase();
            }
        });
        Thread t2 = new Thread(() -> {
            Object o = new Object();
            for(int i = 0; i < 50000; i++) {
                c.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count.num);
    }
}

此外,内存可见性也会导致线程安全

代码示例:

    public static int a = 1;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(a == 1) {

            }
            System.out.println("循环结束");
        });
        t.start();
        Scanner scan = new Scanner(System.in);
        System.out.println("输入一个数字:");
        a = scan.nextInt();
        System.out.println("main结束!");
    }

执行结果: 

 

 我们发现,在我们修改了a的值之后,直到主线程结束t线程也没有结束,这就是内存可见性导致的线程不安全 

当某个线程高频率地读取某个数值,但多次读取发现该值一直不变,那么Java自带的优化策略就会直接从内存当中读取,以提高执行效率。所以,在某些比较重要的变量我们可以使用volatile关键字进行修饰。 

修改后: 

    volatile public static int a = 1;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(a == 1) {

            }
            System.out.println("循环结束");
        });
        t.start();
        Scanner scan = new Scanner(System.in);
        System.out.println("输入一个数字:");
        a = scan.nextInt();
        System.out.println("main结束!");
    }

 

 此外,volatile关键字还可以防止指令重排序。

以上就是今天的全部内容了,喜欢的话就点个赞吧!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值