java并发多线程

2025/05/03 Java 共 10299 字,约 30 分钟

多线程

先看:http://www.cyc2018.xyz/Java/Java%20%E5%B9%B6%E5%8F%91.html#%E4%B8%80%E3%80%81%E4%BD%BF%E7%94%A8%E7%BA%BF%E7%A8%8B

概念

饥饿状态:某几个进程长久得不到运行

活锁:几个进程同时谦让对方,使得没有进程得到资源

Java 中的 Long 是非原子性的,前 32 和后 32 不同写入,int 操作是原子性的

编译器为了减少中断流水线的次数,所以会进行指令重排,串行中不会发生的问题并行中就出现了

线程的生命周期

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。

3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行 wait()方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行 sleep()或 join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)

阻塞三种状态:

  1. BLOCKED: synchronized
  2. WAITING: 无限等待
  3. TIMED_WAITING: 有限时间的等待

5、死亡状态(Dead):线程执行完了或者因异常退出了 run()方法,该线程结束生命周期。

使用线程

有三种使用线程的方法:

  • 实现 Runnable 接口;
  • 实现 Callable 接口;
  • 继承 Thread 类。

实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

实现 Runnable 接口

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

public class MyRunnable implements Runnable {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyRunnable instance = new MyRunnable();
    Thread thread = new Thread(instance);
    thread.start();
}

实现 Callable 接口

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

必须要线程运行,FutureTask.get()才能有结果。 Future不能当做线程运行,必须得是FutureTask.

public class MyCallable implements Callable<Integer> {
    public Integer call() {
        return 123;
    }
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
    MyCallable mc = new MyCallable();
    FutureTask<Integer> ft = new FutureTask<>(mc);
    Thread thread = new Thread(ft);
    thread.start();
    System.out.println(ft.get());
}



        FutureTask<Integer> futureTask = new FutureTask<>(() -> {
            return 3;
        });
        new Thread(futureTask).start();
        try {
            System.out.println(futureTask.get());
        } catch (ExecutionException e) {
            throw new RuntimeException(e);
        }

Callable 接口和 Runnable 接口的不同之处:

  1. Callable 规定的方法是 call,而 Runnable 是 run
  2. call 方法可以抛出异常,但是 run 方法不行
  3. Callable 对象执行后可以有返回值,运行 Callable 任务可以得到一个 Future 对象,通过 Future 对象可以了解任务执行情况,可以取消任务的执行,而 Runnable 不可有返回值

get 方法会一直阻塞,直到结束,或者可以规定时间

多线程的实现有以下 4 个步骤:

1.创建一个线程,创建 Callable 的实现类 Race,并且重写 call 方法

ExecutorService ser=Executors.newFixedThreadPool(线程数目);
Race tortoise = new Race();

2.得到 Future 对象

Future result=ser.submit(tortoise);

3.获取返回值

int num=result.get();

4.停止服务

ser.shutdown();

继承 Thread 类

同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程。

public class MyThread extends Thread {
    public void run() {
        // ...
    }
}
public static void main(String[] args) {
    MyThread mt = new MyThread();
    mt.start();
}

实现接口 VS 继承 Thread

实现接口会更好一些,因为:

  • Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  • 类可能只要求可执行就行,继承整个 Thread 类开销过大。

守护线程

  • 守护线程(后台线程)和非守护线程(前台线程)。 守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。不重要的线程

  • 垃圾回收,内存管理都是后台线程

  • 主线程默认是前台线程,前台线程创建的子线程默认是前台线程,守护线程创建的子线程默认是守护线程。当程序中只有守护线程运行时该程序便可以结束运行。
  • 当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程。
  • main() 属于非守护线程。
  • 在线程启动之前使用 setDaemon() 方法可以将一个线程设置为守护线程。setDaemon(true)必须在 start()前设置

线程的加塞运行

t.join()使 t 线程加塞到当前线程之前获得 CPU,当前线程进入等待状态,直到线程 t 结束为止,当前线程恢复为就绪状态,等待线程调度

可以通过传入线程 t2,并 t2.join()使得按照一定顺序的运行 t2->t

thread.Join 把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程 B 中调用了线程 A 的 Join()方法,直到线程 A 执行完毕后,才会继续执行线程 B。

        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("hello");
        });
        t.start();
        new Thread(() -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("done");
        }).start();


线程的礼让

yield()让当前线程落选,让出 CPU 回到就绪状态,让线程调试器重新调度一次。让给优先级高或相等的线程获得执行的机会,没有相同优先级的线程是就绪状态,yield()就什么也不做,继续运行。

            Thread.yield();

线程的定时执行

  1. Timer 类 定时工具类 cancel(),schedule(TimerTask task,Date time,long period),time 时开始,period 是周期

  2. TimerTask 此类 implements Runable,就是个线程

需要用户 extends 此类。都不是好方式。

        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        // 1. 延迟执行一次(只执行一次)
        scheduler.schedule(() -> System.out.println("延迟2秒执行一次"), 2, TimeUnit.SECONDS);

        // 3. 固定延迟执行(执行完再等指定时间)1s后开始3s执行一次
        scheduler.scheduleWithFixedDelay(() -> {
            System.out.println("固定延迟任务: " + System.currentTimeMillis());
        }, 1, 3, TimeUnit.SECONDS);

线程的中断

Thread.interrupt()通知线程中断,设置中断标志位,但不是立即中断,不会发生作用,需要在 run()中设置中断处理代码。比如 run()中的 while{},增加 if(Thread.currentThread().isInterrupted()),,break,退出线程。线程的正常终止也应是这样的。当在 sleep()期间中断会抛出异常,所以在 catch 块中重新设置中断标志位,来中断线程

            while (!Thread.currentThread().isInterrupted()) {
              xxx
            }
  1. 对于 阻塞状态(如 sleep/wait/join) 的线程,调用 interrupt() 会抛出 InterruptedException,可以在 catch 中退出;
    1. 阻塞队列的put, offer也会有类似异常
  2. 对于 非阻塞状态 的线程,只是设置了 interrupted 标志位,不会抛异常,需要手动检查。

线程的终止

  1. 无限循环中使用退出标志
  2. stop(),不推荐。 stop()方法回立即释放所持有的锁,会将正在写入的数据写坏,比如写坏一半
  3. interrupt()

线程的优先级

每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。

Java 线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。

具有较高优先级的线程对程序更重要,并且应该在低优先级的线程之前分配处理器资源。但是,线程优先级不能保证线程执行的顺序,而且非常依赖于平台。

Java 中线程优先级是通过 Thread.setPriority(int)来设置的

常见问题

错误的加锁

对于 Integer,给 Integer 加锁是经常出错的,因为 Integer 是不变对象,当对其增加时,是另外创建了一个对象并赋值

线程的挂起和解挂

不推荐使用 suspend()和 resume()函数,会出现状态错误,使用自定义标志位,然后在线程中检查标志,实现挂起和解挂

挂起:while:标志为真,wait();

解挂:设置标志,notify()

线程的同步和互斥

用 synchronized 实现线程的互斥

  1. 用在语句块前
  2. 用在方法声明

用 ThreadLocal 实现线程局部变量

就是线程的私有变量,线程之间互不干扰,在线程类中声明定义即可

public ThreadLocal<Integer> sum=new ThreadLocal<Integer>();

    Runnable task = () -> {
        sum.set(0);
				sum.get(); // 每个线程独立的
        System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
    };

用 Object 类的 wait()和 notify()实现线程的同步

  • Java 中的 wait、notify、notifyAll 是 Object 类提供的线程协作机制,用于在多线程中实现等待与通知。

  • wait 方法让当前线程挂起,进入该对象的等待队列,并释放对象的锁,直到被其他线程调用 notify 或 notifyAll 唤醒。被唤醒的线程不会立刻执行,必须重新竞争锁,获取到后才能继续执行。

  • notify 方法唤醒该对象等待队列中的一个线程。被唤醒的线程同样需要重新竞争锁。notify 不释放锁,调用线程要等 synchronized 块执行完才会释放锁。

  • notifyAll 方法唤醒等待队列中的所有线程。这些线程会一起进入锁的阻塞队列等待竞争锁,先抢到的先执行,其它线程继续等待。

  • 调用 wait、notify、notifyAll 必须在 synchronized 块中,否则抛出 IllegalMonitorStateException。wait 方法会抛出 InterruptedException,调用时需要 try-catch 处理。

  • 为了避免虚假唤醒,使用 wait 时推荐搭配 while 判断条件,不建议使用 if。示例:

synchronized(obj) {
    while (!condition) {
        obj.wait();
    }
    // 执行逻辑
}

wait 会释放锁并挂起当前线程,notify/notifyAll 唤醒线程但不释放锁。线程被唤醒后必须重新竞争锁,等持有锁的线程释放后才能继续执行。

总结:

  • wait:释放锁,当前线程挂起。
  • notify:唤醒一个线程,不释放锁。
  • notifyAll:唤醒所有线程,不释放锁。
  • 都只能在同步块内使用。

单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对 Object.wait(),Object.notify()的应用最经典的例子,应该是三线程打印 ABC 的问题了吧,这是一道比较经典的面试题,题目要求如下:

建立三个线程,A 线程打印 10 次 A,B 线程打印 10 次 B,C 线程打印 10 次 C,要求线程同时运行,交替打印 10 次 ABC。这个问题用 Object 的 wait(),notify()就可以很方便的解决。代码如下:

public class MyThreadPrinter2 implements Runnable {

    private String name;
    private Object prev;
    private Object self;

    private MyThreadPrinter2(String name, Object prev, Object self) {
        this.name = name;
        this.prev = prev;
        this.self = self;
    }

    @Override
    public void run() {
        int count = 10;
        while (count > 0) {
            synchronized (prev) {
                synchronized (self) {
                    System.out.print(name);
                    count--;

                    self.notify();
                    //释放对象锁,后面的可以拿到锁
                }
                try {
                    prev.wait();//阻塞,等待prev的notify,才能继续运行
                    //也就是释放了这个对象锁,暂停这个线程
                    //必须有个notify才可以继续运行
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    public static void main(String[] args) throws Exception {
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);
        MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);
        MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);


        new Thread(pa).start();
        Thread.sleep(100);  //确保按顺序A、B、C执行
        new Thread(pb).start();
        Thread.sleep(100);
        new Thread(pc).start();
        Thread.sleep(100);
        
}

自己理解来看,就是设置额外变量设置条件,当条件不满足时,this.wait()阻塞,可以 if/while 块中

以下代码是子进程 5 次,主线程 3,然后循环 10 次的,通过一个 boolean 设置条件,每当条件不满足时,就阻塞下。


public class ThreadTest {
    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        T t=new T();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    t.subthread();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<10;i++){
                    t.mainthread();
                }
            }
        }).start();
    }
}

class T{
    public boolean shouldSub=true;
    public synchronized void subthread(){
        if(!shouldSub){
            try{
                this.wait();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        for(int i=0;i<5;i++){
            System.out.println("sub : ");
        }
        shouldSub=false;
        this.notify();
    }

    public synchronized void mainthread(){
        if(shouldSub){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for(int i=0;i<3;i++){
            System.out.println("main : ");
        }
        shouldSub=true;
        this.notify();
    }
}

内存屏障 和 volatile

参考

内存屏障是硬件之上、操作系统或 JVM 之下,对并发作出的最后一层支持。再向下是是硬件提供的支持;向上是操作系统或 JVM 对内存屏障作出的各种封装。内存屏障是一种标准,各厂商可能采用不同的实现。

内存屏障的实现涉及大量硬件架构层面的知识,又需要操作系统或 JVM 的配合才能发挥威力,单纯从任何一个层面都无法理解。

volatile 变量规则:对 volatile 变量的写入操作必须在对该变量的读操作之前执行。

volatile 变量规则只是一种标准,要求 JVM 实现保证 volatile 变量的偏序语义。结合程序顺序规则、传递性,该偏序语义通常表现为两个作用:

  • 保持可见性
  • 禁用重排序(读操作禁止重排序之后的操作,写操作禁止重排序之前的操作). 指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。

通过volatile 标记,可以解决编译器层面的可见性与重排序问题。而内存屏障则解决了硬件层面的可见性与重排序问题。

volatile 并不能代替锁,只能让其他线程看到线程的修改,也无法保证一些复合操作的原子性。

另外,不能真正的保证线程安全,当两个线程同时修改一个数据时,依然会发生冲突,比如两个 i++,其实只加了一次

volatile 两大作用

1、保证内存可见性 2、防止指令重排

此外需注意 volatile 并不保证操作的原子性。保证的是 load,assign,store. 本身cpu和mem之间还有个cpu缓存。

volatile 关键字可以保证变量的可见性,因为对 volatile 的操作都在 Main Memory 中,而 Main Memory 是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或 Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。

volatile 还有一个作用就是局部阻止重排序的发生,对 volatile 变量的操作指令都不会被重排序,因为如果重排序,又可能产生可见性问题。 在保证可见性方面,锁(包括显式锁、对象锁)以及对原子变量的读写都可以确保变量的可见性。但是实现方式略有不同,例如同步锁保证得到锁时从内存里重新读入数据刷新缓存,释放锁时将数据写回内存以保数据可见,而 volatile 变量干脆都是读写内存。

注意点:

  1. 不能当计数器,因为计数器要保证读取-修改-存取是一个原子性操作
  2. 可以当标志位 boolean

volatile用处

禁止重排序

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

可见性

volatile 关键字可以保证变量的可见性,因为对 volatile 的操作都在 Main Memory 中,而 Main Memory 是被所有线程所共享的,这里的代价就是牺牲了性能,无法利用寄存器或 Cache,因为它们都不是全局的,无法保证可见性,可能产生脏读。

synchronized 和 volatile 有什么区别?

synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!

  • volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块 。
  • volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

Synchronized原理

synchronized 是基于对象头(Object Monitor)的 Monitor Lock 实现的。

每个对象都有一个 Monitor(监视器):

  • Monitor 中包含

    • Owner:当前持有锁的线程。

    • EntryList:所有请求这个锁、但尚未获得的线程,存放在一个双向链表(或队列)中。

    • WaitSet:主动调用 wait() 的线程,即已获得锁后进入等待状态的线程。WaitSet 是 JVM Monitor 结构的一部分,用于管理那些已获取锁但因调用 wait() 方法而挂起的线程; 被唤醒的线程会从 WaitSet 移入 EntryList,等待再次竞争锁;

    • recursions:记录重入次数。

EntryList(同步队列)和 WaitSet(等待队列)。

获取锁 重入锁

步骤行为
1️⃣线程尝试进入同步块,JVM 检查对象 Monitor 的 Owner
2️⃣若无锁,则设置当前线程为 Owner,锁获取成功
3️⃣若 Owner 是当前线程,说明是可重入,再次进入,recursion++
4️⃣若已被其他线程持有,则线程进入 EntryList,被挂起(park)
5️⃣当锁释放,JVM 会从 EntryList 中唤醒一个线程,唤醒策略是“非公平的”(谁先排队不保证先获取)
6️⃣被唤醒线程尝试重新抢锁,抢到后设置为 Owner,继续执行同步代码块

Search

    Table of Contents