多线程
1 程序,进程,线程
1.1 程序
程序(program)是为完成特定任务、用某种语言编写的的一组指令的集合。即指一段静态的代码,静态对象。
1.2 进程
进程 (process)是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程—生命周期
1.3 线程
1.3.1 线程(thread)
进程可进一步细化为线程,是一个程序内部的一条执行路径。
-
若一个线程同一时间并行执行多个线程,就是支持多线程的。
-
线程作为调度和执行的单位,每个线程拥有独立的运行栈和程序计数器(pc),线程切换的开销小。
-
一个进程的多个线程共享相同的内存单元/内存地址空间,多个线程操作共享的系统资源可能就会带来安全隐患。
1.3.2 并行与并发
并行:多个CPU同时执行多个任务。
并发:一个CPU(采用时间片)同时执行多个任务,宏观上表现为多线程。
2 线程的创建和使用
2.1 通过继承Tread类实现
Thread类
Thread类构造器:
-
Thread():创建一个Thread对象
-
Thread(String name):创建一个Thread对象并指定线程名
-
Thread(Runnable target):创建一个Thread对象并指定目标对象
-
Thread(Runnable target, String name):创建一个Thread对象、指定目标对象并指定线程名
Thread类相关方法:
-
void start():启动线程,执行run()方法
-
void run():线程被调度时执行的方法
-
String getName():获取线程的名字
-
void setName(String name):设置线程名
-
static Thread currentThread():获取当前线程
-
static void yield():线程让步。暂停当前线程,把时间片让给优先级相同或更高的线程
-
void jion():在线程执行时调用其他线程的jion()方法,当前线程将暂停执行,等待调用jion的线程执行完后继续执行
-
static void sleep(long millis):暂停当前线程
-
stop():强制线程生命期结束,不安全,不推荐使用
-
boolean isAlive():获取线程是否还活着
-
int getPriority():获取线程的优先级,默认为NORM_PRIORITY(5)
-
setPriority():设置线程的优先级。子线程继承父线程的优先级
java调度方法:先来先服务(FCFS)
线程优先级:
-
MAX_PRIORITY:10
-
MIN_PRIORITY:1
-
NORM_PRIORITY:5
public class ThreadClassTest { public static void main(String[] args) { // 3 通过new方法创建一个线程子类对象 Thread thread = new MyThread(); // 4 通过调用线程的start方法启动线程 thread.start(); } } // 1 MyThread类继承Thread类 class MyThread extends Thread { // 2 类里重写run方法 @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(i); } } }
只能通过start方法启动线程,直接thread.run()只是普通方法。
2.2 通过实现Runnable实现
Runnable接口里只声明了run方法,实现Runnable接口然后重写run方法。
在创建Thread对象时传入Runnable对象,在调用线程对象的run方法时,其实就是调用Runnable对象里的run方法。
public class RunnableTest {
public static void main(String[] args) {
// 3 创建Runnable子类对象
Runnable runnable = new MyRunnable();
// 4 将Runnable子类对象作为实参传入构造器,创建Thread对象
Thread thread = new Thread(runnable);
// 5 调用start方法启动线程
thread.start();
}
}
// 1 实现Runnable接口的类
class MyRunnable implements Runnable {
// 2 重写run方法
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
继承和实现的区别
-
继承Thread:线程代码存放在Thread子类run方法中
-
实现Runnable:线程代码存放在接口的子类run方法中
实现的好处
-
Java无法多继承,使用接口可以避免单继承的局限性
-
通过传入同一个Runnable子类对象创建多个线程,可用于多个相同线程处理同一份资源。
2.3 守护线程
Java中的线程分类:守护线程、用户线程
在线程启动前可通过thread.setDaemon(true)方法将一个线程对象变为守护线程
守护线程和主线程一起销毁,是用来服务用户线程的。
gc(垃圾回收)线程就是一个典型的守护线程。
非守护线程demo:
import lombok.SneakyThrows;
public class DaemonTest {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
@SneakyThrows
public void run() {
for (int i = 0; i < 5; i++) {
Thread.sleep(300);
System.out.println("执行非守护线程");
}
}
});
// thread.setDaemon(true);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("执行主线程");
}
System.out.println("主线程执行完毕");
}
}
执行结果:
守护线程demo:
import lombok.SneakyThrows;
public class DaemonTest {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
@SneakyThrows
public void run() {
for (int i = 0; i < 5; i++) {
Thread.sleep(300);
System.out.println("执行守护线程");
}
}
});
thread.setDaemon(true);
thread.start();
for (int i = 0; i < 5; i++) {
System.out.println("执行主线程");
}
System.out.println("主线程执行完毕");
}
}
执行结果:
因为守护线程随主线程一起销毁,在守护线程第一次等待300s的时候主线程就执行完了,故不会有任何输出。
3 线程的生命周期
线程有五种状态:
-
新建:线程刚创建时属于新建状态
-
就绪:线程调用start方法后,处于就绪状态,被分配到CPU资源时开始运行
-
运行:获取到CPU资源时进入运行状态,线程调用run方法开始运行
-
阻塞:调用了sleep方法或者wait()方法,运行状态转到阻塞状态
-
死亡:run方法执行完毕进入死亡状态,或调用stop方法
4 线程的同步
4.1 问题
多个线程对同一个资源文件的操作可能会引起数据错误
eg:
两个线程同时操作同一个储蓄卡账户
同时取3000元,若没有任何限制,判断余额时都会认为余额有足够的钱,则会出现余额为-1000的情况。
例子:
import lombok.SneakyThrows;
import org.junit.Test;
public class BankTest {
private int money = 5000;
class BankThread extends Thread{
@SneakyThrows
@Override
public void run() {
if (money > 3000){
// 模拟其他操作延时
for (int i = 0; i < 50000; i++) {
i++;
i--;
}
money -= 3000;
}
System.out.println(money);// 输出-1000
}
}
@Test
public void test() {
Thread thread1 = new BankThread();
Thread thread2 = new BankThread();
thread1.start();
thread2.start();
}
}
4.2 解决方法
同步机制:若线程中有共享数据的语句,必须保证一个线程执行完之后,才能让其他线程参与执行。
4.2.1 使用synchronized
使用synchronized修饰代码块或方法,第一个执行到时加上锁,在解锁前其他线程无法访问
同步代码块
synchronized(对象){
// 需要同步的代码块;
}
对象可以是任意对象,很多时候也指定为this或类名.class
同步方法
public synchronized void run() {
// 方法体
}
什么时候释放锁
- 方法/代码块执行完毕之后
- 方法/代码块中遇到break、return终止执行
- 遇到未处理的error或exception时,异常导致结束
- 遇到wait方法,暂停当前线程,释放锁
不会释放锁的操作
-
Thread.sleep():当前线程睡眠
-
Thread.yield()
-
调用当前线程的suspend方法(不推荐)
4.2.2 Lock
JDK5.0后,通过显示定义同步锁对象实现同步
ReentrantLock
ReentrantLock类实现了Lock接口,可以显示加锁(lock.lock()),释放锁(lock.unlock())
import java.util.concurrent.locks.ReentrantLock;
public class LockTest {
public static void main(String[] args) {
new ReentrantLockTest().start();
new ReentrantLockTest().start();
}
}
class ReentrantLockTest extends Thread{
private int money = 5000;
// 新建一个ReentrantLock对象
private final ReentrantLock lock = new ReentrantLock();
@Override
public void run() {
lock.lock();
try{
if (money > 3000){
// 模拟其他操作延时
for (int i = 0; i < 50000; i++) {
i++;
i--;
}
money -= 3000;
}
System.out.println(money);
}finally {
// 释放锁需要在finally里,避免代码出现异常无法释放锁
lock.unlock();
}
}
}
Lock与synchronized
- Lock是显示锁,需要手动创建和关闭,synchronized出了作用域自动释放
- Lock只能锁代码块,synchronized还可以用于方法上
- Lock性能更好,且具有扩展性
5 线程的通信
wait():使当前线程放弃CPU资源以及释放线程锁,等待其他线程调用notify或notifyAll方法唤醒。唤醒后从wait方法往后的代码开始执行
notify():唤醒等待队列中优先级最高的线程
notifyAll():唤醒所有等待队列中的线程
只能在synchronized代码块或synchronized方法中才能使用
例子:
生产消费问题:生产者生产了三个产品之后,消费者才能取;消费者全部取完之后,生产者才能开始生产。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.SneakyThrows;
public class ProducerAndCustomer2 {
public static void main(String[] args) {
Resource resource = new Resource();
Producer productThread = new Producer(resource);
Customer customerThread = new Customer(resource);
productThread.start();
customerThread.start();
}
}
@Data
@NoArgsConstructor
class Resource {
private int product = 0;
}
@Data
@AllArgsConstructor
class Producer extends Thread{
private Resource resource;
@Override
@SneakyThrows
public void run() {
System.out.println("生产者开始生产产品");
while (true) {
// 通过锁资源对象,实现同步
synchronized (resource) {
Thread.sleep((int) (Math.random() * 1000));
if (resource.getProduct() >= 3) {
// 生产三个之后调用wait方法,释放锁,消费线程开始运行
resource.wait();
} else {
resource.setProduct(resource.getProduct() + 1);
System.out.println("生产者生产了第" + resource.getProduct() + "产品");
// 此时只是唤醒另一个线程,但未释放锁,故无法消费
resource.notifyAll();
}
}
}
}
}
@Data
@AllArgsConstructor
class Customer extends Thread{
private Resource resource;
@Override
@SneakyThrows
public void run() {
System.out.println("消费者开始取走产品");
while (true) {
// 通过锁资源对象,实现同步
synchronized (resource) {
Thread.sleep((int) (Math.random() * 1000));
if (resource.getProduct() <= 0) {
// 取完之后调用wait方法,释放锁,消费线程开始运行
resource.wait();
} else {
System.out.println("消费者取走了第" + resource.getProduct() + "个产品");
resource.setProduct(resource.getProduct() - 1);
resource.notifyAll();
}
}
}
}
}
效果:
6 JDK5.0新增线程创建方式
6.1 通过实现Callable接口创建线程
import lombok.SneakyThrows;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class CallableTest {
@SneakyThrows
public static void main(String[] args) {
MyCallable callable = new MyCallable();
// 3 使用该方法需要FutureTask实现类的支持,用于得到返回值
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 4 得到返回值
Integer sum = futureTask.get();
System.out.println(sum);
}
}
// 1 实现Callable接口
class MyCallable implements Callable<Integer> {
// 2 重写call方法,返回值类型即为泛型
@Override
public Integer call(){
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += i;
}
return sum;
}
}
对比实现Runnable接口:
- 可以有返回值
- 可以抛出异常
- 支持泛型的返回值
- 需要借助FutureTask获取返回值
因为FutureTask同时实现了Runnable和Future接口,所以可以作为Runnable子类,作为实参传入Thread构造器。又可以作为Future得到Callable的返回值。
6.2 通过线程池创建线程
需要经常创建和销毁、使用量特别大的资源,可以使用线程池
6.2.1 通过Executors工厂创建
newCachedThreadPool方法
创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间(默认60s)后会回收,若线程数不够,则新建线程。
import lombok.SneakyThrows;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建一个可缓存的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
// 添加线程并执行
executorService.execute(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
});
}
}
}
效果:
线程数不够则新建一个线程
newFixedThreadPool方法
创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待(默认keepAliveTime = 0L)。
import lombok.SneakyThrows;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建一个固定大小为5的线程池
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
});
}
}
}
效果:
一共五个线程复用,不会新建多的线程,超出的线程会在队列中等待。
newScheduledThreadPool方法
创建一个周期性的线程池,支持定时及周期性执行任务。
import lombok.SneakyThrows;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ThreadPoolTest {
public static void main(String[] args) {
// 创建时传入线程池的大小
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
// schedule方法,第一个参数为Runnable子类对象,第二个参数为延迟的时间,第三个参数为时间的单位
// 延迟5s开始一起运行
// 注意方法是ScheduledExecutorService的schedule方法
executorService.schedule(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
}, 5, TimeUnit.SECONDS);
}
}
}
效果:
延迟5s之后开始执行线程
newSingleThreadExecutor方法
创建一个单线程的线程池,可保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
import lombok.SneakyThrows;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
});
}
}
}
效果:
只有一个线程一直在复用
6.2.2 通过ThreadPoolExecutor类自定义
ThreadPoolExecutor类构造器
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
重要参数列表:
-
corePoolSize:核心池的大小
-
maximumPoolSize:最大线程数
-
keepAliveTime:线程没有任务时最长存活时间
-
unit:时间单位
-
workQueue:阻塞队列,用来存储等待执行的任务
BlockingQueue接口的实现类:
(ctrl + alt + 左键可以查看接口的实现)
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- DelayQueue:使用优先级队列实现的无界阻塞队列
- LinkedBlockingDeque:由链表结构组成的双向阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界阻塞队列
- LinkedTransferQueue:由链表结构组成的无界阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列
所有队列均为线程安全,常用的为LinkedBlockingQueue和SynchronousQueue
import lombok.SneakyThrows;
import java.util.concurrent.*;
public class ThreadPoolTest {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(3, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5, true));
for (int i = 0; i < 10; i++) {
executorService.execute(new Runnable() {
@Override
@SneakyThrows
public void run() {
System.out.println(Thread.currentThread().getName());
Thread.sleep(1000);
}
});
}
}
}
推荐使用ThreadPoolExecutor方式创建,可以根据需要创建一个适合场景的线程池。
7 参考资料
http://www.atguigu.com/download_detail.shtml?v=129
https://www.cnblogs.com/pcheng/p/13540619.html
https://blog.csdn.net/weixin_38213517/article/details/82017659