01.线程基础知识(一)

  • Post author:
  • Post category:其他




线程基础知识(一)



线程状态


从操作系统层面5种状态

image-20211020103254108

  • 【初始状态】仅仅在语言层面创建了线程对象,还未与操作系统线程关联
  • 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由cpu调度执行
  • 【运行状态】

    • 指获取了cpu时间片运行中的状态
    • 当cpu时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程上下文切换
  • 【阻塞状态】

    • 如果调用了阻塞的API,如BIO读写文件,这是该线程实际不会调用CPU,会导致线程上下文切换,进入【阻塞状态】
    • 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
    • 与【可运行状态】的区别是:对于【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
  • 【终止状态】表示线程已经执行完毕,生命周期就已经结束,不会再转换为其他状态


从java语言层面六种状态

根据Thread.state的内部枚举决定

image-20211020104812750

  • 【new】线程刚被创建,但是还没有调用start();

  • 【Runnable】当调用了start()方法之后。注意,java层面的【runnable】涵盖了操作系统层面【运行状态】【可运行状态】【IO阻塞状态】。

  • 【Blocked】【Waiting】【Timed_Waiting】是java对阻塞状态的细分

    • 【Blocked】:被动阻塞,线程争取时间片(锁)失败进入
    • 【Waiting】:主动阻塞,程序员主动调用Wait(),join() 是线程进入阻塞状态
    • 【Timed_Waiting】:主动阻塞,程序员主动调用Wait(long) ,sleep(long)是线程进入阻塞状态
  • 【Terminated】终止状态



华罗庚《统筹方法》

想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么

办?

  • 洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开

    了,泡茶喝。

    image-20211020110820367

package com.test;

import lombok.extern.slf4j.Slf4j;



/**
 * @author Seven
 * @create 2021-10-19 22:29
 */
@Slf4j(topic = "c.Test5")
public class Test5 {
    public static void main(String[] args) {
        int time=0;
        Thread t1 = new Thread(() -> {
            log.debug("洗水壶");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            log.debug("烧开水");
            try {
                Thread.sleep(15000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"老周");

        Thread t2 = new Thread(() -> {
            log.debug("洗茶壶");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            log.debug("洗茶杯");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            log.debug("拿茶叶");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            try {
                t1.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            log.debug("泡茶");
        }, "小周");

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


    }
}

11:21:38.970 [小周] DEBUG c.Test5 - 洗茶壶
11:21:38.970 [老周] DEBUG c.Test5 - 洗水壶
11:21:39.973 [老周] DEBUG c.Test5 - 烧开水
11:21:39.973 [小周] DEBUG c.Test5 - 洗茶杯
11:21:41.973 [小周] DEBUG c.Test5 - 拿茶叶
11:21:54.973 [小周] DEBUG c.Test5 - 泡茶



线程安全问题

因为写的指令不能原子化,交错导致程序结果出错。


成员变量和静态变量是否线程安全

  • 如果它们没有被共享,则线程安全。
  • 如果它们被共享了,更具他们的状态是否能被改变,又分为两种情况

    • 如果只有读操作,则线程安全
    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全。


局部变量是否线程安全

  • 局部变量是线程安全的。
  • 但局部变量引用的对象未必

    • 如果该对象没有逃离方法的作用访问,他是线程安全的
    • 如果该对象逃离党法的作用访问,需要考虑线程安全。


局部变量线程安全分析

  • 无共享,无伤害
public static void test(){
	int i=10;
	i++;
}

每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

image-20211022140354411

  • 当局部变量引用的是一个对象

先举一个成员变量的例子

class ThreadUnsafe {
    ArrayList<String> list = new ArrayList<>();
    public void method1(int loopNumber) {
        for (int i = 0; i < loopNumber; i++) {
            // { 临界区, 会产生竞态条件
            method2();
            method3();
           
            // } 临界区
        }
    }
    private void method2() {
        list.add("1");
    }
    private void method3() {
        list.remove(0);
    }
}

执行

	static final int THREAD_NUMBER = 2;
    static final int LOOP_NUMBER = 200;
    public static void main(String[] args) {
        ThreadUnsafe test = new ThreadUnsafe();
        for (int i = 0; i < THREAD_NUMBER; i++) {
            new Thread(() -> {
                test.method1(LOOP_NUMBER);
            }, "Thread" + i).start();
        }
    }

image-20211022141235203

将 list 修改为局部变量,引用不暴露给外部

class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}

分析:

list 是局部变量,每个线程调用时会创建其不同实例,没有共享

  • 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象
  • method3 的参数分析与 method2 相同

image-20211022141655147

暴露给外部

  • 情况1:有其它线程调用 method2 和 method3
  • 情况2:在 情况1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法,即
class ThreadSafe {
    public final void method1(int loopNumber) {
        ArrayList<String> list = new ArrayList<>();
        for (int i = 0; i < loopNumber; i++) {
            method2(list);
            method3(list);
        }
    }
    private void method2(ArrayList<String> list) {
        list.add("1");
       
    }
    private void method3(ArrayList<String> list) {
        list.remove(0);
    }
}
class ThreadSafeSubClass extends ThreadSafe{
    @Override
    public void method3(ArrayList<String> list) {
        new Thread(() -> {
            list.remove(0);
        }).start();
    }
}

如果父类方法为public,final没有,即父类会被子类覆盖。则会出现线程安全



常见的线程安全类

  • String
  • Integer
  • StringBuffer
  • Random
  • Vector
  • Hashtable
  • java.util.concurrent

这里的线程安全是指:多个线程调用它们同一个实例的方法,是线程安全的。他里面的方法是原子的,但组合的时候不一定线程安全。

String和Integer都是不可变类,都是线程安全的。



版权声明:本文为qq_34352643原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。