文章目录
前言
多线程是JAVA中很重要的一个技术,最近在学习多线程,谨以此博客作为学习笔记,也算做一个分享,如果文中有看不懂的东西,文末会对一些概念进行解释。
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是多线程?
提到线程,就不得不提到进程的概念;
进程是计算机系统进行资源分配和调度的基本单位,通俗点来说,一个进程就是一个应用程序,可以将一个进程比喻成一个公司;
线程是进程中的一条执行路径,对应于公司中的“打工人”;
进程是线程的容器。每个进程都有一块独立的内存空间,而线程作为进程的进一步划分,则是多个线程共用同一块内存空间。多个线程之间可以随意切换,并发执行。当公司有了任务时,就会分配给这些打工人(线程),由他们分别完成,进而完成公司的总任务;
一个进程中,最少有一个线程在执行,否则进程也将消亡,没有员工的公司,应该不能称之为公司吧。
二、线程的调度
线程的调度方式分为分时调度和抢占式调度,JAVA中使用的是抢占式调度。
1.分时调度
分时调度又称为轮转调度。实际上,进程也有调度方式,但不是本节的重点,当进程取得CPU的使用权后,给定一个时间片,每个线程一次只能执行一个时间片的时间,称之为分时调度,分时调度的好处是每个线程都会执行相同的时间,某种意义上讲,算是公平的。
2.抢占式调度
抢占式调度是根据某种算法,给每个线程一个优先级,每当上一个线程执行完,或还未执行完时(具体情况要看获取优先级的是何种算法),就执行当前优先级最高的线程。
三、线程的创建
线程的创建有三种方式,下面逐个进行介绍。
1.通过声明为Thread的子类并重写run()方法的方式来创建一个新线程,代码如下:
public class Demo1 {
public static void main(String[] args) {
// 实例化MyThread类
MyThread myThread = new MyThread();
// 启动这个线程
myThread.start();
}
static class MyThread extends Thread{
@Override
public void run() {
// 这里是本线程要执行的任务
System.out.println("通过声明为Thread的子类的方式创建线程");
}
}
}
2.通过实现Runnable接口并实现run()方法的方式来创建新线程,代码如下:
public class Demo2
{
public static void main(String[] args) {
// 实例化一个MyRunnable对象
MyRunnable myRunnable = new MyRunnable();
// 创建一个线程并传入线程需要执行的任务
Thread thread = new Thread(myRunnable);
// 启动新线程thread
thread.start();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
// 这里是线程需要执行的任务
System.out.println("通过实现Runnable接口的方法创建一个线程");
}
}
}
3.通过实现Callable接口并重写call()方法的方式来创建一个新线程,代码如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo3 {
public static void main(String[] args) {
// 实例化MyCallable对象
MyCallable myCallable = new MyCallable();
// 创建FutureTask对象,并传入MyCallable对象
FutureTask<String> task = new FutureTask<>(myCallable);
// 创建一个线程并传入task来启动线程
Thread thread = new Thread(task);
// 启动线程
thread.start();
// MyCallable会返回一个字符串,可以通过task来获取
/* 这里可能出现异常的原因是task的get()方法会等待MyCallable中必要计算的完成
,在等待过程中可能会出现计算被取消、等待被中断或计算发生异常等原因导致出现异常
*/
try {
System.out.println(task.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
static class MyCallable implements Callable<String>{
@Override
public String call() throws Exception {
// 这里是线程需要执行的任务
System.out.println("通过实现Callable接口的方式创建要给新线程");
return "创建成功";
}
}
}
ps: 需要注意的是,这种方法虽然能返回执行结果,但在获取执行结果的时候,即在调用FutureTask.get()方法时,会阻塞主进程。
四、线程安全问题
多线程在带来高效率的同时,也有带来了一些问题。例如:线程在调度时,是按照抢占的方式来获取时间片的,这就导致一个线程在验证某些条件后,还未执行其他部分,就可能被另一个线程抢到了时间片,而这个线程则修改了这些条件,当那个线程再次抢到时间片的时候,并不对条件进行再次验证便执行了其他操作,这就导致了错误。
用个实例来说:桌上有一本书,两个人都要去拿,第一个人看了一眼,桌上是有书的,便伸手去拿,而第二个人在此同时,也看了一眼,书还在,也伸手去拿,而且速度很快,直接拿到了书。此时,桌上已经空了,而第一个人并没有再去检查桌上有没有书,依然伸手去拿,当然拿不到了。这就出现了异常。
解决线程安全的问题也很简单,那就是让他们互斥访问,即给桌子盖个盖子,只留一个口,这样,第一个人手进去时,口子就被挡住了,第二个人就无法伸手,这样一来,线程安全问题就解决了,这个盖子,其实就是一把“锁”。
1.同步代码块
同步代码块的方式就是给需要上锁的那部分代码“上锁”,让调用它的线程互斥访问,或者说排队访问。
public class Demo4 {
public static void main(String[] args) {
// 实例化任务
MyThread myThread = new MyThread();
// 创建并启动多个线程,观察是否是互斥访问的
new Thread(myThread).start();
new Thread(myThread).start();
new Thread(myThread).start();
}
static class MyThread implements Runnable{
int num = 2;
private Object o = new Object();
@Override
public void run() {
// 给部分代码上锁
/*synchronized(o){
if (num>0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "值为:" + --num);
}
}*/
if (num>0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "值为:" + --num);
}
}
}
}
上锁前
上锁后
可以发现,上锁后,不会再发生num值为负数的情况了,即线程是安全的。
2.同步方法
其实同步方法和同步代码块是差不多的,只不过同步代码块是只锁部分代码,而同步方法是将整个方法上锁。
public class Demo5 {
public static void main(String[] args) {
// 实例化任务
MyThread myThread = new MyThread();
// 创建并启动多个线程,观察是否是互斥访问的
new Thread(myThread).start();
new Thread(myThread).start();
new Thread(myThread).start();
}
static class MyThread implements Runnable{
int num = 6;
@Override
public void run() {
while(true) {
if (get()<0) {
break;
}
System.out.println(Thread.currentThread().getName() + ":" + num);
}
}
// 给方法get上锁
public synchronized int get() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
--num;
return num;
}
}
}
上锁前
上锁后
可以看到,也没有出现负数,证明线程安全
3.显式锁Lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo6 {
public static void main(String[] args) {
// 实例化任务
MyThread myThread = new MyThread();
// 创建并启动多个线程,观察是否是互斥访问的
new Thread(myThread).start();
new Thread(myThread).start();
new Thread(myThread).start();
}
static class MyThread implements Runnable{
int num = 4;
// 创建锁对象
Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// 延迟一秒后再检测锁状态,便于体现出是多个线程在运行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 检查锁状态,如果没锁,则将之上锁,执行下面带码;反之,则阻塞
lock.lock();
try {
if (num<=0) {
break;
}
System.out.println(Thread.currentThread().getName() + ":" + --num);
// 延迟两秒后再释放锁,增大出现异常的概率
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}finally {
// 释放锁
lock.unlock();
}
}
}
}
}
上锁前
上锁后
可以看到,上锁后是线程安全的。
既然第三种方法提出了显式锁的概念,那就再赘述一下:同步代码块和同步方法都属于隐式锁,是jvm层面上的锁,而Lock是调用API,由程序员手动上锁和释放锁。
锁又分为公平锁和非公平锁,公平是指当锁释放后,下一个执行的线程是由被阻塞后线程的排队情况决定的;而非公平是指锁释放后,依然是抢占方式,谁抢到,谁就执行。前两种方式都是非公平锁,Lock是可选择的,默认是非公平锁,如果在创建锁对象时指定了锁为公平锁,那这个锁就变成了公平锁。
// 指定锁为公平锁
Lock lock = new ReentrantLock(true);
五、线程死锁
线程死锁的出现其实时需要特殊条件的,即几个线程之间有一条循环等待链。
假如有两个线程a和b,a线程需要b线程执行的结果,而b线程需要a线程的执行结果,a在等待b执行结束返回结果,而b也在等待a执行结束返回结果,b没有得到a返回的结果就不会结束并返回结果给a,a得不到b返回的结果就不会结束并返回结果给b,这样就陷入了一个死循环,称之为死锁。
六、线程间的通信
线程间的通信并不是线程间能够传递值,而是一个线程告诉其他线程“我”执行结束了。
线程间的通信时通过等待操作wait()和notify()或notifiAll()来实现的。
使线程等待:
Thread thread = new Thread(myThread);
thread.start();
try {
// 使线程等待
thread.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
唤醒线程:
Thread thread = new Thread(myThread);
thread.start();
try {
// 使线程等待
thread.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 唤醒线程
thread.notify();
七、进程的六种状态
NEW
尚未启动的线程处于此状态。
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
TERMINATED
已退出的线程处于此状态。
八、线程池
1.概述
一个线程,从创建到运行结束再到销毁所消耗的时间,在很多时候,这些时间大部分都消耗在了创建和销毁上,而我们最关注的执行部分却可能只消耗了一小部分时间。但在某些时候,我们不得不频繁的使用多线程,但每次执行的任务却很小,这就会造成资源的浪费,所以出现了线程池的概念。
线程池就是存储线程的容器,线程池中的线程是可以重复使用的,当需要执行任务时,就将任务分配给一个线程,这样一来,就省去了频繁创建新线程所消耗的时间。
2.JAVA中的四种线程池
(1) 缓存线程池
缓存线程池会在添加任务时检测是否有空闲线程,如果有,则给空闲线程分配任务;如果没有,则创建一个新线程放到线程池中并处理这个任务
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo7 {
public static void main(String[] args) {
// 创建缓存线程池
ExecutorService executorService = Executors.newCachedThreadPool();
// 添加任务到线程池中
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
这里我添加了三个任务进去,可以看到,三个任务是由三个线程来处理的,即没有空闲线程时会新创建线程来处理任务。
(2)定长线程池
定长线程池是指线程池里面的线程最大数量是固定的;
如果没有空闲线程,但是线程池未满,而又有任务时,会创建线程添加到线程池,然后处理这个任务;
如果没有空闲线程,线程池也满了的情况下有了新任务,则会进入等待状态,直到有空闲线程。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo8 {
public static void main(String[] args) {
// 创建定长线程池,长度未为2
ExecutorService executorService = Executors.newFixedThreadPool(2);
// 添加三个任务到线程池
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
可以看到,三个任务,只由两个线程处理,并没有创建出第三个新线程
(3)单线程线程池
单线程线程池效果和定长线程池限定线程池长度为1时一样,如果添加任务时没有空闲线程则等待,直到线程池中的线程空闲。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo9 {
public static void main(String[] args) {
// 创建单线程线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
// 添加三个任务到线程池中
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
});
}
}
可以看到,三个任务都由一个线程处理。
(4)周期性任务定长线程池
周期性任务定长线程池和定长线程池相似,都是线程最大数量被限制,但周期性任务定长线程池会在一定时间延迟后执行,并会在每隔一段时间便执行一次。
参数介绍:
command:Runnable类型的任务
initialDelay:延迟多长时间后触发(即开始执行),单位和后面的一样period:执行间隔
unit:前面两个延迟时间的单位
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Demo10 {
public static void main(String[] args) {
// 创建周期性任务定长线程池
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(2);
// 添加任务到线程池
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ ":周期执行任务1");
}
}, 3, 2,TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ ":周期执行任务2");
}
}, 3, 2,TimeUnit.SECONDS);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+ ":周期执行任务3");
}
}, 3, 2,TimeUnit.SECONDS);
}
}
可以看到,这三个任务是周期执行的,且只有两个线程在处理任务。
九、杂谈一些概念或操作
1.线程休眠
在某些时候,我们需要“故意”消耗一些时间,这时候就可以通过Thread类的静态方法sleep来实现,如:
try {
// sleep()方法中的参数为休眠时间,单位为毫秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
2.线程中断
有时候我们需要在线程还未执行完时将线程提前结束,这时可以采用给线程打上中断标记的方法,告知线程需要提前结束,线程会在某些情况下检查自身是否被打上了中断标记,如休眠和等待时。如果发现了被打上了中断标记,则抛出中断异常,程序员可以捕获,然后进行结束操作。例如:
public class Demo2{
public static void main(String[] args) {
// 实例化一个MyRunnable对象
MyRunnable myRunnable = new MyRunnable();
// 创建一个线程并传入线程需要执行的任务
Thread thread = new Thread(myRunnable);
// 启动新线程thread
thread.start();
// 打上中断标记
thread.interrupt();
}
static class MyRunnable implements Runnable{
@Override
public void run() {
// 这里是线程需要执行的任务
System.out.println("通过实现Runnable接口的方法创建一个线程");
// 让进程休眠一秒并捕获异常
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 当线程抛出中断异常并被我们捕获后,可以在这里执行结束操作
e.printStackTrace();
}
}
}
}
3.守护线程
线程分为守护线程和用户线程,除守护线程外的所有线程都是用户线程。守护线程依赖于用户线程存在,即进程中如果所有的用户线程都销毁了,那么所有的守护线程也会销毁。
如下操作可将一个线程设置为守护线程:
// setDaemon()方法中的boolean类型的参数意义是是否设置为守护线程,如果是true,则将这个线程设置为守护线程
thread.setDaemon(true);
总结
目前所学到的有关多线程的知识就这么多,后续如果有更深入的理解的话再进行补充。