【并发编程】- 线程篇
线程
-
1. 简介
-
1)定义
-
现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。现代操作系统调度的最小单元是线程,也叫轻量级进程(LightWeightProcess),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上快速切换,让使读者感觉到这些线程在同时执行。
-
2) 那么为何要使用多线程
-
更多的处理器核心
-
更快的响应
-
更好的编程模型
-
3)优先级
-
由于是操作系统给线程分配时间片的处理方式。那么便可以通过优先级设置处理的先后,确保处理器不会被独占。
-
4)状态
- 状态变换图
-
2. 启动与终止
-
1)启动与中断
-
启动:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程。
-
中断:即是线程的一个标识位属性,通过调用该线程的interrupt()方法对其进行中断操作。也可以调用静态方法Thread.interrupted()对当前线程的中断标识位进行复位,当抛出InterruptedException之前,
Java虚拟机会先将该线程的中断标识位清除
,即此时调用isInterrupted()方法将会返回false。 -
2)安全地终止线程(优雅)
-
通过设置一个boolean变量控制
public class Shutdown {
public static void main(String[] args) throws Exception {
Runner one = new Runner();
Thread countThread = new Thread(one, "CountThread");
countThread.start();
// 睡眠1秒,main线程对CountThread进行中断,使CountThread能够感知中断而结束
TimeUnit.SECONDS.sleep(1);
countThread.interrupt();
Runner two = new Runner();
countThread = new Thread(two, "CountThread");
countThread.start();
// 睡眠1秒,main线程对Runner two进行取消,使CountThread能够感知on为falseer结束
TimeUnit.SECONDS.sleep(1);
two.cancel();
}
private static class Runner implements Runnable {
private long i;
private volatile boolean on = true;
@Override
public void run() {
while (on && !Thread.currentThread().isInterrupted()) {
i++;
}
System.out.println("Count i = " + i);
}
public void cancel() {
on = false;
}
}
}
-
3. 线程间通信
-
1)volatile和synchronized
- java在多个线程访问一个对象或对象的成员变量,每个线程拥有其拷贝,将其放入各自的缓存中,这样可以加速程序的执行,故线程看到的变量并不一定是最新的。假如需要最新的,此时需要通过
volatile
通知线程间从共享内存
中获取,并刷新回各自的工作内存
,即是保证了对所有线程对变量访问的可见性。 - 关键字
synchronized
可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。- 同步块的实现使用了
monitorenter
和monitorexit
指令。 - 同步方法则是依靠方法修饰符上的
ACC_SYNCHRONIZED
来完成的。
- 同步块的实现使用了
本质:对一个对象的监视器(monitor
)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。
-
任意线程对Object(Object由synchronized保护)的访问,首先要获得Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
-
-
2)等待/通知机制
-
等待/通知机制,是指一个线程A调用了对象O的
wait()
方法进入等待状态,而另一个线程B调用了对象O的notify()
或者notifyAll()
方法,线程A收到通知后从对象O的wait()
方法返回,进而执行后续操作。
- 解决两个问题
- 确保及时性
- 降低开销
- 方法
-
notify
: 通知一个对象上等待的线程,使其从wait方法返回,而返回的前提是线程获取到了对象的锁。 -
notifyAll
:通知所有等待在该对象上的线程。 -
wait
: 调用该方法的线程进入WAITING
状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait
方法后,会释放对象的锁。 -
wait(long)
: 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回。 -
wait(long,int)
: 对于超时时间更细粒度的控制,可以达到纳秒。
-
- 使用注意事项
- 使用
wait()
、notify()
和notifyAll()
时需要先对调用对象加锁。 - 调用
wait()
方法后,线程状态由RUNNING
变为WAITING
,并将当前线程放置到对象的等待队列。 -
notify()
或notifyAll()
方法调用后,等待线程依旧不会从wait()
返回,需要调用notify()
或notifAll()
的线程释放锁之后,等待线程才有机会从wait()
返回。 -
notify()
方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()
方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING
变为BLOCKED
。 - 从
wait()
方法返回的前提是获得了调用对象的锁。
- 使用
上图中,WaitThread首先获取了对象的锁,然后调用对象的
wait()
方法,从而放弃了锁并进入了对象的等待队列WaitQueue中,进入等待状态。由于WaitThread释放了对象的锁,NotifyThread随后获取了对象的锁,并调用对象的notify()
方法,将WaitThread从WaitQueue移到SynchronizedQueue中,此时WaitThread的状态变为阻塞状态。NotifyThread释放了锁之后,WaitThread再次获取到锁并从wait()
方法返回继续执行。
-
3)Thread.join()
定义:如果一个线程A执行了thread.join()语句,当前线程A等待thread线程终止之后才从thread.join()返回。
-
4)ThreadLocal
-
ThreadLocal
,即线程变量,是一个以ThreadLocal
对象为键、任意对象为值的存储结构。 -
ThreadLocal
为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。- 每个Thread对象内部都维护了一个ThreadLocalMap这样一个
ThreadLocal
的Map,可以存放若干个ThreadLocal。 - 当我们在调用get()方法的时候,先获取当前线程,然后获取到当前线程的ThreadLocalMap对象,如果非空,那么取出ThreadLocal的value,否则进行初始化,初始化就是将initialValue的值set到
ThreadLocal
中。 - 当我们调用set()方法的时候,很常规,就是将值设置进
ThreadLocal
中。
- 每个Thread对象内部都维护了一个ThreadLocalMap这样一个
- 采用
ThreadLocal
根本就没有竞争。
内存泄露:
实际上ThreadLocalMap
中使用的key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来ThreadLocalMap
中使用这个ThreadLocal
的key也会被清理掉。但是,value是强引用,不会被清理,这样一来就会出现key为null的value。ThreadLocalMap
实现中已经考虑了这种情况,在调用set()、get()、remove()方法的时候,会清理掉key为null的记录。如果说会出现内存泄漏,那只有在出现了key为null的记录后,没有手动调用remove()方法,并且之后也不再调用get()、set()、remove()方法的情况下。