1 简述
1.1 线程机制
Java使用的是抢占式的线程机制,调度机制周期性地切换上下文,切换线程,从而为每个线程提供时间片。看似同时执行,其实是不停地切换。在这种机制下,一个线程的阻塞不会导致整个进程阻塞。
优先级较低的线程仅仅是执行的频率较低,不会得不到执行。
要实现线程行为,你必须显示地将一个任务(Runnable)附着到线程(Thread)上。Thread类只是驱动赋予它的任务Runnable。
1.2 阻塞(se第四声)
程序中一个任务因为该程序控制范围外的因素(条件通常是IO),而不能继续执行,这个任务线程被阻塞了。
2 Executor 线程池
用new Thread()的方式不利于性能优化,所以采用线程池替代,达到线程的复用。首选CachedThreadPool回收旧线程停止创建新线程。
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new Runnable() {
@Override
public void run() {
}
});
Runnable设返回值,需要实现Callable接口call()方法,并且必须使用ExecutorService.submit()方法调用。
使用 Future.get() 获取返回值。
例如:
public class CallableAndFuture {
static class MyThread implements Callable<String> {
@Override
public String call() throws Exception {
return "Hello world";
}
}
static class MyThread2 implements Runnable {
@Override
public void run() {
}
}
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor();
Future<String> future = threadPool.submit(new MyThread());
try {
System.out.println(future.get());
} catch (Exception e) {
} finally {
threadPool.shutdown();
}
}
}
3 线程安全
3.1 实质
线程安全其实就是解决共享资源竞争的问题。
解决方法是:当一个资源被一个任务使用时,加锁。
解锁的时候,下一个要使用资源的任务并没有按照排队的方式按次序来获取,而是通过竞争获取资源。可以通过yield()和setPriority()来给线程调度器提供建议,这效果取决于具体平台和JVM实现。
3.2 synchronized加锁
对于某个特定对象,其所有的synchronized方法共享同一个锁。
一个任务可以多次获得对象的锁。
判断是否应该加锁:如果你正在写一个变量,这个变量接下来将被另一个线程读取;你正在读一个上一次已经被另一个线程写过的变量。以上两种情况都必须使用同步。并且,读写线程都必须用相同的监视器锁同步。
3.3 使用显式的Lock对象加锁
除了synchronized方式加锁之外,还可以用Lock对象:
private Lock lock = new ReentrantLock();//重入锁
......
lock.lock();
try{
......
return ...;//在try中return确保unlock()不会过早发生
}finally{
lock.unlock();
}
和synchronized的区别:tryLock()
- 可以尝试着获取锁,最终获取失败。
- 可以尝试着获取锁一段时间,然后放弃获取。
如果其他的线程已经获取了这个锁,你可以决定离开,去执行其他一些事,而不是等待直至这个锁释放。提供了比Synchronized更细粒度的控制力。(例如可以释放当前锁之前,捕获下一节点的锁)
4 volatile
volatile关键字主要提供2种作用:
- 可见性:当使用volatile关键字去修饰变量的时候,所有线程都会直接读取该变量并且不缓存它。这就确保了线程读取到的变量是同内存中是一致的
- 禁止指令重排序:应用在标志位
5线程本地存储
防止共享资源产生冲突的第二种方式是根除变量的共享,使用ThreadLocal。
6 终结任务
线程的各个状态
停止线程主要使用标志位,在run()方法中加以判断,是否要执行。
- 自己定义一个volatile boolean的标志位
- 使用interrupt()方法,但是这个方法也是打个停止的标志,并不是真正的停止线程,还是要在run()方法中调用Thread.interrupted()方法判断。
7 线程之间的协作
yield()方法:调用的线程放弃cpu时间让给别的线程。
join()方法:调用的线程先执行。
7.1 wait()
wait()是object的方法,它会释放锁,而sleep()和yield()不会释放。
因此在该对象(未锁定的)中的其他synchronized方法可以在wait()期间被调用。
只能在同步控制方法或者同步控制块里调用wait()、notify()、notifyAll(),因为要操作对象的锁。
wait()必须用一个检查感兴趣的条件的while循环包围,本质是检查感兴趣的条件,并在条件不满足的情况下返回到wait()方法中。
Thread1:
synchronized(sharedMonitor){
<setup condition for Thread2>
sharedMonitor.notify();
}
//错误的使用:时机太晚,错失信号,盲目进入wait(),产生死锁。
Thread2:
while(someConditon){
synchronized(sharedMonitor){
sharedMonitor.wait();
}
}
//正确的使用:防止在someCondition变量上产生竞争条件
Thread2:
synchronized(sharedMonitor){
while(someConditon){
sharedMonitor.wait();
}
}
7.2 notify()和notifyAll()
notify():在众多等待同一个锁的线程中,只有一个会被唤醒去获取锁。
notifyAll():在众多等待同一个锁的线程中,全部会被唤醒去共同竞争锁。
两个同样最终只有一个线程能获取到锁。
可能有多个线程在某单个对象上处于wait状态,因此调用notifyAll()比notify()更安全。
只有一个线程实际处于wait()状态,这时可以用notify()代替notifyAll()优化;
如果有多个线程在等待不同条件,使用notify()就不能知道是否唤醒了恰当的任务。
8 死锁
某个线程在等待另一个线程,而后者又在等待别的线程,这样一直下去,直到这个链上的线程又在等待第一个线程释放锁。这得到了一个状态:线程之间相互等待的连续循环,没有哪个线程能继续。这被称为死锁
死锁的4个条件:
- 互斥条件(资源的特点):线程使用的资源中至少有一个是不能共享的。
- 占用请求条件(单个线程的特点):至少有一个线程,它持有一个资源,并且等待获取另一个当前被别的线程持有的资源。
- 不可剥夺条件(单个线程的特点):已持有的资源不能被别的线程抢占,而它(持有资源的线程)自己也不会主动去抢别的线程的资源。
- 循环等待条件(多个线程形成的特点):必须有循环等待。
解决:让一方先放弃资源作出让步。