简介——Java中的线程
进程与线程
进程
进程是程序的一次执行过程, 是系统运行程序的基本单位。
进程和程序之间的区别在于程序是静态的,而只有动态执行着的程序才成为进程。
结合OS的内容来理解会更加深刻,进程是资源分配的基本单位。
对于每一个进程,操作系统管理对应的进程控制块,并为进程分配了独立的页表。进程的运行是将代码按照结构加载到内存中,静态程序本身是按照对应的段和节的结构组织起来的。
线程
线程是比进程更小的执行单位,一个进程中可以包含多个线程。
一个进程中的所有线程都在该进程的虚拟地址空间中,使用该进程的全局变量和系统资源。
线程有独立的栈空间,但是与进程共享一个页表。
Java线程机制
每个线程有独立的程序计数器和方法调用栈。
程序计数器指示当前正在执行的线程的行号,通过此计数器完成分支、循环、跳转、异常处理、线程恢复等基础功能。
方法调用栈跟踪方法调用过程,栈帧中保存局部变量和数据等。
线程调度
主线程
main()称之为主线程,每个java程序都有一个缺省的主线程。
如果main中创建了其他线程,java程序需要在线程之间轮流切换。
仅仅主线程结束,JVM并不会结束java程序,而是等到所有线程都结束。
调度策略:抢占式
当线程获得执行权,将会持续运行直至结束或阻塞。
注意:区别于通常操作系统调度的时间片轮转算法。
优先级
优先级分为1-10级,数值越大优先级越高
Thread 类定义的 3 个常数:
MIN_PRIORITY最低(小)优先级(值为1)MAX_PRIORITY最高(大)优先级(值为10)NORM_PRIORITY默认优先级(值为5)
1
2
getPriority(); //获得线程的优先级
setPriority(int x); //设置线程的优先级
修改线程的优先级不一定有用,看系统和JVM的具体实现。
线程状态
新建
1
Thread myThread=new Thread();
此时线程已经有了相应的内存空间和其他资源。
就绪
1
myThread.start();
一旦轮到它来享用 CPU 资源时,就可以脱离创建它的主线程,开始自己的生命周期。
运行
当线程对象被调用执行时,它将自动调用本对象的 run() 方法,从第一句开始顺序执行。
阻塞
常见的阻塞状况:
- 等待IO:IO操作结束即可回到就绪态
- 调用
sleep()方法 :等待对应休眠事件结束 - 调用了
wait()方法:使用notify()或notifyAll()
终止
除了自然终止之外,还存在stop() 或 destroy() 方法终止线程的情况
tips:
结合OS:运行态可以变成阻塞态,阻塞态不能变成运行态,就绪和运行可以互相切换。
线程实现
继承Thread类
基本套路
- 自定义线程类继承Thread类
- 重写
run()方法 - 调用创建线程对象,调用
start()开启线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class TestThread extends Thread{
@Override
public void run(){
//子线程
for(int i = 0;i < 2000;i++){
System.out.println("——这里是子线程");
}
}
public static void main(){
//创建线程对象
TestThread thread = new TestThread();
//开启线程
thread.start();
for(int i = 0;i < 2000;i++){
System.out.println("这里是主线程");
}
}
}
主函数中创建对象——开启线程——与主函数并发
期望输出:
这里是主线程
——这里是子线程
多行交替执行
优点:
- 编码简单,上手快
- 可以在子类中增加新的成员变量或者新方法
- 可以直接使用this指针
缺点:
- 不能再扩展(继承)其他的类
应用:多线程下载图片
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
public class TestThread extends Thread{
private String url;
private String name;
public TestThread(String url,String name){
this.url = url;
this.name = name;
}
@Override
public void run(){
WebDownloader downloader = new WebDownloader();
downloader.Downloader(this.url,this.name);
System.out.println("下载" + this.name + "完成");
}
public static void main(){
//创建线程对象,此处用...代替链接(可输入实际url)
TestThread thread1 = new TestThread("...1.jpg","1.jpg");
TestThread thread2 = new TestThread("...1.jpg","2.jpg");
TestThread thread3 = new TestThread("...1.jpg","3.jpg");
//开启线程
thread1.start();
thread2.start();
thread3.start();
}
}
class WebDownloader{
public void Downloader(String url,String name){
try{
//FileUtils文件读写操作工具类
FileUtils.copyURLToFile(new URL(url),new File(name));
}catch(Exception e){
e.printStackTrace();
System.out.println("IO异常");
}
}
}
多线程下载,避免了单一线程下载频繁等待的缺陷
实现Runnable接口
基本套路
- 实现一个Runnable接口类
new Thread(Runnable target)- 调用创建线程对象,调用
start()开启线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class TestTarget implements Runnable{
@Override
public void run(){
//子线程
for(int i = 0;i < 2000;i++){
System.out.println("——这里是子线程");
}
}
public static void main(){
//创建target对象
TestTarget target = new TestTarget();
//创建线程对象
Thread thread = new TestThread(target);
//开启线程
thread.start();
for(int i = 0;i < 2000;i++){
System.out.println("这里是主线程");
}
}
}
优点:
-
线程类还可以继承其他的类
-
实现接口的线程对象还可以用来创建多个线程,可以实现资源共享
关于资源共享:对于
Thread(Runnable target)构造方法创建的线程多个线程,如果都是基于同一个目标对象target创建,因此该目标对象的成员变量就会使这些线程共享的数据单元
缺点:
- 不能使用this指针
实现Callable接口
基本套路
1、实现Callable接口(以下范例:TestCallable,返回值boolean),需要返回值类型
2、重写Call方法,需要抛出异常
3、创建目标对象:TestCallable t1 = new TestCallable(...)
4、创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1)
5、提交执行:Future<Boolean> result1 = ser.submit(t1)
6、获取结果:Boolean r1 = result.get()
7、关闭服务:ser.shutdownNow()
对比Callable与Runnable
1、Callable 使用 call()方法, Runnable 使用 run() 方法
2、Callable的任务执行后可返回值,而Runnable的任务不能有返回值(是void)
3、call()可以抛出受检查的异常,而run()不能抛出受检查的异常。
4、运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。 通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程方法
线程休眠sleep
基本使用
1、sleep(n)指定当前线程阻塞的毫秒数为n
2、sleep 存在异常 InterruptedExcetion
3、sleep 时间结束后,线程进入就绪状态
4、sleep 可以模拟网络延迟,或进行倒计时
5、sleep 期间不会释放锁(后续线程通信时的wait方法可以)
线程调度yield
基本使用
让当前正在执行的线程暂停,但不阻塞
将线程从运行状态转为就绪状态
yield不一定有用,具体看cpu的执行
区别yield与sleep
sleep()使线程转入阻塞状态,而yield()使线程转 入runnable状态yield()给相同优先级或更高的线程运行机会,而sleep()不会考虑线程的优先级sleep()会有中断异常抛出,而yield()不抛出任何异常
线程合并join
使当前正在运行的线程暂停下来, 等待指定的时间后或等待调用该方法的线程结束后,再恢复运行
join可以类比成插队
守护线程
1、通过 thread.setDaemon(true) 设置线程为守护线程
2、线程分为用户线程和守护线程
3、虚拟机必须确保用户线程执行完毕
4、虚拟机不必等待守护线程执行完毕
对于后台记录操作日志,一些监控机制等操作,使用守护线程做一些不是很严格的操作,线程的随时结束不会产生什么不良后果
线程同步
当多个线程同时访问同一个变量,并且一些线程需要修改这个变量,程序应当对这样的问题进行处理
本学期的多门课中都涉及到的相同的思想:
OS进程的同步互斥问题与信号量管程机制
数据库并发:丢失修改、不可重复读、读脏数据问题与三级锁协议
synchronized基本操作
加锁对象
- 成员方法由synchronized修饰
public synchronized void write(); - 静态方法由synchronized修饰
public static synchronized int getValue(); - 语句块由synchronized对象锁定
synchronized (obj) {… … }
误区说明
synchronized锁住的都是对象而非代码,通过方法/语句块锁住对象,实例方法即实例对象,类方法即类对象
区别于OS中临界区的概念:临界区指的是一段代码,同一时刻只能由一方访问
使用示例
买票服务(不加锁非安全状态)
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
public class UnsafeBuyTicket{
public static void main(String args[]){
BuyTicket station = new BuyTicket();
new Thread(station,"老王").start();
new Thread(station,"老李").start();
new Thread(station,"小刘").start();
}
}
class BuyTicket implements Runnable{
private int ticketNum = 5;
boolean flag = true;
@Override
public void run(){
while(flag){
try{
buy();
}catch(Exception e){
e.printStackTrace();
}
}
}
private void buy() throws InterruptedException{
if(ticketNum <= 0){
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNum-- + "号票");
}
}
这里买票的三个线程同时购票,就会出现在ticketNum > 0时同时进入buy()方法并通过了if判断,此后一个线程购票使得ticketNum = 0,但是其他线程仍然可以继续购票
执行结果(顺序不一定)
老李拿到3号票 小刘拿到5号票 老王拿到4号票 老李拿到2号票 小刘拿到1号票 老王拿到0号票 老李拿到-1号票
买票服务加锁
只需要用synchronized修饰buy()方法即可
1
2
3
4
5
6
7
8
private synchronized void buy() throws InterruptedException{
if(ticketNum < 0){
flag = false;
return;
}
Thread.sleep(100);
System.out.println(Thread.currentThread().getName() + "拿到" + ticketNum-- + "号票");
}
执行结果(顺序不一定)
老王拿到5号票 老王拿到4号票 老王拿到3号票 小刘拿到2号票 小刘拿到1号票
使用注意
引入锁机制synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,存在以下问题:
- 一个线程持有锁导致其他所有需要此锁的线程挂起
- 多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时
- 高优先级线程等待低优先级线程释放锁,出现优先级倒挂
synchronized的粒度要尽量细,不要给一个大方法直接修饰synchronized,严重影响效率
线程通信
在多线程任务中,某一个线程的执行结果是另一个线程需要的资源,这样,我们就需要使两个线程的执行顺序做调整,并且使其资源能够流通。多个线程在操作同一份数据时,避免对统一共享变量的争夺,这时需要通过一定的手段使各个线程能有效的利用资源。
主要方法
调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)
wait():使当前线程进入阻塞状态,并释放同步监视器notify():唤醒被wait()的一个线程,若有多个线程,则唤醒优先级高的notifyAll():唤醒所有被wait()的线程。
经典案例
三个线程分别输出ABC,交替输出各十次
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
44
45
46
47
public class ThreadPrint {
public static void main(String[] args) throws InterruptedException{
Object a=new Object();
Object b=new Object();
Object c=new Object();
Thread8 threadA=new Thread8("A",c,a);
Thread8 threadB=new Thread8("B",a,b);
Thread8 threadC=new Thread8("C",b,c);
new Thread(threadA).start();
Thread.sleep(100);
new Thread(threadB).start();
Thread.sleep(100);
new Thread(threadC).start();
Thread.sleep(100);
}
}
class Thread8 implements Runnable{
private String name;
private Object prev;
private Object self;
public Thread8(String name,Object prev,Object self){
this.name=name;
this.prev=prev;
this.self=self;
}
@Override
public void run(){
int count=10;
while(count>0){
synchronized (prev){
synchronized (self){
System.out.print(name);
count--;
self.notify();
}
try{
if(count==0)
break;
else
prev.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
本案例中的思想:通过3个对象a,b,c建立起彼此间的制约关系,每一个对象都是下一个对象的前驱,从而保证了彼此间的交替执行
线程池
背景
线程如果经常创建和销毁,对于使用量很大的资源,比如并发情况下的线程,对性能影响很大
可以提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,避免频繁创建销毁,实现重复利用
好处:
- 提高响应速度(减少了创建新线程的时间)
- 降低资源消耗(无需每次都创建线程)
- 便于线程管理
使用方法
JDK5.0起提供了线程池相关API:ExecutorService和Executors
ExecutorService:真正的线程池接口,常见子类ThreadPoolExecutor
void execute(Runnable command);执行任务/命令,无返回值<T>Future<T>submit(Callable<T> task);执行任务,有返回值void shutdown();关闭线程池
Executors:工具类,用于创建并返回不同类型的线程池
使用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestPool {
public static void main(String[] args){
//参数 nThread 即线程池大小
ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new TestThread());
service.execute(new TestThread());
service.execute(new TestThread());
service.execute(new TestThread());
service.shutdown();
}
}
class TestThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName());
}
}
执行结果(顺序不一定)
pool-1-thread-1 pool-1-thread-3 pool-1-thread-4 pool-1-thread-2