线程是Java学习过程中比较难理解的一part,所以要好好打下基础,之后也会对juc包等其他并发编知识去做一个具体的原理性的学习。
一些概念
一、并发与并行
- 并发:同一个时间间隔内做很多件事情;并行:同一个时刻同时做多件事情。
- 其实对于这句话可以这样理解:并发是两个任务可以在重叠的时间段内启动、运行和完成。并行是任务在同一时间运行,例如,在多核处理器上,并发是独立执行过程的组合,而并行是同时执行的。并发更像是操作系统用线程模型抽象之后站在线程的角度上看到的任务的”同时“执行。
二、临界区
- 表示一种公有区域或者公有数据,但是每一次只有一个线程使用,其他线程想使用必须等待。进程在访问资源的时候必须经过这些步骤:
【进程】–>【进入区(申请资源)】–>【临界区】–>【退出区(释放资源)】 在进入区中资源如果被占用访问,其他进入阻塞队列等待。 - 阻塞:一个线程占用了临界区资源,其他需要这个资源的线程在临界区中等待,导致这些线程挂起。
- 非阻塞:其他线程可以同时进入临界区,但保证公有数据不被改坏。
三、锁
- 死锁(DeadLock):线程之间互相等待释放资源
- 饥饿锁(strarvation):某一个线程或多个线程无法获取资源,导致一直无法执行
- 活锁(liveLock):可以想象为电梯遇到人,同时都往一个方向去给对方让出资源,是个动态的问题
四、并发级别
- 阻塞状态级别
- 非阻塞状态级别(这里面还分为三种)
(1) 无障碍。一种最弱的非阻塞调度,自由进出临界区。无竞争时要求有限步骤内完成操作;无竞争时直接进行回滚数据。
(2) 无锁。保证只有一个线程可以胜出访问临界资源。比如乐观锁(CAS)
(3) 无等待的,并发中最高级别,是无锁的,要求所有的线程都在有限步内完成,并且无饥饿的。比如:读线程和写线程,所有线程都是无等待的。比如CopyOnWriteArrayList写时写副本数据,读时共享读,线程之间是无等待的。
五、并行的两个定律
- 加速比:优化前系统耗时/优化后系统耗时。 说明增加CPU个数不一定增加加速比。
- 古斯塔夫森定律:只要有足够的并行化,那么加速比和CPU个数成正比
六、线程相关知识
线程和进程
- 进程是分配资源的基本单位,线程是CPU调度的基本单位。进程之间的资源是互相独立不可共享的,但是线程之间是可以共享父线程或者进程的资源的。进程之间切换要比线程之间切换消耗资源代价多很多。
线程的状态
新建状态(NEW):新创建了一个线程对象
可运行状态(RUNNABLE):线程对象创建之后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取cpu的使用权。
运行(running):可运行状态的线程获得了cpu时间片,执行程序代码。
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice转到运行(running)状态。其中阻塞的状态分三种:
(1)等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把线程放入到等待队列(waitting queue)中。
(2)同步阻塞:运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(3)其他阻塞:运行(running)的线程执行Thread.Sleep(long ms)或者join方法,或者发出了IO请求,JVM会把该线程置为阻塞状态。当sleep状态超时、join()等待线程终止或者超时、或者IO处理完毕之后,线程重新转入可运行(runnable)状态。死亡(dead):线程run()、main()方法执行结束,或者因一场退出了run()方法,则该线程结束生命周期,死亡的线程不可再次复生。
线程状态扭转的图:
可以看到:
(1)当调用new Threa()方法之后,线程就会处于新建状态。
(2)调用start()方法之后,线程会进入runnable状态,当操作系统选中之后给当前线程分配了时间片线程进入running状态。
(3)当run()方法、main()方法结束或者发生异常,线程会进入dead状态。
(4)当因为synchronize或者lock同步方法,线程没有获取到锁标识就会进入到锁池(lock pool)中等待;同样当调用o.wait()方法之后线程会进入等待队列中,这时会释放锁或者monitor,直到被其他线程的notify()方法或者notifyAll()方法唤醒。
(5)当调用Thread.yield()方法之后,会使线程从running状态转换到runnable状态再去和其他线程一起去竞争时间片资源,所以会出现调用yield()方法之后又重新竞争到了资源变成running状态。
(6)当调用了sleep()/join()方法之后,线程并不会释放锁或者monitor,而当sleep时间到了或者调用join()方法的线程执行完毕之后会继续进入running状态。注意其中线程的一些方法经常在面试中问到的问题:
1.sleep方法和wait方法的区别
(1)sleep是Thread类的方法,wait是Object类的方法
(2)调用sleep方法不会释放锁,wait方法会使线程释放当前的锁。
(3)wait方法必须别的线程执行notify/notifyAll()方法才能重新获取CPU执行时间。
2.join()方法的本质
join方法是把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如main线程中调用t.join()时候,main线程会获得线程对象t的锁,调用对象的wait方法(等待时间),直到该对象唤醒main线程,所以意味着main线程调用t.join()时,必须能够拿到线程t对象的锁。注意join()方法也是要捕捉异常的,关于join()方法的比较好的一篇文章:http://uule.iteye.com/blog/1101994
3.yield()方法yield()方法与sleep()方法类似,只是不能由用户指定暂停多长时间,并且yield()方法只能让同优先级的线程有执行的机会,yield()方法不会释放对象上的锁。
4.wait()和notify()、notifyAll()这三个方法用于协调多个线程对共享数据的存取,所以必须在synchronized中执行使用。synchronized关键字用于保护共享数据,阻止其他线程对共享数据的存取,可以用这三个方法去灵活控制。wait()方法使当前的线程暂停执行并释放对象锁标识,让其他线程可以进入synchronized数据块,当前线程被放入对象等待池中,只有锁标志等待池中线程能获取锁标志;如果锁标志等待池中没有线程,则notify()不起作用。notifyAll()方法则从对象等待池中移走所有等待那个对象的线程并放入锁标志等待池中。
wait()、notify()方法是Object类的方法,因为他们必须要标识它们操作线程的锁,而锁对象可能是任何对象,所有这里这两个方法是Object类的方法。
线程创建几种方式
继承Thread类
- 可以继承Thread实现其中的run()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public class NewThread2 extends Thread {
public NewThread2(String name) {
super(name);
}
@Override
public void run() {
while (!interrupted()) {// 这里的循环是当不被中断的时候 才执行
System.out.println(getName() + " 线程运行");
}
}
public static void main(String[] args) {
NewThread2 t1 = new NewThread2("first thread");
NewThread2 t2 = new NewThread2("second thread");
t1.setDaemon(true);// 后台(守护)线程会随着主线程结束也结束
t2.setDaemon(true);
t1.start();
t2.start();
// 中断不用stop()方法 已经过时
t1.interrupt();
t2.interrupt();
//
try {
// 让主线程sleep两秒
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
实现Runnable接口
其实看到Thread类也是实现了Runnable接口的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44package newthread;
/**
* created by zlj on 2018/5/31
* Runnable接口 创建线程
*/
public class NewThread implements Runnable {
@Override
public synchronized void run() {
while (true) {
try {
// Thread.sleep(1000);// 调用超时等待使得线程进入阻塞状态 到达时间后线程到达就绪状态
wait();// 线程通讯必须在同步代码块中 否则会报错IllegalMonitorStateException
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("自定义线程执行...");
}
}
public static void main(String[] args) {
NewThread newThread = new NewThread();
// 线程初始化
Thread thread = new Thread(newThread);// 构造函数是runnable接口参数
thread.start();// 调用start方法使得线程进入就绪状态
while (true) {
synchronized (newThread) {// 这里同步代码块中监视的是同步的对象 对应上边wait方法获取的是this对象
System.out.println("主线程执行...");
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
newThread.notifyAll();// notify方法必须在同步监视器中 否则会报错
}
}
}
}
实现Callable接口(线程可以有返回值和抛出异常)
1 | package newthread; |
线程池实现
1 | package newthread; |
Executors中提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
(1)public static ExecutorService newFixedThreadpPool(int nThreads) // 创建国定数目线程的线程池。
(2)public static ExecutorService newCachedThreadPool() // 创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有60秒钟未被使用的线程。
(3)public static ExecutorService newSingleThreadExecutor() // 创建一个单线程化的Executor
(4)public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) // 创建一个支持定时及周期性的任务执行线程池,多数情况可以用来替代Timer类。
但是其实在阿里的代码检查工具中,是不建议去使用这个工具类去调用线程池的,建议去手动的写自定义的线程池。
线程中的其他常见方法
方法 | 说明 |
---|---|
setPriority(int priority) | 设置线程的优先级 |
setDaemon(boolean on) | 设置是否为后台线程 |
interrupt() | 中断线程 |
isAlive() | 测试线程是否处于活动状态 |
- 守护线程和用户线程的区别:守护线程依赖于创建它的线程,而用户线程则不依赖。举个简单的例子:如果在main线程中创建了一个守护线程,当main方法允许完毕之后,守护线程也会随着消亡,而用户线程则不依赖会一直运行到完毕为止,在JVM中像垃圾回收线程就是守护线程。但是要注意,设置守护线程要在thread.start()方法之前,否则会报IllegalThreadStateException异常。不应该所有的线程都可以分配给Deamon线程来进行服务,比如读写操作或计算逻辑,因为在Deamon Thread没来得及进行操作时,虚拟机可能已经退出了。
停止线程的方法
- 使用退出标志,使线程正常退出。
- 使用stop方法终止线程(已过时不推荐)
- while判断 + interrupt方法终止线程。其中interrupt方法不会终止正在运行的线程,所以要加入一个判断去完成线程的优雅退出。
一些面试题
线程和进程有什么区别?
答:一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。Thread类的sleep()方法和对象的wait()方法都可以让线程暂停执行,它们有什么区别?
答:sleep()方法(休眠)是线程类(Thread)的静态方法,调用此方法会让当前线程暂停执行指定的时间,将执行机会(CPU)让给其他线程,但是对象的锁依然保持,因此休眠时间结束后会自动恢复(线程回到就绪状态)。wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。线程的sleep()方法和yield()方法有什么区别?
答:
① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会;yield()方法只会给相同优先级或更高优先级的线程以运行的机会;
② 线程执行sleep()方法后转入阻塞(blocked)状态,而执行yield()方法后转入就绪(ready)状态;
③ sleep()方法声明抛出InterruptedException,而yield()方法没有声明任何异常;
④ sleep()方法比yield()方法(跟操作系统CPU调度相关)具有更好的可移植性。请说出与线程同步以及线程调度相关的方法。
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
先写到这
关于线程的知识总结还有很多,后边关于并发编程还要更深入的理解,这里先上一张知识总结图吧。