多线程
并发
在同一时刻,有多个指令在单个CPU上交替执行
CPU在多个线程之间交替执行
并行
在同一时刻,有多个指令在多个CPU上同时执行
多线程的实现方式
- 继承Thread类的方法进行实现
- 实现Runnable接口的方式进行实现
- 利用Callable接口和Future接口方式实现
继承于Thread类
//Thread子类
public class MyThread extends Thread {
public MyThread(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + "Hello world!");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread("thread1: ");
MyThread thread2 = new MyThread("thread2: ");
thread2.start();
thread1.start();
}
}
Runnable方法实现
- 自定义一个类实现Runnable接口
- 重写里面的run方法
- 创建自己的类对象
- 创建一个Thread对象开启线程
public class MyThread implements Runnable {
public MyThread() {
}
@Override
public void run() {
Thread curThread = Thread.currentThread();
for (int i = 0; i < 10; i++) {
System.out.println(curThread.getName() + " Hello");
}
}
}
public class Main {
public static void main(String[] args) {
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
Thread thread3 = new Thread(thread1);
Thread thread4 = new Thread(thread2);
thread3.start();
thread4.start();
}
}
Callable与Future接口
特点:可以获取多线程运行的结果
- 创建一个MyCallable实现Callable接口
- 重写call(有返回值的,表示多线程运行的结果)
- 创建MyCallable对象,表示多线程要执行的任务
- 创建Future对象(用来管理多线程运行的结果)
- Future是一个接口,要创建它的实现类FutureTask
- 创建Thread类对象然后启动线程
import java.util.concurrent.Callable;
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 100;
}
}
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable mc = new MyCallable();
FutureTask<Integer> fc = new FutureTask<>(mc);
Thread thread = new Thread(fc);
thread.start();
System.out.println(fc.get());
}
}
Thread常见的成员方法
方法 | 作用 |
---|---|
String getName() | 返回此线程的名称 |
void setName(String name) | 设置线程名字,构造方法也可以设置 |
static Thread currentThread() | 获取当前线程对象 |
static void sleep(long time) | 让线程休眠指定的时间,单位为毫秒 |
setPriority(int newPriority) | 设置线程的优先级 |
final int getPriority() | 获取线程的优先级 |
final void setDaemon(boolean on) | 设置为守护线程 |
public static void yield() | 出让/礼让线程 |
public static void join() | 插入/插队线程 |
get/setName上面已经使用过
需要注意的是,如果不设置线程的名字,线程有默认的名字Thread-X
格式(x从0开始计数)
关于主线程的一点:
当JVM虚拟机启动之后,会自动启动多条线程,其中有一条叫做main线程,它的作用就是去调用main方法并执行里面的代码,我们以前所写的所有代码都是运行在main线程中
优先级
抢占式调度:随机性
非抢占式调度:顺序性
随机等级:1-10
默认优先级都为5
main线程的优先级也为5
优先级越高抢到cpu的概率也越高
可以使用对应的get/setPriority方法调整优先级
守护线程
setDaemon设置为守护线程(备胎线程)
其他非守护线程执行完毕后,守护线程也会陆续结束
public class Main {
public static void main(String[] args) {
MyCallable thread1 = new MyCallable();
MyCallable thread2 = new MyCallable();
Thread thread3 = new Thread(thread1, "3: ");
Thread thread4 = new Thread(thread2, "4: ");
thread4.setPriority(1);
thread3.setPriority(10);
thread4.setDaemon(true);
thread3.start();
thread4.start();
}
}
线程3执行完后就会告诉线程4他不需要继续执行了,然后他就会停止
比如说聊天窗口和文件发送,聊天窗口关闭后,正在发送的文件也没必要继续发送了
礼让线程
出让线程的执行权,让当前抢到CPU的线程让出其执行权,再来让所有的线程竞争执行权。
尽可能的让线程更加的均匀。
public void run() {
Thread curThread = Thread.currentThread();
for (int i = 0; i < 100; i++) {
System.out.println(curThread.getName() + " Hello" + i);
Thread.yield();
}
}
插入线程
把对应线程插入到当前线程之前,对应线程执行完毕后再执行当前线程
Thread thread = new Thread(new MyCallable());
thread.start();
thread.join();
for (int i = 0; i < 10; i++) {
System.out.println("main");
}
线程的执行周期
同步代码块
把操作共享数据的代码块锁起来
格式如下:
synchronized(锁){
操作共享数据的代码
}
- 锁默认打开,当有一个线程进去了,锁自动关闭
- 里面的代码全部执行完毕,线程出来,锁自动打开
其中的锁对象要是唯一的,可以是任意一个对象,但是在多个线程中也要是唯一的,可以在类中声明一个静态的对象作为锁。
public class MyCallable extends Thread {
static private int tickets = 100;
static private Object lock = new Object();
@Override
public void run() {
synchronized (lock) {
while (true) {
if (tickets <= 0)
break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets--;
System.out.println("卖出了一张票,还有" + tickets + "张票");
}
}
}
}
上面的代码中有一点小问题,其中synchronized块不可以放在循环的外边,因为如果放在外边,在第一个线程抢夺到CPU的执行权后就会开始循环,就会将票全部卖出去,因此需要放到循环内部,卖出一张票就解开锁。
public void run() {
while (true) {
synchronized (lock) {
if (tickets <= 0)
break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets--;
System.out.println("卖出了一张票,还有" + tickets + "张票");
}
}
}
还需要注意,锁对象一定要唯一,如果不唯一,相当于没有写
这个锁一般来写本文件的字节码对象:
类名.class
synchronized (MyCallable.class) {
if (tickets <= 0)
break;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
tickets--;
System.out.println("卖出了一张票,还有" + tickets + "张票");
}
}
同步方法
如果要把一个方法中的所有代码块都锁起来,可以把关键字加到方法上
修饰符 synchronized 返回类型 方法名(参数){
}
- 同步方法是锁住方法中的所有代码
- 锁对象不能自己指定
- 非静态时锁对象为this
- 静态时锁对象为当前类的字节码文件对象
把锁住的代码块做成一个方法,然后加上修饰符即可
StringBuilder对象是多线程不安全的,如果要多线程构造字符串请使用StringBuffer
Lock锁
JDK5以后提供了一个新的锁对象Lock,方便我们更加清晰地表达如何加锁释放锁
void lock();获得锁
void unlock();释放锁
手动上锁,手动释放锁
Lock是一个接口,我们需要使用它的实现类ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class MyCallable extends Thread {
private int tickets = 100;
private Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
lock.lock();
if (saleTicket()) break;
}
}
private boolean saleTicket() {
try {
if (tickets <= 0)
return true;
Thread.sleep(10);
tickets--;
System.out.println(Thread.currentThread().getName() + " 卖出了一张票,还有" + tickets + "张票");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return false;
}
}
要注意锁不打开直接break出循环,造成其他两个线程在锁前等待的情况。所以通常要用try catch块将代码包围,然后使用finally解锁
死锁
是一种书写代码的错误
比如说下面的情况
等待唤醒机制
生产者消费者是典型的多线程协作模式
消费者机制:
- 判断桌子上是否有食物
- 如果没有就等待
- 如果有就开吃
- 吃完之后唤醒厨师继续做
生产者机制:
- 判断桌子上是否有食物
- 有则等待
- 无则制作
- 把食物放在桌子上
- 叫醒等待的消费者开吃
方法 | 作用 |
---|---|
void wait() | 当前线程等待,直到被其他线程唤醒 |
void notify() | 随机唤醒单个线程 |
void notifyAll() | 唤醒所有线程 |
//生产者
public class Productor extends Thread {
public void run() {
while (true) {
synchronized (tempFlag.lock){
if (tempFlag.count == 0)
break;
else {
if (tempFlag.tempFlag == 1) {
try {
tempFlag.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
System.out.println("厨师做了一碗面条");
tempFlag.tempFlag = 1;
tempFlag.lock.notifyAll();
}
}
}
}
}
}
//消费者
public class Consumer extends Thread {
public void run() {
while (true) {
synchronized (tempFlag.lock) {
if (tempFlag.count == 0)
break;
else {
if (tempFlag.tempFlag == 0) {
try {
tempFlag.lock.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
tempFlag.count--;
System.out.println("还可以再吃" + tempFlag.count + "碗");
tempFlag.tempFlag = 0;
tempFlag.lock.notifyAll();
}
}
}
}
}
}
//桌子
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class tempFlag {
public static int tempFlag = 1;
public static int count = 10;
public static final Lock lock = new ReentrantLock();
}
//测试类
public class Main {
public static void main(String[] args) throws InterruptedException {
Productor productor = new Productor();
Consumer consumer = new Consumer();
productor.setName("生产者");
consumer.setName("消费者");
productor.start();
consumer.start();
}
}
以上是一种实现等待唤醒机制的方式,下面还有第二种方式,阻塞队列方式实现:
需要注意生产者和消费之需要使用同一个阻塞队列
阻塞的话是不需要锁的,底层自动有锁和释放(put和take)
线程池
- 创建一个池子,池子是空的
- 提交任务时,池子会创建新的线程对象,任务执行完毕后,线程归还给池子,下次再提交任务时,不需要创建新的线程,直接复用已有的线程即可
- 但是如果提交任务时,池子中没有空闲线程,也无法创建新的线程,任务就会排队等待
方法 | 作用 |
---|---|
public static ExecutorService newCachedThreadPool() | 创建一个没有上限的线程池 |
public static ExecutorService newFixedThreadPool(int nThreads) | 创建有上限的线程池 |
submit(参数) | 将任务交给线程池 |
shutdown() | 销毁线程池 |
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(new A());
pool.submit(new A());
pool.submit(new A());
pool.submit(new A());
pool.submit(new A());
pool.submit(new A());
pool.shutdown();
自定义线程池
任务拒绝策略
策略 | 说明 |
---|---|
ThreadPoolExecutor. AbortPolicy | 默认策略,抛出异常 |
ThreadPoolExecutor. DiscardPolicy | 丢弃任务但是不抛弃异常 |
ThreadPoolExecutor. DiscardOldestPolicy | 丢弃队列中等待最久的任务,然后把当前任务加入队列中 |
自定义线程池可以使用ThreadPoolExecutor对象,在创建时定义指定的参数即可。
ThreadPoolExecutor();
//参数一:核心线程数量
//参数二:最大线程数
//参数三:空闲线程最大存活时间
//参数四:时间的单位
//参数五:任务队列
//参数六:创建线程工厂
//参数七:任务的拒绝策略,注意此为静态内部类
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 6, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
然后使用submit提交shutdown关机即可
最大并行数
可以使用下面的代码查看电脑的最大并行数
System.out.println(Runtime.getRuntime().availableProcessors());
线程池多大合适
如果为CPU密集型运算:最大并行数加一
数据运算较多,IO较少
加一是为了预防前面的线程出问题,可以当备用线程
IO密集型运算:
最大并行数
∗
期望
C
P
U
利用率
∗
总时间
C
P
U
计算时间
总时间
=
C
P
U
计算时间
+
等待时间
最大并行数*期望CPU利用率*\frac{总时间}{CPU计算时间}\\ 总时间=CPU计算时间+等待时间
最大并行数∗期望CPU利用率∗CPU计算时间总时间总时间=CPU计算时间+等待时间
CPU计算时间等需要使用工具测试,比如说thread dump
读取数据库或者其他IO操作较多