JAVA多线程预习

sdjasj

线程概述

线程和进程区别

  • 一个任务通常就是一个程序,每个运行中的程序就是一个进程。当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程
  • 归纳起来可以这样说∶操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

并发和并行

并发性(concurrency)和并行性(parallel)是两个概念,并行指在同一时刻,有多条指令在多个处理器上同时执行;并发指在同一时刻只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

线程的创建和启动

三种创建线程的方法:

  • 通过实现 Runnable 接口;

实现 Runnable 接口来创建并启动多线程的步骤如下

1.定义Runnable 接口的实现类,并重写该接口的 run方法,该run方法的方法体同样是该线程的线程执行体。

2.创建 Runnable 实现类的实例,并以此实例作为Thread 的 target 来创建 Thread 对象,该 Thread 对象才是真正的线程对象。

1
2
3
SecThread sc = new SecThread() //SecThread类实现Runnable接口
new Thread(sc).start() //以sc建立新线程
new Thread(sc,'lbh666') //创建线程时指定名字

注意:

1.采用Runnable 接口的方式创建的多个线程可以共享线程类的实例变量。这是因为在这种方式下,程序所创建的 Runnable 对象只是线程的 target,而多个线程可以共享同一个target,所以多个线程可以共享同一个线程类的实例变量

2.可用lambda表达式创建Runnable对象,因为Runnable是一个函数式接口

3.执行start()方法的顺序不代表线程启动的顺序

  • 通过继承 Thread 类本身

image-20220209151412300

注意:

使用继承Thread类的方法来创建线程类时,多个线程之间无法共享线程类的实例变量。

  • 通过 Callable 和 Future 创建线程。

创建过程

1.创建Callable 接口的实现类,并实现 call方法,该call方法将作为线程执行体,且该 call方法有返回值,再创建 Callable 实现类的实例。从Java 8开始,可以直接使用Lambda表达式创建Callable 对象。

2.使用FutureTask类来包装Callable 对象,该 FutureTask 对象封装了该Callable 对象的 call(方法的返回值。

3.使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

4.调用 FutureTask 对象的 get方法来获得子线程执行结束后的返回值。

1
2
3
4
5
6
7
8
9
10
//创建FutureTask对象,callable对象使用lambda表达式
FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>) ()->{
for (int i = 0; i < 100; i++){
System.out.println("lbh666"+" curnum:" + i);
}
return 666;
});

//创建新线程
new Thread(task,"lbh6666666")
  • 三种方法区别

通过继承Thread 类或实现 Runnable、Callable 接口都可以实现多线程,不过实现 Runnable 接口与实现Callable 接口的方式基本相同,只是Callable 接口里定义的方法有返回值,可以声明抛出异常而已。因此可以将实现 Runnable 接口和实现Callable 接口归为一种方式。这种方式与继承 Thread 方式之间的主要差别如下。

##### 采用实现Runnable、Callable 接口的方式创建多线程的优缺点∶

优势:线程类只是实现了Runnable 接口或Callable 接口,还可以继承其他类。
在这种方式下,多个线程可以共享同一个 target 对象,所以非常适合多个相同线程来处理同一
份资源的情况,从而可以将 CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。

劣势:编程稍稍复杂,如果需要访问当前线程,则必须使用Thread.currentThread方法

采用继承 Thread类的方式创建多线程的优缺点∶

劣势:因为线程类已经继承了Thread类,所以不能再继承其他父类。

优势:编写简单,如果需要访问当前线程,则无须使用Thread.currentThread方法,直接使用
this即可获得当前线程。

鉴于上面分析,因此一般推荐采用实现 Runnable 接口、Callable 接口的方式来创建多线程。

线程的生命周期

image-20220209155349216

线程死亡

线程结束后就处于死亡状态,有三种结束方式

  • run()或者call()方法执行完毕,线程正常结束
  • 线程抛出一个未捕获的异常或error
  • 使用线程的stop()方法结束线程(容易导致死锁,不推荐)

已经死亡的线程不能再start

控制线程

几个概念

  • join线程

某个程序执行流调用其他线程的join()方法后,调用线程将会被阻塞,直到被join()方法加入的join()线程执行完毕

通常在主程序中将大问题分割为小问题,每个小问题用join()执行完毕,主线程再继续执行下去

  • 后台线程 SetDaemon
  • 线程睡眠 Thread.sleep

当前线程变为阻塞,过了阻塞时间才会转入就绪状态,不关心其他线程的优先级

  • 线程让步 yield

当前线程变为就绪而不是阻塞,只给优先级相同或较高的线程执行机会

线程优先级

a.SetPriority()改变a线程的优先级,优先级高的线程执行机会更大,线程的默认优先级与创建它的父线程优先级相同

线程同步

同步代码块

1
2
3
4
5
synchronized (obj){
...
//同步代码块
}
//obj是同步监视器,线程开始执行同步代码块时必须先获得同步监视器的锁定

任何线程在进入同步代码块时,首先对obj加锁,在加锁期间其他线程无法获得锁,无法进入同步代码块,当该线程执行完该同步代码块后,释放锁。

同步方法

  • synchronized关键字修饰的方法,同步监视器是this(对象),保证了任意时刻只有一个线程能获得该对象的锁
  • synchronized关键字不能修饰构造器、成员变量等
  • 线程安全会降低程序运行效率,只对会改变竞争资源的方法进行同步

同步锁(Lock)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A{
private final Lock lock = new ReentrantLock();

//需要保证线程安全的方法
public void lbh{
lock.lock();//加锁
try{
...//保证线程安全的代码
}
finally{
lock.unlock();
}
}
}

死锁

  • 定义:集合中的每一个进程都在等待只能由本集合中的其他进程才能引发的事件,那么该组进程是死锁的
  • 例子:进程A锁住了记录1并等待记录2,而进程B锁住了记录2并等待记录1,这样两个进程就发生了死锁现象

线程通信

1.传统线程通信

  • 三种方法

  1. wait()∶导致当前线程等待,释放当前线程的锁,直到其他线程调用该同步监视器的 notify()方法或 notifyAll()方法来唤醒该线程
  2. notify()∶唤醒在此同步监视器上等待的单个线程。如果所有线程都在此同步监视器上等待,则会选择唤醒其中一个线程。选择是任意性的。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
  3. notifyAll()∶ 唤醒在此同步监视器上等待的所有线程。只有当前线程放弃对该同步监视器的锁定后,才可以执行被唤醒的线程。
  • 调用手段(同步监视器对象调用)

  1. 对于synchronized修饰的同步方法,可以在方法中直接调用(this就是同步监视器)
  2. 对于synchronized修饰的同步代码块,用同步监视器对象即synchronized括号后的对象调用

2.Condition控制线程通信

  • 使用情况:使用Lock来保证同步时
  • 三种方法
  1. await()
  2. signal()
  3. signalAll()
  • 类似于Lock对象充当同步监视器,Condition对象来暂停、唤醒线程
  • 举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class A{
private final Lock lock = new ReentrantLock();
private final Condition cond = new lock.newCondition();
//需要保证线程安全的方法
public void lbh{
lock.lock();//加锁
try{
cond.await()
...//保证线程安全的代码
cond.signal();
}
finally{
lock.unlock();
}
}
}

3.BlockingQueue阻塞队列控制线程通信

  • 方法

image-20220210061627598

  • 实现类

image-20220210061939323

线程组

  • 线程在创建时可以指定线程组Thread(ThreadGroup group, Runnable target, String name),没有指定线程组则属于默认线程组,线程的线程组一旦确定不可更改
  • 线程组可对一组的线程进行操作

线程池

复用一组线程把,很多小任务让一组线程来执行,而不是一个任务对应一个新线程,这种能接收大量小任务并进行分发处理的就是线程池

简单地说,线程池内部维护了若干个线程,没有任务的时候,这些线程都处于等待状态。如果有新任务,就分配一个空闲线程执行。如果所有线程都处于忙碌状态,新任务要么放入队列等待,要么增加一个新线程进行处理

Java标准库提供了ExecutorService接口表示线程池,它的典型用法如下:

1
2
3
4
5
6
7
8
// 创建固定大小的线程池:
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);

线程安全

每个线程都有自己独立的调用栈,局部变量保存在线程各自的调用栈里,不会共享,不存在并发安全问题

原子性

即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

JVM规范定义了几种原子操作

  • 基本类型(longdouble除外)赋值,例如:int n = m
  • 引用类型赋值,例如:List<String> list = anotherList。(但是,如果是多行赋值语句,就必须保证是同步操作)

Volatile

volatile关键字的两层语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2.禁止进行指令重排序。

3.volatile不保证原子性(i++会有问题)

4.volatile解决可见性问题,不解决原子性问题

ReadWriteLock

适用于允许多个线程同时读,但只要有一个线程在写,其他线程就必须等待的情况

允许 不允许
不允许 不允许

使用ReadWriteLock可以提高读取效率:

  • ReadWriteLock只允许一个线程写入;
  • ReadWriteLock允许多个线程在没有写入时同时读取;
  • ReadWriteLock适合读多写少的场景
  • 標題: JAVA多线程预习
  • 作者: sdjasj
  • 撰寫于: 2022-02-10 10:38:03
  • 更新于: 2023-03-05 15:24:35
  • 連結: https://redefine.ohevan.com/2022/02/10/JAVA多线程预习/
  • 版權宣告: 本作品采用 CC BY-NC-SA 4.0 进行许可。
 留言