1. 多线程
1.1 并发与并行
- 并发:指两个或多个事件在同一个时间段内发生。
- 并行:指两个或多个事件在同一时刻发生(同时发生)。
在操作系统中,安装了多个程序,并发指的是在一段时间内宏观上有多个程序同时运行,这在单 CPU 系统中,每一时刻只能有一道程序执行,即微观上这些程序是分时的交替运行,只不过是给人的感觉是同时运行,那是因为分时交替运行的时间是非常短的。
而在多个 CPU 系统中,则这些可以并发执行的程序便可以分配到多个处理器上(CPU),实现多任务并行执行,即利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。目前电脑市场上说的多核 CPU,便是多核处理器,核 越多,并行处理的程序越多,能大大的提高电脑运行的效率。
单核处理器的计算机肯定是不能并行的处理多个任务的,只能是多个任务在单个CPU上并发运行。同理,线程也是一样的,从宏观角度上理解线程是并行运行的,但是从微观角度上分析却是串行运行的,即一个线程一个线程的去运行,当系统只有一个CPU时,线程会以某种顺序执行多个线程,我们把这种情况称之为线程调度。
1.2 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
一个程序运行后至少有一个进程,一个进程中可以包含多个线程
线程调度:
-
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
-
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
- 设置线程的优先级
-
抢占式调度详解
大部分操作系统都支持多进程并发运行,现在的操作系统几乎都支持同时运行多个程序。比如:现在我们上课一边使用编辑器,一边使用录屏软件,同时还开着画图板,dos窗口等软件。此时,这些程序是在同时运行,”感觉这些软件好像在同一时刻运行着“。
实际上,CPU(中央处理器)使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核而言,某个时刻,只能执行一个线程,而 CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。
其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。
1.3 实现多线程的方式
1.3.1 Thread类
Java使用java.lang.Thread
类代表线程,所有的线程对象都必须是Thread类或其子类的实例。每个线程的作用是完成一定的任务,实际上就是执行一段程序流即一段顺序执行的代码。Java使用线程执行体来代表这段程序流。Java中通过继承Thread类来创建并启动多线程的步骤如下:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把run()方法称为线程执行体。
- 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
代码如下:
测试类:
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
自定义线程类:
public class MyThread extends Thread {
//定义指定线程名称的构造方法
public MyThread(String name) {
//调用父类的String参数的构造方法,指定线程的名称
super(name);
}
/**
* 重写run方法,完成该线程执行的逻辑
*/
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}
【Thread】类常用方法
构造方法:
public Thread()
:分配一个新的线程对象public Thread(String name)
:分配一个指定名字的心得的线程对象public Thread(Runnable target)
:分配一个带有指定目标新的线程对象public Thread(Runnable target, String name)
:分配一个带有指定目标新的线程对象并指定名字
常用方法:
public String getName()
:获取当前线程的名称public void start()
:导致此线程开始执行;Java虚拟机调用此线程的run()
方法public void run()
:此线程要执行的任务在此处定义代码public static void sleep(long millis)
:使当前正在执行的线程以指定毫秒数暂停public static Thread currentThread()
:返回对当前正在执行的线程对象的引用
1.3.2 Runnable接口
采用java.lang.Runnable
也是非常常见的一种,我们只需要重写run()
方法即可。
步骤如下:
- 定义
Runnable
接口的实现类,并重写该接口的run()
方法,该run()
方法的方法体同样是该线程的线程执行体。 - 创建
Runnable
实现类的实例,并以此实例作为Thread
的target来创建Thread
对象,该Thread
对象才是真正的线程对象。 - 调用线程对象的
start()
方法来启动线程。
代码如下:
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
通过实现Runnable
接口,使得该类有了多线程类的特征。run()
方法是多线程程序的一个执行目标。所有的多线程代码都在run()
方法里面。Thread
类实际上也是实现了Runnable
接口的类。
在启动的多线程的时候,需要先通过Thread
类的构造方法Thread(Runnable target)
构造出对象,然后调用Thread
对象的start()
方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread
的start()
方法来运行的。因此,不管是继承Thread
类还是实现Runnable
接口来实现多线程,最终还是通过Thread
的对象的API来控制线程的,熟悉Thread
类的API是进行多线程编程的基础。
Runnable
对象仅仅作为Thread
对象的target,Runnable
实现类里包含的run()
方法仅作为线程执行体,而实际的线程对象依然是Thread
实例,只是该Thread
线程负责执行其target的run()
方法。
1.3.3 匿名内部类
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
- 把子类继承父类,重写父类的方法,创建子类对象合一步完成
- 把实现类实现接口,重写接口方法,创建实现类对象合一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
public class Demo {
public static void main(String[] args) {
// 线程父类是Thread
new Thread(){
@override
public void run(){
// 要执行的任务
}
}.start();
// 线程的接口Runnable
Runnable r = new Runnable(){
@override
public void run(){
// 要执行的任务
}
}
new Thread(r).start();
// 可以直接这么写
new Thread(
new Runnable(){
@override
public void run(){
// 要执行的任务
}
}
).start();
}
}
1.4 Thread和Runnable的区别
如果一个类继承Thread
,则不适合资源共享。但是如果实现了Runnable
接口的话,则很容易的实现资源共享。
实现Runnable
接口比继承Thread
类所具有的优势:
- 适合多个相同的程序代码的线程去共享同一个资源。
- 可以避免 java 中的单继承的局限性。
- 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和线程独立。
- 线程池只能放入实现
Runnable
或Callable
类线程,不能直接放入继承Thread
的类。
在java中,每次程序运行至少启动2个线程。一个是main线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个 JVM,每一个 JVM 其实在就是在操作系统中启动了一个进程。
2. 线程安全问题
2.1 线程安全
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
我们通过一个案例,演示线程的安全问题:
电影院要卖票,我们模拟电影院的卖票过程。假设要播放的电影是 “葫芦娃大战奥特曼”,本次电影的座位共100个(本场电影只能卖100张票)。我们来模拟电影院的售票窗口,实现多个窗口同时卖 “葫芦娃大战奥特曼”这场电影票(多个窗口一起卖这100张票)
需要窗口,采用线程对象来模拟;需要票,Runnable
接口子类来模拟
模拟票:
public class Ticket implements Runnable {
private int ticket = 100;
/*
* 执行卖票操作
*/
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
测试类:
public class Demo {
public static void main(String[] args) {
//创建线程任务对象
Ticket ticket = new Ticket();
//创建三个窗口对象
Thread t1 = new Thread(ticket, "窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
结果中有一部分这样现象:
发现程序出现了两个问题:
- 相同的票数,比如4这张票被卖了三回。
- 不存在的票,比如0票与-1票,是不存在的。
这种问题,几个窗口(线程)票数不同步了,这种问题称为线程不安全。
线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
2.2 线程同步
2.2.1 同步代码块
- 同步代码块:
synchronized
关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。
格式:
synchronized(同步锁){
需要同步操作的代码
}
同步锁:
对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁
- 锁对象 可以是任意类型。
- 多个线程对象 要使用同一把锁。
在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。
public class Ticket implements Runnable {
private int ticket = 100;
// 创建一个锁对象
Object obj = new Object();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
// 同步代码块
synchronized(obj){
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
}
2.2.2 同步方法
- 同步方法:使用
synchronized
修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。
格式:
public synchronized void method(){
// 可能会产生线程安全问题的代码
}
对于同步方法,同步锁是什么呢?
- 对于非
static
方法,同步锁就是this
- 对于
static
方法,我们使用当前方法所在类的字节码对象(类名.class
)
使用同步方法代码如下:
public class Ticket implements Runnable {
private int ticket = 100;
// 创建一个锁对象
Object obj = new Object();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
sellTicket();
}
}
private synchronized void sellTicket() {
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
2.2.3 Lock锁
java.util.concurrent.locks.Lock
机制提供了比synchronized
代码块和synchronized
方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock
都有,除此之外更强大,更体现面向对象。
Lock
锁也称同步锁,加锁与释放锁方法化了,如下:
public void lock()
:加同步锁。public void unlock()
:释放同步锁。
使用如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Ticket implements Runnable {
private int ticket = 100;
Lock lock = new ReentrantLock();
@Override
public void run() {
//每个窗口卖票的操作
//窗口 永远开启
while (true) {
lock.lock();
if (ticket > 0) {//有票 可以卖
//出票操作
//使用sleep模拟一下出票时间
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
//获取当前线程对象的名字
String name = Thread.currentThread().getName();
System.out.println(name + "正在卖:" + ticket--);
}
}
}
}
3. 线程状态
3.1 线程状态概述
3.2 Time Waiting(计时等待)
3.3 BLOCKED(锁阻塞)
3.4 Waiting(无限等待)
一个调用了某个对象的Object.wait()
方法的线程会等待另一个线程调用此对象的Object.notify()
方法或Object.notifyAll()
方法。
其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,多个线程会争取锁,同时相互之间又存在协作关系。就好比在公司里你和你的同事们,你们可能存在晋升时的竞争,但更多时候你们更多是一起合作以完成某些任务。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()
方法那么A线程就进入了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了notify()
方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。
一条有意思的tips:
我们在翻阅 API 的时候会发现Timed Waiting(计时等待) 与 Waiting(无限等待) 状态联系还是很紧密的,比如Waiting(无限等待) 状态中wait方法是空参的,而timed waiting(计时等待) 中wait方法是带参的。这种带参的方法,其实是一种倒计时操作,相当于我们生活中的小闹钟,我们设定好时间,到时通知,可是如果提前得到(唤醒)通知,那么设定好时间在通知也就显得多此一举了,那么这种设计方案其实是一举两得。如果没有得到(唤醒)通知,那么线程就处于Timed Waiting状态,直到倒计时完毕自动醒来;如果在倒计时期间得到(唤醒)通知,那么线程从Timed Waiting状态立刻唤醒。
【示例】
public class Demo {
public static void main(String[] args) {
//创建锁对象,保证唯一
Object obj = new Object();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//一直等着买包子
while(true){
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("顾客1告知老板要的包子的种类和数量");
//调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子已经做好了,顾客1开吃!");
System.out.println("---------------------------------------");
}
}
}
}.start();
// 创建一个顾客线程(消费者)
new Thread(){
@Override
public void run() {
//一直等着买包子
while(true){
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("顾客2告知老板要的包子的种类和数量");
//调用wait方法,放弃cpu的执行,进入到WAITING状态(无限等待)
try {
obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
//唤醒之后执行的代码
System.out.println("包子已经做好了,顾客2开吃!");
System.out.println("---------------------------------------");
}
}
}
}.start();
//创建一个老板线程(生产者)
new Thread(){
@Override
public void run() {
//一直做包子
while (true){
//花了5秒做包子
try {
Thread.sleep(5000);//花5秒钟做包子
} catch (InterruptedException e) {
e.printStackTrace();
}
//保证等待和唤醒的线程只能有一个执行,需要使用同步技术
synchronized (obj){
System.out.println("老板5秒钟之后做好包子,告知顾客,可以吃包子了");
//做好包子之后,调用notify方法,唤醒顾客吃包子
//obj.notify();//如果有多个等待线程,随机唤醒一个
obj.notifyAll();//唤醒所有等待的线程
}
}
}
}.start();
}
}
4. 线程池
4.1 线程池思想概述
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:
如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?
在Java中可以通过线程池来达到这样的效果。
4.2 线程池概念
- 线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:
合理利用线程池能够带来三个好处:
- 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。
4.3 线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor
,但是严格意义上讲Executor
并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService
。
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors
线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors
工程类来创建线程池对象。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
获取到了一个线程池ExecutorService
对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:
-
public Future<?> submit(Runnable task)
:获取线程池中的某一个线程对象,并执行Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
- 创建线程池对象。
- 创建Runnable接口子类对象。(task)
- 提交Runnable接口子类对象。(take task)
- 关闭线程池(一般不做)。
Runnable实现类代码:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我要一个教练");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("教练来了: " + Thread.currentThread().getName());
System.out.println("教我游泳,交完后,教练回到了游泳池");
}
}
线程池测试类:
public class ThreadPoolDemo {
public static void main(String[] args) {
// 创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
// 创建Runnable实例对象
MyRunnable r = new MyRunnable();
//自己创建线程对象的方式
// Thread t = new Thread(r);
// t.start(); ---> 调用MyRunnable中的run()
// 从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
// 再获取个线程对象,调用MyRunnable中的run()
service.submit(r);
service.submit(r);
// 注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。
// 将使用完的线程又归还到了线程池中
// 关闭线程池
//service.shutdown();
}
}