java 的线程类型是 Thread ,多线程的学习是从 Thread 开始的
创建 Thread 的2种方式:
class MyThread extends Thread{
private String name ;
public MyThread(){
name = "AA";
}
@Override
public void run() {
System.out.println(name);
}
}
// 启动 Thread 线程对象
MyThread thread = new MyThread();
thread.start();
class MyRunnable implements Runnable{
public MyRunnable() {
}
@Override
public void run() {
System.out.println("子线程ID:"+Thread.currentThread().getId());
}
}
// 把 Runnable 作为参数传递给 Thread 对象
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
线程调度
上面我们先回味了一下 Thread 怎么创建,怎么简单的跑起来,食髓知味,我们也要开始深入内里学习原理,夯实我们多线程的基础知识
目前来说线程调度有2种方式:
- 协同式 - 线程之间的系统执行时间,由线程本身进行进行控制,这种线程调度方式就像接力赛,一个执行完毕后交由下一个接力,如当前线程执行完毕后,通知系统调度到其他线程执行。这玩意就是协程,我已经在 kotlion 中领略过了,线程的调度不再交给系统内核完完成,完全由 coder 自己控制,何时挂起,何时恢复
- 抢占式 - 线程之间的系统执行时间,是由系统进行控制,而抢占式的线程调度对线程的不可预知,系统定期的中断当前正在执行的线程,将CPU执行权切换到下一个等待的线程。所以任何一个线程都不能独占CPU。正因为这种定期的线程切换导致线程之间存在不同的问题。当线程执行过程中,某个线程出现问题的时候,由线程对CPU不具有独占性。因此不会造成阻塞。我们所使用的操作系统都是是用抢占性的线程调度。如果使用协同式的线程调度情况下,如果我们再使用某个软件出现问题时候,操作系统处于阻塞状态,导致整个操作系统崩溃,我们肯定会抓狂。
我们只要知道 Java线程调度就是抢占式的就行了,线程之间通过竞争时间段来实现执行任务
Thread 线程的状态
不废话直接看图:
线程状态:
创建(new)状态
我们刚刚 创建 new 一个 Thread 对象时就绪(runnable)状态:
Thread 对象所需要的都准备完毕,此时可以调用 start() 方法了,剩下的就是等待CPU进行调度运行(running)状态:
执行 run() 方法让线程跑起来阻塞(blocked)状态
暂时停止执行, 可能将资源交给其它线程使用终止(dead)状态:
线程销毁
线程运行描述:
通常当我们 new 一个线程对象来执行任务时,线程对象不会立即进入就绪状态,因为线程的运行需要一些条件(比如内存资源,在前面的JVM内存区域划分一篇博文中知道程序计数器、Java栈、本地方法栈都是线程私有的,所以需要为线程分配一定的内存空间),只有线程运行需要的所有条件满足了,才进入就绪状态。
当线程进入就绪状态后,不代表立刻就能获取CPU执行时间,也许此时CPU正在执行其他的事情,因此它要等待。当得到CPU执行时间之后,线程便真正进入运行状态。
线程在运行状态过程中,可能有多个原因导致当前线程不继续运行下去,比如用户主动让线程睡眠(睡眠一定的时间之后再重新执行)、用户主动让线程等待,或者被同步块给阻塞,此时就对应着多个状态:time waiting(睡眠或等待一定的事件)、waiting(等待被唤醒)、blocked(阻塞)。
当由于突然中断或者任务执行完毕,线程就会被消亡。
blocked、waiting、time waiting 都可以叫阻塞,详细区别下是有必要的,涉及的方法不同
Thread 类方法解析
上面我们说了如何创建一个 Thread 线程对象并执行,获取到结果。其实 Thread 自身还有一些很重要的方法需要记住。
我简单点直接放张图
- currentThread()
返回所在线程对象,这是个静态方法
System.out.println(Thread.currentThread().getName());
- sleep()
让当先线程睡眠指定的毫秒时间,sleep 方法会阻塞当前线程,但是不会释放当前线程所持有的对象锁
// 当前线程睡眠1秒
Thread.currentThread().sleep(1000);
- yield()
让当前线程交出本次 CPU 时间,并释放所持有的对象锁,竞争下次 CPU 时间。需要注意的是 yield 只是释放本次的 CPU 执行机会,下次该竞争还竞争。调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。还有这也是一个静态方法
Thread.yield();
start()
线程启动方法,当调用start方法后,系统才会开启一个新的线程并分配需要的资源run()方法
run方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。getId()
获取当前线程唯一标识
Thread t= Thread.currentThread();
System.out.println(t.getName()+" "+t.getId());
- isAlive()
判断当前线程是否处于活动状态,不是 running 状态都不叫活跃状态
Thread t1 = new Thread();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("AA", Thread.currentThread().getName() + "isAlive:" + t1.isAlive());
可以看到就绪状态都不行,都不算是活跃状态,其他的可想而知
- join()
让当前线程进入阻塞,等待被 join 方法标记的线程执行完毕再执行。感觉上是不是有点像上面那个 callable 和 Future ,相当于给被 join 标记的线程添加一个返回值,我们等待这个返回值获取到数据再继续我们当前的线程。
Thread t2 = new Thread() {
@Override
public void run() {
super.run();
try {
Thread.sleep(2000);
Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
try {
Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
t2.start();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d("AA", Thread.currentThread().getName() + "时间,time:" + System.currentTimeMillis());
可以看到 UI 线程被成功的阻塞了,和我们的预期一样,和 callable 和 Future 的效果是一样的
getName和setName
用来得到或者设置线程名称。getPriority和setPriority
用来获取和设置线程优先级。setDaemon和isDaemon
用来设置线程是否成为守护线程和判断线程是否是守护线程。
守护线程和用户线程的区别在于:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法运行完毕之后,守护线程也会随着消亡。而用户线程则不会,用户线程会一直运行直到其运行完毕。在JVM中,像垃圾收集器线程就是守护线程。
找到张图表示线程各方法的效果:
停止线程
线程停止是个重要的点,有3中写法:
- Thread.stop()
这个方法已经被废弃不用了, - interrup()
interrup 有个问题是不一定能终止线程,这点我不是很了解,建议大家再去找找资料 - 加标记位
大家知道,线程执行方法完成了,那么自然就会终止线程了,所以我们在 线程的 run 方法里都是加一个无限虚循环的 while(true),我们只要把这个 true 用标志位来代替,想终止的时候把这个标记改成 false 自然就能安全的退出线程了。
线程的优先级
在Java中,线程的优先级分为1~10这10个等级,如果小于1或大于10,则JDK抛出异常throw new IllegalArgumentException()。
JDK中使用3个常量来预置定义优先级的值,代码如下:
- public final static int MIN_PRIORITY = 1;
- public final static int NORM_PRIORITY = 5;
- public final static int MAX_PRIORITY = 10;
Thread thread = new Thread();
t1.setPriority( Thread.NORM_PRIORITY );
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);//-20~19,通过进程设置
这两种设置方式是相对独立的,在Android中,一般建议通过Process进程设置优先级
线程优先级特性:
- 继承性
比如A线程启动B线程,则B线程的优先级与A是一样的。 - 规则性
高优先级的线程总是大部分先执行完,但不代表高优先级线程全部先执行完。 - 随机性
优先级较高的线程不一定每一次都先执行完。
守护线程
在Java线程中有两种线程,一种是User Thread(用户线程),另一种是Daemon Thread(守护线程)。
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
守护线程并非虚拟机内部可以提供,用户也可以自行的设定守护线程,方法:public final void setDaemon(boolean on) ;但是有几点需要注意:
thread.setDaemon(true)必须在thread.start()之前设置,否则会跑出一个IllegalThreadStateException异常。你不能把正在运行的常规线程设置为守护线程。 (备注:这点与守护进程有着明显的区别,守护进程是创建后,让进程摆脱原会话的控制+让进程摆脱原进程组的控制+让进程摆脱原控制终端的控制;所以说寄托于虚拟机的语言机制跟系统级语言有着本质上面的区别)
在Daemon线程中产生的新线程也是Daemon的。 (这一点又是有着本质的区别了:守护进程fork()出来的子进程不再是守护进程,尽管它把父进程的进程相关信息复制过去了,但是子进程的进程的父进程不是init进程,所谓的守护进程本质上说就是“父进程挂掉,init收养,然后文件0,1,2都是/dev/null,当前目录到/”)
不是所有的应用都可以分配给Daemon线程来进行服务,比如读写操作或者计算逻辑。因为在Daemon Thread还没来的及进行操作时,虚拟机可能已经退出了。
最后
我这里写的也不是很全,有没看懂的建议大家再去找资料,这里主要是给我自己看,按照的我的记忆顺序写的, 以后忘了再看方便的多。
有不全的大家见谅,有错误请下面留言我更正