一、多线程技术概述
1、线程与进程
进程
是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间
线程
是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
2、线程调度
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),
Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核新而言,某个时刻,
只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时
刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使
用率更高。
3、同步与异步
同步:排队执行 , 效率低但是安全.(在一条道路上一起执行)
异步:同时执行 , 效率高但是数据不安全.(在多条道路上同时执行)
4、并发与并行
并发:指两个或多个事件在同一个时间段内发生。
重点为“一个时间段区间内”,如Cpu在该时间段内同时执行了多件事情
并行:指两个或多个事件在同一时刻发生(同时发生)。
重点为“某一时刻”,如CPU为多核,可以在某一时刻执行不同的事情。
二、线程常用方法
1、Thread类常用方法
具体可以参照jdk的API,本次仅列出部分常用方法:
| 方法 | 概述 |
|---|---|
| 1.start() | 启动当前线程;调用当前线程的run()方法 |
| 2.run() | 通常重写继承Thread类中的此方法,将创建多线程要执行的操作声明在此方法中 |
| 3.currentThread() | 静态方法,返回执行当前代码的线程 |
| 4.getName() | 获取当前线程的名字 |
| 5.setName() | 设置当前线程的名字 |
| 6.setDaemo(boolean on) | 将此线程标记为daemon线程或用户线程 |
| 7.join() | 线程a中调用线程b的join(),此时线程a就会进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态 |
| 8.sleep(long millis) | 让当前线程“休眠”指定millis毫秒。在改时间段内,当前线程处于阻塞状态 |
| 9.isAlive() | 判断当前线程是否存活 |
2、线程优先级
| 变量和类型 | 字段 | 描述 |
|---|---|---|
| static int | MAX_PRIORITY | 线程拥有最大优先级 |
| static int | MIN_PRIORITY | 线程拥有最低优先级 |
| static int | NORM_PRIORITY | 分配给线程的默认优先级 |
三、线程的创建各种方法
1、方式一:继承Thread类
- 步骤
- 创建一个继承于Thread类的子类
- 重写Thread类的run()方法 –>将此线程执行的操作声明在run()中
- 创建Thread类的子类对象
- 通过此对象调用start()方法
2、方式二:实现Runnable接口
- 步骤
- 创建一个实现Runnable接口的类
- 实现类中去实现Runnable接口中的抽象方法:run()
- 创建实现类对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
3、方式三:实现Callable接口(不常用),带返回值的接口
- 步骤
- 创建一个实现Callable接口的类
- 实现call()方法,将线程中需要执行的操作声明在call()中,可以有返回值
- 创建实现类对象
- 将此对象作为参数传递到FutureTask的构造器中,比高创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,并创建Thread对象,再调用start().
- 获取Callable中call()方法的返回值
- 步骤
- 创建一个继承于Thread类的子类
- 重写Thread类的run()方法 –>将此线程执行的操作声明在run()中
- 创建Thread类的子类对象
- 通过此对象调用start()方法
2、方式二:实现Runnable接口
- 步骤
- 创建一个实现Runnable接口的类
- 实现类中去实现Runnable接口中的抽象方法:run()
- 创建实现类对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
3、方式三:实现Callable接口(不常用),带返回值的接口
- 步骤
- 创建一个实现Callable接口的类
- 实现call()方法,将线程中需要执行的操作声明在call()中,可以有返回值
- 创建实现类对象
- 将此对象作为参数传递到FutureTask的构造器中,比高创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,并创建Thread对象,再调用start().
- 获取Callable中call()方法的返回值
- 步骤
- 创建一个实现Callable接口的类
- 实现call()方法,将线程中需要执行的操作声明在call()中,可以有返回值
- 创建实现类对象
- 将此对象作为参数传递到FutureTask的构造器中,比高创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,并创建Thread对象,再调用start().
- 获取Callable中call()方法的返回值
4、比较创建线程的方式
- 实际开发中:优先选择实现接口方法,常用Runnable接口
- 优点:
- 实现的方式没有类单继承的局限性
- 实现的方式更适合来处理多个线程共享数据的情况(可以把实现Runnable接口的实现类看做一个任务,多个线程共同执行一项任务)
四、线程的生命周期
1、新建和就绪状态
当程序使用new关键字创建一个线程以后,该线程就处于新建状态,此时它和其他的JAVA对象一样,仅仅由JAVA虚拟机为其分配内存,并初始化成员变量的值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
当线程对象调用start()方法以后,该线程进入就绪状态,JAVA虚拟机会为其创建方法调用栈和程序计数器。处于这种状态中的线程并没有开始运行,只是表示当前线程可以运行了,至于什么时候运行,则取决于JVM里线程调度器的调度。所以线程的执行是由底层平台控制, 具有一定的随机性。
2、运行和阻塞状态
当处于就绪状态的线程获得CPU,它就会执行run()方法(所以run()方法是由线程获得CPU以后自动执行),线程就进入了运行状态。如果一个计算机只有一个CPU,那么在任意时刻只有一个线程处于运行状态。相对的,如果有多个CPU,那么在同一时刻就可以有多个线程并行执行。但是,当处于就绪状态的线程数大于处理器数时,仍然会存在多个线程在同一CPU上轮换执行的现象,只是计算机的运行速度非常快,人感觉不到而已。
当一个线程开始运行后,它不可能一直持有CPU(除非该线程执行体非常短,瞬间就执行结束了)。所以,线程在执行过程中需要被中断,目的是让其它线程获得执行的CPU的机会。线程的调度细节取决于底层平台所采用的策略。对于抢占式策略的系统而言,系统会给每一个可执行线程一个时间段来处理任务,当该时间结束后,系统就会剥夺该线程所占用资源(即让出CPU),让其它线程获得执行机会。
所有的现代桌面和服务器操作系统都采用抢占式调度策略,但一些小型设备,如:手机,则可能采用协作式调度策略。在这样的系统中,只有当一个线程调用了它的sleep()或yield()方法后才会放弃所占用的资源。也就是说,此时必须该线程主动放弃占用资源,才能轮到其他就绪状态的线程获得CPU,不然必须要等当前线阻塞/死亡以后,其他线程才有机会运行。
当如下情况发生时,线程会进入阻塞状态:
- 线程调用sleep()方法主动放弃占用的处理器资源;
- 线程调用了一个阻塞式IO方法,在该方法返回以前,该线程被阻塞;
- 线程试图获得一个同步监视器,但该监视器被其他线程持有;
- 线程在等待某个通知;
- 线程调用suspend()方法将该线程挂起。但这个方法容易导致死锁,不建议使用;
当正在执行的线程被阻塞以后,其他线程可以获得执行的机会,被阻塞的线程会在合适的时候进入就绪状态,而不是进入运行状态。也就是说,当线程阻塞解除后,必须重新等待线程调度器再次调度它,而不是马上获得CPU。所以针对上述线程阻塞情况,如何让线程重新进入就绪状态,有如下几种情况:
- 调用sleep()方法的线程经过了指定时间;
- 线程调用的阻塞式IO方法已经返回;
- 线程成功地获得了试图取得的同步监视器;
- 线程在等待通知时,其他线程发出了一个通知;
- 处于挂起状态的线程被调用了resume()恢复方法;
线程从阻塞状态只能进入就绪状态,而不能直接进入运行状态。而就绪状态到运行状态之间的转换通常不受程序控制,而由系统线程调度所决定,当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器器资源时,该线程进入就绪状态。但有一个方法可以控制线程从运行状态转为就绪状态,那就是yiled()方法。
3、线程死亡
线程会在如下几种情况结束(结束后就处于死亡状态):
- run()/call()方法执行完成,线程正常结束;
- 线程抛出一个未捕获的Exception或Error;
- 直接调用线程的stop()方法结束该线程——该方法已过时且容易导致死锁,不建议使用。
五、线程同步
- 关键字:synchronized:任何对象都能作为锁放在synchronized(){}的括号里,通常this表示当前类实现对象充当锁
1、同步代码块:
synchronized(this){
//要执行的操作
}
2、同步方法:
synchronized(this){
//要执行的操作
}
2、同步方法:
哪个类调用这个同步方法,就用这个类的this充当锁
public synchronized boolean sale(){
//要执行的操作
}
(注意同步方法声明为静态时,使用调用该方法的类名.class当做锁)
3、ReentrantLock(显示锁)
两个构造方法:
| 构造器 | 描述 |
|---|---|
| ReentrantLock() | 创建一个 ReentrantLock的实例。 |
| ReentrantLock(boolean fair) | 使用给定的公平策略创建 ReentrantLock的实例。 |
常用方法:lock() //获得锁 ; unlock() //尝试释放此锁定
六、线程通信
1、线程通信的例子
使用以下厨师做菜,服务员端菜的例子。线程交替执行
public class Demo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread {
private Food food;
public Cook(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i % 2 == 0) {
food.setNameAndTaste("老干妈小米粥", "香辣味");
} else {
food.setNameAndTaste("煎饼果子", "甜辣味");
}
}
}
}
//服务员
static class Waiter extends Thread {
private Food food;
public Waiter(Food food) {
this.food = food;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
food.get();
}
}
}
//食物
static class Food {
private String name;
private String taste;
//true 表示可以生产
private boolean flag = true;
public synchronized void setNameAndTaste(String name, String taste) {
if (flag) {
this.name = name;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.taste = taste;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get() {
if (!flag) {
System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2、涉及到的三个方法
- wait():一旦执行此方法,当前线程进入阻塞状态,并释放同步监视器。
- notify():一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait,就会唤醒优先级最高的线程,如优先级一致则先唤醒最先进入阻塞状态的线程。
- notifyAll():一旦执行此方法,就会唤醒所有被wait的线程。
七、线程池
- 背景:在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
1、线程池概述
- 背景:在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
1、线程池概述
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
2、线程池的好处
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
3、Java中的常见四种线程池
1、CachedThreadPool:可缓存的线程池
1、CachedThreadPool:可缓存的线程池
该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
public class Demo {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
//向线程池中 加入 新的任务
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
}
}
2、FixedThreadPool:定长的线程池
有核心线程,核心线程的即为最大的线程数量,没有非核心线程
public class Demo {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(2);
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
}
}
3、SingleThreadPool:单线程线程池
只有一条线程来执行任务,适用于有顺序的任务的应用场景。
public class Demo {
public static void main(String[] args) {
//效果与定长线程池 创建时传入数值1 效果一致.
ExecutorService service = Executors.newSingleThreadExecutor();
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
service.execute(new Runnable() {
@Override
public void run() {
System.out.println("线程的名称:" + Thread.currentThread().getName());
}
});
}
}
4、SecudleThreadPool:周期性任务定长线程池
周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
public class Demo {
public static void main(String[] args) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
service.schedule(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
}, 5, TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("俩人相视一笑~ 嘿嘿嘿");
}
}, 5, 2, TimeUnit.SECONDS);
}
}



