文章目录
文章大部分内容摘自gitbook:深入浅出Java多线程
进程和线程概念
批处理操作系统
批处理操作系统,把⼀系列需要操作的指令写下来,形成⼀个清单,⼀次性交给计算机。用户将多个需要执行的程序写在磁带上,然后交由计算机去读取并逐个执行这些程序,并将输出结果写在另⼀个磁带上。
批处理操作系统在⼀定程度上提高了计算机的效率,但是由于批处理操作系统的指令运行方式仍然是串行的,内存中始终只有⼀个程序在运行,后面的程序需要等待前面的程序执行完成后才能开始执行,而前面的程序有时会由于I/O操作、网络等原因阻塞,所以批处理操作效率也不高。
进程的提出
批处理操作系统的瓶颈在于内存中只存在⼀个程序,那么内存中能不能存在多个程序呢? 这是人们亟待解决的问题。于是,科学家们提出了进程的概念。
进程就是应用程序在内存中分配的空间,也就是正在运行的程序,各个进程之间互不干扰。同时进程保存着程序每⼀个时刻运行的状态。此时,CPU采用时间片轮转的方式运行进程:CPU为每个进程分配⼀个时间段,称作它的时间片。如果在时间片结束时进程还在运行,则暂停这个进程的运行,并且CPU分配给另⼀个进程(这个过程叫做上下文切换)。如果进程在时间片结束前阻塞或结束,则CPU立即进行切换,不用等待时间片用完。使用进程+CPU时间片轮转方式的操作系统,在宏观上看起来同⼀时间段执行多个任务,换句话说,进程让操作系统的并发成为了可能。虽然并发从宏观上看有多个任务在执行,但在事实上,对于单核CPU来说,任意具体时刻都只有⼀个任务在占用CPU资源。
线程的提出
虽然进程的出现,使得操作系统的性能大大提升,但是随着时间的推移,⼈们并不满足⼀个进程在⼀段时间只能做⼀件事情,如果⼀个进程有多个子任务时,只能逐个得执行这些子任务,很影响效率。
比如杀毒软件在检测用户电脑时,如果在某一项检测中卡住了,那么后面的检测项也会受到影响。或者说当你使用杀毒软件中的扫描病毒功能时,在扫描病毒结束之前,无法使用杀毒软件中清理垃圾的功能,这显然无法满足人们的要求。
于是⼈们又提出了线程的概念,让⼀个线程执行⼀个子任务,这样一个进程就包含了多个线程,每个线程负责⼀个单独的子任务。
进程与线程的区别
进程让操作系统的并发性成为了可能,而线程让进程的内部并发成为了可能。
使用多线程的方式实现并发有以下几个好处:
1:进程间的通信比较复杂,而线程间的通信比较简单,通常情况下,我们需要使用共享资源,这些资源在线程间的通信比较容易。
2:进程是重量级的,而线程是轻量级的,故多线程方式的系统开销更小。
进程和线程的区别:
进程是⼀个独立的运行环境,而线程是在进程中执行的⼀个任务。他们两个本质的区别是是否单独占有内存地址空间及其它系统资源(比如I/O):
- 进程单独占有⼀定的内存地址空间,所以进程间存在内存隔离,数据是分开的,数据共享复杂但是同步简单,各个进程之间互不干扰;而线程共享所属进程占有的内存地址空间和资源,数据共享简单,但是同步复杂。
- 进程单独占有⼀定的内存地址空间,⼀个进程出现问题不会影响其他进程,不影响主程序的稳定性,可靠性高;⼀个线程崩溃可能影响整个程序的稳定性,可靠性较低。
- 进程单独占有⼀定的内存地址空间,进程的创建和销毁不仅需要保存寄存器和栈信息,还需要资源的分配回收以及页调度,开销较大;线程只需要保存寄存器和栈信息,开销较小。
- 进程是操作系统进行资源分配的基本单位,而线程是操作系统进行调度的基本单位,即CPU分配时间的单位 。
上下文切换
上下文切换(有时也称做进程切换或任务切换)是指 CPU 从⼀个进程(或线程)切换到另⼀个进程(或线程)。上下文是指某⼀时间点 CPU 寄存器和程序计数器的内容。
CPU通过为每个线程分配CPU时间片来实现多线程机制。CPU通过时间片分配算法来循环执行任务,当前任务执行⼀个时间片后会切换到下⼀个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的,意味着此操作会消耗大量的 CPU 时间,故线程也不是越多越好。
Java多线程入门类和接口
继承Thread类
package 线程;
public class MyThread extends Thread{
private MyThread() {
super();
}
public MyThread(String name) {
//第二种方法给线程设置名称,通过调用Thread构造方法
super(name);
}
@Override
public void run() {
//一般被多线程执行的代码比较耗时
for (int i = 0; i < 1000; i++) {
System.out.println("你好," + getName());
}
}
public static void main(String[] args) {
MyThread my1 = new MyThread();
//第一种获取线程名称
my1.setName("线程1");
MyThread my2 = new MyThread("线程2");
//run方法为普通方法调用,无法实现多线程
/*my.run();
my.run();*/
//start方法使该线程执行;java虚拟机调用该线程的run方法
my1.start();
my2.start();
//为主线程设置名称
Thread.currentThread().setName("主线程");
//返回当前正在执行的线程名称
System.out.println(Thread.currentThread().getName());
}
}
实现Runnable类
package 线程的创建;
/*
* 此种创建线程使用了静态代理设计模式
*/
class MyThread1 implements Runnable {
//重写Runnable接口的run方法
public void run() {
int i =0;
while (i++<10) {
System.out.println(Thread.currentThread().getName() + "的run()方法在运行");
}
}
}
public class TestRunnable {
public static void main(String[] args) {
// TODO 自动生成的方法存根
//创建Runnable接口实现类的实例化
//(创建真实角色)
MyThread1 myThread = new MyThread1();
//使用Thread(Runnable target, String name)构造方法创建线程对象
//(创建代理角色+真实角色引用)
Thread thread1 = new Thread(myThread, "thread1");
//调用线程对象的start()方法启动线程
thread1.start();
//创建并启动另一个线程thread2
Thread thread2 = new Thread(myThread, "thread2");
thread2.start();
}
}
Callable、Future与FutureTask
package 线程;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Object> {
@Override
public Object call() throws Exception {
Thread.sleep(1000);
return "Callable有返回值";
}
public static void main(String[] args) throws Throwable {
// 创建线程池工具
ExecutorService executor = Executors.newCachedThreadPool();
// 新建任务
FutureTask<Object> futureTask = new FutureTask<>(new MyCallable());
// 通过线程池工具提交任务
executor.submit(futureTask);
// 输出线程任务返回值
System.out.println(futureTask.get());
}
}
Java线程组和线程优先级
线程组
Java中用ThreadGroup来表示线程组,可以使用线程组对线程进行批量控制每个Thread必然存在于一个ThreadGroup中,Thread不能独立于ThreadGroup存在。执行main()方法线程的名字是main,如果在new Thread时没有显式指定,那么默认将父线程(当前执行new Thread的线程)线程组设置为自己的线程组。
package 线程组;
public class ThreadGroup1 {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
// 获取当前线程组的名字(使用线程组对线程批量控制)
System.out.println("thread当前线程组名字:" +
Thread.currentThread().getThreadGroup().getName());
// 获取当前线程名字
System.out.println("thread线程名字:" + Thread.currentThread().getName());
});
// 开启线程
thread.start();
//获取主线程的名字
System.out.println("执行main方法的线程名字:" + Thread.currentThread().getName());
}
}
线程的优先级
Java中线程优先级可以指定,范围是1~10。但是并不是所有的操作系统都支持10级优先级的划分(比如有些操作系统只⽀持3级划分:低,中,高),Java只是给操作系统一个优先级的参考值,线程最终在操作系统的优先级是多少还是由操作系统决定。
package 线程;
/*
* priority:
* 调用方式:线程名称.priority(),括号中填入数字,不可以超过1000(最大值为10,最小为1,默认为5)
* 位置:放在start之前
*
*/
public class MyPriority extends Thread{
private MyPriority(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i + ",你好," + getName());
}
}
public static void main(String[] args) {
// TODO 自动生成的方法存根
MyPriority my1 = new MyPriority("线程1");
MyPriority my2 = new MyPriority("线程2");
//设置线程优先级,在线程启动前设置
//my1.setPriority(1000);1000值过大出现非法异常
/*
* 线程优先级:最大10,最小1,Java默认线程优先级5
* 线程执行顺序是由调度程序来决定
* 线程执行是随机(概率的大小问题)的,并不是值越大,越先执行
* Java程序中对线程所设置的优先级只是给操作系统一个建议,操作系统不一定采纳
* 而真正的调度顺序由操作系统的线程调度算法决定
*/
my1.setPriority(1);
my2.setPriority(10);
//启动线程
my1.start();
my2.start();
}
}
一个线程必然存在于一个线程组中,那么当线程和线程组的优先级不一致的时候将会怎样呢?
package 线程组;
public class ThreadGroupPriority {
public static void main(String[] args) {
ThreadGroup threadGroup = new ThreadGroup("group1");
threadGroup.setMaxPriority(6);
Thread thread = new Thread(threadGroup, "thread1");
// 如果某个线程的优先级大于线程组的优先级,那么线程的优先级会失效,取而代之的是线程组的优先级
thread.setPriority(9);
System.out.println("我是线程组的优先级:" + threadGroup.getMaxPriority());
System.out.println("我是线程的优先级:" + thread.getPriority());
}
}
输出如下:
所以,如果某个线程优先级大于线程所在线程组的最大优先级,那么该线程的优先级将会失效,取而代之的是线程组的最大优先级。
线程组常用方法
1:获取当前线程名字
Thread.currentThread().getThreadGroup().getName();
2:复制线程组(复制一个线程数组到另一个线程组)
ThreadGroup threadGroup = new ThreadGroup("Group1");
// 创建一个新的线程数组
Thread[] threads = new Thread[threadGroup.activeCount()];
// 将线程数组复制到线程组中
threadGroup.enumerate(threads);
3:线程组统一异常处理
ThreadGroup threadGroup2 = new ThreadGroup("Group2") {
// 继承ThreadGroup并重新定义以下方法
// 在线程成员抛出unchecked exception
// 会执行此方法
public void uncaughtException(Thread t, Throwable e) {
System.out.println(t.getName() + ": " + e.getMessage());
}
};
// 这个线程是Group的一员
Thread thread = new Thread(threadGroup2, new Runnable() {
@Override
public void run() {
throw new RuntimeException("测试异常");
}
});
输出结果如下:
Java线程的状态及主要转化方法
操作系统中的线程状态转换
Java线程的六个状态
- NEW
// 处于NEW状态的线程此时尚未启动(未调用start方法)
public static void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState());
thread.start();
System.out.println(thread.getState());
//在调用一次start()之后,threadStatus的值会改
// 变(threadStatus !=0),此时再次调⽤start()⽅法会抛出
// IllegalThreadStateException异常。
thread.start();
}
- RUNNABLE
public static void testStateRunnable() {
Thread thread = new Thread(() -> {});
// 开启线程
thread.start();
System.out.println(thread.getState());
}
- BLOCKED
public static void main(String[] args) {
/**
* 处于BLOCKED状态的线程正等待锁的释放以进入同步区。
* 1:该状态没有占用锁
* 2:不需要其他线程唤醒
*
* 假如今天你下班后准备去食堂吃饭。你来到食堂仅有的⼀个窗口,发现前面
已经有个人在窗口前了,此时你必须得等前面的⼈从窗口离开才行。
假设你是线程t2,你前⾯的那个人是线程t1。此时t1占有了锁(食堂唯⼀的
窗口),t2正在等待锁的释放,所以此时t2就处于BLOCKED状态。
*/
}
- WAITING
public static void main(String[] args) {
/**
*
* 处于等待状态的线程变成RUNNABLE状态需要其他线程唤醒。
* 1:该状态占有锁
* 2:需要其它线程唤醒
*
* 调⽤如下3个⽅法会使线程进⼊等待状态:
Object.wait():使当前线程处于等待状态直到另⼀个线程唤醒它;(要是其他线程不进行唤醒,那么此线程会一直等待)
Thread.join():等待线程执⾏完毕,底层调⽤的是Object实例的wait⽅法;
LockSupport.park():除⾮获得调⽤许可,否则禁⽤当前线程进⾏线程调度。
*/
}
- TIME_WAITING
public static void main(String[] args) {
/**
* TimedWaitingThread:线程等待⼀个具体的时间,时间到后会被⾃动唤醒。
*
* 以下方法会是线程进入超时等待状态
* 1:Thread.sleep(long millis):使当前线程睡眠指定时间;
* 2:Object.wait(long timeout):线程休眠指定时间,等待期间可以通过
notify()/notifyAll()唤醒;
* 3:Thread.join(long millis):等待当前线程最多执⾏millis毫秒,如果millis为0,则
会⼀直执⾏;
* 4:LockSupport.parkNanos(long nanos): 除⾮获得调⽤许可,否则禁⽤当前线
程进⾏线程调度指定时间;
* 5:LockSupport.parkUntil(long deadline):同上,也是禁⽌线程进⾏调度指定时
间;
*/
}
- TERMINATED
public static void main(String[] args) {
/**
*
* 终⽌状态。此时线程已执⾏完毕。
*
*/
}
线程状态的转换
根据上面的六个状态可以得到下面的线程状态转换图:
Java线程间的通信
锁与同步
在Java中,锁的概念都是基于对象的,所以我们⼜经常称它为对象锁。线程和锁的关系,我们可以⽤婚姻关系来理解。⼀个锁同⼀时间只能被⼀个线程持有。也就是说,⼀个锁如果和⼀个线程“结婚”(持有),那其他线程如果需要得到这个锁,就得等这个线程和这个锁“离婚”(释放)。可以以解释为:线程同步是线程之间按照⼀定的顺序执⾏。为了达到线程同步,我们可以使⽤锁来实现它。
package 线程通信;
/**
* 锁和同步:
* 为了达到线程同步,可以使用锁来实现它
* @author 15447
*
*/
public class ObjectLock {
private static Object lock = new Object();
/**
* 在循环体外添加了lock对象线程锁
*/
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread A" + i);
}
}
}
}
/**
* 在循环体外添加了lock对象线程锁
*/
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 100; i++) {
System.out.println("Thread B" + i);
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
}
输出结果为:根据线程和锁的关系,同⼀时间只有⼀个线程持有⼀个锁,那么线程B就会等线程A执⾏完成后释放 lock ,线程B才能获得锁 lock 。
等待/通信机制
上⾯⼀种基于“锁”的⽅式,线程需要不断地去尝试获得锁,如果失败了,再继续尝试。这可能会耗费服务器资源。⽽等待/通知机制是另⼀种⽅式。Java多线程的等待/通知机制是基于 Object 类的 wait() ⽅法和 notify() , notifyAll() ⽅法来实现的。
package 线程通信;
/**
* wait、notify一定要放在同步块中
* @author 15447
*
*/
public class WaitAndNotify {
private static Object lock = new Object();
static class ThreadA implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadA:" + i);
try {
// 唤醒另一个线程
lock.notify();
// 当前线程进入等待状态
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// lock.notify();
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
synchronized (lock) {
for (int i = 0; i < 5; i++) {
System.out.println("ThreadB:" + i);
try {
lock.notify();
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// lock.notify();
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
}
输出结果如下:
信号量
volatile 关键字的⾃⼰实现的信号量通信。让线程A输出0,然后线程B输出1,再然后线程A输出2…以此类推。应该怎样实现呢?
package 线程通信;
/**
* volitile(实现信号量通信)关键字能够保证内存的可⻅性,如果⽤volitile关键字声明了⼀个变
量,在⼀个线程⾥⾯改变了这个变量的值,那其它线程是⽴⻢可⻅更改后的值的。
* @author 15447
*
*/
public class Signal {
// volatile关键字的⾃⼰实现的信号量通信
private static volatile int signal = 0;
static class ThreadA implements Runnable {
@Override
public void run() {
while (signal < 10) {
if (signal % 2 == 0) {
System.out.println("ThreadA:" + signal);
synchronized (this) {
signal++;
}
}
}
}
}
static class ThreadB implements Runnable {
@Override
public void run() {
while (signal < 10) {
if (signal % 2 == 1) {
System.out.println("ThreadB:" + signal);
synchronized (this) {
signal++;
}
}
}
}
}
public static void main(String[] args) {
new Thread(new ThreadA()).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new ThreadB()).start();
}
}
输出结果如下:
使⽤了⼀个 volatile 变量 signal 来实现了“信号量”的模型。这⾥需要注意的是, volatile 变量需要进⾏原⼦操作。 signal++ 并不是⼀个原⼦操作,所以我们需要使⽤ synchronized 给它“上锁”。
管道
管道是基于“管道流”的通信⽅式。JDK提供了 PipedWriter 、 PipedReader 、 PipedOutputStream 、 PipedInputStream 。其中,前⾯两个是基于字符的,⾯两个是基于字节流的。
package 线程通信;
import java.io.IOException;
import java.io.PipedReader;
import java.io.PipedWriter;
/**
* 线程管道
* @author 15447
*
*/
public class Pipe {
static class ReaderThread implements Runnable {
private PipedReader reader;
public ReaderThread(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
System.out.println("this is reader");
int receive = 0;
try {
while ((receive = reader.read()) != -1) {
System.out.print((char)receive);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
static class WriterThread implements Runnable {
private PipedWriter writer;
public WriterThread(PipedWriter writer) {
this.writer = writer;
}
@Override
public void run() {
System.out.println("this is writer");
int receive = 0;
try {
writer.write("test");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
/**
* 1. 线程ReaderThread开始执⾏,
2. 线程ReaderThread使⽤管道reader.read()进⼊”阻塞“,
3. 线程WriterThread开始执⾏,
4. 线程WriterThread⽤writer.write("test")往管道写⼊字符串,
5. 线程WriterThread使⽤writer.close()结束管道写⼊,并执⾏完毕,
6. 线程ReaderThread接受到管道输出的字符串并打印,
7. 线程ReaderThread执⾏完毕。
*/
PipedReader pipedReader = new PipedReader();
PipedWriter pipedWriter = new PipedWriter();
try {
pipedWriter.connect(pipedReader);
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new ReaderThread(pipedReader)).start();
new Thread(new WriterThread(pipedWriter)).start();
}
}
输出结果如下:
管道通信的应⽤场景:使⽤管道多半与I/O流相关。当我们⼀个线程需要先另⼀个线程发送⼀个信息(⽐如字符串)或者⽂件等等时,就需要使⽤管道通信了。