01.简单了解多线程
是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。
02.并发和并行
- 并行:在同一时刻,有多个指令在多个CPU上同时执行。
- 并发:在同一时刻,有多个指令在单个CPU上交替执行。
03.进程和线程
进程 : 是正在运行的软件
- 独立性 : 进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
- 动态性∶进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
- 并发性 : 任何进程都可以同其他进程一起并发执行
线程 : 是进程中的单个顺序控制流,是一条执行路径。
- 单线程 : 一个进程如果只有一条执行路径,则称为单线程程序。
- 多线程 : 一个进程如果有多条执行路径,则称为多线程程序
04.多线程的实现方式
- 继承Thread类的方式进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable和Future接口方式实现
方案一:继承Thread类
- 定义一个类MyThread继承Thread类
- 在MyThread类中重写run()方法
- 创建MyThread类的对象
- 启动线程
package com.itheima.threaddemo;
public class MyThread extends Thread{
@Override
public void run() {
//代码就是线程在开启之后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("线程开启了" + i);
}
}
}
--------------------------------------
package com.itheima.threaddemo;
public class Demo {
public static void main(String[] args) {
//创建一个线程对象1
MyThread myThread1 = new MyThread();
//创建另一个线程对象2
MyThread myThread2 = new MyThread();
//开启线程1
myThread1.start();
//开启线程2
myThread2.start();
}
}
两个小问题:
-
为什么要重写**run()**方法?
因为**run()**是用来封装被线程执行的代码
-
run( ) 方法和 **start( )**方法的区别?
run( ) ∶ 封装线程执行的代码,直接调用,相当于普通方法的调用,并没有开启线程。
start( ) : 启动线程,然后由 JVM 调用此线程的run()方法
方案二:实现Runnable接口
- 定义一个类MyRunnable实现Runnable接口
- 在MyRunnable类中重写run()方法
- 创建MyRunnable类的对象
- 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
- 启动线程
package com.itheima.threaddemo2;
public class MyRunnable implements Runnable {
@Override
public void run() {
//线程启动后执行的代码
for (int i = 0; i < 100; i++) {
System.out.println("Runnable接口实现多线程" + i);
}
}
}
----------------------------------
package com.itheima.threaddemo2;
public class Demo {
public static void main(String[] args) {
//创建一个参数对象
MyRunnable myRunnable = new MyRunnable();
//创建一个线程对象,并把参数传递给这个线程
//在线程启动之后,执行的就是参数里面的run()方法
Thread thread1 = new Thread(myRunnable);
//开启线程1
thread1.start();
MyRunnable myRunnable2 = new MyRunnable();
Thread thread2 = new Thread(myRunnable2);
//开启线程2
thread2.start();
}
}
方案三:Callable和Future
- 定义一个类MyCallable实现Callable接口
- 在MyCallable类中重写call()方法
- 创建MyCallable类的对象
- 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
- 创建Thread类的对象,把FutureTask对象作为构造方法的参数
- 启动线程
- 再调用get()方法,就可以获取线程结束之后的结果
package com.itheima.threaddemo3;
import java.util.concurrent.Callable;
public class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
for (int i = 0; i < 100; i++) {
System.out.println("跟女孩表白:" + i);
}
//返回值就表示线程运行结束之后的结果
return "OK";
}
}
---------------------------------------
package com.itheima.threaddemo3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//线程开启之后需要执行里面的call()方法
MyCallable myCallable = new MyCallable();
//可以获取线程执行完毕后的结果,也可以作为参数传递给Thread对象
FutureTask<String> futureTask = new FutureTask<>(myCallable);
//创建线程对象
Thread thread1 = new Thread(futureTask);
//开启线程1
thread1.start();
String s = futureTask.get();//get()方法一定要写在start()方法之后
System.out.println("s = " + s);
}
}
05.三种方式的对比
06.线程类的常用方法
获取线程的名称
- String getName() : 返回此线程的名称
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(getName() + " ---> " + i);
}
}
}
//线程是有默认名字的, 格式: Thread-编号 如 Thread-0
设置线程的名称
- void setName(String name) : 设置线程的名称
- 通过构造方法也可以设置线程名称
获取当前线程的对象
- public static Thread currentThread() :返回当前正在执行线程的引用
线程休眠
- public static void sleep(long time) : 让线程休眠指定的时间,单位为毫秒ms
线程调度
多线程的并发运行
计算机中的CPU,在任意时刻只能执行一条机器指令。每个线程只有获得CPU的使用权才能执行代码。
各个线程轮流获得CPU的使用权,分别执行各自的往务。
线程有两种调度模型
- 分时调度模型:所有线程**轮流**获得CPU的使用权,平均分配每个线程占用CPU的时间片
- 抢占式调度模型:优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会**随机**选择一个,优先级高的线程获取的CPU的时间片的概率相对大一些 (但在执行过程中,不一定真的能抢到,只是概率大一些)
Java中使用的是 抢占式调度模型
线程的优先级
- public final void setPriority(int new Priority) : 设置线程的优先级 (范围[1,10] 默认为5)
- public final int getPriority() : 获取线程的优先级
07.守护线程(后台线程)(备胎线程)
- public final void setDaemon(boolean on) : 设置为守护线程
当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了。
08.线程安全问题
为什么出现问题?(这也是我们判断多线程程序是否会有数据安全问题的标准)
- 多线程操作共享数据
如何解决多线程安全问题呢?
- 基本思想:让程序没有安全问题的环境
怎么实现呢?
- 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可
同步代码块 ( Synchronized 关键字)
锁多条语句操作共享数据,可以使用同步代码块实现
- 格式
synchronized(任意对象){
多条语句操作共享数据的代码;
}
- 默认情况下是打开的,只要有一个线程进去执行代码了,锁就会关闭。
- 当线程执行完出来了,锁才会自动打开
package com.itheima.threaddemo9;
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true){
synchronized (obj){//多个线程必须使用同一把锁
if (ticket <= 0) {
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + " 在卖票, 还剩 " + ticket + " 张票");
}
}
}
}
同步的好处与弊端:
- 好处:解决了多线程的数据安全问题
- 弊端:当线程很多时,因为每个线程都会判断同步上的锁,这是很耗费资源的,会降低程序的运行效率。
同步方法
同步方法:就是把synchronized关键字加到方法上
- 格式
修饰符 synchronized 返回值类型 方法名(参数列表) { }
同步代码块和同步方法的区别:
- 同步代码块可以锁住指定代码,同步方法是锁住方法中的所有代码
- 同步代码块可以指定锁对象,同步方法不能指定锁对象
同步方法的锁对象是什么呢?
- this
package com.itheima.threaddemo9;
public class Ticket implements Runnable {
//票的数量
private int ticket = 100;
private Object obj = new Object();
@Override
public void run() {
while (true){
boolean result = synchronizedMethod();
if(result) {
break;
}
}
}
private synchronized boolean synchronizedMethod() {
if (ticket <= 0) {
return true;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName() + " 在卖票, 还剩 " + ticket + " 张票");
return false;
}
}
同步静态方法
同步静态方法:就是把 synchronized 关键字加到静态方法上
- 格式
修饰符 static synchronized 返回值类型 方法名(参数列表) { }
同步静态方法的锁对象是什么呢?
- 类名.class
Lock锁
虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,未来更清晰地表达如何加锁和释放锁,JDK5以后新提供了一个锁对象Lock。
Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作
Lock中提供了获得锁和释放锁的方法:
- void lock()∶获得锁
- void unlock():释放锁
Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化
ReentrantLock的构造方法:
- ReentrantLock( ) : 创建一个ReentrantLock的实例
package com.itheima.threaddemo12;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
//初始票数
private int ticket = 100;
//锁对象lock
private ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
try {
lock.lock();//加锁
if (ticket <= 0){
break;
}
Thread.sleep(100);
ticket--;
System.out.println(Thread.currentThread().getName() + "--->"+"在买票,还剩" + ticket+"张票");
}catch (InterruptedException e){
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
}
}
}
09.死锁
线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。
public class Demo {
public static void main(String[] args) {
Object objA = new Object();
Object objB = new Object();
//线程1
new Thread(()->{
while (true) {
synchronized (objA) {
synchronized (objB) {
System.out.println("OOO 正在走路");
}
}
}
}).start();
//线程2
new Thread(()->{
while (true) {
synchronized (objB) {
synchronized (objA) {
System.out.println("XXX 正在走路");
}
}
}
}).start();
}
}
10.生产者消费者
生产者消费者模式概述
- 生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻
等待和唤醒方法
为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供我们使用,这几个方法在Object类中
Object类的等待和唤醒方法 :
11.线程池
背景
- 经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大
思路
- 提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的交通工具。
好处
- 提高响应速度(减少了创建新线程的时间)
- 降低资源损耗(重复利用线程池中的线程,不需要每次都创建)
- 便于线程管理(线程是稀缺资源,使用线程池可以统一分配调优监控)
线程池参数
- corePoo1Size :代表核心线程数,也就是正常情况下创建工作的线程数,这些线程创建后并不会消除,而是—种常驻线程
- maxinumPoo1size :代表的是最大线程数,它与核心线程数相对应,表示最大允许被创建的线程数,比如当前任务较多,将核心线程数都用完了,还无法满足需求时,此时就会创建新的线程,但是线程池内线程总数不会超过最大线程数
- keepAliveTime、unit 表示超出核心线程数之外的线程的空闲存活时间,也就是核心线程不会消除,但是超出核心线程数的部分线程如果空闲一定的时间则会被消除,我们可以通过setKeepA1iveTime来设置空闲时间
- workQueue :用来存放待执行的任务,假设我们现在核心线程都已被使用,还有任务进来则全部放入队列,直到整个队列被放满但任务还再持续进入则会开始创建新的线程
- ThreadFactory : 实际上是一个线程工厂,用来生产线程执行任务。我们可以选择使用默认的创建工厂,产生的线程都在同一个组内,拥有相同的优先级,且都不是守护线程。当然我们也可以选择自定义线程工厂,一般我们会根据业务来制定不同的线程工厂
- Handler : 任务拒绝策略,有两种情况,第一种是当我们调用shutdown等方法关闭线程池后,这时候即使线程池内部还有没执行完的任务正在执行,但是由于线程池已经关闭,我们再继续想线程池提交任务就会遭到拒绝。另一种情况就是当达到最大线程数,线程池已经没有能力继续处理新提交的任务时,这是也就拒绝
线程池相关API
JDK 5.0起提供了线程池相关API : ExecutorService 和 Executors
ExecutorService : 真正的线程池接口。常见实现类 ThreadPoolExecutor
- void execute (Runnable command):执行任务/命令,没有返回值,一般用来执行Runnable
- Future submit(Callable task):执行任务,有返回值,一般又来执行Callable
- void shutdown() : 关闭连接池
Executors : 工具类、线程池的工厂类,用于创建并返回不同类型的线程池
- Executors.newCachedThreadPool(): 创建一个可根据需要创建新线程的线程池
- Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
- Executors.newSingleThreadExecutor() : 创建一个只有一个线程的线程池
- Executors.newScheduledThreadPool(n): 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
package com.itheima.threaddemo14;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
class NumberThread implements Runnable{
@Override
public void run() {
System.out.println("线程执行的方法");
}
}
public class ThreadPool {
public static void main(String[] args) {
//提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
//设置线程池的属性
System.out.println(service.getClass());
service1.setCorePoolSize(145);
// service1.setKeepAliveTime();
//2.执行指定的线程的操作。需要提供实现Runnable接口或callable接口实现类的对象
service1.execute(new NumberThread());//适合适用于Runnable
// service.submit(Callable callable);//适合使用于Callable
// 3.关闭连接池
service1.shutdown();
}
}