线程的状态
状态是针对当前线程调度的情况来描述的.现在我们认为线程是调度的基本单位.所以状态更应该是线程的属性.(后面我们谈到的状态都是线程的状态)
观察线程状态
public class ThreadState {
public static void main(String[] args) {
for (Thread.State state : Thread.State.values()) {
System.out.println(state);
}
}
}
在Java对于线程的状态,进行了细化.
-
NEW 表示创建了Thread对象,但是还没调用start(内核中还没创建对应的PCB)
-
TERMINATED 表示内核中的pcb已经执行完毕了,但是Thread对象还在.
-
RUNNABLE 表示可运行的.(a.正在CPU上执行的,b.在就绪队列里,随时可以去CPU上执行.)
-
WAITING 阻塞
-
TIMED_WAITING 阻塞
-
BLOCKED 阻塞
线程的状态转移
线程状态图转换简单清晰:
观察线程的状态和转移
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100000000; i++) {
// 循环体中什么都不干 也不sleep
}
});
//启动之前,获取t的状态 即NEW状态
System.out.println("start 之前: " + t.getState());
t.start();
t.join();
//线程执行完毕之后,就是 TERMINATED
System.out.println("t 结束之后: " + t.getState());
}
}
加上个查看执行中的状态.
现在我们加上sleep:
我们看不到TERMINATED是因为 t 线程sleep时间太长了.把for循环的次数改小我们就可以看到了.
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动之前,获取t的状态 即NEW状态
System.out.println("start 之前: " + t.getState());
t.start();
System.out.println("t 执行中的状态: " + t.getState());
t.join();
//线程执行完毕之后,就是 TERMINATED
System.out.println("t 结束之后: " + t.getState());
}
}
若我们想看到TIMED_WAITING状态只需要加个for循环即可.
public class ThreadDemo11 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
//启动之前,获取t的状态 即NEW状态
System.out.println("start 之前: " + t.getState());
t.start();
for (int i = 0; i < 1000; i++) {
System.out.println("t 执行中的状态: " + t.getState());
}
t.join();
//线程执行完毕之后,就是 TERMINATED
System.out.println("t 结束之后: " + t.getState());
}
}
通过循环获取就能看到这里的交替状态了.当前获取到的状态完全取决于系统里的调度操作.
案例
写个代码来感受下,单个线程和多个线程之间,执行速度的差别。
程序分成:
-
CPU密集,包含了大量的算数运算。
-
IO密集,涉及到读写文件,读写控制台,读写网络。
单线程-串行执行
//串行执行,一个线程完成
public static void serial(){
//为了衡量代码执行速度 加上个计时操作
//获取当前系统的ms级时间戳
long beg =System.currentTimeMillis();
long a = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
a++;
}
long b = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
b++;
}
long end = System.currentTimeMillis();
System.out.println("单线程执行时间: " + (end - beg) + " ms");
}
执行结果时间误差在20s左右,可以接受.
多线程-并发执行
public static void concurrency(){
// 使用两个线程 分别完成自增
Thread t1 = new Thread(()->{
long a = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
a++;
}
});
Thread t2 = new Thread(()->{
long b = 0;
for (long i = 0; i < 100_0000_0000L; i++) {
b++;
}
});
long beg = System.currentTimeMillis();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("并发执行时间: " + (end - beg) + " ms");
}
我们也要考虑清楚当前的join是哪个线程调用的:main调用t1.join就是main来等待t1,如果t2调用t1.join,就是t2等待t1.
此处使用两个线程并发执行,时间缩短很明显,使用多线程快是因为多线程可以充分利用到多核心cpu资源.实际上t1和t2在执行过程中,会经历很多次调度,这些次调度,有些是并发执行的(在一个核心上),有些是并行执行的(正好在两个核心上),另一方面线程调度自身也是有时间消耗的.所以缩短的时间可能是50%,也有可能不是50%.但是它缩短的时间是具有很大的意义的.
线程安全
线程安全是多线程编程中最难的地方,也是最重要的地方.
多线程的安全问题出现是多线程抢占式执行的随机性带来的.
如果没有多线程,此时程序代码执行顺序就是固定的(只有一条路),代码顺序固定,程序的结果就是固定的.如果有了多线程,此时抢占式执行下,代码的执行的顺序会出现更多的变数.此时代码执行顺序的可能性就从一种情况变成了无数种情况.所以就需要保证这种无数种线程调度顺序的情况下,代码的执行结果都是正确的.
只要有一种情况下,代码结果不正确,就都视为是有bug,线程不安全.
能否消除这种随机性?
调度的源头来自于操作系统的内核实现,想改变不太现实.
观察线程中不安全的情况
class Counter{
public int count = 0;
public void add(){
count++;
}
}
public class ThreadDemo13 {
public static void main(String[] args) {
Counter counter = new Counter();
//搞两个线程 分别针对counter 来调用5w次add方法
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
//等待两个线程结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印最终的count值
System.out.println("count = " + counter.count);
}
}
程序不符合需求就是bug,我们的需求是两个线程各自自增5w次,一共自增10w次,预期结果是10w,实际结果不是10w而且每次都不一样.
这个现象就是典型的线程安全问题.为什么程序出现了上面的情况?
如果是两个线程并发的执行count++,此时就相当于两组load add save进行执行,此时不同的线程调度顺序就可能会产生一些结果上的差异.
上面的调度随机性是无穷无尽的.下面的各种情况都会出现线程安全问题:
下面各自分析一下:
线程安全概念
如果多线程环境下代码运行的结果是符合我们预期的,即在单线程环境应该的结果,则说这个程序是线程安全的。
线程不安全的原因
- [根本原因]抢占式执行,随机调度.
- 代码结构:多个线程同时修改同一个变量.
- 原子性:如果修改操作是非原子的,出现的概率就非常高.(原子:不可拆分的基本单位)
- 内存可见性,如果一个线程读,一个线程改可能就出现此处读的结果不太符合预期.
- 指令重排序.本质上是编译器优化出bug了.
解决线程不安全问题
针对线程安全问题,最主要的手段就是从原子性入手,把非原子的操作变成原子的,即加锁.我们会用到synchronized关键字.
这个关键字表示加锁,方法前面加上,表示进入方法就会加锁,出了方法就会解锁.如果两个线程同时尝试加锁,此时一个能获取成功,另一个只能阻塞等待(BLOCKED),一直阻塞到刚才的线程释放锁(解锁),当前线程才能加锁成功.
加锁说是保证原子性,其实不是说让这里的三个操作一次完成,也不是让这三步操作过程中不进行调度,而是让其它也想操作的线程阻塞等待了.所以加锁本质就是把并发变成了串行.
class Counter{
public int count = 0;
synchronized public void add(){
count++;
}
}
public class ThreadDemo13 {
public static void main(String[] args) {
Counter counter = new Counter();
//搞两个线程 分别针对counter 来调用5w次add方法
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
//等待两个线程结束
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印最终的count值
System.out.println("count = " + counter.count);
}
}
此时我们看到代码始终是100000,加锁后,线程安全问题得到了改善.