Java之多线程
- 1. 线程与进程
- 2. 同步与异步&并发与并行
- 3. 类继承Thread(创建线程)(第一种线程方式)
- 4. 实现Runnable(创建任务)(第二种线程方式)
- 5. 实现Runnable 与 继承Thread相比的优势:
- 6. Thread类
- 7. 线程休眠sleep
- 8. 线程阻塞
- 9. 线程的中断
- 10. 守护线程setDaemon()
- 11. 线程安全问题
- 12. 线程安全1-同步代码块
- 13. 线程安全2-同步方法
- 14. 线程安全3-显示锁Lock 子类 ReentrantLock
- 15. 公平锁与非公平锁
- 16. 线程死锁
- 17. 多线程通信问题:生产者与消费者
- 18. 线程的六种状态
- 19. 实现Callable接口(创建带返回值的线程)(第三种线程方式)
- 20. Runnable与Callable
- 21. 线程池ExecutorService
- 22. 缓存线程池
- 23. 定长线程池
- 24. 单线程线程池
- 25. 周期定长线程池
- 26. Lambda表达式
1. 线程与进程
概念:
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间。
- 线程:
- 是进程中的一个执行路径,共享一个内存空间,线程之间可以自由切换,并发执行。一个进程最少有一个线程。
- 线程实际上是在进程基础之上的进一步划分,一个进程启动之后,里面的若干执行路径又可以划分成若干个线程。
- 每个线程都拥有自己的栈空间,共用一份堆内存。
线程调度:
- 分时调度:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
- 抢占式调度:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
CPU使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU的一个核心而言,某个时刻, 只能执行一个线程,而 CPU在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时 刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的 使 用率更高。
2. 同步与异步&并发与并行
同步: 排队执行,效率低但是安全。
异步: 同时执行,效率高但是数据不安全。
并发: 指两个或多个事件在同一个时间段内发生。
并行: 指两个或多个事件在同一时刻发生(同时发生)
3. 类继承Thread(创建线程)(第一种线程方式)
概念: 继承Thread的类可以实现多线程。在类中重写run()方法,run()方法中的代码就是另一条线程,在主方法调用start()方法才会执行类中的run()。
格式:
public static void main(String[] args){
MyThread m = new MyThread;
m.start();
}
class MyThread extends Thread{
//这里的代码,就是一条新的执行路径。
//这个代码的触发方式不是调用run(),而是通过Thread对象的strat()来启动任务
@Override
public void run(){
}
}
4. 实现Runnable(创建任务)(第二种线程方式)
概念: 实现Runnable接口的类可以编写线程的任务,任务对象传给线程构造方法能够用线程实现任务。
格式:
/**
* 多线程技术
*/
public static void main(String[] args){
//实现Runnable
//1. 创建一个任务对象
MyRunnable r = new MyRunnable();
//2. 创建一个线程,并为其分配一个任务
Thread t = new Thread(r);
//3. 执行这个线程
t.start();
}
/**
* 用于给线程执行的任务
*/
class MyRunnable implements Runnable{
@Override
public void run(){
//编写线程的任务
}
}
5. 实现Runnable 与 继承Thread相比的优势:
- 通过创建任务,然后给线程分配的方式来实现的多线程,更适合多个线程同时执行相同任务的情况。
- 可以避免单继承所带来的局限性。
- 任务与线程本身是分离的,提高了程序的健壮性。
- 线程池技术,接收Runnable类型的任务,不接收Thread类型的线程。
6. Thread类
字段:
变量和类型 | 字段 | 描述 |
---|---|---|
static int | MAX_PRIORITY | 线程可以拥有的最大优先级。 |
static int | MIN_PRIORITY | 线程可以拥有的最低优先级。 |
static int | NORM_PRIORITY | 分配给线程的默认优先级。 |
常用构造方法:
构造器 | 描述 |
---|---|
Thread() | 分配新的 Thread对象。 |
Thread(Runnable target) | 分配新的 Thread对象。 |
Thread(Runnable target, String name) | 分配新的 Thread对象。 并为其起名称,可以使用getName()方法获取。 |
Thread(String name) | 分配新的 Thread对象。 |
常用方法:
变量和类型 | 方法 | 描述 |
---|---|---|
static Thread | currentThread() | 返回对当前正在执行的线程对象的引用。 |
long | getId() | 返回此Thread的标识符。 |
String | getName() | 返回此线程的名称。 |
int | getPriority() | 返回此线程的优先级。 |
void | setName(String name) | 将此线程的名称更改为等于参数 name 。 |
void | setPriority(int newPriority) | 更改此线程的优先级。 |
void | setDaemon(boolean on) | 将此线程标记为 daemon线程(守护线程)或用户线程。(在start()之前设置) |
static void | sleep(long millis) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,具体取决于系统计时器和调度程序的精度和准确性。 |
static void | sleep(long millis, int nanos) | 导致当前正在执行的线程休眠(暂时停止执行)指定的毫秒数加上指定的纳秒数,具体取决于系统定时器和调度程序的精度和准确性。 |
void | interrupt() | 中断此线程。(为线程打上标记可以被catch捕获) |
注意: 程序中main()为主线程,其中开启的线程为子线程,当主线程死亡时,如果子线程还没死亡,那么子线程会继续执行直到自我死亡,即死亡由自己决定,统称为用户线程。守护线程则是指当进程中所有用户线程死亡时,所有守护线程也会自动死亡,无论自我死亡与否。
eg:
/**
* 获取主线程名称和子线程名称
*/
public static void main(String[] args){
//获取主线程名称
System.out.println(Thread.currentThread().getName());
new Thread(new MyRunnable());
}
static class MyRunnable implements Runnable{
@Override
public void run(){
//获取子线程名称
System.out.println(Thread.currentThread().getName());
}
}
7. 线程休眠sleep
eg:
/**
* 线程休眠
*/
public static void main(String[] args) throws InterruptedException {
//线程每次休眠时间1秒
for (int i = 0; i < 10; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
8. 线程阻塞
概念: 也称为耗时操作。是指所有比较消耗时间的操作,比如常见的文件读取操作,接收输入操作等。
9. 线程的中断
一个线程是一个独立的执行路径,它是否应该结束,应该由其自身决定。
eg:
/**
* 线程中断
*/
public static void main(String[] args) throws InterruptedException {
//主线程结束则杀死子线程
Thread t = new Thread(new MyRunnable());
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(i);
Thread.sleep(1000);
}
t.interrupt(); //为子线程打上可以被catch的标记
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return; //杀死线程
}
}
}
}
10. 守护线程setDaemon()
eg:
/**
* 守护线程
*/
public static void main(String[] args) throws InterruptedException {
//用户线程死亡,守护线程自动死亡
Thread t = new Thread(new MyRunnable());
t.setDaemon(true); //将t设置为守护线程
t.start();
for (int i = 0; i < 5; i++) {
System.out.println(i);
Thread.sleep(1000);
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
}
11. 线程安全问题
概念: 多线程异步执行可能会出现线程不安全的问题。
解决方法(均为不公平锁):
- 隐式锁:
- 同步代码块
- 同步方法
- 显式锁
- Lock
eg: 结果可能会出现符负数的原因是因为,假设三个线程为A,B,C,当count=1的时候,A进入while(),但是由于线程阻塞,还没到count–操作时,B和C也进入了while(),所以导致A执行了count–后count等于0时,B和C线程仍然会执行while()。
/**
* 线程不安全情况
*/
public static void main(String[] args){
Runnable run = new Count();
//三个线程执行同个任务
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Count implements Runnable{
private int count = 10;
@Override
public void run() {
while (count>0){ //不允许出现负数
count--;
System.out.println(count); //结果可能会出现负数
}
}
}
12. 线程安全1-同步代码块
概念: 被Synchronized锁住的代码快称为同步代码块,当一个线程执行进入同步代码块时,锁对象会变为上锁状态,其他线程会询问锁对象状态,等到在同步代码块中的线程执行出去时,锁对象才会变为解锁状态,此时其他线程一旦询问锁对象为开锁状态,就会进行抢占进入同步代码块,从而实现同步运行。
注意: Java中所有对象都可以作为锁对象。实现同步要确保所有线程询问的是同一个锁对象!
格式:
Synchronized(锁对象){
//同步代码块
}
eg:
/**
* 线程安全情况1:同步代码块
*/
public static void main(String[] args){
Runnable run = new Count();
//三个线程执行同个任务
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Count implements Runnable{
private int count = 10;
private Object o = new Object();
@Override
public void run() {
while (true){
synchronized (o){ //锁位置
if (count>0){ //不允许出现负数
count--;
System.out.println(count); //结果不会出现负数
}else {break;}
}
}
}
}
13. 线程安全2-同步方法
概念: 即用Synchronized修饰方法。此时锁对象为this。如果是静态方法,即用static修饰的方法,此时锁对象为类名.class,即类的字节码。
eg:
/**
* 线程安全情况2:同步方法
*/
public static void main(String[] args){
Runnable run = new Count();
//三个线程执行同个任务
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Count implements Runnable{
private int count = 10;
@Override
public void run() {
while (true){
//如果有线程执行下面这个代码块时deCount()也不会被线程执行,因为用的是同一把锁
synchronized (this){}
if (!deCount()){break;}
}
}
//同步方法
public synchronized boolean deCount(){
//锁对象为this
//如果是static修饰的方法锁对象为Count.class
if (count>0){
count--;
System.out.println(count);
return true;
}
return false;
}
}
14. 线程安全3-显示锁Lock 子类 ReentrantLock
概念: 可以自己标记锁和开锁位置。
常用方法:
- 锁住:lock()
- 开锁:unlock()
eg:
/**
* 线程安全情况3:隐式锁Lock
*/
public static void main(String[] args){
Runnable run = new Count();
//三个线程执行同个任务
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Count implements Runnable{
private int count = 10;
//创建锁对象l
private Lock l = new ReentrantLock();
@Override
public void run() {
while (count>0){
l.lock(); //锁住
if (count>0){
count--;
System.out.println(count);
}else {break;}
l.unlock(); //开锁
}
}
}
15. 公平锁与非公平锁
概念:
- 公平锁:比如A线程先到锁前等待,等锁解开就是A线程先执行。
- 非公平锁:无论哪个线程先等待,等锁解开后所有线程一起抢占。
注意: 三个线程安全解决方案均为非公平锁
公平锁实现方法: 创建显式锁Lock时指定为true(默认为false)。
公平锁创建格式: ReentrantLock(true)
16. 线程死锁
概念: 线程A在锁AA中,线程B在锁BB中,A在等待BB开锁进入BB,而B在等待AA开锁进入AA。
避免线程死锁产生: 在任何有可能导致锁产生的方法里不要再调用其他方法让其他锁产生。
eg:
/**
* 线程死锁
*/
public static void main(String[] args){
A a = new A();
B b = new B();
//子线程要b调用bOut()。可是现在主线程b还在调用say(),等待a调用aOut()
new MyThread(a, b).start();
//主线程要a调用aOut。可是现在子线程a还在调用say(),等待b调用bOut()
b.say(a);
}
static class MyThread extends Thread{
private A a;
private B b;
MyThread(A a, B b){
this.a = a;
this.b = b;
}
@Override
public void run() {
a.say(b);
}
}
static class A{
public synchronized void say(B b){
System.out.println("A:你从BB出来我就从AA出来");
b.bOut();
}
public synchronized void aOut(){
System.out.println("A从AA出来进入BB");
}
}
static class B{
public synchronized void say(A a){
System.out.println("B:你从AA出来我就从BB出来");
a.aOut();
}
public synchronized void bOut(){
System.out.println("B从BB出来进入AA");
}
}
17. 多线程通信问题:生产者与消费者
概念: 使用等待和唤醒的方法让多线程进行交互合作。实现A生产时B休眠,B消费时A休眠的循环。
Object类常用方法:
变量和类型 | 方法 | 描述 |
---|---|---|
void | notify() | 随机唤醒正在此对象监视器上等待的单个线程。 |
void | notifyAll() | 唤醒等待此对象监视器的所有线程。 |
void | wait() | 导致当前线程等待它被唤醒,通常是 通知或 中断。 |
void | wait(long timeoutMillis) | 导致当前线程等待它被唤醒,通常是 通知或 中断,或者直到经过一定量的实时。 |
void | wait(long timeoutMillis, int nanos) | 导致当前线程等待它被唤醒,通常是 通知或 中断,或者直到经过一定量的实时。 |
eg:
/**
* 多线程通信问题:生产者与消费者
* 唤醒A,休眠B,A生产--休眠A,唤醒B,B消费
*/
public static void main(String[] args){
Product p = new Product();
new Producer(p).start();
new Consumer(p).start();
}
//生产者按规律生产10种产品
static class Producer extends Thread{
private Product p;
public Producer(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i%2==0){
p.setNameAndPrice("中华香烟", "46");
}else {
p.setNameAndPrice("利群香烟", "17");
}
}
}
}
//消费者按生产者生产顺序买走产品
static class Consumer extends Thread{
private Product p;
public Consumer(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
p.buy();
}
}
}
//产品类
static class Product{
private String name;
private String price;
private boolean flag = true;
//生产者生产商品
public void setNameAndPrice(String name, String price) {
if (flag){
this.name = name;
this.price = price;
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//消费者买走商品
public void buy(){
if (!flag){
System.out.println("消费者买走的产品名称:"+name+",价格:"+price);
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
18. 线程的六种状态
- New: 尚未启动的线程处于此状态。(新生、就绪)
- Runnable: 在Java虚拟机中执行的线程处于此状态。(运行)
- Blocked: 被阻塞等待监视器锁定的线程处于此状态。(阻塞)
- Waiting: 无限期等待另一个线程执行特定操作的线程处于此状态。(锁定)
- Timed Waiting: 正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。(等待)
- Terminated: 已退出的线程处于此状态。(死亡)
19. 实现Callable接口(创建带返回值的线程)(第三种线程方式)
概念: 主线程指派给子线程任务,然后给主线程返回结果,这期间主线程可以进行等待。
接口格式:
public interface Callable<V> {
V call() throws Exception;
}
Callable获取返回值: Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执 行,如果不调用不会阻塞。
使用格式:
//1. 编写类实现Callable接口 , 实现call方法
class XXX implements Callable<T> {
@Override
public <T> call() throws Exception {
return T;
}
}
//2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象
FutureTask<Integer> future = new FutureTask<>(callable);
//3. 通过Thread,启动线程
new Thread(future).start()
FutureTask常用方法:
变量和类型 | 方法 | 描述 |
---|---|---|
protected boolean | cancel(boolean x) | 传入true则表示取消该线程。返回值为true表示线程还未执行完毕,成功取消;返回值为false表示线程已经执行完毕,取消失败。 |
V | get() | 如果需要等待计算完成,然后检索其结果。(主线程会等待子线程执行完返回结果再执行) |
V | get(long timeout,TimeUnit unit) | 如果需要,最多等待计算完成的给定时间,然后检索其结果(如果可用)。 |
protected void | isDone() | 判断该子线程是否执行完毕。 |
20. Runnable与Callable
相同点:
- 都是接口
- 都可以编写多线程程序
- 都采用Thread.start()启动线程
不同点:
- Runnable没有返回值;Callable可以返回执行结果
- Callable接口的call()允许抛出异常;Runnable的run()不能抛出
21. 线程池ExecutorService
线程池的概念: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。
线程池的好处:
- 降低资源消耗。
- 提高响应速度。
- 提高线程的可管理性。
Java中四种线程池:
- 缓存线程池
- 定长线程池
- 单线程池
- 周期定长线程池
前三个线程池对象获取格式均为: ExecutorService service =… …;
向线程池中加入并执行新的任务: Service.execute(new Runnable(){…});
22. 缓存线程池
长度无限制
创建线程对象格式: ExecutorService service = Executors.newCachedThreadPool();
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在,则创建线程并放入线程池,然后使用
23. 定长线程池
长度是指定的数值
创建线程对象格式(创建时指定线程池长度nThread): ExecutorService service = Executors.newFixedThreadPool(int nThreads);
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
24. 单线程线程池
长度为1,排队执行
创建线程对象格式: ExecutorService service = Executors.newSingleThreadExecutor();
执行流程:
- 判断线程池的那个线程是否空闲
- 空闲则使用
- 不空闲,则等待池中的单个线程空闲后使用
25. 周期定长线程池
周期任务 长度指定
执行流程:
- 判断线程池是否存在空闲线程
- 存在则使用
- 不存在空闲线程,且线程池未满的情况下,则创建线程并放入线程池,然后使用
- 不存在空闲线程,且线程池已满的情况下,则等待线程池存在空闲线程
创建线程对象格式:ScheduledExecutorService service = Executors.newScheduledThreadPool (int nThread);
周期性任务执行时:
定时执行,当某个时机触发时,自动执行某任务
定时执行仅一次格式: service.schedule(new Runnable(){},num2,num3);
- 参数1:runnable类型的任务
- 参数2:时长数字
- 参数3:时长数字的单位,TimeUnit.SECONDS表示秒
周期执行格式: service.scheduleAtFixedRate(new Runnable(){},num2,num3,num4);
- 参数1:runnable类型的任务
- 参数2:时长数字(延迟执行的时长,即第一次执行等待的时间)
- 参数3:周期时长(每次执行的间隔时间)
- 参数4:时长数字的单位,TimeUnit.SECONDS表示秒
eg:
//五秒后每间隔一秒执行一次
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
},5,1, TimeUnit.SECONDS);
26. Lambda表达式
概念: 函数式编程思想。将方法省略至只有参数和方法体。
格式: (方法参数) –> {方法体}
eg:
public static void main(String[] args) {
//使用Lambda表达式后
print((int x,int y) -> {
return x+y;
},100,200);
//使用前
print(new MyMath() {
@Override
public int sum(int x, int y) {
return x+y;
}
},100,200);
}
//定义方法实现接口
public static void print(MyMath m,int x,int y){
int num = m.sum(x,y);
System.out.println(num);
}
//定义一个接口
static interface MyMath{
int sum(int x,int y);
}