简介——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