【多线程】状态和线程安全

目录

1.观察线程的所有状态

2、线程转换简图

 3.线程安全

3.1不安全状态

 3.2安全状态(加锁synchronized)

 4.线程不安全原因

4.1线程是抢占式执行的,线程间的调度充满随机性(根本原因)

4.3多个线程对同一个变量进行修改操作

4.3针对变量的操作不是原子行的

4.内存不可见性

 5.指令重排序


1.观察线程的所有状态

线程的状态是一个枚举类型 Thread.State
public class ThreadState {
    public static void main(String[] args) {
        for (Thread.State state : Thread.State.values()) {
            System.out.println(state);
       }
   }
}
1. NEW: 安排了工作 , 还未开始行动

把Thread 类对象创建好了,但是还没有调用start,如下

public static void main(String[] args) {
        Thread t = new Thread(() ->{
           
                
            });
            System.out.println(t.getName());
}

通过 this.getName() 方法获取到指定线程的状态,通过 t 这个对象调用 getStare, 就是获取到了 t 的状态

2. TERMINATED: 工作完成了.

 操作系统中的线程执行完毕,销毁了,但是 Thread 对象还在,通过t.getState() 获取到的状态


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

        });
        t.start();
        Thread.sleep(1000);
        System.out.println(t.getState());
    }
}

 3、RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作.

 线程处于就绪状态,就是在就绪队列中,随时可以被调度到 CPU 上,如果代码中没有进行 sleep,也没有进行其他的可能导致阻塞的操作,代码可能处于 Runnable 状态

4、 TIMED_WAITING: 这几个都表示排队等着其他事情 

 代码中,调用了sleep 就会进入到  TIMED_WAITING,即在当前的线程一定时间内,处于阻塞状态

 5、BLOCKED: 这几个都表示排队等着其他事情

 当前线程在等待锁,导致了阻塞。

6、WAITING: 这几个都表示排队等着其他事情

 当前线程在等待唤醒,导致了阻塞。

2、线程转换简图

 3.线程安全

在操作系统中,调度线程的时候是随机的(抢占式执行),所以会导致程序执行出现一些 bug。

如果因为这样的调度随机性引入了bug ,就认为代码是线程不安全的。

如果因为这样的调度随机性没有带来 bug ,就认为代码是线程安全的。

例子:使用两个线程,对同一个整型变量进行自增,每个线程自增 5w 

3.1不安全状态

因为两个线程是并发执行,即在下面的例子是两个抢占式执行的,有两个都在同时相加,因为使用的调用的是同一个 increse()方法进行 count ++;所以最终结果不准确


//两个线程对同一个变量进行自增,
//可能两个线程同时自增,而不是一个线程增完再到另一个,所以不准
//线程不安全
class Counter {
    public int count;

    public void increase() {
        count++;
    }
}
public class Test14 {
    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();
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t2.start();

        //必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完

        System.out.println(counter.count);//main线程
    }
}

 

 3.2安全状态(加锁synchronized)

通过对一个线程进行加锁(其他线程处于阻塞),即先执行完第一个线程再到下一个线程(阻塞解除),按顺序执行就不会出现错乱情况。

虽然加锁之后,并发执行程度降低了,但是数据更靠谱了

class Counter {
    public int count;

    /*public void increase() {
        count++;
    }*/

    //加锁 synchronized 即可解决线程安全


   synchronized public void increase() {
        count++;
   }


}
public class Test14 {
    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();
            }
        });
        t1.start();

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                counter.increase();
            }
        });
        t2.start();

        //必须要在 t1 和 t2 都执行完了之后, 在打印 count 的结果.
        // 否则, main 和 t1 t2 之间都是并发的关系~~, 导致 t1 和 t2 还没执行完, 就先执行了下面的 打印 操作
        t1.join();
        t2.join();//让main线程进入阻塞状态,等t1、t2线程执行完

        System.out.println(counter.count);//main线程
    }
}

 给方法加上 synchronized 关键字,当一个线程进程此方法就会自动加锁,成功后,其他线程进入尝试加锁时就会触发阻塞等待(处于BLOCKED状态),阻塞会一直持续到占用锁的线程释放为止

 

 通过加锁,得出的结果是正确的。

 4.线程不安全原因

4.1线程是抢占​​​​​​​式执行的,线程间的调度充满随机性(根本原因)

4.3多个线程对同一个变量进行修改操作

4.3针对变量的操作不是原子行的

原子性:

我们把一段代码想象成一个房间,每个线程就是要进入这个房间的人。如果没有任何机制保证, A 进入房间之后,还没有出来;B 是不是也可以进入房间,打断 A 在房间里的隐私。这个就是不具备原子性的。
那我们应该如何解决这个问题呢?是不是只要给房间加一把锁, A 进去就把门锁上,其他人是不是就进不来了。这样就保证了这段代码的原子性了。
有时也把这个现象叫做同步互斥,表示操作是互相排斥的。
一条 java 语句不一定是原子的,也不一定只是一条指令
比如刚才我们看到的 n++ ,其实是由三步操作组成的:
1. 从内存把数据读到 CPU
2. 进行数据更新
3. 把数据写回到 CPU
不保证原子性会给多线程带来什么问题
如果一个线程正在对一个变量操作,中途其他线程插入进来了,如果这个操作被打断了,结果就可能是错误的。

4.内存不可见性

可见性指 , 一个线程对共享变量值的修改,能够及时地被其他线程看到。
Java 内存模型 (JMM) : Java 虚拟机规范中定义了 Java 内存模型 .

线程之间的共享变量存在主内存 (Main Memory).
每一个线程都有自己的 " 工作内存 " (Working Memory) .
当线程要读取一个共享变量的时候 , 会先把变量从主内存拷贝到工作内存 , 再从工作内存读取数据 .
当线程要修改一个共享变量的时候 , 也会先修改工作内存中的副本 , 再同步回主内存 .

因此 t1 就不在从内存读取数据了,而是直接从寄存器里面读取。因此一旦  t1 执行了该操作,t2进行了修改, t1 就感知不到了

 内存不可见性代码示例

public class Test15 {

    private static  int isQuit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while (isQuit == 0) {
               
            }
            System.out.println("循环结束,t 线程退出");
        });
        t.start();


        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入一个isQuit 的值:");
        isQuit = scanner.nextInt();
        System.out.println("main线程执行完毕");
    }


}

 t 线程一直在读取,感知不到内存的修改,即内存不可见性

(1)可以使用synchronized 关键字,既能保证指令的原子性同时也保证内存可见性

(2)使用 volatile 关键字,volatile和原子性无关,但能保证内存可见性

即内存可见性

 5.指令重排序

指令重排序会影响到线程安全问题,也是编译器优化中的一种操作

编译器对于指令重排序的前提是 " 保持逻辑不发生变化 ". 这一点在单线程环境下比较容易判断 , 但是在多线程环境下就没那么容易了, 多线程的代码执行复杂程度更高 , 编译器很难在编译阶段对代码的执行效果进行预测, 因此激进的重排序很容易导致优化后的逻辑和之前不等价
例如看着菜单去买东西,可以不按菜单的顺序,而是按超市摆放的先后顺序

猜你喜欢

转载自blog.csdn.net/m0_60494863/article/details/124764296