多线程二

58 阅读2分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

线程状态

1.NEW              安排了工作但是没有去执行
2.RUNNABLE         可工作的,又可以分成正在工作和即将开始工作
3.BLOCKED          表示排队等着其他事情
4.WAITING          表示排队等待其他事情
5.TIMED_WAITING    表示排队等待其他事情
6.TERMINATED       工作完成了
这些可以通过t.getState()来了解指定线程的状态
public class Demo11 {
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(true){
                System.out.println("hello");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        System.out.println(t.getState());
        t.start();
        while(true){
            System.out.println(t.getState());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

这是关于线程状态的转换,我画了一个简图。比较好了解。

线程安全问题

是什么引起的线程安全问题呢?
是之前提到的线程抢占式执行,就让我们程序执行什么线程变成了随机。让我们有点捉摸不透。
有一段代码可以明显看出这一点。
class test{
    public int count;
    public void add(){
        count++;
    }
}
public class Demo12 {
    public static test t = new test();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for(int i = 0;i<5000;i++){
                t.add();
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i<5000;i++){
                t.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t2.join();
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(t.count);
    }
}

屏幕截图 2022-09-18 154226.jpg

这个是我执行之后的结果,当你多运行几次之后你会发现,这个数字不等于10000,我们按道理来讲,这两个线程都让t里面的count执行++操作5000次,那么加起来是10000,那为什么会没有达到呢?

这就是抢占式的弊端。因为我们在进行add()这个方法的时候,并不是只有一个操作。是有三个操作的,第一步是读取,第二步是加1,第三步是写入。既然这个操作没有原子性,那么其他的线程就会在这个线程在做某一步的时候强行插队。就会导致最后的答案不一样。两个线程对于一个变量的操作是比较危险的。

如何解决上面的问题呢?

那就是加个锁

加锁

加锁就可以让我们避免前面的问题。让++的操作原子性。这个就好像是什么呢?
就好像去银行ATM取钱,在我们取钱的时候都会把门给锁上,这样别人就进不来了,
而且这边的重点是,这个门只有一个锁,
如果有两把锁,两把都可以开这个门的话,就会让锁没有用了。
加锁之后弊端就是,并发变成了串行。速度变慢了,但是安全了。

synchronized

这个词还是很重要的。
class test{
    public int count;
    synchronized public void add(){   //对于方法加锁
        count++;
    }
}
public class Demo12 {
    public static test t = new test();
    public static void main(String[] args) {
        Thread t1 = new Thread(()->{
            for(int i = 0;i<5000;i++){
                t.add();
            }
        });
        Thread t2 = new Thread(()->{
            for(int i = 0;i<5000;i++){
                t.add();
            }
        });
        t1.start();
        t2.start();
        try {
            t2.join();
            t1.join();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(t.count);
    }
}
加锁之后输出就是10000了。

内存可见性的安全问题

当两个线程对于一个变量分别同时做读和写的操作,这个就会有线程安全问题
当我们对于一个变量进行了频繁的读取的时候,而且这个变量没有改变的时候,
我们的编译器就会对这个线程进行优化,就不会进行读取操作,
如果这个时候,其他线程对这个变量进行了修改,那么优化过之后线程就不会读取到了。
来看代码
public class Demo13 {
    private static int quit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(quit == 0){

            }
            System.out.println("Thread end");
        });
        t.start();
        Scanner sc = new Scanner(System.in);
        quit = sc.nextInt();
        System.out.println("main end");
    }
}
当我们输入1的时候,正常情况下面进程会结束,但是实际操作之后发现结果不是这个。
我们发现main end 打印了,但是迟迟没有打印Thread end。
这个就是内存可见性的危害

解决方法

1.加锁  synchronized
2. volatile
public class Demo13 {
    private static volatile int quit = 0;
    public static void main(String[] args) {
        Thread t = new Thread(()->{
            while(quit == 0){

            }
            System.out.println("Thread end");
        });
        t.start();
        Scanner sc = new Scanner(System.in);
        quit = sc.nextInt();
        System.out.println("main end");
    }
}
以上代码就线程安全了。

指令重排序

这个就和上面那个一样,是编译器优化带来的线程安全问题。
指令重排序就是让一些指令改变操作顺序来提高效率。

解决方法

1.加锁 synchronized

synchronized 的用法

1.直接修饰普通的方法
synchronized public void add(){

}
2.修饰代码块
public void add(int index){
    synchronized (Demo14.class){
        index++;
    }
}
3.修饰静态方法
synchronized public static int add(int a , int b){
    return a+b;
}