长时间不用,已经忘得差不多了,差不多快到实习了,我先复习一下线程相关的知识。分两部分,第一部分是多线程基础,第二部分是JUC相关。
文章目录
Java 多线程
使用多线程的四种方法
- 继承 Thread
- 实现 Runnable 接口
- 实现 Callable 接口
- 线程池
继承 Thread
简单说就是继承 Thread 类,然后重写 run 方法,用该类创建对象,该对象调用 start 方法开启新的线程,完成 run 中重写的逻辑。
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread1 = new MyThread();
myThread1.start();
// 也可以用匿名内部类
new Thread(){
@Override
public void run(){
super.run();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+ " 第 " + i + " 次循环" );
}
}
}.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+ " 第 " + i + " 次循环" );
}
}
}
class MyThread extends Thread {
public MyThread() {
super();
}
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+ " 第 " + i + " 次循环" );
}
}
}
KEY POINT:
-
如果自己手动调用run()方法,那么就只是普通方法,没有启动多线程模式。
-
run()方法由JVM调用,什么时候调用,执行的过程控制都有操作系统的CPU调度决定。
-
想要启动多线程,必须调用start方法。
-
一个线程对象只能调用一次start()方法启动,如果重复调用了,则将抛出以上的异常
IllegalThreadStateException
。线程生命周期一旦结束,就不能再次调用start方法。
实现 Runnable 接口
Java 是单继承的,因此上面的方法会有一定的限制,还有我们比较推荐使用接口实现多线程。
简单说就是创建一个 Thread,将 实现的 Runnable 接口对象传入。
public class ThreadTest {
public static void main(String[] args) {
MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
Thread thread = new Thread(myThreadRunnable);
// 也可以用 lambda 表达式 这也是一种静态代理思想
// Thread 类是实现了 Runnable 接口的,Thread类用我们自己写的逻辑代替了本身的run逻辑
new Thread(() -> {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 第 " + i + " 次循环");
}
}).start();
thread.start();
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+ " 第 " + i + " 次循环" );
}
}
}
class MyThreadRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+ " 第 " + i + " 次循环" );
}
}
}
KEY POINT:
- 实现接口后当作形参传入 Thread 类 中 实例化
- 没有类的单继承性的限制
- 如果线程之间有共享数据,更适合用这种方式实现,不用static(只创建一个对象,之后把这个对象分别传给不同的Thread)
public class ThreadTest {
public static void main(String[] args) {
MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
Thread thread1 = new Thread(myThreadRunnable);
Thread thread2 = new Thread(myThreadRunnable);
thread1.start();
thread2.start();
}
}
class MyThreadRunnable implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (ticket > 0) {
System.out.println(
Thread.currentThread().getName() + "售出一张票,还剩 " + --ticket + " 张");
}
}
}
实现 Callable 接口
Callable 接口类似于 Runnable 接口,区别在于它能抛出异常,也能有返回值,另外把 run 方法换成了 call 方法。
Callable 接口实现的线程类,需要 FutureTask
实现类的支持,用于接收运算结果。 FutureTask
是 Future 接口的实现类
public class ThreadTest {
public static void main(String[] args) throws Exception {
MyThreadCallable myThreadCallable = new MyThreadCallable();
FutureTask<String> task2 = new FutureTask<>(myThreadCallable);
FutureTask<String> task1 = new FutureTask<>(myThreadCallable);
new Thread(task1).start();
new Thread(task2).start();
System.out.println("窗口1公告: " + task1.get());
System.out.println("窗口2公告: " + task2.get());
}
}
class MyThreadCallable implements Callable<String> {
private int ticket = 20;
@Override
public String call() throws Exception {
while(ticket > 0)
System.out.println(Thread.currentThread().getName() + "卖出一张,还剩票数:" + --ticket);
return "没有余票";
}
}
# 还有一种写法
public class ThreadTest {
public static void main(String[] args) throws Exception {
MyThreadCallable myThreadCallable = new MyThreadCallable();
ExecutorService service = Executors.newFixedThreadPool(1);
Future<String> future = service.submit(myThreadCallable);
System.out.println("窗口1公告: " + future.get());
service.shutdown();
}
}
class MyThreadCallable implements Callable<String> {
private int ticket = 20;
@Override
public String call() throws Exception {
while(ticket > 0)
System.out.println(Thread.currentThread().getName() + "卖出一张,还剩票数:" + --ticket);
return "没有余票";
}
}
这里使用到了 FutureTask
这个 Future
接口的唯一实现类,顺便简单介绍一下 Future 接口
- Future 接口 可以对 Runnable ,Callable 任务的执行结果进行取消,查询是否完成,获取结果等等。
FutureTask
同时实现了 Runnable 和 Callable 接口,它既可以作为 Runnable 被线程执行,又可以作为 Future 获取 Callable 的返回值(没试过)
这点能说很多,也很重要,我们后面再补充
java.util.concurrent.*;
ExecutorService
Future
线程常用方法
start(): 启动当前线程,调用当前线程的 run()
Thread.currentThread(): 静态方法,返回执行当前方法的线程
getName() & setName():顾名思义,线程的名字与设置
Thread.yield(): 不释放锁,释放cpu, 重新和别的线程竞争 cpu,所以可能 yield 不了
wait(): 等待,释放锁,释放cpu,需要被notify() 或者 notifyAll()
Thread.sleep(): 不释放锁,释放cpu 需要 try catch
join():父线程释放锁,子线程抢占cpu。用我的话讲就是插队,a线程执行过程中调用b线程的 threadB.join(), a就进入阻塞状态,等待b线程逻辑执行完,a结束阻塞,实际是一个子线程是否存活的判断,存活就 wait() 所以是要释放锁的
isAlive(): 判断线程是否存活
setPriority() & getPriority():设置线程优先级 最小是1,最大是10,只是抢占 cpu 的概率更大,不是一定获得
简单说,只有 wait() 会释放锁
线程一般不推荐使用 stop(),destroy() 方法停止,一般等待自己运行完毕,或者设置标志位
public class ThreadTest {
public static void main(String[] args) {
MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
Thread thread = new Thread(myThreadRunnable);
thread.start();
new Thread(() -> {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + " 第 " + i + " 次循环");
if(i == 10){
Thread.sleep(3000)
myThreadRunnable.stop();
System.out.println("停止子线程");
}
}
}).start();
}
}
class MyThreadRunnable implements Runnable{
private boolean flag = true;
@Override
public void run(){
while(flag){
System.out.println(Thread.currentThread().getName() + "正在运行");
}
}
// 对外暴露停止方法
public void stop(){
this.flag = false;
}
}
守护线程
Java中的线程分为两类:一种是守护线程,一种是用户线程。
它们在几乎每个方面都是相同的,唯一的区别是判断JVM何时离开。
守护线程是用来服务用户线程的,通过在start()方法前调用thread.setDaemon(true)可以把一个用户线程变成一个守护线程。
Java垃圾回收就是一个典型的守护线程。
若JVM中都是守护线程,当前JVM将退出,jvm 不关心守护线程是否执行完毕,只等待用户线程执行完毕
public class ThreadTest {
public static void main(String[] args) {
MyThreadRunnable myThreadRunnable = new MyThreadRunnable();
Thread thread = new Thread(myThreadRunnable);
thread.setDaemon(true);
thread.start();
}
}
线程的同步 / 同步监视器 / 锁
java 实际上是使用了操作系统中互斥同步的管程技术
我们锁的是要操作的共享对象
synchronized 同步代码块
注意,谁是变的就锁谁,这个地方锁的是 account,但是如果锁的是 this 就错了
优先于同步方法,落后于 ReentrantLock
public class ThreadTest {
public static void main(String[] args) {
Account account = new Account(1000,"gale");
Drawing myDrawing = new Drawing(500, account);
Drawing hisDrawing = new Drawing(600, account);
new Thread(myDrawing).start();
new Thread(hisDrawing).start();
}
}
class Account {
int money;
String name;
public Account(int money, String name) {
this.money = money;
this.name = name;
}
}
class Drawing implements Runnable {
int drawMoney;
Account account;
public Drawing(int drawMoney, Account account) {
this.drawMoney = drawMoney;
this.account = account;
}
@Override
public void run() {
synchronized (account) {
if (account.money < drawMoney) {
System.out.println("余额不足");
} else {
account.money -= drawMoney;
System.out.println("钱已经取出,当前余额" + account.money);
}
}
}
}
synchronized 同步方法
两个方法可以被同一个同步监视器监视,这个地方锁的是这个方法,或者说是this
public class ThreadTest {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread thread0 = new Thread(buyTicket);
Thread thread1 = new Thread(buyTicket);
Thread thread2 = new Thread(buyTicket);
thread0.start();
thread1.start();
thread2.start();
}
}
class BuyTicket implements Runnable {
private int ticketNum = 10;
boolean flag = true;
@Override
public void run() {
while (flag) {
try {
buy();
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized void buy() throws InterruptedException {
if (ticketNum <= 0) {
flag = false;
return;
}
System.out.println(
Thread.currentThread().getName() + "买了一张票,还剩 " + --ticketNum + "张");
}
}
lock锁 / ReentrantLock
显式定义锁,需要手动释放锁
public class ThreadTest {
public static void main(String[] args) {
BuyTicket buyTicket = new BuyTicket();
Thread thread0 = new Thread(buyTicket);
Thread thread1 = new Thread(buyTicket);
Thread thread2 = new Thread(buyTicket);
thread0.start();
thread1.start();
thread2.start();
}
}
class BuyTicket implements Runnable {
private int ticket = 100;
// 实例化锁
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while(true){
try {
// 调用锁定方法
lock.lock();
if (ticket > 0){
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "售出车票: 车票号" + ticket--);
}else{
break;
}
} finally {
// 调用解锁方法
lock.unlock();
}
}
}
}
释放锁和死锁
- 释放锁
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用**Thread.sleep()、Thread.yield()**方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的**suspend()**方法将该线程挂起,该线程不会释放锁(同步监视器)。
**应尽量避免使用suspend()和resume()**来控制线程
- 死锁
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
线程的通信
- wait() 如果不被 notify() / notifyAll() 唤醒一个/所有被wait的线程 就一直阻塞
- wait() 方法会释放锁
- 只能在同步代码块和同步方法中,reentranlock都不行
- 这三个方法的调用者必须是同步代码块的同步监视器 synchronized(这里的对象)
class Communication implements Runnable {
private int i = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
notify();
if (i <= 100) {
System.out.println(Thread.currentThread().getName() + ":" + i++);
} else
break;
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
生产消费者模式
- 管程法,中间增加缓冲区
- 信号灯法,用 flag
二者的区别在于,后者只能我生产一个,你消费一个,消费完了我才能再生产,前者有缓冲区,可以一次性生产多个。
import java.sql.Connection;
import java.util.concurrent.locks.ReentrantLock;
/*
* 生产者消费者模型 管程法
* */
public class ThreadTest{
public static void main(String[] args) {
SynContainer synContainer = new SynContainer();
Consumer consumer = new Consumer(synContainer);
Productor productor = new Productor(synContainer);
consumer.start();
productor.start();
}
}
class Productor extends Thread {
SynContainer container;
public Productor(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i < 101; i++) {
System.out.println("生产者生产了ID为" + i + "的鸡");
container.push(new Chicken(i));
}
}
}
class Consumer extends Thread {
SynContainer container;
public Consumer(SynContainer container) {
this.container = container;
}
@Override
public void run() {
for (int i = 1; i < 101; i++) {
System.out.println("消费者消费了ID为" + container.pop().id + "的鸡");
}
}
}
//产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
//缓冲区
class SynContainer {
Chicken[] chickens = new Chicken[10];
int count = 0;
public synchronized void push(Chicken chicken) {
while (count == chickens.length) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
chickens[count] = chicken;
count++;
this.notifyAll();
}
public synchronized Chicken pop() {
while (count == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
count--;
Chicken chicken = chickens[count];
this.notifyAll();
return chicken;
}
}
/*
* 生产者消费者模型 信号灯标志位法
* */
public class ThreadTest{
public static void main(String[] args) {
Restaurant restaurant = new Restaurant();
Consumer consumer = new Consumer(restaurant);
Productor productor = new Productor(restaurant);
consumer.start();
productor.start();
}
}
class Productor extends Thread {
Restaurant restaurant;
public Productor(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
for (int i = 1; i < 101; i++) {
restaurant.cook(new Chicken(i));
}
}
}
class Consumer extends Thread {
Restaurant restaurant;
public Consumer(Restaurant restaurant) {
this.restaurant = restaurant;
}
@Override
public void run() {
for (int i = 1; i < 101; i++) {
restaurant.eat();
}
}
}
//产品
class Chicken {
int id;
public Chicken(int id) {
this.id = id;
}
}
class Restaurant{
boolean flag = true;
Chicken chicken;
public synchronized void cook(Chicken chicken){
if (!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者生产了" + chicken.id);
this.notifyAll();
this.chicken = chicken;
this.flag = !this.flag;
}
public synchronized void eat(){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费者消费了 " + chicken.id);
this.notifyAll();
this.flag = !this.flag;
}
}
线程池
创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长
线程池由任务队列和工作线程组成,它可以重用线程来避免线程创建的开销,在任务过多时通过排队避免创建过多线程来减少系统资源消耗和竞争,确保任务有序完成
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
如果线程的创建和销毁时间相比执行任务时间较长,推荐使用线程池技术
使用线程池的好处:
-
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
-
提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
-
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
线程池也可以看作是一种缓存策略
public class ThreadPoolTest {
public static void main(String[] args) throws InterruptedException {
Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<>();
ExecutorService executorService0 = Executors.newFixedThreadPool(10);
// ExecutorService executorService0 = Executors.newCachedThreadPool();
// ExecutorService executorService0 = Executors.newSingleThreadExecutor();;
for(int i = 0; i < 100; i++){
executorService0.execute(new MyTask(i));
}
executorService0.shutdown();
executorService0.awaitTermination(1,TimeUnit.DAYS);
System.out.println("时间:" + (System.currentTimeMillis() - start)); // 16889 : 30
System.out.println("大小:" + list.size());
}
}
class MyTask implements Runnable{
int i = 0;
public MyTask(int i) {
this.i = i;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "--" + i);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
处理流程
提交一个任务到线程池中,线程池的处理流程如下:
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
核心线程 —— 工作队列 —— 非核心线程 —— 饱和策略
线程池中的7种重要的参数
1. corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收
2. maximumPoolSize就是线程池中可以容纳的最大线程的数量
3. keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间,因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,
4. util,就是计算这个时间的一个单位。
5. workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
6. threadFactory,就是创建线程的线程工厂。
7. handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
五种线程池的使用场景
newSingleThreadExecutor:一个单线程的线程池,可以用于需要保证顺序执行的场景,并且只有一个线程在执行。
newFixedThreadPool:一个固定大小的线程池,可以用于已知并发压力的情况下,对线程数做限制。
使用LinkedBlockingQueue队列无限增加,内存受限
newCachedThreadPool:一个可以无限扩大的线程池,比较适合处理执行时间比较小的任务。
是线程无限增加,会受硬件影响,不可能无限扩大,受CPU限制
newScheduledThreadPool:可以延时启动,定时启动的线程池,适用于需要多个后台线程执行周期任务的场景。
newWorkStealingPool:一个拥有多个任务队列的线程池,可以减少连接数,创建当前可用cpu数量的线程来并行执行。
本质上他们都是调用的 ThreadPoolExecutor 方法
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
线程池都有哪几种工作队列
1、ArrayBlockingQueue
是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
2、LinkedBlockingQueue
一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
3、SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
4、PriorityBlockingQueue
一个具有优先级的无限阻塞队列。
提交优先级和执行优先级
核心线程 —— 工作队列 —— 非核心线程 —— 饱和策略
但是执行优先级
核心线程 —— 非核心线程 —— 工作队列
线程池的拒绝策略
当请求任务不断的过来,而系统此时又处理不过来的时候,我们需要采取的策略是拒绝服务。RejectedExecutionHandler接口提供了拒绝任务处理的自定义方法的机会。在ThreadPoolExecutor中已经包含四种处理策略。
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。一般采用
CallerRunsPolicy 策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前的被丢弃的任务。
DiscardOleddestPolicy策略: 该策略将丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。
DiscardPolicy策略:该策略默默的丢弃无法处理的任务,不予任何处理。
除了JDK默认提供的四种拒绝策略,我们可以根据自己的业务需求去自定义拒绝策略,自定义的方式很简单,直接实现RejectedExecutionHandler接口即可。
执行 execute()方法和 submit()方法的区别是什么呢
两个方法都可以向线程池提交任务
execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;
submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 Future 的 get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。
Java JUC
写在一起太长了,分两篇吧,见下篇。