线程基础知识(一)
线程状态
从操作系统层面5种状态
- 【初始状态】仅仅在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由cpu调度执行
-
【运行状态】
- 指获取了cpu时间片运行中的状态
- 当cpu时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程上下文切换
-
【阻塞状态】
- 如果调用了阻塞的API,如BIO读写文件,这是该线程实际不会调用CPU,会导致线程上下文切换,进入【阻塞状态】
- 等BIO操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是:对于【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期就已经结束,不会再转换为其他状态
从java语言层面六种状态
根据Thread.state的内部枚举决定
-
【new】线程刚被创建,但是还没有调用start();
-
【Runnable】当调用了start()方法之后。注意,java层面的【runnable】涵盖了操作系统层面【运行状态】【可运行状态】【IO阻塞状态】。
-
【Blocked】【Waiting】【Timed_Waiting】是java对阻塞状态的细分
- 【Blocked】:被动阻塞,线程争取时间片(锁)失败进入
- 【Waiting】:主动阻塞,程序员主动调用Wait(),join() 是线程进入阻塞状态
- 【Timed_Waiting】:主动阻塞,程序员主动调用Wait(long) ,sleep(long)是线程进入阻塞状态
-
【Terminated】终止状态
华罗庚《统筹方法》
想泡壶茶喝。当时的情况是:开水没有;水壶要洗,茶壶、茶杯要洗;火已生了,茶叶也有了。怎么
办?
-
洗好水壶,灌上凉水,放在火上;在等待水开的时间里,洗茶壶、洗茶杯、拿茶叶;等水开
了,泡茶喝。
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,会在每个线程的栈帧内存中被创建多份,因此不存在共享
- 当局部变量引用的是一个对象
先举一个成员变量的例子
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();
}
}
将 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 相同
暴露给外部
- 情况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都是不可变类,都是线程安全的。