多线程
- 认识线程
- 进程
- 线程
- 多线程的优势
- 编写线程类
- 使用Thread类创建线程。
- 使用Runnable接口创建线程
- 使用Callable接口创建线程
- 线程的状态
- 新生状态(New Thread)
- 可运行状态(Runnable)
- 阻塞状态(Blocked)
- 死亡状态(Dead)
- 线程调度
- 线程优先级
- 实现线程调度的方法
- join()方法
- sleep()方法
- yield()方法
- 实现线程同步
- 同步方法
- 同步代码块
- 线程安全类型
- 总结
认识线程
计算机的操作系统多采用多任务和分时设计,多任务是指在一个操作系统中可以同时运行多个程序,例如,可以在使用QQ聊天的同时听音乐,既有多个独立运行的任务,每个任务对应一个进程,每个进程又可产生多个线程。
进程
认识进程先从程序开始。程序是对数据描述与操作的代码的集合,如Office中的Word、暴风影音等应用程序。
进程是程序的一次动态执行过程,它对应了从代码加载、执行至执行完毕的一个完整过程,这个过程也是进程本身从产生、发展至消亡的过程。操作系统同时管理一个计算机系统中的多个进程,让计算机系统中的多个进程轮流使用CPU资源,或者共享操作系统的其他资源。
进程有如下特点:
进程是系统运行程序的基本单位。
每一个进程都有自己独立的一块内存空间、一组系统资源。
每一个进程的内部数据和状态都是完全独立的。
线程
线程是进程中执行运算的最小单位,-一个进程在其执行过程中可以产生多个线程,而线程必须在某个进程内执行。
线程是进程内部的一个执行单元, 是可完成一个独 立任务的顺序控制流程, 如果在一个进程中同时运行了多个线程,用来完成不同的工作,则称之为多线程。
线程按处理级别可以分为核心级线程和用户级线程。
(1)核心级线程
核心级线程是和系统任务相关的线程,它负责处理不同进程之间的多个线程。允许不同进程中的线程按照同一相对优先调度方法对线程进行调度,使它们有条不紊地工作,可以发挥多处理器的并发优势,以充分利用计算机的软/硬件资源。
(2)用户级线程
在开发程序时,由于程序的需要而编写的线程即用户级线程,这些线程的创建、执行和消亡都是在编写应用程序时进行控制的。对于用户级线程的切换,通常发生在一一个应用程序的诸多线程之间,如迅雷中的多线程下载就属于用户线程。
多线程可以改善用户体验。具有多个线程的进程能更好地表达和解决现实世界的具体问题,多线程是计算机应用开发和程序设计的一项重要的实用技术。
线程和进程既有联系又有区别,具体如下:
①一个进程中至少要有一个线程。
②资源分配给进程,同一进程的所有线程共享该进程的所有资源。
③处理机分配给线程,即真正在处理机上运行的是线程。
多线程的优势
多线程有着广泛的应用,下载工具“迅雷”是一款典型的多线程应用程序,在这个下载工具中,可以同时执行多个下载任务。这样不但能够加快下载的速度,减少等待时间,而且还能够充分利用网络和系统资源。
多线程的好处如下:
①多线程程序可以带来更好的用户体验,避免因程序执行过慢而导致出现计算机死机或者白屏的情况。
②多线程程序可以最大限度地提高计算机系统的利用效率,如迅雷的多线程下载。
编写线程类
每个程序至少自动拥有一个线程,称为主线程。当程序加载到内存时启动主线程。Java程序中的public static void main()方法是主线程的入口,运行Java程序时,会先执行这个方法。
开发中,用户编写的线程一般都是指除了主线程之外的其他线程。
使用一个线程的过程可以分为如下4个步骤:
(1)定义一一个线程,同时指明这个线程所要执行的代码,即期望完成的功能。
(2)创建线程对象。
(3)启动线程。
(4)终止线程。
定义一个线程类通常有两种方法,分别是继承java.lang.Thread类和实现java.lang.Runnable接口。
使用Thread类创建线程。
Java提供了java.lang.Thread类支持多线程编程,该类提供了大量的方法来控制和操作线程,常用方法如下所示:
方法 | 说明 |
void run() | 执行任务操作的方法 |
void start() | 使该线程开始执行 |
void sleep(long millis) | 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行) |
String getName() | 返回该线程的名称 |
int getPriority() | 返回线程的优先级 |
void setPriority(int newPriority) | 更改线程的优先级 |
Thread.State getState() | 返回该线程的状态 |
boolean isAlive() | 测试线程是否处于活动状态 |
void join() | 等待该线程终止 |
void interrupt() | 中断线程 |
void yield() | 暂停当前正在执行的线程对象,并执行其他线程 |
创建线程时继承Thread类并重写Thread类的run()方法。Thread类的run()方法是线程要执行操作任务的方法,所以线程要执行的操作代码都需要写在run()方法中,并通过调用start()方法来启动线程。
示例一:创建两个子线程,每个线程均输出20次消息数字、“你好”、线程名。
代码展示:
// An highlighted block
public class test2 extends Thread {
@Override
public void run() {
for (int i = 1; i <= 20; i++) {
System.out.println(i+".你好,来自线程"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
test2 t = new test2();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
输出结果:
使用Runnable接口创建线程
使用继承Thread类的方式创建线程简单明了,符合大家的习惯。但它也有个缺点,如果定义的类已经继承了其他类则无法再继承Thread类。使用Runnable接口创建线程的方式可以解决问题。
Runnable接口中声明了一个run()方法,即public void run()。一个类可以通过实现Runnable接口并实现其run()方法完成线程的所有活动,已实现的run()方法称为该对象的线程体。任何实现Runnable接口的对象都可以作为一个线程的目标对象。
示例二:修改示例一,要求线程类使用实现Runnable接口的方式创建。
代码展示:
public class test1 implements Runnable{
@Override
public void run() {
for (int i = 1; i <=20 ; i++) {
System.out.println(i+".你好,来自线程"+Thread.currentThread().getName());
}
}
public static void main(String[] args) {
test1 t = new test1();
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
t1.start();
t2.start();
}
}
输出结果:
两种创建线程的方式有各自的特点和应用领域:直接继承Thread类的方式编写简单,可以直接操作线程,适用于单重继承;实现Runnable接口的方式,当一个线程继承了另一个类时,就只能实现Runnable接口的方法来创建线程,而且这种方法还可以使多个线程之间使用同一个Runnable对象。
使用Callable接口创建线程
Callable接口的用法跟Runnable接口的用法大致相似。
示例三:
import java.util.concurrent.*;
public class TestCallable implements Callable<Integer> {
@Override
public Integer call()throws Exception {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + ":" + (i + 1));
}
return 3;
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
TestCallable t=new TestCallable();
FutureTask<Integer> f = new FutureTask<Integer>(t);
Thread th = new Thread(f);
th.start();
Integer a = f.get();
System.out.println(a);
//线程池
// ExecutorService single = Executors.newSingleThreadExecutor();
// ExecutorService executorService = Executors.newFixedThreadPool(3);
}
}
输出结果:
线程的状态
线程的生命周期可以分为4个阶段,即线程的4中状态,分别为新生状态、可运行状态、阻塞状态和死亡状态。
一个具有生命的线程,总是处于这4种状态。线程的生命周期如下所示:
新生状态(New Thread)
创建线程对象之后,尚未调用其start()方法之前,这个线程就有了生命,此时线程仅仅是一个空对象,系统没有为其分配资源。此时只能启动和终止线程,任何其他操作都会引发异常。
可运行状态(Runnable)
当调用start()方法启动线程之后,系统为该线程分配除CPU外的所需资源,这个线程就有了运行的机会,线程处于可运行的状态,在这个状态当中,该线程对象可能正在运行,也可能尚未运行。对于只有一个CPU的机器而言,任何时刻只能有一个处于可运行状态的线程占用处理机,获得CPU资源,此时系统真正运行线程的run()方法。
阻塞状态(Blocked)
一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态。阻塞状态是一种“不可运行”的状态,而处于这种状态的线程在得到一个特定的事件之后会转回可运行状态。
导致一个线程被阻塞有以下原因:
(1)调用了Thread类的静态方法sleep()。
(2)一个线程执行到一个I/O操作时,如果I/O操作尚未完成,则线程将被阻塞。
(3)如果一个线程的执行需要得到一个对象的锁,而这个对象的锁正被别的线程占用,那么此线程会被阻塞。
(4)线程的suspend()方法被调用而使线程被挂起时,线程进入阻塞状态。但suspend()容易导致死锁,已经被JDK列为过期方法,基本不再使用。
处于阻塞状态的线程可以转回可运行状态,例如,在调用sleep()方法之后,这个线程的睡眠时间已经达到了指定的间隔,那么它就有可能重新回到可运行状态,或当一个线程等待的锁变得可用的时候,那么这个线程也会从被阻塞状态转入可运行状态。
死亡状态(Dead)
一个线程的run()方法运行完毕、stop()方法被调用或者在运行过程中出现未捕获的异常时,线程进入死亡状态。
线程调度
当同一时刻有多个线程处于可运行状态,它们需要排队等待CPU资源,每个线程会自动获得一个线程的优先级(Priority),优先级的高低反映线程的重要或紧急程度。可运行状态的线程按优先级排队,线程调度依据建立在优先级基础上的“先到先服务”原则。
线程调度管理器负责线程排队和在线程间分配CPU,并按线程调度算法进行调度。当线程调度管理器选中某个线程时,该线程获得CPU资源进入运行状态。
线程调度是抢占式调度,即在当前线程执行过程中如果有一个更高优先级的线程进入可运行状态,则这个更高优先级的线程立即被调度执行。
线程优先级
线程的优先级用1~10表示,10表示优先级最高,默认值是5,。每个优先级对应一个Thread类的公用静态常量
线程的优先级可以通过setPriority(int grade)方法更改,此方法的参数表示要设置的优先级,它必须是1~10的整数。
实现线程调度的方法
join()方法
join()方法使当前线程暂停,等待调用该方法的线程结束后再继续执行本线程。它有三种重载形式。
public final void join();
public final void join(long mills);
public final void join(long mills,int nanos)
示例四:使用join()方法阻塞线程。
实现步骤如下:
(1)定义线程类,输出5次当前线程的名称。
(2)定义测试类,使用join()方法阻塞主线程。
代码展示:
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 5; i++) {
//输出当前线程的名称
System.out.println(Thread.currentThread().getName() + "" + i);
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
if (i == 5) {//主线程运行5次后,开始MyThread线程
MyThread tempjt = new MyThread("MyThread");
try {
tempjt.start();
tempjt.join();//把该线程通过join()方法插入到主线程前面
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName()+""+i);
}
}
}
输出结果:
在示例三中,使用join()方法阻塞指定的线程直到另一个线程完成以后再继续执行,其中tempjt.join();表示让当前线程即主线程加到tempjt的末尾,主线程被阻塞,tempjt执行完以后主线程才能继续执行。Thread.currentThread().getName()用于获取当前线程的名称。
从线程 返回数据时也经常使用到join()方法。
sleep()方法
sleep()方法的语法格式如下:
public static void sleep(long millis);
sleep()方法会让当前线程睡眠(停止执行)millis毫秒,线程由运行中的状态进入不可运行状态,睡眠时间过后线程会再次进入可运行状态。
示例五:使用sleep()方法阻塞线程。
实现步骤如下:
(1)定义线程。
(2)在run()方法中使用sleep()方法阻塞线程。
(3)定义测试类。
代码展示:
public class Wait {
public static void bySec(long s) {
for (int i = 0; i < s; i++) {
System.out.println((i + 1) + "秒");
try {
Thread.sleep(1000);//睡眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
System.out.println("Wait");//提示等待
Wait.bySec(5);//让主线程等待5秒再执行
System.out.println("start");//提示恢复执行
}
}
输出结果:
yield()方法
yield()方法的语法格式如下:
public static void yield()
yield()方法可让当前线程暂停执行,允许其他线程执行,但该线程仍处于可运行状态,并不变为阻塞状态。此时,系统选择其他相同或更高优先级线程执行,若无其他相同或更高优先级线程,则该线程继续执行。
示例六:使用yield()方法暂停线程。
实现步骤如下:
(1)定义两个线程。
(2)在run()方法中使用yield()方法暂停线程。
(3)定义测试类。
代码展示:
public class FirstThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第一个线程的第" + (i + 1) + "次运行");
Thread.yield();//暂停线程
}
}
public static class SecThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("第二个线程的第" + (i + 1) + "次运行");
Thread.yield();
}
}
}
public static void main(String[] args) {
FirstThread mt = new FirstThread();
SecThread mntt = new SecThread();
mt.start();
mntt.start();
}
}
输出结果:
在实例五中,调用了yield()方法之后,当线程并不是转入被阻塞状态,它可以与其他等待执行的线程竞争CPU资源,如果此时它又抢占到CPU资源,就会出现连续运行几次的情况。
sleep()方法与yield()方法在使用时容易混淆,这两个方法之间的区别如下所示:
sleep()方法 | yield()方法 |
使当前线程进入被阻塞状态 | 将当前线程转入暂停执行的状态 |
即使没有其他等待运行的线程,当前线程也会等待指定的时间 | 如果没有其他等待的线程,当前线程会马上恢复执行 |
其他等待执行的线程的机会是均等的 | 会运行优先级相同或更高的线程 |
示例七:模拟多人爬山。每个线程代表一个人,可设置每人爬山速度,每爬完100米显示信息,爬到终点时给出相应提示。
代码展示:
public class test3 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+"爬完"+(i*100)+"米!");
}
System.out.println(Thread.currentThread().getName()+"到达终点!");
}
public static void main(String[] args) throws InterruptedException {
System.out.println("***开始爬山***");
test3 t = new test3();
Thread t1 = new Thread(t, "年轻人");
Thread t2 = new Thread(t, "老年人");
t1.start();
t2.start();
t2.sleep(100);
}
}
输出结果:
示例八:模拟叫号看病。某科室一天需看普通号50个,特需号10个。特需号看病时间是普通号的2倍,开始时普通号和特需号并行叫号,叫到特需号的概率比普通号高,当普通号叫完第10号时,要求先看完全部特需号,再看普通号
使用多线程模拟这一过程。
代码展示:
public class test5 implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i+"号病人在看病!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
test5 t = new test5();
Thread t1 = new Thread(t, "特需号");
t1.setPriority(10);
t1.start();
for (int i = 1; i <= 50; i++) {
Thread.currentThread().setName("普通号");
System.out.println(Thread.currentThread().getName() + ":" + i + "号病人在看病!");
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 10) {
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
输出结果:
实现线程同步
当两个或多个线程需要访问同一资源时,需要以某种顺序来确保该资源在某一时刻只能被一个线程使用的方式称为线程同步。
采用同步线程来控制线程的执行就是同步方法和同步代码块两种方法。
同步方法
通过在方法声明中加入synchronize关键字来声明同步方法。
使用synchronize修饰的方法控制对类成员变量的访问。每个类实例对应一把锁,方法一旦执行,就独占该锁,知道该方法返回时才能将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对应每一个实例,其所声明为synchronize的方法只能有一个处于可执行状态,从而有效地避免了类成员变量的访问冲突。
同步方法的语法格式如下:
访问修饰符 synchronize 返回类型 方法名 {}
或者
synchronize 访问修饰符 返回类型 方法名{}
注意:
synchronize是关键字。
访问修饰符是指public、private等。
同步代码块
同一时刻只能有一个线程进入synchronized(this)同步代码块。
当一个线程访问一个synchronized(this)同步代码块时,其他synchronized(this)同步代码块同样被锁定。
当一个线程访问一个synchronized(this)同步代码块时,其他线程可以访问该资源的非synchronized(this)同步代码。
示例九:使用同步方法解决抢票问题。
代码展示:
public class TestTicket implements Runnable {
int ticket = 10;
int sold = 0;
@Override
public void run() {
while (ticket > 0) {
synchronized (this) {
ticket--;
sold++;
if (ticket < 0) return;
System.out.println(Thread.currentThread().getName() +
"买到了第" + sold + "张票,还剩余" + ticket);
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
TestTicket t = new TestTicket();
Thread t1 = new Thread(t, "12306");
Thread t2 = new Thread(t, "携程");
t1.start();
t2.start();
}
}
输出结果:
同步方法的缺陷:如果将一个运行时间比较长的方法声明成synchronize将会影响效率。例如,将线程中的run()方法声明成synchronize,由于在线程的整个生命周期内它一直在运行,这样有可能导致run()方法会执行很长时间,那么其他的线程及得一直等到run()方法结束了才能执行。
示例十:模拟接力赛跑。多人参加1000米接力跑,每人跑100米,换下个选手,每跑10米显示信息。
代码展示:
public class test6 implements Runnable {
int chang=1000;
@Override
public void run() {
while (chang > 0) {
chang-=100;
synchronized (this) {
System.out.println(Thread.currentThread().getName() + "号选手拿到了接力棒!");
for (int i = 10; i <= 100; i = i + 10) {
System.out.println(Thread.currentThread().getName()+"跑了"+i+"米!");
}
}
}
}
public static void main(String[] args) {
test6 t = new test6();
for (int i = 1; i <=10 ; i++) {
Thread t1 = new Thread(t,i+"");
t1.start();
}
}
}
输出结果:
线程安全类型
常见类型比较:
StringBuffer && StringBuilder
前者线程安全,后者非线程安全
线程安全:synchronized 保证线程间隔离
String:不可被改变,真正意义上的安全
在频繁字符串拼接的情况下,速度非常慢
StringBuffer:线程安全,速度慢StringBuilder:线程不安全,速度快
总结
线程的创建方式:
继承Thread类
实现Runnble接口
实现Callable接口
线程的五个状态:
创建、就绪、阻塞、运行、死亡