第十五章、多线程
多线程
1. 进程与线程初识
1.1 进程
-
进程是程序的一次动态执行过程, 占用特定的地址空间。
-
每个进程由3部分组成:cpu、data、code。每个进程都是独立的,保有自己的cpu时间,代码和数据,即便用同一份程序产生好几个进程,它们之间还是拥有自己的这3样东西,这样的缺点是:浪费内存,cpu的负担较重。
-
多任务(Multitasking)操作系统将CPU时间动态地划分给每个进程,操作系统同时执行多个进程,每个进程独立运行。以进程的观点来看,它会以为自己独占CPU的使用权。
-
进程的查看:
-
Windows系统: Ctrl+Alt+Del,启动任务管理器即可查看所有进程。
-
Unix系统: ps or top。
-
1.2 线程
-
一个进程内部的一个执行单元,它是程序中的一个单一的顺序控制流程。
-
一个进程可拥有多个并行的(concurrent)线程。
-
一个进程中的多个线程共享相同的内存单元/内存地址空间,可以访问相同的变量和对象,而且它们从同一堆中分配对象并进行通信、数据交换和同步操作。
-
由于线程间的通信是在同一地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快。
-
线程的启动、中断、消亡,消耗的资源非常少。
1.3 线程和进程的区别
- 每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销。
- 线程可以看成是轻量级的进程,属于同一进程的线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换的开销小。
- 线程和进程最根本的区别在于:进程是资源分配的单位,线程是调度和执行的单位。
- 多进程: 在操作系统中能同时运行多个任务(程序)。
- 多线程: 在同一应用程序中有多个顺序流同时执行。
- 线程是进程的一部分,所以线程有的时候被称为轻量级进程。
- 一个没有线程的进程是可以被看作单线程的,如果一个进程内拥有多个线程,进程的执行过程不是一条线(线程)的,而是多条线(线程)共同完成的。
- 系统在运行的时候会为每个进程分配不同的内存区域,但是不会为线程分配内存(线程所使用的资源是它所属的进程的资源),线程组只能共享资源。那就是说,除了CPU之外(线程在运行的时候要占用CPU资源),计算机内部的软硬件资源的分配与线程无关,线程只能共享它所属进程的资源。
2. 多线程实现方案
2.1 继承Thread类实现
-
实现步骤
-
自定义类线程类(例:MyThread),继承Thread类
-
MyThread重写run方法
-
创建线程对象
-
启动线程
-
-
线程方法
run() 线程执行和普通方法一致 start() 开启线程 setName() 设置当前线程名 getName() 获取当前线程名 currentThread() 获取当前主线程
-
代码示例
public class TestThread extends Thread {//自定义类继承Thread类 //run()方法里是线程体 public void run() { for (int i = 0; i < 10; i++) { System.out.println(this.getName() + ":" + i);//getName()方法是返回线程名称 } } public static void main(String[] args) { TestThread thread1 = new TestThread();//创建线程对象 thread1.setName("线程1")//设置线程名称 thread1.start();//启动线程 TestThread thread2 = new TestThread(); thread2.setName("线程2") thread2.start(); } }
2.2 Runnable接口实现
-
实现步骤
-
自定义类线程类(例:MyRunnable),实现Runnable接口
-
MyRunnable重写run方法
-
创建线程对象
-
启动线程
-
-
线程方法:和继承Thread类实现多线程一致
-
代码示例
public class TestThread2 implements Runnable {//自定义类实现Runnable接口; //run()方法里是线程体; public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + ":" + i); } } public static void main(String[] args) { //创建线程对象,把实现了Runnable接口的对象作为参数传入; Thread thread1 = new Thread(new TestThread2()); thread1.setName("线程1")//设置线程名称 thread1.start();//启动线程; Thread thread2 = new Thread(new TestThread2()); thread2.setName("线程1")//设置线程名称 thread2.start(); } }
3. 线程调度和线程控制
- 线程休眠 / 睡眠
- public static void sleep(long millis)
- 线程加入 / join线程(放在start()方法后调用)
- public final void join() 等待线程终止,才继续执行下一个线程
- 线程礼让 \ 线程让步
- public static void yield() 放在线程体中操作
- 暂停当前正在执行的线程对象(及放弃当前拥有的cup资源),并执行其他线程。让多个线程的执行更和谐,但是不能靠他保证一人一次。
- 后台线程 / 守护线程
- public final void setDaemon(boolean on)
- 将指定线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
- 中断线程
- public final void stop() 让线程停止,过时了,但是还可以使用。
- public void interrupt() 中断线程。 把线程的状态终止,并抛出一个InterruptedException。
4. 线程生命周期
5. 线程同步
-
概念:处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。
-
synchronized 方法实现
-
代码实现
package demo6; public class MyCinema implements Runnable { static int poll = 100; Object obj = new Object(); @Override public void run() { while (true) { // synchronized (obj) { // synchronized (this) { 等同于 同步方法 // synchronized (MyCinema.class) { 等同有静态同步方法 show(); // } } } // 同步方法就是把同步方法关键字加到方法上 // 同步方法锁对象是谁 锁对下那个就是this // 静态方法所对象是谁 MyCinema.class public synchronized void show() { if (poll > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (poll--) + "张票。"); } } }
package demo6; /* * 需求: * 某电影院目前正在上映喜剧大片,共有100张票,而它有3个售票窗口售票,设计一个程序模拟该电影院售票 */ public class TestMyCinema { public static void main(String[] args) { MyCinema my = new MyCinema(); Thread t1 = new Thread(my, "张三"); Thread t2 = new Thread(my, "李四"); Thread t3 = new Thread(my, "王五"); t1.start(); t2.start(); t3.start(); } }
-
-
Lock类实现
-
代码实现
package demo7; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /* * 虽然我们可以理解同步代码块合同部方法的锁对象问题,但是我们并没有直接看到在哪里加锁 * 在哪里释放锁,为了更加清晰的表达如何加锁和释放锁,JDK5以后的版本提供了一个新的锁对象Lock * Lock 接口 * void lock() 加锁 * void unlock() 释放锁 * ReentrantLock 实现类 */ public class MyLock implements Runnable { static int poll = 100; Object obj = new Object(); // 声明lock锁 Lock lock = new ReentrantLock(); @Override public void run() { // TODO Auto-generated method stub while (true) { // synchronized (obj) { // 加锁 lock.lock(); if (poll > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "正在出售第" + (poll--) + "张票。"); } // 释放锁 lock.unlock(); // } } } }
package demo7; public class TestMyLock { public static void main(String[] args) { MyLock my = new MyLock(); Thread t1 = new Thread(my, "张三"); Thread t2 = new Thread(my, "李四"); Thread t3 = new Thread(my, "王五"); t1.start(); t2.start(); t3.start(); } }
-
6. 死锁
-
概念: 多个线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。
-
代码实现
package demo8; public class DeadLock extends Thread { private boolean flg; public DeadLock(boolean flg) { this.flg = flg; } @Override public void run() { // flg = true 中国人;flg = false 美国人 if (flg) { synchronized (LockObject.obj1) { System.out.println("一根筷子"); synchronized (LockObject.obj2) { System.out.println("一把叉子"); } } } else { synchronized (LockObject.obj2) { System.out.println("一把刀"); synchronized (LockObject.obj1) { System.out.println("一根筷子"); } } } } }
package demo8; public class LockObject { static Object obj1 = new Object(); static Object obj2 = new Object(); }
package demo8; /* * 死锁: * 美国人吃饭用一把叉子和一把刀子 * 中国人吃饭用一双筷子 * * 现在的情况是: * 美国人手里一把叉子和一根筷子 * 中国人手里一根筷子和一把刀子 * * 两者相互等待对方将东西交出来,却没有人主动交,所以陷入死锁 */ public class TestLock { public static void main(String[] args) { DeadLock d1 = new DeadLock(true); // 中国人 DeadLock d2 = new DeadLock(false); // 美国人 Thread t1 = new Thread(d1, "锁1"); Thread t2 = new Thread(d2, "锁2"); t1.start(); t2.start(); /* * DeadLock d1 = new DeadLock(true); // 中国人 DeadLock d2 = new * DeadLock(false); // 美国人 * * d1.start(); d2.start(); */ } }
7. 线程通信(生产者消费者模型)
7.1 相关概念
-
生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。
-
缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
7.2 相关方法
- 以上方法均是java.lang.Object类的方法,这些方法只能在同步方法或同步代码中使用,否则抛异常。
7.3 代码实现
// 生产对象
package demo9;
public class Book {
private String name;
private int price;
// 如果为true说明仓库里面有书,如果为false说明仓库里面没有书
// 如果有书,就通知消费者去消费
// 如果没有书,就通知生产者取生产
public boolean flag;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
// 消费者
package demo9;
public class Consumption implements Runnable {
private Book book;
public Consumption(Book book) {
this.book = book;
}
@Override
public void run() {
// 为了模拟一直消费
while (true) {
synchronized (book) {
if (!book.flag) {
try {
book.wait(); // 等待,如果生产者那边有书就继续往下走
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else {
System.out.println(
Thread.currentThread().getName() + "买了" + book.getName() + "价格是" + book.getPrice());
book.flag = false;
book.notify();
}
}
}
}
}
// 生产者
package demo9;
public class Production implements Runnable {
private Book book;
int num = 0;
public Production(Book book) {
this.book = book;
}
@Override
public void run() {
// 为了模拟一直生产
while (true) {
synchronized (book) {
if (book.flag) {
try {
book.wait(); // 如果有书,生产者线程就进入等待,不生产了
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (num % 2 == 0) {
book.setName("不能承受生命之轻");
book.setPrice(123);
System.out.println(
Thread.currentThread().getName() + "生产了" + book.getName() + "价格是" + book.getPrice());
} else {
book.setName("作为表象与意志的世界");
book.setPrice(99);
System.out.println(
Thread.currentThread().getName() + "生产了" + book.getName() + "价格是" + book.getPrice());
}
num++;
book.flag = true;
book.notify(); // 唤醒消费线程
}
}
}
}
// 实现类
package demo9;
/*
* 生产者:
* 先看是否有数据,有就等待,没有就生产,生产完成后就通知消费者过来消费
* 消费者:
* 先看是否有数据,悠久消费,没有就等待,通知生产者过来生产
*
*
* 等待唤醒
* wait() 等待
* notify() 唤醒单个线程
* ntifyAll() 唤醒所有等待的线程
*
* wait和sleep的区别?
* wait等待需要被唤醒
* sleep休眠,一定时间会自动苏醒
*/
public class TestThread {
public static void main(String[] args) {
Book b = new Book();
Consumption c = new Consumption(b);
Production p = new Production(b);
Thread t1 = new Thread(c, "消费者"); // 消费者
Thread t2 = new Thread(p, "生产者"); // 生产者
t1.start();
t2.start();
}
}
8.线程组
-
概念:对一批线程进行分类管理,Java允许程序直接对线程组进行控制。
-
代码实现
package demo10; public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { try { Thread.sleep(100); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " " + i); } } }
package demo10; /* * 线程组,把多个线程组合到一起 * 他可以对一批线程进行分类管理,Java允许程序直接对线程进行控制 */ public class ThreadGroupDemo { public static void main(String[] args) { // 我们如何修改线程所在的线程组呢? // 创建一个线程组 // 创建其他线程的时候,把其他线程的组制定为我们新建的线程组 // ThreadGroup(String name) 构造一个新线程组 ThreadGroup tg = new ThreadGroup("这是一个新线程组"); MyRunnable my = new MyRunnable(); // 如果想指定下列线程属于某个组,怎么办? Thread t1 = new Thread(tg, my, "线程1"); Thread t2 = new Thread(tg, my, "线程2"); // 线程类中方法:getThreadGroup() 返回该线程所属的线程组 ThreadGroup threadGroup1 = t1.getThreadGroup(); ThreadGroup threadGroup2 = t2.getThreadGroup(); // 线程组方法:getName() String name1 = threadGroup1.getName(); String name2 = threadGroup2.getName(); // 通过结果我们知道,线程默认情况下属于main线程组 System.out.println(name1); System.out.println(name2); tg.setDaemon(true); // 通过组名设置后台线程,表示该线程组的线程都是后台线程 } }
9. 线程池
9.1 线程池的优势
- 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))
- 提供更强大的功能,延时定时线程池
9.2 线程池流程
- 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则
- 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
- 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
9.3 Executors实现线程池
方法名 | 功能 |
---|---|
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newCachedThreadPool() | 创建一个不限线程数上限的线程池,任何提交的任务都将立即执行 |
-
具体方法
- submit() 创建线程对象
- shutdown() 结束线程池
-
代码实现
package demo11; public class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } }
package demo11; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /* * 线程池的好处:线程池里的每一个线程代码结束后,并不会死亡,而是再次返回到线程池中成为空闲状态,等待下一个对象来使用 * * 如何实现线程池代码? * 1.创建线程池对象,控制要创建几个线程对象 * public static ExecutorsService new FixedThreadPool(int nThreads) * 2.线程池的线程可以执行 * 可以执行Runnable对象或者Callable对象代表的线程 * 3.调用方法如下: * Future<?> submit(Runnable task) * <T> Future submit((Callable)<T> task) */ public class ExecutorsDemo { public static void main(String[] args) { // 创建线程池对象,控制要创建几个线程对象 ExecutorService pool = Executors.newFixedThreadPool(2); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); pool.submit(new MyRunnable()); // shutdown() 启动一次,顺序关闭,执行以前提交的任务,但不能接收新的任务 // 结束线程 pool.shutdown(); } }
10. 定时器
-
java.util.Timer
- 概念:Timer类作用是类似闹钟的功能,也就是定时或者每隔一定时间触发一次线程。其实,Timer类本身实现的就是一个线程,只是这个线程是用来实现调用其它线程的。
-
java.util.TimerTask
- 概念:TimerTask类是一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。在这种实现方式中,通过继承TimerTask使该类获得多线程的能力,将需要多线程执行的代码书写在run方法内部,然后通过Timer类启动线程的执行。
-
具体方法
- schedule() 添加计时器
- cancel() 结束计时器
-
代码实现
package demo12; import java.util.Timer; import java.util.TimerTask; /* * 定时器:可以让我们在指定的时间内做某件事情,还可以重复地做某件事情 * * 依赖timer和TImerTask这里两个类 * timer:定时 * public Timer() * public void schedule(TimerTask task,long delay) 在指定延迟后执行指定任务 * public void schedule(TimerTask task,long delay,long period) 安排指定的任务(task)从指定的延迟后(delay)开始进行重复与固定的延时时间 (period) * cancel() 终止此计时器,丢弃所有当前已安排的任务 * * TimerTask:任务 */ public class TimerDemo1 { public static void main(String[] args) { // 创建定时器对象 Timer t = new Timer(); // 三秒后上厕所 t.schedule(new MyTask(), 3000); // 结束任务 t.schedule(new MyTask1(t), 3000); // 设置循环执行时间 t.schedule(new MyTask2(), 3000, 2000); } } class MyTask1 extends TimerTask { private Timer t; public MyTask1(Timer t) { this.t = t; } @Override public void run() { System.out.println("上厕所中...."); t.cancel(); } }