本文主要是学习了 老马说编程 的 计算机程序的思维逻辑中关于线程部分的知识,并对其进行回顾总结。
多线程的实现
- Thread
创建一个Thread的子类,并复写其run()方法,并在需要启动的地方调用其start方法
public class TestThread extends Thread {
@Override
public void run() {
System.out.println("I'm test thread");
}
}
public static void main(String[] args) {
Thread testThread = new TestThread();
testThread.start();
}
- Runnable
创建一个实现了Runnable接口的类,并实现run方法,并在需要启动的地方创建一个Thread对象,并将此类作为参数传入,调用此thread对象的start方法。
public class TestThread implements Runnable{
@Override
public void run() {
System.out.println("I'm test thread");
}
}
public static void main(String[] args) {
Thread testThread = new Thread(new TestThread());
testThread.start();
}
两者的区别在于Thread需要通过继承实现,而Runnable是通过接口,在单继承的java中,后者的使用应该更为便利。
线程的属性和方法
- Thread.currentThread():返回当前线程对象
- getName():获取线程名
- getId():获取线程对应ID
- 线程优先级:1~10,默认为5。优先级效果仅为建议非强制。
public final void setPriority(int newPriority)
: 设置优先级public final int getPriority()
: 获取优先级
public State getState()
: 获取线程状态
- New : 未被start
- TERMINATED : 运行结束
- RUNNABLE : 正在执行run且未被阻塞
- BLOCKED : 等待锁
- WAITING : 等待状态
- WAITING_TIME : 等待sleep时间结束
public final native boolean isAlive()
: 线程是否存活,线程启动后且在run结束前,返回True- daemon线程 : 程序一般为辅助线程,当程序中仅剩下daemon线程时,程序会退出。例如 负责垃圾回收的线程
public final void setDaemon(boolean on)
: 设置是否为Daemon线程
public final boolean isDaemon()
: 返回该线程是否为Daemon线程public static native void sleep(long millis) throws InterruptedException;
: 该方法使当前线程睡眠millis时间(毫秒),当被中断时,sleep会抛出InterruptedException异常
public static native void yield();
: 让出CPU,仅为建议作用。public final void join() throws InterruptedException
: 调用线程等待该线程运行结束后再退出,可以被中断,当被中断时会抛出InterruptedException异常,可传入一个long参数,表示等待最长时间(毫秒)。
共享内存及问题
- 共享内存:虽然每个线程都有自己的栈和程序计数器,但是它们可以访问和操作同一对象。
- 竞态条件:当多个线程访问和操作同一对象时,最终执行结果和执行时序有关,不一样能得到预期结果。
当两个线程同时取到了同一个共享变量,并都对其进行操作,就会导致得不到预期的结果。 - 内存可见性:一个线程对一个共享变量的修改,另一个线程不一定能马上能看到,甚至永远也看不到。
数据有可能被缓存在寄存器或CPU的缓存中,当去访问一个变量时可能直接在缓存中获取,而不一定会在内存中获取;同理,当修改一个变量时,可能先存入缓存,稍后才会同步到内存中。
Synchronized
- 用法
Synchronized用于保护对象。竞态条件和内存可见性问题均可用synchronized来解决。 - 修饰实例方法
public synchronized void test(){}
Synchronized用于保护实例对象this。
当使用Synchronized修饰方法,方法内的代码就变成了原子操作。即多个线程只能有一个调用同一对象的synchronized实例方法。
当一个线程想要调用synchronized方法时,它首先会尝试获取锁,如果不能够获取锁进入等待队列,阻塞并等待唤醒,否则执行实例方法代码,万仇释放锁,并从等待队列中取一个线程唤醒,不保证公平性。 - 静态方法
public static synchronized void test(){}
保护的是被修饰的静态方法的类对象。 - 代码块
synchronized(this){}
synchronized括号里面的就是保护的对象。 - 可重入性
在对同一个执行线程,它在获得了锁之后再调用其他需要同样的锁的代码时可以直接调用,该特性是通过记录锁的持续线程和持有数量来进行实现。 - 死锁
- 原因:由于两个不同线程的分别等待对方所持有的锁。
- 避免:避免在持有一个锁去申请另一个锁,如果确实需要多个锁,所有代码都应该按照相同的顺序去申请锁。或在获取不到锁时放弃已持有的锁再次尝试,防止死锁。
线程的优点及成本
- 优点
- 充分利用多CPU的运算能力
- 充分利用硬件资源和网络。
- 在GUI保持程序的响应性。
- 简化建模和IO处理。
- 成本
- 创建线程时损耗的资源,必要的数据结构、栈、程序计数器
- 线程切换时,需要保存当前线程的上下文到内存,并恢复切换的线程的上下文状态,此过程不仅耗时,且会使CPU的缓存失效。
协作机制
wait/notify两个方法是线程的协作的基本方法。只能在synchronized代码块中调用。
- wait方法
public final void wait() throws InterruptedException
用于线程等待,可传入参数表示等待时间(毫秒),等待时间内可被中断,中断时抛出InterruptedException异常。
每个对象都有一个等待队列,即条件队列,当调用此方法时,会将线程置于此队列中释放锁持有的锁并对线程进行阻塞,等待一个通过其他线程改变的条件。
此时线程的状态变为:WAITING或TIMED_WAITING - notify方法
public final native void notify();
public final native void notifyAll();
notify方法用于条件队列中随机选一个线程移除并唤醒,notifyAll方法用于唤醒条件队列中所有线程。
如果被唤醒的线程能获得所需锁,则状态变为:RUNNABLE;否则状态变为BLOCKED。 - InterruptedException异常
改异常为受检异常,线程需要对其进行处理,处理方式一般有两种。
- 向上传递异常,由调用者进行处理
- 有些无法向上传递的情况,如run方法,需要捕获异常并进程清理操作,然后调用Thread的interrupt方法设置中断标志位。
线程的中断
- 机制
中断时一个线程的主要机制,不是指强制停止,而是一种协作机制,通过传递一个信号,由线程决定何时退出。 - Thread中关于中断的方法
public boolean isInterrupted()
:返回对应线程的中断标志位是否为true。public void interrupt()
:返回对应线程的中断标志位是否为true,且会清空中断标志位。public static boolean interrupted()
:中断对应线程,与线程的状态有关。
- RUNNABLE:如没有执行I/O操作时,该方法会设置中断标志位为true,线程应在合适的位置检测中断标志位。
- WAITING/TIMED_WAITING:该方法会使线程抛出InterruptedException,且会清除中断标志位。
- NEW/TERMINATE:该方法在调用后无任何效果。
- 正确的中断线程
如果不明白线程在做什么,不应该贸然调用interrupted()方法,线程应当提供单独的取消/中断方法给调用者。