Java多线程知识汇总(二)

发布时间:2024年01月20日

目录


一、Java多线程

1、进程与线程

进程

  • 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
  • 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备

线程

  • 一个进程内可分为多个线程
  • 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令

2、并行与并发

并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu

并行:多核cpu运行 多线程时,真正的在同一时刻运行

线程的并行与并发
线程的并行与并发

二、线程的礼让

????????yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。

示例代码:

// 方法的定义
public static native void yield();

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        Thread.yield(); //礼让
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();

运行结果:

11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield -             ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518

????????如上结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。

三、线程的优先级

线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5

? cpu比较忙时,优先级高的线程获取更多的时间片

? cpu比较闲时,优先级设置基本没用

 public final static int MIN_PRIORITY = 1;

 public final static int NORM_PRIORITY = 5;

 public final static int MAX_PRIORITY = 10;

 // 方法的定义
 public final void setPriority(int newPriority) {
 }

cpu比较忙时

Runnable r1 = () -> {
    int count = 0;
    for (;;){
       log.info("---- 1>" + count++);
    }
};
Runnable r2 = () -> {
    int count = 0;
    for (;;){
        log.info("            ---- 2>" + count++);
    }
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();

// 可能的运行结果
11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135903
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135904
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135905
11:59:00.696 [t2] INFO thread.TestYieldPriority -             ---- 2>135906

四、守护线程

默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。

默认的线程都是非守护线程。

垃圾回收线程就是典型的守护线程

// 方法的定义
public final void setDaemon(boolean on) {
}

Thread thread = new Thread(() -> {
    while (true) {
    }
});
// 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
// 默认是false,当主线程结束后,thread继续运行,程序不停止
thread.setDaemon(true);
thread.start();
log.info("结束");

五、线程的阻塞

线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种:

  1. BIO阻塞,即使用了阻塞式的io流
  2. sleep(long time) 让线程休眠进入阻塞状态
  3. a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
  4. sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
  5. 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
  6. LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)

?六、线程的打断

// 相关方法的定义
public void interrupt() {
}
public boolean isInterrupted() {
}
public static boolean interrupted() {
}

打断标记:线程是否被打断,true表示被打断了,false表示没有

isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记

interrupt()方法用于中断线程:

  1. 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
  2. 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true

interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)

interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下

@Slf4j
class TwoPhaseTerminal{
    // 监控线程
    private Thread monitor;

    public void start(){
        monitor = new Thread(() ->{
           // 不停的监控
            while (true){
                Thread thread = Thread.currentThread();
                 // 判断当前线程是否被打断
                if (thread.isInterrupted()){
                    log.info("当前线程被打断,结束运行");
                    break;
                }
                try {
                    Thread.sleep(1000);
                    // 监控逻辑中被打断后,打断标记为true
                    log.info("监控");
                } catch (InterruptedException e) {
                    // 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false
                    // 在调用一次中断 使得中断标记为true
                    thread.interrupt();
                }
            }
        });
        monitor.start();
    }

    public void stop(){
        monitor.interrupt();
    }
}

?七、线程的相关方法总结

主要总结Thread类中的核心方法

方法名称是否static方法说明
start()让线程启动,进入就绪状态,等待cpu分配时间片
run()重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑
yield()线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片
sleep(time)线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断
join()/join(time)调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片
isInterrupted()获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记
interrupt()打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记
interrupted()获取线程的打断标记。调用后会清空打断标记
stop()停止线程运行 不推荐
suspend()挂起线程 不推荐
resume()恢复线程运行 不推荐
currentThread()获取当前线程

Object中与线程相关方法

方法名称方法说明
wait()/wait(long timeout)获取到锁的线程进入阻塞状态
notify()随机唤醒被wait()的一个线程
notifyAll();唤醒被wait()的所有线程,重新争抢时间片

同步锁

线程安全

  • 一个程序运行多个线程本身是没有问题的
  • 问题有可能出现在多个线程访问共享资源

    • 多个线程都是读共享资源也是没有问题的
    • 当多个线程读写共享资源时,如果发生指令交错,就会出现问题

临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。

注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。

线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。

如下面不安全的代码:

// 对象的成员变量
private static int count = 0;

public static void main(String[] args) throws InterruptedException {
  // t1线程对变量+5000次
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count++;
        }
    });
  // t2线程对变量-5000次
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            count--;
        }
    });

    t1.start();
    t2.start();

    // 让t1 t2都执行完
    t1.join();
    t2.join();
    System.out.println(count);
}

// 运行结果 
-1399

上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。

但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。

线程安全的类一定所有的操作都线程安全吗?

开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的

成员变量和静态变量是否线程安全?

  • 如果没有多线程共享,则线程安全
  • 如果存在多线程共享

    • 多线程只有读操作,则线程安全
    • 多线程存在写操作,写操作的代码又是临界区,则线程不安全

局部变量是否线程安全?

  • 局部变量是线程安全的
  • 局部变量引用的对象未必是线程安全的

    • 如果该对象没有逃离该方法的作用范围,则线程安全
    • 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全

synchronized

同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。

该关键字是用于保证线程安全的,是阻塞式的解决方案。

让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。

注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。

当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程

synchronized实际上使用对象锁保证临界区的原子性?临界区的代码是不可分割的 不会因为线程切换所打断

基本使用:

// 加在方法上 实际是对this对象加锁
private synchronized void a() {
}

// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b(){
    synchronized (this){

    }
}

// 加在静态方法上 实际是对类对象加锁
private synchronized static void c() {

}

// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d(){
    synchronized (TestSynchronized.class){

    }
}

// 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方
 0 aload_0
 1 dup
 2 astore_1
 3 monitorenter
 4 aload_1
 5 monitorexit
 6 goto 14 (+8)
 9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return

线程安全的代码:

private static int count = 0;

private static Object lock = new Object();

private static Object lock2 = new Object();

 // t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count++;
            }
        }
    });
    Thread t2 = new Thread(() -> {
        for (int i = 0; i < 5000; i++) {
            synchronized (lock) {
                count--;
            }
        }
    });

    t1.start();
    t2.start();

    // 让t1 t2都执行完
    t1.join();
    t2.join();
    System.out.println(count);
}

重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效

线程通信

wait+notify

线程间通信可以通过共享变量+wait()&notify()来实现

wait()将线程进入阻塞状态,notify()将线程唤醒

当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)

如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁。

  1. Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
  2. Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
  3. 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
  4. 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁

注意:

  1. Blocked状态和Waitting状态都是阻塞状态
  2. Blocked线程会在owner线程释放锁时唤醒
  3. wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常
  • wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
  • notify()随机唤醒一个waitSet里的线程
  • notifyAll()唤醒waitSet中所有的线程
static final Object lock = new Object();
new Thread(() -> {
    synchronized (lock) {
        log.info("开始执行");
        try {
              // 同步代码内部才能调用
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("继续执行核心逻辑");
    }
}, "t1").start();

new Thread(() -> {
    synchronized (lock) {
        log.info("开始执行");
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("继续执行核心逻辑");
    }
}, "t2").start();

try {
    Thread.sleep(2000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
log.info("开始唤醒");

synchronized (lock) {
  // 同步代码内部才能调用
    lock.notifyAll();
}
// 执行结果
14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑

本文转自:万字图解Java多线程 - 个人文章 - SegmentFault 思否

文章来源:https://blog.csdn.net/haohaizi_liu/article/details/135717061
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。