Thread的应用
1.如何启动一个线程
Thread类使用start方法启动一个线程,但对于一个线程,只能调用一次start方法,否则会发生异常。
2.start和run的区别(经典面试题)
启动线程时,如果用run,那么只是执行线程的run方法,并不是多个线程同时执行,如果run方法有循环的话,那么就是一直循环;如果用start,可以同时执行其他线程。
3.终止一个线程
通常当一个线程的run方法执行完成后,线程就终止了,有时为了让线程提前终止,可以引入一个标志位,可以在另一个线程中在合适时机修改标志位。但设置标志位时要在main类外设置一个static的标志位。因为lambda要获取final型的变量。只有设置在main外才可以。
Thread也自带标志量,通过Thread.currentThread.isInterrupted()来调用,默认为false,可以调用t.interrupt()来唤醒其他线程。
4.java为什么没有引入硬性终止线程
因为线程一旦强行终止,会产生许多临时文件,如果线程在打开图片或访问文件时,强行终止,会导致图片/文件产生不可预知的错误。
5.线程执行的顺序
多个线程的执行顺序是不确定的,但引入join就可以确定顺序。当一个线程调用join后,其他线程要等待该线程执行完成之后才可以运行。在使用join时,要注意抛出InterruptedException异常。
Thread t = new Thread(()->{
for (int i = 0; i < 5; i++) {
System.out.println("线程正在执行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
t.join();
System.out.println("这是主线程,在t之后执行");
在这个例子中,main线程要等待t线程执行完成后才会执行。
6.join的不同版本
1)死等版本,join(),不科学,容易造成死锁现象。
2)带有超时时间的版本,有一个等待上限,join(long millis),也可以使用interupt方法,将等待运行join线程的线程提前唤醒。
6.获取线程的引用
Thread.currentThread()获取到当前线程的引用(Thread的引用)
如果是继承Thread,直接使用this那到线程实例/
如果是Runnable或者lambda的方式,this就无能为力了,此时this已经不再指向Thread对象了,只能使用Thread.currentThread(),
Thread t = new Thread(()->{
Thread t1 = Thread.currentThread();
System.out.println(t1.getName());
});
t.start();
线程的状态
1.NEW:Thread对象创建好了,但是还没有调用start方法在系统内创建线程。
2.TERMINATED: Thread对象仍然存在,但是系统内部的线程已经执行完毕了。
3.RUNNABLE:就绪状态,表示这个线程正在cpu上执行,或者准备就绪随时可以去cpu上执行。
4.TIMED_WAITING: 指定时间的阻塞,就在到达一定时间后自动解除阻塞,使用sleep会进入这个状态,使用带有超时时间的join也会。
5.WAITING:不带时间的阻塞,必须要满足一定条件才会接触阻塞。join和wait都会进入WAITINNG。
6.BLOCKED:由于锁竞争而引起的阻塞。
NEW---调用start--->RUNNABLE--sleep--->TIMED_WITING;---join/wait---->WAITING;----run方法执行完毕---->TERMINATED
可以通过jconsole来查看线程的状态。
7.线程安全(较难)
引入多线程,是为了实现“并发编程”,但是实现“并发编程”,并不能仅仅依靠多线程,后世的一些其他编程语言,引入了封装层次更高的并发编程模型,往往会比java多线程,要更方便,更简单,更不易出错。
典型代表:erlang: actor模型
go: csp模型
js: 基于定时器/异步模型..........
某个代码,无论是在单个线程下执行,还是多线程下执行,都不会产生bug,这个情况就称为“线程安全”,但某个线程在单个线程下执行安全,在多线程下执行产生bug,就称为“线程不安全”。
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
count++;
}
});
t1.start();;
t2.start();
t1.join();
t2.join();
System.out.println("count= "+count);
第一次运行:count= 7053;
第二次运行:count= 6052;
很明显,这个代码有问题,结果应该是10000,但是每次结果都不同,这就是“不安全线程”。
为什么?因为count++其实是由cpu三个指令构成的。
1)load 从内存中读取数据到cpu的寄存器。
2)add 把寄存器中的值+1.
3)save 将寄存器中的值写回到内存中。
如果是一个线程执行上述三个指令,当然没问题,但是如果是两个线程,并发执行上述操作,此时就会存在变数,因为线程间的调度是不确定的!这三个指令会无规律的执行,导致答案错误。
1.根本原因:操作系统上的线程是“抢占式执行” ”随机调度“--->给线程的执行顺序带来了许多变数。
2.代码结构有问题:代码中多个线程,同时修改同一个变量。
3.直接原因:上述多线程修改操作,本身不是“原子操作”;
其他会导致线程不安全的原因:
4.内存可见性问题。
5.指令重排序问题。
解决方法:
针对1,可以通过修改操作系统内核来改变,但是意义不大/
针对2,有时可以通过修改代码来解决,有时则不可以。
针对3,可以通过特殊方法,将三个指令打包到一起,成为“整体”,其中“加锁”就是一种方法,“锁”有“互斥”和“排他”的特性。在Java中,有好几种方式加锁,我们主要使用“synchornized”关键字来加锁,读作“xing ke rou nai zi de”。
要加锁时,需要准备好“锁对象”,加锁解锁都是依托于锁对象来展开的,如果一个线程,针对一个对象加上锁后,其他线程,也尝试对这个对象加锁,就会产生阻塞(BLOCKED),一直阻塞到前一个线程释放锁为止。在Java中任何一个对象都可以是锁对象。对于下面的例子,t1线程的count++就变成了一个原子操作,每次执行count++时都会加锁,这时t2线程就竞争不到锁,就处于阻塞状态,直到t1线程的count++执行完毕。
加锁后,好像变成了串行执行,但加锁虽然会影响到效率,但执行速度还是要远远大于串行执行的,因为加锁只是锁住了某几个指令,大部分指令还是可以并发执行的,
Object lack = new Object();
Thread t1 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lack){
count++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 5000; i++) {
synchronized (lack){
count++;
}
}
});
t1.start();;
t2.start();
t1.join();
t2.join();
System.out.println("count= "+count);
易错点:
1)如果一个线程加锁,另一个不加锁,仍然会出现“线程安全”问题。
2)如果两个线程,针对不同的对象加锁,也会出现线程安全问题。
3)针对加锁操作的一些混淆的理解
class Test{
public int count=0;
public void add(){
synchronized (this){
count++;
}
}
}
public class Thread16 {
public static void main(String[]args) throws InterruptedException {
Test t = new Test();
Thread t1 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
t.add();
}
});
Thread t2 = new Thread(()->{
for (int i = 0; i < 50000; i++) {
t.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("count= "+t.count);
}
}
这样写代码,并没有线程安全问题,在t1线程中this指的是t,t2线程中this也是指的t,这样也会存在锁竞争,线程是安全的。
如果把锁对象换成Test.class也是ok的,
public void add(){
synchronized (Test.class){
count++;
}
}
每个类都有自己的类对象,通过类名.class获取,类对象里包含了类的各种信息,比如有什么属性,每个属性(变量)的名字,方法的名字,参数........
一个类的类对象是唯一的,也就是说锁是唯一的,这时线程也是安全的。
类对象也是反射机制的依据。
如果是synchronized(this)也可以等价写作把synchronized加到方法上。
public void add(){
synchronized (this){
count++;
}
}
}
synchronized public void add(){
count++;
}
这两种方法等价。
如果synchronized写到static方法上,相当于给类对象加锁。
public static void fun(){
synchronized (Test.class){
count++;
}
}
synchronized public static void fun(){
count++;
}
这两种方式等价。