进程与线程
进程与线程
什么是进程
计算机中独立运行的应用程序。
所谓进程(process)就是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。进程中所包含的一个或多个执行单元为线程(thread)。进程中还拥有一个私有的虚拟地址空间,该空间仅能被所有包含的线程访问。
线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的进程。操作系统中有若干个线程在“同时”运行。通常,操作系统上运行的每一个应用程序都运行在一个进程中。
进程并不是真正意义上的同时运行,而是并发运行。
什么是线程
**一个线程是一个进程的顺序执行流。同类的多个线程共享一块内空间和一组系统资源,线程本身有一个供程序执行时的堆栈。**线程在切换时负荷小,因此,线程也被称为轻负荷进程。一个进程中可以包含多个线程。
线程就是进程中的一个或多个执行流。如果一个进程序列包含多个线程,我们就说这个程序是多个线程的。
切换——线程并发时的一种现象。
进程与线程的区别
进程与线程最大的区别:资源共享。
- 其区别主要包括:
- 一个进程至少包含一个线程。线程的规划尺度小于进程,使得多个线程程序的并发性高。
- 线程在执行过程中与进程的区别在于每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口
- 从逻辑上看,多线程的意义在于一个应用程序中,有多个执行部分可以“同时”执行。
并发原理
多个线程或进程“同时”运行只是人为感官上的理解。
进程和线程是并发运行的,OS的时间调度机制将时间划分为很多时间片段(时间片),尽可能均匀分配给正在运行的程序,获取CPU时间片的线程或进程得以被执行,其他则等待。而CPU则在这些进程或线程上来回切换运行。微观上所有的进程和线程是走走停停的,宏观上都在运行,这种都运行的现象叫并发,但是不是绝对意义上的“同时发生”。
注:
- 之所以这样做是因为CPU只有一个,同一时间只能做一件事情。但随着计算机的发展,出现了多核心CPU。(例如两核心的CPU可以实现真正意义上的两个线程同时运行,但因为CPU的时间片段分配给那个线程或进程是由线程调度决定,所以不一定两个线程是属于同一个进程的,无论如何线程或进程是并发运行的)
- OS:Operating System 操作系统
- 线程调度机制是OS提供的一个用于并发处理的程序。Java虚拟机自身也提供了线程调度机制,用于减轻OS切换线程带来的更多负担。
线程状态
对于程序而言,我们实际上关心的是线程而非进程。
- 线程在其生命周期中的各个状态:
New
(新建状态):当创建一个线程时,该线程并没有纳入线程调度,其处于一个new状态。Runnable
(就绪状态):当调用线程start方法后,该线程将纳入线程调度的控制,其处于一个可运行状态,等待分配时间片段以并发运行。Running
(运行状态):当该线程分配到了时间片段后被CPU运行,这时线程处于running状态。Blocked
(阻塞状态):当线程在运行过程中可能会出现阻塞状态,比如等待用户输入信息等。但阻塞状态不是百分百出现的,具体要看代码中是否有相关需求。Dead
(死亡状态):当线程的任务全部运行完毕,或在运行过程中抛出一个未捕获的异常,那么线程结束,等待GC回收。
创建线程
使用Thread创建并启动线程
java.lang.Thread类是线程类,其每一个实例表示一个可以并发运行的线程。我们通过继承该类并重写run方法来定义一个具体的线程。其中重写run方法的目的是定义该线程要执行的逻辑。启动线程时调用线程的start()方法而非直接调用run() 方法。start()方法会将当前线程纳入线程调度,适当的线程可以并发运行。当线程获取时间片段后自动开始执行run方法中的逻辑。
public class ThreadTest extends Thread{
//使用Thread创建线程,需要继承Thread
//创建和启动线程:
public static void main(String[] args) {
ThreadTest th = new ThreadTest();
th.start(); //启动线程
}
//重写run方法
public void run() {
for(int i=0; i<10; i++) {
System.out.println("我是线程");
}
}
}
使用Runnable创建并启动线程
实现Runnable接口并重写run方法来定义线程体,在创建线程的时候将Runnable的实例传入并启动线程。
这样做的好处在于可以将线程要执行的任务分离开减少耦合,同时Java单单继承的,定义一个类实现Runnable接口这样的做法可以更好的去实现其他父类或接口,因为接口是多继承关系。
public class RunnableTest implements Runnable{
public static void main(String[] args) {
RunnableTest rt = new RunnableTest();
//重写了run()方法,而其是Thread的方法
Thread th = new Thread(rt);
th.start(); //启动线程
}
//重写run()方法
public void run() {
for(int i=0; i<10; i++) {
System.out.println("我是Runnable!");
}
}
}
使用内部类创建线程
通常我们可以通过匿名内部类的方式创建线程,使用该方法可以简化编写代码的复杂度,当一个线程仅需要一个实例时我们通常使用这种方式来创建。
-
例如:
-
继承Thread方式:
public void test1(){ Thread t = new Thread() { public void run() { for(int i=0; i<10; i++) System.out.println("我是Thread匿名类"); } }; t.start(); }
-
继承Runnable方式:
public void test2() { Runnable r = new Runnable() { public void run() { for(int i=0; i<10; i++) System.out.println("我是Runnable匿名类"); } }; Thread t = new Thread(r); t.start(); }
-
线程操作
Thread.currentThread方法
Thread的静态方法currentThread方法可以用来获取运行当前代码片段的线程。
-
其语法格式为:
Thread th = Thread.currentThread();
获取线程信息
- Thread提供了获取线程信息的相关方法:
long getId();
返回该线程的标识符String getName();
返回该线程的名称int getPriority();
返回该线程的优先级Thread.state getState();
获取线程的状态boolean isAlive();
测试线程是否处于活动状态boolean isDaemon();
测试线程是否为守护线程boolean isInterrupted();
测试线程是否已经中断
public class CurrentThreadTest {
public static void main(String[] args) {
Thread th = Thread.currentThread();
System.out.println("运行main方法"+Thread.currentThread());
System.out.println(th.getId());//返回该线程的标识符
System.out.println(th.getName());//返回该线程的名称
System.out.println(th.getPriority());//返回该线程的优先级
Thread.State state = th.getState();//获取该线程的状态
System.out.println(state);
System.out.println(th.isAlive());//线程是否处于活动状态
System.out.println(th.isDaemon());//测试线程是否为守护线程
System.out.println(th.isInterrupted());//测试线程是否中断
}
线程的优先级
线程的切换是由线程调度控制的,虽然无法通过代码干涉,但是可以提高线程的优先级来最大程度的改善线程获取时间片的几率。
线程优先级被划分为10级,值分别为1~10,其中1最低、10最高。
- 线程提供了三个常量来表示最低、最高以及默认优先级:
- Thread.Min_PRIORITY
- Thread.Max_PRIORITY
- Thread.NORM_PRIORITY
- 设置优先级的方法:
void setPriority(int priority);
守护线程
守护线程(后台线程)与普通线程在表现上没有区别,其特点是当那个线程中只剩下守护线程时,所有守护线程强行终止。
-
其语法格式为:
void setDaemon(boolean b);
当参数为true时该线程为守护线程
设置线程为后台线程要在该线程启动之前设置。
Thread daemonThread = new Thread();
daemonThread.setDaemon(true);
daemonThread.start();
sleep方法
Thread的静态方法sleep用于使当前线程进入阻塞状态。
- 其语法格式为:
static void sleep(long ms);
该方法会使当前线程进入阻塞状态指定毫秒,当指定毫秒阻塞后,当前线程会重新进入Runnable状态,等待分配时间片。
该方法声明抛出一个InterruptException。所以使用该方法时需要捕获这个异常。
//示例
public static void main(String[] args) {
Thread th = new Thread() {
public void run() {
while(true) {
System.out.println("守护线程");
try {
Thread.sleep(500);
}catch(Exception e){
e.printStackTrace();//在日志中输出
}
}
}
};
th.setDaemon(true);
th.start();
try {
Thread.sleep(1000);//该方法是一个阻塞方法
//当指定的时间过后,当前线程会重新进入Runnable状态,等待分配时间片
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("main线程结束了");
}
yield方法
Thread的静态方法yield方法用于使当前线程主动让出当前CPU时间片回到Runnable状态,等待分配时间片。
- 其语法格式为:static void yield();
join方法
Thread的方法join方法用于等待当前进程结束,此方法是一个阻塞方法。
-
其语法格式为:void join();
该方法声明抛出InterruptException。
//示例
public class DownloadPhoto {
public static void main(String[] args) {
Thread t1 = new Thread() {
public void run() {
for(int i=0; i<=10 ; i++) {
System.out.println("t1正在下载图片:"+i*10+"%");
try {
Thread.sleep(1000);
}catch(Exception e) {
e.getStackTrace();
}
}
System.out.println("下载完毕");
};
};
Thread t2 = new Thread() {
public void run() {
System.out.println("等待图片下载完毕");
try {
t1.join();
}catch(Exception e) {
e.getStackTrace();
}
System.out.println("打开图片");
};
};
t1.start();
t2.start();
}
}
线程同步
synchronized关键字
ArrayList是非线程同步的。多个线程并发读写同一个临界资源时会发生“线程并发安全问题”。
- 常见的临界资源:
- 多线程共享实例变量
- 多线程共享静态公共变量
若想解决线程安全问题,需要将异步的操作变为同步操作。
-
同步与异步
所谓异步操作是指多线程并发的操作,相当于各干各的
所谓同步操作是指有先后顺序的操作,相当于你干完我再干
Java中有一个关键字名为synchronized,该关键字是同步锁,用于将某段代码变为同步操作,从而解决线程并发安全问题。
锁机制
Java提供了一种内置的锁机制来支持原子性:同步代码块(synchronized关键字)
同步代码块包含两部分:一个作为锁的对象的引用,一个作为由这个锁保护的代码块。
-
其语法格式为:
synchronized(同步监视器——锁对象的引用){ ... //代码块 }
若方法所有代码都需要同步也可以给方法直接加锁。
注:每个Java对象都可以用作一个实现同步的锁,线程静如同步代码块之前会自动获得锁,并且在退出同步代码块时释放锁,而且无论是通过正常路径退出锁还是通过抛出异常退出都一样,获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
//模拟售票
public class Dell implements Runnable{
//模拟100张票
private int tick = 100;
//定义一把锁
private Object obj = new Object();
@Override
public void run() {
// TODO Auto-generated method stub
while(true) {
//假设t1抢到时间片,t1进来
//t2又抢到时间片,t2要进来发现又锁则等待
synchronized(obj) {
if(tick>0) {
try {
Thread.sleep(300);
}catch(Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"正在出售第 "+(tick--)+" 张票");
}
}
}
}
}
//程序测试
public class DellTest {
public static void main(String[] args) {
Dell d = new Dell();
Thread t1 = new Thread(d);
Thread t2 = new Thread(d);
Thread t3 = new Thread(d);
t1.start();
t2.start();
t3.start();
}
}
选择合适的锁对象/范围
选择合适的锁对象
使用synchronized需要对一个锁对象上锁保证线程同步。
这个锁对象应当注意多个需要同步的线程在访问该同步块时,看到的应该是同一个锁对象的引用,否则达不到同步的效果。通常我们会使用this来作为锁对象。
选择合适的锁范围
在使用同步块时,应当尽量在允许的情况下减少同步范围,以提高并发的执行效率。
静态方法锁
当我们对一个静态方法加锁,如:public synchronized static void XXX(){...}
那么该方法锁的对象时类对象。每个类都有唯一的一个类对象。获取对象的方式:ClassName.class;
静态方法与非静态方法同时声明了synchronized,他们之间时非互斥关系的。原因在于,静态方法锁是类对象而非静态方法锁是当前方法所属对象。
线程安全API与非线程安全API
StringBuffer 是同步的 synchronized append(); 安全的
StringBuilder 不是同步的 append();
相对而言StringBuffer在处理上稍逊于StringBuilder,但是其是线程安全的。当不存在并发时首选应当使用StringBuilder。
线程池
线程定义
当一个程序中若创建大量线程,并在任务结束后销毁,会给系统带来过度消耗资源,以及过度切换线程的危险,从而可能导致系统崩溃。为此我们应使用线程池来解决这个问题。
- 线程池有两个主要作用:
- 控制线程数量
- 重用线程
线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。
在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
线程池的管理
ExecutorService是java提供的用于管理线程池的类。
- 线程池有以下几种实现策略:
- Executors.newCachedThreadPool()
创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。 - Executors.newFixedThreadPool(int nThreads)
创建一个可重用固定线程集合的线程池,以共享的无界队列方式来运行这些线 程。 - Executors.newScheduledThreadPool(int corePoolSize)
创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 - Executors.newSingleThreadExecutor()
创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。
- Executors.newCachedThreadPool()
//创建具有30个线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(30);
Runnable r1 = new Runable(){
public void run(){
... //线程体
}
};
//将任务交给线程池,其会分配空闲线程来运行这个任务。
threadPool.execute(r1);