概述
依靠
CPU
,计算机同时执行多个程序
并行/并发
- 在同一时间有多个指令(多件事)在 “多个
CPU
(多个核心)” 同时 执行叫做并行 - 在同一时刻,有多个指令在 “单个
CPU
核心” 交替 执行
进程
正在运行的软件
特性
- 独立性: 进程是一个独立运行的基本单位,同时也是系统分配和调度资源的独立单位
- 动态性: 进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
- 并发性: 任何进程都可以同其他进程一起并发执行
线程
是进程中的单个顺序控制流,是一条执行路径
特性
- 单线程: 一个进程如果只有一条执行路径,则称为单线程程序
- 多线程: 一个进程如果有多条执行路径,则称为多线程程序
线程状态
- 新建状态: 创建线程对象
- 就绪状态:
start()
方法 - 阻塞状态: 无法获得锁对象
- 等待状态:
wait()
方法 - 计时等待:
sleep()
方法 - 结束状态: 全部代码运行完毕,线程死亡
多线程实现
注意事项
- 一个运行的软件,最少要有一个线程,我们以前写的代码都是单线程的程序,这个线程我们叫做
main
线程, 也叫主线程 - 哪个线程抢到
CPU
,哪个线程就干活,但是哪一个线程能抢到CPU
我们是无法控制的,所以每一次运行代码,看到的打印效果都是不一样的
方法1(继承Thread类)
步骤
- 自定义一个类并继承
Thread
- 重写
run()
方法 - 在测试类中创建自定义对象
- 根据自定义的方法使用
start()
方法启动线程
例:
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程: " + super.getId() + "..." + i);
}
}
}
public class MyThreadTest {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
System.out.println("线程1: " + myThread1.getName());
MyThread myThread2 = new MyThread();
myThread2.start();
System.out.println("线程2: " + myThread2.getName());
}
}
方法2(实现Runnable接口)
步骤
- 自定义一个类实现
Runnable
接口 - 重写
Run()
方法 - 在测试类中自定义类的对象
- 创建
Thread
类的对象,并且将自定义类的对象作为参数传递给Thread
类的对象 - 使用
Thread
类的对昂调用start()
方法启动线程
例:
public class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("线程: " + i);
}
}
}
public class MyThreadTest2 {
public static void main(String[] args) {
MyThread2 myThread1 = new MyThread2();
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread1);
t1.start();
Thread t2 = new Thread(myThread2);
t2.start();
}
}
方法3(Callable和Future)
步骤
- 自定义一个类实现
Callable
接口并指定泛型 - 重写
call()
方法 - 在测试类中创建一个自定义类对象
- 创建一个自定义类的对象,并且将自定义对象传递给
FutureTask
类的对象 - 创建
Thread
类的对象,并且将Futuretask
类的对象传递给Thread
类的对象 - 使用
Thread
类的对象调用start()
方法启动线程
例:
public class MyThread3 implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("线程 " + i);
}
// 返回值就表示线程运行完毕之后的结果
return "结束";
}
}
public class MyThreadTest3 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyThread3 myThread = new MyThread3();
FutureTask futureTask = new FutureTask(myThread);
Thread thread = new Thread(futureTask);
thread.start();
System.out.println(futureTask.get());
}
}
其他
new Thread() {
@Override
public void run() {
System.out.println("Test");
}
}.start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Test");
}
}).start();
注意事项
get()
方法如果没有得到线程运行call()
方法结束后的结果,就会死等结果,所以应放在start()
之后
三种实现方式的对比
Thread类中常见的方法
getName()
: 获取线程名称setName()
: 设置线程的名称(也可以使用带参构造设置线程名称)
run方法和start方法的区别
start()
方法: 作用是启动线程,只能启动一次run()
方法: 和之前创建对象,调用方法相同,并没有开启线程,主要作用是在内部定义我们要进行的操作,可以调用多次
获取当前运行线程的线程对象
Thread.currentThread()
:CPU
当前被哪个线程占用,就获取哪个线程
线程休眠
Thread.sleep()
线程调度
- 分时调度模型: 所有线程轮流使用
CPU
的使用权,平均分配每个线程占用CPU
的时间片 - 抢占式调度模型: 优先让优先级高的使用
CPU
线程的优先级
getPriority()
setPriority()
: 设置失败的概率较高,集体和操作系统/CPU
厂商有关
守护线程
当普通线程运行完毕之后,守护线程也就没有必要继续运行下去了,不会立即结束,但是也不会运行完毕
setDaemon()
线程安全
出现线程安全的前提条件
当多条线程操作共享数据时
为什么会出现线程安全问题
当线程再次抢到
CPU
时,可能此时数据已经发生改变
线程安全问题解决思路
多线程开发中,能不共享数据,就不共享数据
方法1(同步代码块)
- 格式:
synchronized (任意对象) { // 多个线程必须使用同一个锁
}
- 例1:
public class MyThread implements Runnable {
private int ticket = 100;
Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket <= 0) {
break;
} else {
ticket--;
System.out.println(Thread.currentThread().getName() + "在买票, 还剩下: " +
ticket + " 张票");
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class ThreadLockTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
Thread thread1 = new Thread(myThread);
thread1.setName("售票口1");
Thread thread2 = new Thread(myThread);
thread2.setName("售票口2");
thread1.start();
thread2.start();
}
}
- 例2:
public class MyThread1 extends Thread {
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true) {
synchronized (obj) {
if (ticket <= 0) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + "在买票, 还剩下: " +
ticket + " 张票");
}
}
}
}
}
public class ThreadLockTest1 {
public static void main(String[] args) {
MyThread1 myThread1 = new MyThread1();
MyThread1 myThread2 = new MyThread1();
myThread1.setName("窗口1");
myThread2.setName("窗口2");
myThread1.start();
myThread2.start();
}
}
方法2(同步方法)
同步方法时的锁对象是
this
在同一个tomcat中时,一个线程结束,另一个线程才可以调用该方法,但是多个tomcat之间起不到同步作用
- 例:
/**
* 同步方法
*/
public class MyThread4 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
if ("窗口1".equals(Thread.currentThread().getName())) {
boolean res = synchronizedMethod();
if (res) {
break;
}
}
if ("窗口2".equals(Thread.currentThread().getName())) {
synchronized (this) {
if (ticket == 0) {
break;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() +
"在卖票, 还剩: " + ticket);
}
}
}
}
}
private synchronized boolean synchronizedMethod() {
if (ticket == 0) {
return true;
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() +
"在卖票, 还剩: " + ticket);
return false;
}
}
}
public class ThreadLockTest4 {
public static void main(String[] args) {
MyThread4 myThread4 = new MyThread4();
Thread t1 = new Thread(myThread4);
Thread t2 = new Thread(myThread4);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
和同步代码块的区别
- 同步代码块可以锁定代码,同步方法是锁住方法中的所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
注意
- 普通同步方法的锁对象: 只能是
this
- 静态同步方法的锁对象,只能是
类名.class
方法3(lock)
步骤
- 创建
lock
对象 - 加锁
- 释放锁
- 例:
public class MyThread2 implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();
if (ticket <= 0) {
break;
} else {
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() +
"在买票, 还剩: " + ticket + "张");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
public class ThreadLockTest2 {
public static void main(String[] args) {
MyThread2 myThread2 = new MyThread2();
Thread t1 = new Thread(myThread2);
Thread t2 = new Thread(myThread2);
t1.setName("窗口1");
t2.setName("窗口2");
t1.start();
t2.start();
}
}
锁的作用
- 可以解决线程安全问题
- 可以进行线程之间的通信
同步的好处和弊端
- 好处: 解决了多线程的数据安全问题
- 弊端: 当线程很多时,因为每个线程都会去判断同步上的锁,这很消耗资源,会降低程序的运行效率
生产者和消费者
等待和唤醒的方法(Object)
wait()
notify()
: 唤醒正在等待对象监视器的单个线程notifyAll()
: 唤醒正在等待对象监视器的所有线程
例
/**
* 桌子
*/
public class Desk {
/**
* 表示桌子上是否有汉堡
* true 表示有汉堡
* false 表示没有汉堡
*/
public static boolean flag = false;
/**
* 表示现在桌子上可以放多少个汉堡
*/
public static int num = 10;
/**
* 用于线程唯一对象,表示只有一个桌子
*/
public static final Object desk = new Object();
}
/**
* 生产者
*/
public class Cooker extends Thread {
@Override
public void run() {
while (true) {
// 同步控制
synchronized (Desk.desk) {
// 如果今天的总量已经用完,结束掉该线程
if (Desk.num == 0) {
System.out.println("今天的10个汉堡做完了, 下班了");
break;
} else {
// 如果桌子上没有汉堡
if (!Desk.flag) {
System.out.println("生产者生产汉堡, 今日汉堡还剩: " + Desk.num);
Desk.flag = true;
Desk.desk.notifyAll();
} else {
try {
System.out.println("桌子上还有1个汉堡, 等待消费者吃掉它");
// 等待消费者消耗汉堡
Desk.desk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 消费者
*/
public class Foodie extends Thread {
@Override
public void run() {
while (true) {
// 同步控制
synchronized (Desk.desk) {
// 如果总量已经用完,结束该线程
if (Desk.num == 0) {
System.out.println("今天的10个汉堡吃完了, 明天再来吧");
break;
} else {
// 如果桌子上有汉堡
if (Desk.flag) {
System.out.println("消费者吃汉堡, 今日汉堡还剩: " + Desk.num);
// 桌子上已经没有汉堡
Desk.flag = false;
// 总数减1
Desk.num--;
// 唤醒所有线程
Desk.desk.notify();
} else {
try {
System.out.println("桌子上没有汉堡, 等待生产者做汉堡");
Desk.desk.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 测试
*/
public class Demo {
public static void main(String[] args) {
Cooker cooker = new Cooker();
Foodie foodie = new Foodie();
cooker.start();
foodie.start();
}
}
线程池
步骤
- 创建线程池
ExecutorService es = Executors.newCachedThreadPool();
- 有任务执行时,先看线程池中有没有空闲线程,没有的话创建,使用完后
submit
进线程池,有的话复用旧线程 - 没有任务需要执行时,关闭线程池(
shutdown
)
submit注意事项
- 每次调用
submit
方法,就会从线程池中征用一个线程- 对象如果没有空闲的线程对象,拿不到,创建一个新的线程对象,存储线程池中
- 如果有空闲的线程,可以拿到,直接使用,就不会创建新的线程对象
- 线程对象使用完之后,不会销毁
方式1(基本不用): 最大线程数为int的最大数
Executors.newCachedThreadPool()
方式2(优选): 可指定最多线程数量的线程池
ExecutorService es = Executors.newFixedThreadPool(long);
例:
ExecutorService executorService = Executors.newFixedThreadPool(10);
ThreadPoolExecutor pool = (ThreadPoolExecutor) executorService;
System.out.println(pool.getPoolSize());
for (int i = 0; i < 20; i++) {
executorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + "执行");
});
}
System.out.println(pool.getPoolSize());
executorService.shutdown();
方式3: ThreadPoolExecutor
构造方法:
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
参数
- 核心线程数: 不会因为超时被清理
- 最大线程数: 核心线程+临时线程
- 空闲线程最大存活时间: 超时将临时线程清理
- 时间单位:
TimeUnit
- 任务队列(阻塞队列): 线程用完之后将任务放进队列
- 创建线程工厂: 底层帮我们新建线程 可省略,使用默认
- 任务的拒绝策略: 任务数超出最大线程数和队列数之后进行操作 可省略,使用默认
ThreadPoolExecutor.AbortPolicy
(最推荐): 对其任务并抛出RejectedExecutionException
异常ThreadPoolExecutor.DiscardPolicy
: 丢弃任务,不抛出异常ThreadPoolExecutor.DiscardOldestPolity
: 抛弃队列中等待时间最久的任务,然后把当前任务加入队列中ThreadPoolExecutor.CallerRunsPolicy
: 调用任务的run()
方法绕过线程池直接执行
工作流程
1、如果正在运行的线程数量小于 `corePoolSize`,那么马上创建线程运行这个任务
2、如果正在运行的线程数量大于或等于 `corePoolSize`,那么将这个任务放入队列
3、如果这时候队列满了,而且正在运行的线程数量小于 `maximumPoolSize`,那么还是要创建非核心线程立刻运行这个任务
4、如果队列满了,而且正在运行的线程数量大于或等于 `maximumPoolSize`,那么线程池会抛出异常 `RejectExecutionException`
例:
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,
5, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10));
for (int i = 0; i < 100; i++) {
pool.submit(() -> {
System.out.println(Thread.currentThread().getName() +
"在执行");
});
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
pool.shutdown();
这时候队列满了,而且正在运行的线程数量小于 `maximumPoolSize`,那么还是要创建非核心线程立刻运行这个任务
4、如果队列满了,而且正在运行的线程数量大于或等于 `maximumPoolSize`,那么线程池会抛出异常 `RejectExecutionException`
例:
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,
5, 2, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(10));
for (int i = 0; i < 100; i++) {
pool.submit(() -> {
System.out.println(Thread.currentThread().getName() +
"在执行");
});
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
}
pool.shutdown();
如何定义线程池
- io密集型: 推荐线程数是
cpu核数的两倍
- cpu密集型: 推荐线程数是
cpu核数