多线程基础
1.进程和线程
1.1 什么是进程
正在进行的程序。每一个进程执行都有一个执行顺序,该顺序是一个执行路径,或者叫一个控制单元。
所谓进程(process)就是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元称为线程(thread)。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。操作系统中有若干个线程在"同时"运行。通常,操作系统上运行的每一个应用程序都运行在一个进程中,例如:QQ,IE等等。
注:进程并不是真正意义上的同时运行,而是并发运行。后面我们会具体说明。
1.2. 什么是线程
进程内部的一条执行路径或者一个控制单元。
一个线程是进程的一个顺序执行流。同类的多个线程共享一块内存空间和一组系统资源,线程本身有一个供程序执行时的堆栈。线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。
注:切换——线程并发时的一种现象,后面讲解并发时会具体说明。
1.3. 进程与线程的区别
一个进程至少有一个线程。线程的划分尺度小于进程,使得多线程程序的并发性高。另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。
线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用来实现进程的调度和管理以及资源分配。
1.4. 线程使用的场合
线程通常用于在一个程序中需要同时完成多个任务的情况。我们可以将每个任务定义为一个线程,使他们得以一同工作。
例如我们在玩某个游戏时,这个游戏由操作系统运行,所以其运行在一个独立的进程中,而在游戏中我们会听到某些背景音乐,某个角色在移动,出现某些绚丽的动画效果等,这些在游戏中都是同时发生的,但实际上,播放音乐是在一个线程中独立完成的,移动某个角色,播放某些特效也都是在独立的线程中完成的。这些事情我们无法在单一线程中完成。
也可以用于在单一线程中可以完成,但是使用多线程可以更快的情况。比如下载文件。
比如迅雷,我们尝尝会开到它会打开很多个节点来同时下载一个文件。
1.5. 并发原理
实质: 多线程并发执行,看起来像是多个线程在同时执行,但实际上,某一时刻,一个CPU上之后一个线程在执行。
通过上面几节知识我们知道进程与线程都是并发运行的,那么什么是并发呢?
多个线程或进程”同时”运行只是我们感官上的一种表现。事实上进程和线程是并发运行的,OS的线程调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但是不是绝对意义上的“同时发生。
注1:之所以这样做是因为CPU只有一个,同一时间只能做一件事情。但随着计算机的发展,出现了多核心CPU,例如两核心的CPU可以实现真正意义上的2线程同时运行,但因为CPU的时间片段分配给那个进程或线程是由线程调度决定,所以不一定两个线程是属于同一个进程的,无论如何我们只需要知道线程或进程是并发运行即可。
注2:OS—Operating System我们称为:操作系统
注3:线程调度机制是OS提供的一个用于并发处理的程序。java虚拟机自身也提供了线程调度机制,用于减轻OS切换线程带来的更多负担。
1.5.1.什么是并发(concurrency单CPU)和并行(parallellism多CPU)
解释一:并行是指两个或者多个事件在同一时刻发生,所以无论从微观还是从宏观来看,二者都是一起执行的。
并发是指两个或多个事件在同一时间间隔发生,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的。
解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
解释三:并行是在多台处理器上同时处理多个任务。如 hadoop 分布式集群,并发是在一台处理器上“同时”处理多个任务。
所以并发编程的目标 是充分的利用处理器的每一个核,以达到最高的处理性能。
1.6. 线程状态
对于程序而言,我们实际上关心的是线程而非进程。通过上面学习的只是,我们了解了什么是线程以及并发的相关知识。那么我们来看看线程在其生命周期中的各个状态:
新建(New)、可运行(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)
状态:就绪,运行,synchronize阻塞,wait和sleep挂起,结束。wait必须在synchronized内部调用。
调用线程的start方法后线程进入就绪状态,线程调度系统将就绪状态的线程转为运行状态,遇到synchronized语句时,由运行状态转为阻塞,当synchronized获得锁后,由阻塞转为运行,在这种情况可以调用wait方法转为挂起状态,当线程关联的代码执行完后,线程变为结束状态。
- 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
- 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,该线程纳入线程调度的控制,其处于一个可运行状态,等待分配时间片段,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
- 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
- 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态,比如等待用户输入信息等。阻塞的情况分三种:
(01) 等待阻塞 – 通过调用线程的wait()方法,让线程等待某工作的完成。
(02) 同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
(03) 其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。 - 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期,等待GC回收。
2. 创建线程
2.1.多线程有几种实现方法?同步有几种实现方法?
多线程有两种实现方法,分别是继承Thread类与实现Runnable接口。
同步的实现方面有两种,分别是synchronized,wait与notify。
wait():使一个线程处于等待状态,并且释放所持有的对象的lock。
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常。
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级。
Allnotity():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。
2.2.当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?分几种情况:
1.其他方法前是否加了synchronized关键字,如果没加,则能。
2.如果这个方法内部调用了wait,则可以进入其他synchronized方法。
3.如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。
4.如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。
2.3. 使用Thread创建线并启动线程
java.lang.Thread类是线程类,其每一个实例表示一个可以并发运行的线程。我们可以通过继承该类并重写run方法来定义一个具体的线程。其中重写run方法的目的是定义该线程要执行的逻辑。启动线程时调用线程的start()方法而非直接调用run()方法。start()方法会将当前线程纳入线程调度,使当前线程可以开始并发运行。当线程获取时间片段后会自动开始执行run方法中的逻辑。
例如:
public class TestThread extends Thread{
public void run() {
for(int i=0;i<100;i++){
System.out.println("我是线程");
}
}
}
创建和启动线程:
…
Thread thread = new TestThread();//实例化线程
thread.start();//启动线程
…
当调用完start()方法后,run方法会很快执行起来。
2.4. 使用Runnable创建并启动线程
实现Runnable接口并重写run方法来定义线程体,然后在创建线程的时候将Runnable的实例传入并启动线程。
这样做的好处在于可以将线程与线程要执行的任务分离开减少耦合,同时java是单继承的,定义一个类实现Runnable接口这样的做法可以更好的去实现其他父类或接口。因为接口是多继承关系。
例如:
public class TestRunnable implements Runnable{
public void run() {
for(int i=0;i<100;i++){
System.out.println("我是线程");
}
}
}
启动线程的方法:
…
Runnable runnable = new TestRunnable();
//实例化线程并传入线程体
Thread thread = new Thread(runnable);
thread.start();//启动线程
…
2.5. 使用内部类创建线程
通常我们可以通过匿名内部类的方式创建线程,使用该方式可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这种方式来创建。
例如:
继承Thread方式:
Thread thread = new Thread(){//匿名类方式创建线程
public void run(){
//线程体
}
};
thread.start();//启动线程
Runnable方式:
Runnable runnable = new Runnable(){//匿名类方式创建线程
public void run(){
}
};
Thread thread = new Thread(runnable);
thread.start();//启动线程
2.6.启动一个线程是用run()还是start()?
启动一个线程是调用start()方法,使线程就绪状态,以后可以被调度为运行状态,一个线程必须关联一些具体的执行代码,run()方法只是 thread 的一个普通方法,调用 run 方法不能实现多线程。
3. 线程操作API
线程的api:
1. Thread.currentThread() -- 得到当前线程对象
2. getName() -- 获取线程名称
3. setName(String) -- 给线程起名
4. isAlive():boolean 判断线程是否是活着的状态
5. getPriority():int 优先级:1-10 ,5:默认的优先级 数值越大,优先级越大
1. 常量:MAX_PRIORITY:10 MIN_PRIORITY:1 NORM_PRIORITY:5
6. setPriority(int):void
7. sleep(long mills):调用该方法,线程进入阻塞状态,当指定时间结束,线程阻塞状态解除,进入就绪状态
8. yield():让当前线程暂停,调用该方法,线程进入就绪状态
9. join():实现线程间的同步执行(顺序执行),调用join方法之后,线程进入阻塞状态
10. interrupt():中断线程的阻塞状态,唤醒线程
1. 当线程处于运行状态时,调用该方法无效
2. 当线程处于阻塞状态,调用该方法,会唤醒阻塞的线程
11. isDeamon():boolean 判断当前线程是否是守护线程
12. setDeamon(boolean):设置线程为后台线程
附:后台线程和前台线程的区别?
1. 后台线程和前台线程在使用上完全一致,唯一的区别在于结束时机不同,前台线程执行完就结束,后台线程是在所有的前台线程执行结束后,才强制终止,进程结束。
2. java中典型的后台线程:GC
3.1. Thread.currentThread方法
Thread的静态方法currentThread方法可以用于获取运行当前代码片段的线程。
Thread current = Thread.currentThread();
3.2. 获取线程信息
*Thread提供了获取线程信息的相关方法:
long getId():返回该线程的标识符
String getName():返回该线程的名称
int getPriority():返回线程的优先级
Thread.state getState():获取线程的状态
boolean isAlive():测试线程是否处于活动状态
boolean isDaemon():测试线程是否为守护线程
boolean isInterrupted():测试线程是否已经中断
例如:
public class TestThread {
public static void main(String[] args) {
Thread current = Thread.currentThread();
long id = current.getId();
String name = current.getName();
int priority = current.getPriority();
Thread.State state = current.getState();
boolean isAlive = current.isAlive();
boolean isDaemon = current.isDaemon();
boolean isInterrupt = current.isInterrupted();
System.out.println("id:"+id);
System.out.println("name:"+name);
System.out.println("priority:"+priority);
System.out.println("state:"+state);
System.out.println("isAlive:"+isAlive);
System.out.println("isDaemon:"+isDaemon);
System.out.println("isInterrupt:"+isInterrupt);
}
}
3.3. 线程优先级
线程的切换是由线程调度控制的,我们无法通过代码来干涉,但是我们可以通过提高线程的优先级来最大程度的改善线程获取时间片的几率。
线程的优先级被划分为10级,值分别为1-10,其中1最低,10最高。线程提供了3个常量来表示最低,最高,以及默认优先级:
Thread.MIN_PRIORITY,
Thread.MAX_PRIORITY,
Thread.NORM_PRIORITY
设置优先级的方法为:
void setPriority(int priority)
3.4. 守护线程
守护线程与普通线程在表现上没有什么区别,我们只需要通过Thread提供的方法来设定即可:
(在 Java 中垃圾回收线程就是特殊的守护线程。)
void setDaemon(boolean )
当参数为true时该线程为守护线程。
守护线程的特点: 当进程中只剩下守护线程时,所有守护线程强制终止。
GC就是运行在一个守护线程上的。
需要注意的是,设置线程为后台线程要在 -该线程启动前设置。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
3.5. sleep方法(来自 Thread 类)
Thread的静态方法sleep用于使当前线程进入 -阻塞状态:
static void sleep(long ms)
该方法会使当前线程进入阻塞状态指定毫秒,当指定毫秒阻塞后,
当前线程会重新进入Runnable状态,等待分配时间片。
该方法声明抛出一个InterruptException。所以在使用该方法时需要捕获这个异常。
例如:电子钟程序
public static void main(String[] args) {
SimpleDateFormat sdf =
new SimpleDateFormat("hh:mm:ss");
while(true){
System.out.println(sdf.format(new Date()));
try {
//每输出一次时间后阻塞1秒钟
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
注:改程序可能会出现"跳秒"现象,因为阻塞一秒后线程并非是立刻回到
running状态,而是出于runnable状态,等待获取时间片。那么这段
等待时间就是"误差"。所以以上程序并非严格意义上的每隔一秒钟执行
一次输出。
3.6. yield方法
Thread的静态方法yield:
static void yield()
该方法用于使当前线程主动让出当次CPU时间片回到Runnable状态,
等待分配时间片。
3.7. join方法
Thread的方法join:
void join()
该方法用于等待当前线程结束。此方法 -是一个阻塞方法。
该方法声明抛出InterruptException。
例如:
public static void main(String[] args) {
final Thread t1 = new Thread(){
public void run(){
//一些耗时的操作
}
};
Thread t2 = new Thread(){
public void run(){
try {
//这里t2线程会开始阻塞,直到t1线程的run方法执行完毕
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//以下是当前线程的任务代码,只有t1线程运行完毕才会运行。
}
};
}
4. 线程同步
4.1. synchronized关键字
多个线程并发读写同一个临界资源时候会发生”线程并发安全问题”
常见的临界资源:
多线程共享实例变量
多线程共享静态公共变量
若想解决线程安全问题,需要将异步的操作变为同步操作。
所谓异步操作是指多线程并发的操作,相当于各干各的。
所谓同步操作是指有先后顺序的操作,相当于你干完我再干。
而java中有一个关键字名为:synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。
4.2. 锁机制
Java提供了一种内置的锁机制来支持原子性:
同步代码块(synchronized 关键字 ),同步代码块包含两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块。
synchronized (同步监视器—锁对象引用){
//代码块
}
若方法所有代码都需要同步也可以给方法直接加锁。
每个Java对象都可以用做一个实现同步的锁,线程进入同步代码块之前会自动获得锁,并且在退出同步代码块时释放锁,而且无论是通过正常路径退出锁还是通过抛异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
4.3. 选择合适的锁对象
使用synchroinzed需要对一个锁对象上锁以保证线程同步。
那么这个锁对象应当注意:多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象引用。否则达不到同步效果。 通常我们会使用this来作为锁对象。
4.4. 选择合适的锁范围
在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。
4.5. 静态方法锁
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){
….
}
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class
静态方法与非静态方法同时声明了synchronized,他们之间是非互斥关系的。原因在于,静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
4.6. wait和notify
多线程之间需要协调工作。
例如,浏览器的一个显示图片的 displayThread想要执行显示图片的任务,必须等待下载线程downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。
以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。
4.7. 线程安全API与非线程安全API
之前学习的API中就有设计为线程安全与非线程安全的类:
StringBuffer:是同步的 synchronized append();
StringBuilder:不是同步的 append();
相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。
同样的:
Vector 和 Hashtable 是线程安全的而
ArrayList 和 HashMap则不是线程安全的。
对于集合而言,Collections提供了几个静态方法,可以将集合或Map转换为线程安全的:
例如:
Collections.synchronizedList() :获取线程安全的List集合
Collections.synchronizedMap():获取线程安全的Map
List<String> list = new ArrayList<String>();
list.add("A");
list.add("B");
list.add("C");
//将ArrayList转换为线程安全的集合
list = Collections.synchronizedList(list);
//[A,B,C] 可以看出,原集合中的元素也得以保留
System.out.println(list);
...
4.8. 使用ExecutorService实现线程池
当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
ExecutorService是java提供的用于管理线程池的类。
线程池有两个主要作用:控制线程数量/重用线程
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
线程池有以下几种实现策略: 创建一个
Executors.newCachedThreadPool()
可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
Executors.newFixedThreadPool(int nThreads)
可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线程。
Executors.newScheduledThreadPool(int corePoolSize)
线程池,它可安排在给定延迟后运行命令或者定期地执行。
Executors.newSingleThreadExecutor()
使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
可以根据实际需求来使用某种线程池。
例如,创建一个有固定线程数量的线程池:
//创建具有30个线程的线程池
ExecutorService threadPool =
Executors.newFixedThreadPool(30);
Runnable r1 = new Runable(){
public void run(){
//线程体
}
};
//将任务交给线程池,其会分配空闲线程来运行这个任务。
threadPool.execute(r1);
...
4.9.线程池:
- 通常,服务器端接受的请求量很大(任务量大),即要创建的线程量很大,在服务端频繁的创建和销毁线程,以及创建大量的线程,对服务器的压力很大,降低了性能,消耗了大量的资源,所以通常服务器端会使用线程池。
- 原理:在线程池内部事先创建好一定量的线程对象,将来若有任务,调用任一空闲的线程对象去执行,执行结束,将该线程对象还回给线程池 ,重复利用线程对象。
- 优点:
- 实现了线程的重用
- 控制了线程的数量
- api:
- 创建线程池
- ExecutorService service = Executors.newFixedThreadPool(int nThread);
- 将任务提交给线程池
- service.execute(Runnable);
- 关闭线程池
- 通常任务应用的服务器一般都不会关闭,如果涉及到服务器升级,迁移,通常都是在后半夜用户活跃度低的时候进行的。
- service.shutdown() – 不再接受新的任务,将已经接受的任务执行结束后完毕线程池
- service.shutdownNow():List – 立即停止线程池,若有正在执行的任务,中断执行,若有已经接受但还没来得及执行的任务,返回给List集合。
- 创建线程池
5.0. 使用BlockingQueue
BlockingQueue是双缓冲队列。
在多线程并发时,若需要使用队列,我们可以使用Queue,但是要解决一个问题就是同步,但同步操作会降低并发对Queue操作的效率。
BlockingQueue内部使用两条队列,可允许两个线程同时向队列一个做存储,一个做取出操作。在保证并发安全的同时提高了队列的存取效率。
双缓冲队列有一下几种实现:
ArrayBlockingDeque:
规定大小的BlockingDeque,其构造函数必须带一个int参数来指明其大小.
其所含的对象是以FIFO(先入先出)顺序排序的。
LinkedBlockingDeque:
大小不定的BlockingDeque,若其构造函数带一个规定大小的参数,
生成的BlockingDeque有大小限制,若不带大小参数,所生成的BlockingDeque的
大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的。
PriorityBlockingDeque:
类似于LinkedBlockDeque,但其所含对象的排序不是FIFO,
而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序。
SynchronousQueue:
特殊的BlockingQueue,对其的操作必须是放和取交替完成的。
面试题:
当一个线程进入一个对象的一个synchronized方法后,其它线程是否可进入此对象的其它方法?
分几种情况:
1.其他方法前是否加了synchronized关键字,如果没加,则能。
2.如果这个方法内部调用了wait,则可以进入其他synchronized方法。
3.如果其他个方法都加了synchronized关键字,并且内部没有调用wait,则不能。
4.如果其他方法是static,它用的同步锁是当前类的字节码,与非静态的方法不能同步,因为非静态的方法用的是this。