文章目录
Thread类中的常见方法
上一次,我们讲解了线程的基本概念,以及构造线程的几种方法,这一次我们来聊一聊,为什么要这么构建,以及构建完成线程以后,我们可以做一些什么事情
构造方法
方法 | 说明 |
---|---|
Thread() | 创建线程对象 |
Thread(Runnable target) | 使用Runnable对象创建线程 |
Thread(String name) | 创建一个线程使用name命名 |
Thread(Runnable target,String name) | 根据Runnable对象来创建线程,并用name命名 |
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("hello ");
}
}
public class Demo3 {
public static void main(String[] args) {
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable);
Thread t3 = new Thread("这是一个线程的名字");
Thread t4 = new Thread(new MyRunnable(),"这是另一个线程的名字");
}
}
常见属性以及Getter
属性 | 获取方法 |
---|---|
ID | getId() |
名称 | getName() |
状态 | getState() |
优先级 | getPriority() |
是否后台线程 | isDaemon() |
是否存活 | isAlive() |
是否被中断 | isInterrupted() |
- ID 是线程的唯一标识,不同的线程ID不会重复
- 名称是各种调试工具使用到的
- 状态标识线程当前所处的情况 何为线程状态
- 优先级高的线程,理论上来说更容易被调度到
- JVM在一个进程的所有非后台线程结束后,才会结束运行
- 如果线程是后台线程,不影响进程的退出
- 如果线程不是后台线程,就会影响线程退出
- run方法运行结束后,线程不再存活
- 操作系统中的线程是否正在运行,
Thread
对象的生命周期与内核中对应的线程生命周期并不完全一致
,创建出线程对象之后,在调用start之前,系统中是没有对应线程的,在run
方法执行完成之后,系统中的线程就已经销毁了,但是java对象可能还存在,通过isAlive
就能判定当前系统的线程运行情况 - 如果调用
start
之后,run
执行完之前,isAlive
返回true - 如果调用
start
之前,或run
执行完之后,isAlive
就返回false
- 操作系统中的线程是否正在运行,
- 线程的中断 何为线程中断
Thread中的一些重要方法
中断线程
线程停下来的关键,就是让线程对应的run
方法执行完毕
对于main
来说,main方法
执行完了,线程就结束了
操作方法
- 可以手动设置一个标志位来控制线程是否要结束
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
public static boolean isQuit = false;
Thread t = new Thread(()->{
while (!isQuit){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(3000)
isQuit = true;
}
}
- 可以使用Thread中内置的一个标志位来进行判定
Thread.interrupted();
Thread.currentThread.isInterrupted();
实例方法,其中currentThread
可以获取到当前线程的实例对象
public class Demo9 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while (!Thread.currentThread().isInterrupted()){
System.out.println("hello thread");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
Thread.sleep(5000);
t.interrupt();
}
}
调用t.interrupt()
可能出现两种情况
- 如果t线程处于休眠状态,就直接设置线程的标志位为
true
- 如果线程处于阻塞状态(休眠),就会触发一个
InterruptException
- 调用这个
intertupt
方法,就会让sleep
触发一个异常,从而导致线程从阻塞状态中被唤醒 - 当出现异常之后,进入
catch
段,使用break;
就可以使得线程终止
- 调用这个
关于线程中断的一些问题
判断线程是否中断,推荐使用isInterrupted()
方法
interrupted()
这个方法判定的标志位是Thread 的静态成员,这就导致了,一个程序中只有一个标志位,这显然是不合理的Thread.currentThread().isInterrupted()
这个方法判定的是Thread的普通成员,每个实例都有自己的标志位 —也就是下图这个东西
线程等待
线程等待,是控制线程执行顺序的一种手段,此处的线程等待,主要是控制线程结束的先后顺序
线程等待的方法
join
- 调用
join
的时候,哪个线程调用的join
,哪个线程就会进入阻塞等待,直到join
进来的线程执行完毕为止 join
操作默认情况下,会一直等待到join
进来的线程结束为止(没有超时时间)- 可以手动的指定超时时间,如果超过指定的时间,线程仍然没有结束,join也会 直接返回
在下图的代码段中,main
线程会等待t
线程 10s,如果再10s内,t
线程执行完毕,那么main
线程就会解除阻塞,继续运行,否则十秒后才会继续运行
获取线程的引用
Thread.currentThread()
就可以获取到当前线程的引用,使用哪个线程调用的currentThread()
得到的就是哪个线程的对象
我为什么要是使用这个方法?我不是定义过了一个变量吗
一般都是在没有线程对象又需要获得线程信息时通过Thread.currentThread()获取当前代码段所在线程的引用,比如使用Runnable 或者 Callable接口创建线程的时候
线程休眠
sleep()
无休止的睡下去sleep(int time)
睡一会儿
线程的状态
进程是有状态的
- 就绪
- 阻塞
这里的状态决定了系统按照什么样的态度来调度这个进程(相当于是针对 一个进程中只有一个线程的情况)
更常见的情况下:一个进程包含了多个线程,所谓的状态,其实是绑定在线程上
就绪 和 阻塞 都是针对系统层面上
在Java Thread类中,对于线程的状态,进一步细化了
TERMINATED: 操作系统中的线程已经执行完毕,销毁了,但是对象还存在
RUNNABLE:处于就绪状态的线程,随时可以被调度到CPU上
TIMED_WAITING: 代码中调用了sleep
或者 join
(超时时间) 意思是当前的线程在一定时间之内,是阻塞状态,一定时间到了之后,阻塞状态解除
BLOCKED: 点前线程在等待锁,导致了阻塞 synchronized
WAITING:当前线程在等待唤醒,导致了阻塞 wait
线程安全
操作系统调度线程的时候是随机的
- 如果因为这样的调度随机性引入了bug,就认为代码是线程不安全的
- 如果没有带来bug,那么久认为是线程安全的
一个线程不安全的案例
使用两个线程,对同一个整形变量进行自增操作
- 结果永远小于10000,并且大于5000 这就是最简单的线程不安全案例
怎么解决这种安全问题呢
- 加锁(
synchronized
)- 通过加锁,把乱序的并发,变成一个串行操作
- 加锁之后,并发程度变低,数据更靠谱,但是速度更慢了;
加锁的方式
Java中加锁的方式有很多种 最常使用 synchronized 这样的关键字
给方法加上synchronized关键字,此时进入方法 就会自动加锁,离开方法 就会自动解锁
当一个线程加锁成功的时候,其他线程尝试加锁,就会触发阻塞等待(此时对应的线程,就出在BLOCKED状态)
阻塞会一直持续到占用锁的线程将锁释放为止
什么样的代码会产生线程不安全问题呢
首先: 不是所有的多线程代码都要加锁
产生线程不安全的原因
-
线程是抢占式执行的,线程间的调度充满随机性(线程不安全的根本原因)
-
多个线程对同一个变量进行修改操作,如果是多个线程针对不同变量进行修改,或者多个线程对同一个变量读也不会出现线程安全问题
-
针对变量的操作不是原子的
- 此处说的操作原子性类似数据库事务原子性
- 有些操作,比如读取变量的值,只是对应一跳机器指令,此时这样的操作就可以看做是原子的
- 通过加锁操作,就是把好几个指令给打包成一个原子的了
-
内存可见性,也会影响到线程安全
-
指令重排序,也会影响到线程安全问题
- 指令重排序,也是编译器优化中的一种操作
- 使用synchronized关键字
内存可见性导致的线程不安全问题实例
解决内存不安全导致线程不安全的问题
-
使用==
synchronized
== 关键字- 不光可以保证执行的原子性,同时也能保证内存可见性
-
使用==
volatile
==关键字- volatile和原子性无关,但是能够保存内存可见性
- 禁止编译器做出上述优化,编译器每次执行 都会从内存中重新读取
-
加上sleep编译器优化就消失了,也没有内存可见性问题了