这篇是Java多线程的入门基础,主要介绍什么是多线程、多线程的创建方式、常用方法。引发的问题及锁的相关知识。
📌什么是多线程?
⭐️进程与线程的区别:
进程:进程是指运行在电脑内存中的应用程序,一个进程至少有一个线程。例如360杀毒软件、QQ。
线程:线程指一个进程中的执行流程,例如360杀毒软件是一个进程,我们可以在让它杀毒的同时,也让他清理垃圾,这就开启了两条线程。
⭐️ 普通方法与多线程
我们都知道,在main()方法里调用其他方法时,会等所调方法执行完,再继续执行main()方法。
而多线程是与其他线程同时执行的。
📌多线程的创建方式
多线程有四种创建方式,这里先说前三种,带大家感受一下什么是多线程。
⭐️继承Thread类,重写run方法
package thread;
/**
* @ClassName: ExtendsThread
* @Description: 通过继承Thread创建多线程
* @Author: Wen
* @date: 2022/5/11 8:30
*/
//继承Thread类
public class ExtendsThread extends Thread{
//重写run方法
@Override
public void run() {
//方法体:打印50次“我是线程A---”及次数
for(int i = 0;i <=50;i++){
System.out.println("我是线程A---" + i);
}
}
public static void main(String[] args) {
//我们new一个对象并且让他调用start进入“就绪”状态,等待CPU调度
new ExtendsThread().start();
//主函数里我们让其打印1000次“我是主线程”
for(int i = 0;i<=1000;i++){
System.out.println("我是主线程---" + i);
}
/**
* 如果是普通方法的话,他的执行顺序应该是:1.打印50次“我是线程A” 2.打印1000次“我是主线程”
* 但是我们写的是多线程,预测执行应该是“我是主线程”和“我是线程A”交替打印。
* ps:因为我们没加入sleep(),所以你可能看到的不是交替打印的。可以多执行几次,或者拉倒最上面看
*/
}
}
执行结果如下图,可以看出,两条线程是交替乱序执行的。谁抢到CPU谁就执行。如果没有这种效果的小伙伴可以多执行几次代码(这里先不说让线程睡眠)。
⭐️实现Runnable接口创建多线程(重点,常用)
package thread;
/**
* @ClassName: ImplementsRunnable
* @Description: 通过实现Runnable创建多线程
* @Author: Wen
* @date: 2022/5/11 8:59
*/
//实现Runnable接口
public class ImplementsRunnable implements Runnable{
//重写run方法
@Override
public void run() {
for(int i = 0;i <=50;i++){
System.out.println("我是线程A---" + i);
}
}
public static void main(String[] args) {
//
ImplementsRunnable implementsRunnable = new ImplementsRunnable();
new Thread(implementsRunnable).start();
//主函数里我们让其打印1000次“我是主线程”
for(int i = 0;i<=1000;i++){
System.out.println("我是主线程---" + i);
}
}
}
执行结果如下图,也是交替执行。
⭐️实现Callable接口创建有返回值的多线程(了解即可)
package thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* @ClassName: ImplementsCallable
* @Description: 通过实现Callable创建多线程
* @Author: Wen
* @date: 2022/5/11 9:50
*/
public class ImplementsCallable implements Callable {
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 0;i <=50;i++){
sum += i;
}
return sum;
}
public static void main(String[] args) {
ImplementsCallable implementsCallable = new ImplementsCallable();
FutureTask futureTask = new FutureTask(implementsCallable);
new Thread(futureTask).start();
try{
Object sum = futureTask.get();
System.out.println(sum);
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
小结:继承Thread类的方法,我们不建议使用,因为Java是单继承。推荐使用实现Runnable接口的方法,因为他避免了单继承的局限性,方便同一个对象被多个线程使用。
📌 线程安全问题
多个线程使用共享数据时,会引发线程安全问题,下面是个小demo,来体验一下。
package com.example.study2.util;
/**
* @ClassName: ThreadCount
* @Description: 线程安全问题
* @Author: Wen
* @date: 2022/7/29 9:25
*/
public class ThreadCount implements Runnable{
private int count = 100;
@Override
public void run() {
while (true){
if (count>=1){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +" " + count--);
}
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
上面的代码,我们实现从100递减打印到1,开启两条线程,中间让线程休眠以让另一条线程可以抢到cpu。
可以看到,两条线程确实交替打印,但是会有重复数据的情况,这便是线程安全问题。
引发的原因很简单也很好理解,两条线程同时读取到count值是100,进行打印。
⭐️解决线程安全问题
我们可以使用锁来解决线程安全问题,锁分为synchronized锁和lock锁,其中synchronized锁属于API级别,lock锁属于jvm级别。
🍺synchronized解决线程安全问题
- synchronized修饰代码块
package com.example.study2.util;
/**
* @ClassName: ThreadSyncTest
* @Description: TODO
* @Author: Wen
* @date: 2022/8/2 11:39
*/
public class ThreadSyncTest implements Runnable{
//全局共享变量
private static int count = 100;
//自定义锁
private String sync = "sync";
@Override
public void run() {
while (true){
if (count>=1){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
* synchronized修饰代码块,sync是我们声明的String类型对象
*/
synchronized (sync){
System.out.println(Thread.currentThread().getName() +" " + count--);
}
}
}
}
public static void main(String[] args) {
//两条线程
new Thread(new ThreadSyncTest()).start();
new Thread(new ThreadSyncTest()).start();
}
}
由上图我们可以看到,线程交替执行,并且没有重复的数据。证明加锁成功。
- synchronized修饰实例方法
package com.example.study2.util;
/**
* @ClassName: ThreadSyncTest
* @Description: TODO
* @Author: Wen
* @date: 2022/8/2 11:39
*/
public class ThreadSyncTest implements Runnable{
//全局共享变量
private static int count = 100;
//自定义锁
private String sync = "sync";
@Override
public void run() {
while (true){
if (count>=1){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
print();
}
}
}
public synchronized void print(){
System.out.println(Thread.currentThread().getName() +" " + count--);
}
public static void main(String[] args) {
//两条线程
ThreadSyncTest threadSyncTest = new ThreadSyncTest();
new Thread(threadSyncTest).start();
new Thread(threadSyncTest).start();
}
}
synchronized修饰实例方法相当于this锁,此时new Thread(threadSyncTest).start();中要使用同一个对象,否则还会出现线程安全问题。
synchronized修饰静态方法
public synchronized static void print(){
System.out.println(Thread.currentThread().getName() +" " + count--);
}
修饰静态方法相当于Object.class锁
🍺Lock锁解决线程安全问题
package com.example.study2.util;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @ClassName: ThreadSyncTest
* @Description: TODO
* @Author: wenlong
* @date: 2022/8/2 11:39
*/
public class ThreadSyncTest implements Runnable{
//全局共享变量
private static int count = 100;
//自定义锁
private String sync = "sync";
private final Lock lock = new ReentrantLock();
@Override
public void run() {
while (true){
if (count>=1){
try {
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
print();
}
}
}
public void print(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() +" " + count--);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
//两条线程
ThreadSyncTest threadSyncTest = new ThreadSyncTest();
new Thread(threadSyncTest).start();
new Thread(threadSyncTest).start();
}
}
需要注意的是,Lock锁是手动加锁和释放锁,所以我们需要把释放锁写在finally里,防止因为出现异常而没有释放锁,导致死锁问题。
📌线程之间的通讯
线程之间是抢占式执行的,哪条线程先执行,哪条后执行,是CPU说了算,我们如果要让线程按照我们的预期去执行,就必须完成线程之间的通信。最常用的就是wait,notify,notifyall这三个方法。
- wait():使当前线程阻塞,释放锁。wait必须在synchronized代码块或者方法中使用。
- notify():唤醒使用当前锁的线程,使其可以抢占CPU
- notifyAll():唤醒所有线程
📌使用线程池创建线程
⭐️使用ThreadPoolExecutor创建线程池
先看一下ThreadPoolExecutor的七个参数,也是面试常问题。
- corePoolSize:核心线程,指定线程池的核心数量
- maximumPoolSize:最大线程数(临时线程数)>=核心线程数,指定线程池所支持的最大线程数
- keepAliveTime:指定临时线程的最大存活时间,不能小于0
- unit:时间单位,秒,分,时,天
- workQueue:指定任务队列。不能为null
- threadFactory:指定用哪个线程工厂创建线程,不能为null
- handler:指定线程忙,任务满的时候,新任务来了怎么办,不能为null
问题:什么时候创建临时线程?
当核心线程满了,任务队列也满了,此时会创建临时线程。如果核心线程满了,任务队列也满了,临时线程也满了,此时会执行handler指定的策略。
🍺handler执行策略
- AbortPolicy:丢弃并抛出异常,是默认策略
- DiscardPolicy:丢弃任务,不抛出异常,不推荐
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中
- CallerRunsPolicy:由主线程负责调用任务的run方法,从而绕过线程池直接执行
🍺ThreadPoolExecutor创建线程池demo
package com.example.study2.threadPool;
import java.util.concurrent.*;
/**
* @ClassName: ThreadPool
* @Description: TODO
* @Author: wenlong
* @date: 2022/8/3 11:48
*/
public class ThreadPool {
public static void main(String[] args) {
// int corePoolSize,
// int maximumPoolSize,
// long keepAliveTime,
// TimeUnit unit,
// BlockingQueue<Runnable> workQueue,
// ThreadFactory threadFactory,
// RejectedExecutionHandler handler
ExecutorService executor = new ThreadPoolExecutor(3,5,6,TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5),Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
Runnable testRunnable = new TestRunnable();
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
executor.execute(testRunnable);
}
}
⭐️使用Executors(线程池的工具类)创建线程池
- newCachedThreadPool:线程数量随着任务增加而增加,如果线程任务执行完毕且空闲一段时间,则被回收
- newFixedThreadPool:创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它
- newSingleThreadExecutor:创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程
- newScheduledThreadPool:创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务
🍺Executors创建线程池demo
package com.example.study2.threadPool;
import java.util.concurrent.*;
/**
* @ClassName: ThreadPool
* @Description: TODO
* @Author: wenlong
* @date: 2022/8/3 11:48
*/
public class ThreadPool {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Runnable testRunnable = new TestRunnable();
executor.execute(testRunnable);
}
}
总结:阿里java开发手册不允许使用Executors创建线程池,原因如下:使用newFixedThreadPool和newSingleThreadExecutor,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求。使用newCachedThreadPool和newScheduledThreadPool允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程。