1.进程与线程的概念
(1)进程
程序有指令与数据组成,指令要运行,数据要读写,就必须指令加载到CPU。数据加载到内容,指令运行需要用到磁盘。
当一个程序被运行时,从磁盘加载这个程序的代码至内存,这时开启了一个进程。
(2)线程
一个进程之内可以分为一个或多个线程。
一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。
Java中的 最小调度单位是线程,最小资源分配单位是进程。
2.并发与并行的概念
并发:同一时间应对多件事情的能力。
并行:同一时间动手做多件事情的能力。
3.异步与同步的概念
异步:不需要等待结果返回,就能继续运行
同步:需要等待结果返回,才能继续运行
同步在多线程中是让多个线程步调一致
4.多线程提高效率的结论
(1)单核CPU下,多线程不能实际提高程序运行效率,只是为了能够在不同任务之间切换,不同
线程轮流使用CPU,不至于一个线程总是占用一个CPU,别的线程没法干活。
(2)多核CPU下,可以并行运行多个线程,能否提高效率要分情况:
一些任务可以通过设计,将任务拆分,并行执行,可以提高运行效率。
不是所有任务都需要拆分,任务的目的不同。
(3)IO操作不占用CPU,只是我们一般拷贝文件使用的是 阻塞IO ,相当于线程虽然不用CPU,但是需要等待IO结束,没能充分利用线程,才有了 非阻塞IO 和 异步IO优化
5.创建线程
5.1 Thread的方式
Thread thread = new Thread(){
@Override
public void run(){
System.out.println("Hello JUC");
}
};
thread.start();
5.2 Runnable的方式
Runnable runnable = new Runnable(){
@Override
public void run(){
System.out.println("Hello JUC Runnable");
}
};
Thread thread = new Thread(runnable);
thread.start();
5.3 Lambda的方式
5.3.1 Thread的Lambda写法
Thread thread = new Thread(()->{ System.out.println("Hello JUC Lambda");});
thread.start();
5.3.2 Runnable的Lambda写法
Runnable runnable = ()->{
System.out.println("Hello JUC Lambda");
};
Thread thread = new Thread(runnable);
thread.start();
5.4 FutureTaks 配合 Thread
FutureTasks 能够接收Callable类型的参数,用于·处理有返回结果的情况
@Test
void testFutureTask() throws ExecutionException, InterruptedException {
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
System.out.println("Running ...");
Thread.sleep(3000);
return 100;
}
});
Thread thread = new Thread(task);
thread.start();
System.out.println(task.get());
}
6.线程运行
线程运行现象:线程运行交替执行。谁先谁后,不由我们所控制。
查询线程的方式:
(1)windows:
可以通过任务管理器查看进程和线程数
tasklilst 查看进程
taskkill 杀死进程
(2)linux:
ps -fe 查看所有进程
ps -fT -p <PID> 这将显示PID进程及其所有线程的详细信息。
kill 杀死进程
top 按大写H切换是否显示线程
top -H -p <PID> 查看某个进程(PID)
(3)Java:
jps 命令查看所有Java进程
jstack <PID> 查看某个Java进程(PID)的所有线程状态
jconsole 来查看某个Java进程中线程的运行情况
7.线程运行的原理
7.1栈帧
JVM是由堆、栈、方法区所组成的,其中栈内存 存放就是线程,每个线程启动后,虚拟机就会分配一块栈内存。
每个栈有多个栈帧,对应着每次方法调用时所占用的内存。
每个线程只有一个活动栈帧,对应着当前正在执行的那个方法。
7.2上下文切换(Thread Context Switch)
可能因为
- 线程的cpu时间用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
当Thread Context Switch发送时,需要操作系统保存当前线程的状态,并恢复另一个线程的状态,Java中对应的概念就是程序计数器,它的作用记录下一条jvm指令的执行地址,是线程私有的。
状态包括程序计数器,虚拟机中每个栈帧的信息,如局部变量,操作数栈,返回地址等
Thread Context Switch频繁发生会影响性能
8.线程中的常见方法
|------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 方法 | 描述 |
| start() | 启动新线程并执行该线程的run()
方法。线程开始执行时,它的run()
方法会被调用。 |
| run() | 当线程启动时,run()
方法会被调用。这个方法应该包含线程的执行代码。 |
| join() | 等待调用join()
方法的线程终止。例如,thread.join()
会使当前线程等待thread
线程终止。 |
| sleep(long millis) | 使当前线程暂停执行指定的毫秒数。线程不会失去任何监视器的所有权。 |
| interrupt() | 中断线程。如果线程在调用Object
类的wait()
、wait(long)
、wait(long, int)
、join()
、join(long, int)
或者sleep(long, int)
方法时被阻塞,那么它的中断状态将被清除,并且它将接收到InterruptedException
。 |
| isAlive() | 测试线程是否处于活动状态。如果线程已经启动且尚未终止,则线程处于活动状态。 |
| setName(String name) | 改变线程的名称,可以通过getName()
方法获取线程的名称。 |
| getPriority() | 返回线程的优先级。线程的优先级可以设置为MIN_PRIORITY
(1)、NORM_PRIORITY
(5)或MAX_PRIORITY
(10)。 |
| setPriority(int newPriority) | 变线程的优先级。线程优先级只是建议给调度器,实际调度可能会忽略它。 |
| yield() | 暂停当前正在执行的线程对象,并执行其他线程。 |
| getState() | 返回线程的状态。线程的状态可以是NEW
、RUNNABLE
、BLOCKED
、WAITING
、TIMED_WAITING
或TERMINATED
。 |
| isInterrupted() | 测试线程是否已经中断。不同于interrupted()
方法,这个方法不会改变线程的中断状态。 |
8.1 run 与 start 的区别
(1)
run() 方法:
run()
方法是线程的执行主体,它包含了线程要执行的任务。- 当线程被启动时,
run()
方法会被自动调用。 run()
方法可以直接调用,就像调用普通方法一样,它在当前线程中执行,而不是在新线程中执行。- 如果直接调用
run()
方法,程序不会创建新的线程,而是在当前线程中顺序执行run()
方法中的代码。
(2)
start() 方法:
start()
方法用于启动一个新线程,并执行该线程的run()
方法。- 当
start()
方法被调用时,Java 虚拟机会创建一个新的线程,并执行run()
方法中的代码。 start()
方法只能被调用一次,多次调用会抛出IllegalThreadStateException
。- 调用
start()
方法后,线程可能会立即开始执行,也可能因为线程调度器的安排而在稍后执行。
8.2 Sleep和yield的区别
(3)Sleep
- 调用sleep让当前线程从Running进入Timed Waiting(阻塞)状态
- 其他线程可以通过interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptedException
- 睡眠解释后的线程未必会立即执行
- 建议使用Timeunit的sleep代替Thread的sleep,有更好的可读性
(4)yield
- 调用yield 会使当前线程从Running进入Runnable就绪状态,然后调度执行其他线程
- 具体的实现依赖操作系统的任务调度器
- 在Java中,
Thread.yield()
也是一个静态方法,它使当前线程从运行状态转到可运行状态,但不会释放所占有的任何资源。 - 通常,
yield
的使用并不频繁,因为它对线程调度提供的信息有限,且线程调度器可能会忽略这个提示。
8.3 线程优先级
- 线程优先级会提示调度器优先调度线程
- 如果CPU比较忙,那么优先级高的线程会获得更多的时间片;如果CPU比较空闲,优先级几乎没有什么作用。
8.4 Sleep实现
在没有利用CPU实现计算时,不要让while(true)空转浪费CPU,这时可以使用yield或sleep使CPU的使用权让给其他的程序
8.5 join实现
等待thread
线程运行结束或终止。
如果join(long n)带参数的话,就是等待线程运行结束的最多等待n毫秒
8.6 打断阻塞
阻塞
打断sleep的线程,清空打断状态
示例:
@Test
void test() throws ExecutionException, InterruptedException {
Thread t1 = new Thread(()->{
try{
TimeUnit.SECONDS.sleep(12);
}catch (Exception e){
e.printStackTrace();
}
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
System.out.println(t1.isInterrupted());
}
注意: 使用 isInterrupted()来判断是否打断成功,对于打断sleep 的结果为false则表示打断成功,同理wait和join 都是false标记为打断成功。
8.7 打断正常
打断正常的则isInterrupted()为true则表示打断成功
示例:
@Test
void test() throws InterruptedException {
Thread t1 = new Thread(()->{
try{
while (true){
boolean interrupted = Thread.currentThread().isInterrupted();
if (interrupted){
log.info("打斷...");
break;
}
}
}catch (Exception e){
e.printStackTrace();
}
},"t1");
t1.start();
TimeUnit.SECONDS.sleep(3);
log.info("interrupt");
t1.interrupt();
}
9.两阶段终止
问题:在一个线程T1中如何优雅的终止线程T2呢?
错误的思路:
- 使用stop方法终止线程,这种如果此时线程锁住了公共享资源,将其杀死后就再也没有机会释放资源,导致其他线程永远无法获取锁。
- 使用System.exit(int)方法停止线程,目的是仅停止一个线程,但是会让整个程序都停止
正确的思路:
@Test
void test() throws InterruptedException {
Thread t1 = new Thread(()->{
while (true){
Thread current = Thread.currentThread();
if(current.isInterrupted()){
log.info("打断....");
break;
}
try {
Thread.sleep(800);
log.info("正在记录中.....");
}catch (InterruptedException e){
e.printStackTrace();
//重置设置打断标识,因为sleep过程中进行interrupt是标识false,那么需要正常的interrupt使其标识为true
current.interrupt();
}
}
});
t1.start();
TimeUnit.SECONDS.sleep(3);
t1.interrupt();
}
10.打断park线程
打断线程后不会影响标记
11.不推荐的方法
这些方法过时,容易破坏同步代码块,造成线程死锁
|-----------|------------|
| 方法名 | 说明 |
| stop() | 停止线程运行 |
| suspend() | 挂起(暂停)线程运行 |
| resume() | 恢复线程运行 |
12.主线程与守护线程
默认情况下,Java进程需要等待线程运行都结束,才会结束。有一种特殊的线程是守护线程,只要其他的线程运行结束了,即使守护线程的代码还没有执行结束,也会强制结束。
示例:设置t1线程为守护线程:
t1.setDaemon(true);
运用:
- 垃圾回收器线程就是一种守护线程
- 接收shutdown命令后,不会等待Acceptor和Poller守护线程处理完当前请求
13.线程的五种状态
五种状态:初始状态、可运行状态、运行状态、终止状态、阻塞状态
- 初始状态:仅在语言层面创建了线程对象,还未与操作系统线程关联
- 可运行状态;也称(就绪状态),指线程已经被创建(与操作系统线程未关联),可以由CPU调度执行
- 运行状态:指获取CPU时间片运行中的状态
- 阻塞状态:如果调用了阻塞API,这时线程实际不会用到CPU,会导致线程上下文切换,进入阻塞状态
- 终止状态:表示线程已经执行完毕,生命周期已经结束,不会转换为其他状态
14.共享资源的线程安全(synchronized)
竞态条件:发生在至少两个线程竞争同一资源时,而最终的结果取决于这些线程的执行顺序。在大多数情况下,竞态条件的出现是由于程序设计上的缺陷,例如没有适当的同步机制来控制对共享资源的访问。
为了避免临界的竞态的条件发生,可以使用以下的手段实现:
- 阻塞式的解决方案:Synchronized,Lock
- 非阻塞式的解决方案:原子变量
使用阻塞式的解决方案:Synchronized(对象锁),采用互斥的方式让同一时刻至多只有一个线程能持有 对象锁 ,其他线程再想获取这个 对象锁就会被阻塞住。这样可以保证线程可以安全的执行临界区内的代码,不用担心线程的上下文切换
- 互斥是保证临界区的竞态条件发生,同一时刻只有一个线程执行临界区代码
- 同步是由于线程执行的先后,顺序不同,需要一个线程等待其他线程运行到某个点
示例代码:
int counter = 0;
Object lock = new Object();
@Test
void test() throws InterruptedException {
Thread t1 = new Thread(()->{
for (int i = 0;i<=50000;i++){
synchronized(lock){
counter++;
}
}
});
Thread t2 = new Thread(()->{
for (int i = 0;i<=50000;i++){
synchronized(lock){
counter--;
}
}
});
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("Counter:{}",counter);
}
synchronized用对象锁保证了临界区内代码的原子性,临界区的代码对外不可分割,不会被线程切换所打断
思考:
如果synchronized(obj)放在for循环外面,如何理解? --原子性
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎么运行?--锁对象
如果 t1 synchronized(obj)而 t2没有加会怎么样?--锁对象
14.1 通过面向对象的方式实现:
创建实体:
public class Room {
private int counter = 0;
public void increment(){
synchronized (this){
counter++;
}
}
public void decrement(){
synchronized (this){
counter--;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
}
实现代码:
Room room = new Room();
@Test
void test() throws InterruptedException {
Thread t1 = new Thread(()->{
room.increment();
});
Thread t2 = new Thread(()->{
room.decrement();
});
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.info("Counter:{}",room.getCounter());
}
14.2 synchronized加在方法上
(1)
public class Test {
public synchronized void test(){
}
}
相当于:
public class Test {
public void test(){
synchronized(this){
}
}
}
(2)static方法
public class Test {
public synchronized static void test(){
}
}
相当于:
public class Test {
public static void test(){
synchronized(Test.class){
}
}
}
15.变量线程安全分析
15.1 成员变量和静态变量是否线程安全?
(1)如果没有共享,则线程安全
(2)如果共享,根据他们的状态是否能够改变,分为两种情况:
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
15.2 局部变量是否线程安全?
(1)局部变量是线程安全的
(2)但是局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用访问,它是线程安全的
- 如果该对象逃离方法的作用范围,则需要考虑线程安全
16.常用的线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.utils.concurrent包下的类
这里的线程安全是多线程调用它们同一个实例的某个方法时,线程是安全的。可以理解为
- 它们的每个方法是原子
- 但是它们的多个方法的组合不是原子的(不是线程安全的)
不可改变线程安全
String、Integer等都是不可变类,因为其内部的状态不可改变,因此它们的方法都是线程安全的
虽然String有replace、substring等方法可以改变值、但是根据源码可知都是创新一个新的对象,没有改变原有的对象的值,所以线程安全
17.买票问题习题
(1)线程安全问题示例
package org.example;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Vector;
// 主类,用于执行售票模拟
public class ExerciseSell {
// 执行售票模拟的入口点
public static void main(String[] args) throws InterruptedException {
// 创建一个 TicketWindow 实例,初始票数为1000
TicketWindow ticket = new TicketWindow(1000);
// 创建一个线程列表,用于存放即将创建的线程
List<Thread> threadList = new ArrayList<>();
// 创建一个Integer类型的列表,用于存放每个线程卖出的票数
List<Integer> amountList = new Vector<>();
// 创建80000个线程,每个线程将模拟卖出一定数量的票
for(int i = 0; i < 80000; i++) {
Thread thread = new Thread(() -> {
// 每个线程卖出的票数是随机生成的
int amount = ticket.sell(randomAmount());
try {
// 模拟售票操作后的延迟
Thread.sleep(10);
} catch (InterruptedException e) {
// 如果线程在睡眠中被中断,抛出运行时异常
throw new RuntimeException(e);
}
// 将卖出的票数添加到amountList中
amountList.add(amount);
});
// 将新创建的线程添加到线程列表中
threadList.add(thread);
// 启动线程
thread.start();
}
// 等待所有线程完成
for (Thread thread : threadList) {
thread.join();
}
// 打印剩余票数
System.out.println("余票:" + ticket.getAmount());
// 计算所有线程卖出的票数总和并打印
System.out.println("卖出的票数:" + amountList.stream().mapToInt(i -> i).sum());
}
// 生成随机售票数的方法
static Random random = new Random();
public static int randomAmount() {
// 返回1到5之间的随机整数,包括1和5
return random.nextInt(5) + 1;
}
}
// 表示售票窗口的类
class TicketWindow {
// 私有属性,表示售票窗口的票数
private int amount;
// 构造函数,初始化票数为传入的参数
public TicketWindow(int amount) {
this.amount = amount;
}
// 获取当前票数的方法
public int getAmount() {
return amount;
}
// 售票方法,尝试从窗口卖出指定数量的票
public int sell(int amount) {
if (this.amount >= amount) {
// 如果票数足够,减少票数并返回卖出的票数
this.amount -= amount;
return amount;
} else {
// 如果票数不足,返回0
return 0;
}
}
}
(2)解决线程安全问题方法
这段代码存在线程安全问题。具体来说,TicketWindow
类的 amount
成员变量在被多个线程访问和修改时,没有使用任何同步机制,这可能导致多个线程同时修改 amount
,从而引发数据不一致的问题。
所以要给amount 进行共享资源读写的时进行加锁操作(这段代码,只需要对sell方法加上synchronized即可加锁)
public synchronized int sell(int amount){
if(this.amount >= amount){
this.amount -= amount;
return amount;
}else {
return 0;
}
}
18. 转账习题
(1)线程安全问题示例
package org.example;
import java.util.Random;
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Amount a = new Amount(1000);
Amount b = new Amount(1000);
Thread t1 = new Thread(()->{
for(int i = 0;i < 100;i++){
a.transfer(b,randomAmount());
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i < 100;i++){
b.transfer(a,randomAmount());
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("总金额:"+a.getMoney() + b.getMoney());
}
static Random random = new Random();
public static int randomAmount(){
return random.nextInt(100) + 1;
}
}
class Amount{
private int money;
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
public Amount(int money) {
this.money = money;
}
public void transfer(Amount amount,int transferMoney){
if(this.money >= transferMoney){
this.setMoney(this.getMoney() - transferMoney);
amount.setMoney(amount.getMoney() + transferMoney);
}
}
}
(2)解决线程安全问题方法(两个共享变量)
public void transfer(Amount amount,int transferMoney){
synchronized(Amount.class){
if(this.money >= transferMoney){
this.setMoney(this.getMoney() - transferMoney);
amount.setMoney(amount.getMoney() + transferMoney);
}
}
}
因为如果
public synchronized void transfer(Amount amount,int transferMoney){
if(this.money >= transferMoney){
this.setMoney(this.getMoney() - transferMoney);
amount.setMoney(amount.getMoney() + transferMoney);
}
}
相当于保护this.money,但是不保护amount的money
public void transfer(Amount amount,int transferMoney){
synchronized(this){
if(this.money >= transferMoney){
this.setMoney(this.getMoney() - transferMoney);
amount.setMoney(amount.getMoney() + transferMoney);
}
}
}