Java 多线程

创建线程

方法一:继承 Thread 类,重写 run() 方法

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new MyThread();
        t.start();
    }
}

run() 方法就是新创建的线程要执行的。创建完这样一个类,还要创建实例,并调用 start() 方法启动这样一个线程

这个写法,线程和任务是绑定在一起的


使用 jconsole 工具可以观察正在运行的线程,该工具的 jdk 的 bin 目录下面

Java 多线程_第1张图片

使用 Thread.sleep() 方法可以使线程休眠。

面试题:谈谈 Thread 的 run 和 start 的区别

直接调用 run,并没有创建新的线程,而只是在之前的线程中,执行 run 里的内容

使用 start,则是创建新的线程,新的线程会调用 run。新线程和旧线程之间是并发执行的关系

方法二:创建一个类,实现 Runnable 接口

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello thread");
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();
        Thread t = new Thread(runnable);
        t.start();
    }
}

此处创建的 Runnable,相当于定义了一个“任务”,还是需要 Thread 实例,把任务交给 Thread ,然后调用 start 来创建线程

这个写法,线程和任务是分离的(更好的解耦合)

方法三:使用匿名内部类来继承 Thread

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread() {
            @Override
            public void run() {
                System.out.println("Hello thread");
            }
        };
        t.start();
    }
}

方法四:使用匿名内部类的方式使用 Runnable

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello thread");
            }
        });
        t.start();
    }
}

方法五:其实 Runnable 接口是个函数式接口,可以使用 Lambda 表达式:

public class Demo1 {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello thread"));
        t.start();
    }
}

Thread 类及常用方法

Thread 类是 JVM 中用来管理线程的一个类,每个线程都有一个唯一的 Thread 对象与之关联

Thread 的常见构造方法

方法 说明
Thread() 分配一个新的线程对象。此构造函数与 Thread (null, null, gname) 具有相同的效果,其中 gname 是新生成的名称。自动生成的名称的形式为"Thread-"+n,其中 n 是一个整数。
Thread(Runnable target) 此构造函数与 Thread (null, target, gname) 具有相同的效果
参数:target–此线程启动时调用该对象的 run 方法。如果为 null,则该类 run 方法不执行任何操作。
Thread(String name) 此构造函数与 Thread (null, null, name) 具有相同的效果。
参数:name–新线程的名称
Thread(Runnable target, String name) 此构造函数与 Thread (null, target, name) 具有相同的效果
Thread(ThreadGroup group, Runnable target) 此构造函数与 Thread (group, target, gname) 具有相同的效果
参数:group–线程组。如果为 null 并且存在安全管理器,则组由 SecurityManager.getThreadGroup() 确定。如果没有安全管理器或者 SecurityManager.getThreadGroup() 返回 null,则组被设置为当前线程的线程组。
异常:SecurityException–如果当前线程无法在指定的线程组中创建线程

Thread 的常见属性

属性 获取方法
ID long getId()
名称 String getName()
状态 State getState()
优先级 int getPriority()
是否后台线程 boolean isDaemon()
是否存活 boolean isAlive()
是否被中断 boolean isInterrupted()
  • getId() 获取的是 JVM 里的标识。线程的身份标识有几个:PCB上,用户态线程库里(pthread),JVM 里
  • getState() 获取的状态来自于 JVM 里设立的状态体系,这个状态比操作系统内置的状态更丰富一些
  • isDaemon() , 前台线程:会阻止进程结束,进程会保证所有前台线程都执行完才退出;后台线程:不会阻止进程结束,进程退出不用管后台线程有没有执行完。一个线程创建出来默认是前台线程。通过 setDaemon() 可以设置线程的前后台属性。

中断线程

  1. 使用自己创建的标志位来区分线程是否结束

    public class Demo1 {
        public static boolean isQuit = false;
    
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                while (!isQuit) {
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("新线程执行结束");
            });
            t.start();
    
            Thread.sleep(5000);
            System.out.println("控制新线程退出");
            isQuit = true;
        }
    }
    
  2. 使用 Thread 自带的标志位

    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            Thread t = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) { // currentThread用来获取当前线程
                    System.out.println("线程运行中");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();
    
            Thread.sleep(5000);
            System.out.println("控制线程退出");
            t.interrupt();
        }
    }
    // 5秒后抛出异常,t线程继续运行
    

    interrupt() 方法的行为:

    • 如果 t 线程没有处在阻塞状态,此时 interrupt 会修改内置的标志位
    • 如果 t 线程正处于阻塞状态,此时 interrupt 会让线程内部产生阻塞的方法,如 sleep 抛出 InterruptedException

    捕获到异常是个好事情,我们可以自行选择如何处理:可以立即退出,也可以等一会退出,也可以不退出

    } catch (InterruptedException e) {
    	//e.printStackTrace();
    	// 立即退出
    	//break;
    	// 稍后退出
    	//try {
    	//    Thread.sleep(1000);
    	//} catch (InterruptedException ex) {
    	//    e.printStackTrace();
    	//}
    	//break;
    	// 不退出,忽略异常
    }
    

    判断标志位:

    Thread.currentThread().isInterrupted()Thread.interrupted() 的区别:

    • Thread.interrupted() 的标志位会自动清除,比如控制它中断,标志位会先设为true,读取的时候会读到这个 true,但是读完之后,这个标志位又自动恢复成 false 了
    • Thread.currentThread().isInterrupted() 状态不会自动恢复

线程等待

join 方法原型

/**
* 等待线程死亡。
* 异常:
* InterruptedException–如果任何线程中断了当前线程。当抛出此异常时,当前线程的中断状态将被清除。
*/
public final void join() throws InterruptedException {
    join(0);
}

join 的行为:

  1. 如果被等待的线程还没执行完,就阻塞等待
  2. 如果被等待的线程已经执行完,就直接返回

join 的带参数版本:

方法 说明
void join(long millis) 最多等 millis 毫秒
void join(long millis, int nanos) 同上,但更高精度

使用 join() 控制三个线程的结束顺序:

public class Demo1 {
    private static Thread t1 = null;
    private static Thread t2 = null;
    public static void main(String[] args) throws InterruptedException {
        System.out.println("main begin");
        t1 = new Thread(() -> {
            System.out.println("t1.begin");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1 end");
        });
        t1.start();
        t2 = new Thread(() -> {
            System.out.println("t2 begin");
            try {
                t1.join(); // 等待 t1 结束
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("t2 end");
        });
        t2.start();

        t2.join(); // 等待 t2 结束
        System.out.println("main end");
    }
}
/* 输出:
main begin
t1.begin
t2 begin
t1 end
t2 end
main end
*/

获取线程引用

如果是继承 Thread,然后重写 run 方法,直接在 run 中使用 this 即可获取到线程的实例,

但是如果是 Runnable 或者 lambda,this 就不行了。

更通用的办法:Thread.currentThread()

休眠线程

Thread 类的静态方法 sleep

方法 说明
void sleep(long millis) 休眠 millis 毫秒
void sleep(long millis, int nanos) 同上,但更精确的版本

线程的状态

  • NEW:安排了工作,还未开始行动(创建了 Thread 对象,但是还没调用 start)
  • RUNNABLE:可工作的,又可以分成正在工作中和即将开始工作(就绪状态,1.正在CPU上运行 2.还没在CPU上运行,但是已经准备好了)
  • BLOCKED:阻塞,等待锁
  • WAITING:阻塞,通过 wait 等待,需要其他线程主动唤醒
  • TIMED_WAITING:阻塞,通过 sleep 进入阻塞
  • TERMINATED:工作完成了(系统里的线程已经执行完毕,销毁了,但是 Thread 对象还在)
public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("Hello thread");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 在 start 之前
        System.out.println(t.getState()); // NEW

        t.start();
        System.out.println(t.getState()); // RUNNABLE
        Thread.sleep(500);
        System.out.println(t.getState()); // TIMED_WAITING
        t.join();

        // 在 join 之后获取
        System.out.println(t.getState()); // TERMINATED
    }
}

yield() 调用者暂时放弃 CPU,重新在就绪队列里排队,相当于 sleep(0)

线程安全

导致线程安全问题的5种原因:

  1. 系统的随机调度(根本原因,无法避免)
  2. 多个线程同时修改同一个变量(通过代码结构调整进行一定的规避)
  3. 修改操作不是原子的(加锁)
  4. 内存可见性
  5. 指令重排序

后两个原因是编译器优化导致的,通过 volatile 关键字解决


synchronized

我们知道多个线程对一个变量 ++ 会产生不同的结果,这是因为 ++ 操作不是原子的

Java中,使用 synchronized 关键字来实现线程互斥访问

  1. 直接修饰普通方法

    两个线程对同一个变量进行 ++ 的例子:

    class Counter {
        public int count;
    
        // 使用 synchronized 修饰,该方法成为原子操作
        public synchronized void increase() {
            ++count;
        }
    }
    
    public class Demo1 {
        public static void main(String[] args) throws InterruptedException {
            Counter counter = new Counter();
    
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < 50000; ++i) {
                    counter.increase();
                }
            });
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < 50000; ++i) {
                    counter.increase();
                }
            });
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            System.out.println(counter.count); // 100000
        }
    }
    // 结果正确了
    
  2. 修饰静态方法

    public class SynchronizedDemo {
        public synchronized static void method() {
        }
    }
    
  3. 修饰代码块

    public void increase() {
        synchronized (this) {
    	    ++count;
        }
    }
    

    () 里放的是锁,直接修饰普通方法相当于是用 this 进行加锁,修饰静态方法相当于用类对象进行加锁

    // func1 func2 等价
    public synchronized void func1() {
    
    }
    public void  () {
        synchronized (this) {
    
        }
    }
    // func3 func4 等价
    public synchronized static void func3() {
    
    }
    public static void func4() {
        synchronized (Counter.class) {
    
        }
    }
    

    这是 Java 的一个特点,在 Java 里,任何对象都可以用来作为锁对象(放在 synchronized 的括号中)。其他主流语言,都是专门使用一类特殊的对象来作为锁对象。这是因为 Java 的每个对象,在内存空间中有一个特殊的区域,对象头,其中就有和加锁相关的标记信息。

    注意

    • 无论使用哪种用法,都要明确锁对象,只有当两个线程使用同一个对象加锁的时候才会发生竞争
    • 因为一个类只有一个类对象,所以 synchronized 修饰静态方法,会导致只要是调用这个静态方法的线程之间都会产生竞争
    • this 指代的对象不是唯一的,所以 synchronized 修饰普通方法,调用这个普通方法的线程之间不一定会产生竞争

标准库中的线程安全类

Vector(不推荐使用)

HashTable(不推荐使用)

ConcurrentHashMap

StringBuffer

String,虽然没有加锁,但是因为不可修改,所以也是线程安全的

volatile

在 Java 中,volatile 关键字主要用于确保变量的可见性,禁止编译器在运行时对该变量进行重排序优化,以及禁止线程在读取变量时进行缓存。

例:

下列代码创建了一个线程,死循环直到 count 不为 0,主线程从键盘上读一个值修改 count

import java.util.Scanner;

class Counter {
    public int count; // 如果不加 volatile,则输入不为0的值后线程t1也不会结束
    
    public void increase() {
        ++count;
    }
}

public class Demo1 {
    public static void main(String[] args) {
        Counter counter = new Counter();

        Thread t1 = new Thread(() -> {
            System.out.println("t1 线程开始");
            while (counter.count == 0) {
            }
            System.out.println("t1 线程退出");
        });
        t1.start();
        
        Scanner scanner = new Scanner(System.in);
        counter.count = scanner.nextInt();
    }
}

结果:

t1 线程开始
1

加上 volatile,问题解决:

public volatile int count;

结果:

t1 线程开始
1
t1 线程退出

wait notify控制线程执行顺序

wait

调用 wait 的线程,会进入阻塞等待的状态(WAITING)

注:wait 有设置最大等待时间的重载版本

notify

调用 notify 可以把对应的 wait 线程唤醒(从阻塞状态恢复到就绪状态)

  • 什么是对应?比如,使用 o1.notify() 就可以唤醒调用了 o1.wait() 的线程,而使用 o2.notify() 就不能。
  • waitnotify 都是 Object 的方法

注:与之相关的方法还有 notifyAll,用来唤醒在此锁上等待的所有线程,然后它们会一起竞争一把锁。notify 则是只随机唤醒一个线程

wait 的执行过程:

  1. 释放锁

  2. 等待通知

  3. 当通知到达之后,就会被唤醒,并且尝试重新获取锁

    • wait 一上来就释放锁,所以在调用 wait 之前要先拿到锁。

    • 所以,wait 必须要放到 synchronized 中使用

    • 而且 synchronized 用的锁和调用 wait 方法的对象必须是同一个对象

wait 使用:

public static void main(String[] args) throws InterruptedException {
    Object object = new Object();
    synchronized (object) {
        System.out.println("wait 前");
        object.wait();
        System.out.println("wait 后");
    }
}
// 输出:wait 前

执行到 wait 就等待了,程序不会执行下去了。


notify 唤醒 wait 的线程:

public class Demo1 {
    // 锁对象
    public static final Object locker = new Object();

    public static void main(String[] args) {
        // 用来等待
        Thread waitTask = new Thread(() -> {
            synchronized (locker) {
                try {
                    System.out.println("wait 开始");
                    locker.wait(); // 释放锁,等待,直到有线程通知
                    System.out.println("wait 结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        waitTask.start();

        // 用来通知
        Thread notifyTask = new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            System.out.println("输入任意内容,开始通知");
            scanner.next(); // 阻塞,直到用户输入

            synchronized (locker) {
                System.out.println("notify 开始");
                locker.notify();
                System.out.println("notify 结束");
            }
        });
        notifyTask.start();
    }
}
// 输出:
// wait 开始
// 输入任意内容,开始通知
// 1
// notify 开始
// notify 结束
// wait 结束

开始时,notifyTask 会被 scanner.next() 阻塞,不会抢到锁,waitTask 抢到锁后,调用 wait() 释放锁并开始等待,当用户输入后,notifyTask 拿到锁,唤醒 waitTask,此时锁还在 notifyTask 手上,waitTask 尝试重新获取锁失败,等执行完 notifyTaskwaitTask 获取锁并执行完。

单例模式

饿汉模式

对象的实例设置为静态,构造方法设置为私有,只提供get方法,这样在整个程序中只会有一个实例。

由于是静态成员属性,所在生命周期伴随整个进程,是在进程启动时创建的,是饿汉模式

class Singleton {
    private static final Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Singleton() {}
}

懒汉模式

修改了实例化的时机,仅第一次调用get方法的时候创建实例

class SingletonLazy {
    private static SingletonLazy instance = null;

    public static SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
    
    private SingletonLazy() {}
}

懒汉模式在第一次调用 getInstance 方法时,可以会出现线程不安全的问题,解决方法,加锁:

线程安全的懒汉模式

双重检查加锁:外层检查避免创建完实例后再调用get方法时,频繁的加锁解锁带来的开销,内层检查确保不会实例化多个,造成线程不安全。

class SingletonLazy {
    private static volatile SingletonLazy instance = null; // 建议加上volatile

    public static SingletonLazy getInstance() {
        if (instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
    
    private SingletonLazy() {}
}

阻塞队列

这里说的阻塞队列和操作系统进程调度里的阻塞队列是两个概念。

阻塞队列能够保证线程安全,满足:如果队列为空,尝试出队列,就会阻塞;如果队列满,尝试入队列,也会阻塞

无锁队列:也是一种线程安全的队列,实现内部没有使用锁,更高效,但是消耗更多的 CPU 资源

消息队列:在队列中涵盖多种不同“类型”的元素。取元素的时候可以按照某个类型来取,做到针对该类型的“先进先出”


Java 标准库提供了现成的阻塞队列,BlockingQueue

该队列继承自 Queue,所以支持 offerpoll 等普通队列的方法,但是只有用 put、take 来入队出队才能达到阻塞的效果。

使用阻塞队列的生产者消费者模型:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class Demo1 {
    public static void main(String[] args) {
        BlockingQueue<Integer> queue = new LinkedBlockingQueue<>();

        Thread customer = new Thread(() -> {
            while (true) {
                try {
                    int value = queue.take();
                    System.out.println("消费元素:" + value);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        customer.start();

        Thread producer = new Thread(() -> {
            int n = 0;
            while (true) {
                try {
                    System.out.println("生产元素:" + n);
                    queue.put(n);
                    ++n;
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        producer.start();
    }
}
/*
生产元素:0
消费元素:0
生产元素:1
消费元素:1
生产元素:2
消费元素:2
生产元素:3
消费元素:3
*/

自己实现一个阻塞队列:

class MyBlockingQueue {
    // 最大 1000 个元素
    private int[] items = new int[1000];
    // 队首的位置
    private int head = 0;
    // 队尾的位置
    private int tail = 0;
    // 队列的元素个数
    private volatile int size = 0;

    // 涉及临界资源的修改,加锁保证线程安全
    public synchronized void put(int value) throws InterruptedException {
        while (size == items.length) { // 使用循环判定的方式确保等待结束后的状态是正确的
            this.wait(); // 队列为满,等待
        }
        items[tail] = value;
        ++tail;
        if (tail == items.length) {
            tail = 0;
        }
        ++size;
        this.notify(); // 添加了元素,唤醒等待的线程
    }

    public synchronized Integer take() throws InterruptedException {
        while (size == 0) { // 使用循环判定的方式确保等待结束后的状态是正确的
            this.wait(); // 队列为空,等待
        }
        int ret = items[head];
        ++head;
        if (head == items.length) {
            head = 0;
        }
        --size;
        this.notify(); // 取走了元素,唤醒等待的线程
        return ret;
    }
}

定时器

标准库中的定时器

import java.util.Timer;
import java.util.TimerTask;

public class Demo1 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        // 安排一个任务, 3000ms 后执行
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("这是一个要执行的任务");
            }
        }, 3000);
    }
}

上面的程序,3 秒后打印了指定的字符串,执行完成进程也不结束。这是因为 Timer 里面有线程,是它阻止了进程的退出。

正因为用到了多线程,所以在定时器计时的过程中,主线程还可以做其他事。

自己实现一个定时器

import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask> {
    private final Runnable command;
    private final long time;

    public MyTask(Runnable command, long after) {
        this.command = command;
        this.time = System.currentTimeMillis() + after;
    }

    public void run() {
        command.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    // 使用线程安全的阻塞优先级队列,来保存若干任务
    private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
    
    // 指派任务,将任务存入队列
    public void schedule(Runnable command, long after) {
        MyTask myTask = new MyTask(command, after);
        queue.put(myTask);
    }

    // 构造方法启动一个线程,不断从队列中找任务做
    public MyTimer() {
        Thread t = new Thread(() -> {
            // 循环不断尝试从队列中获取元素,然后判断时间是否到
            while (true) {
                try {
                    MyTask myTask = queue.take();
                    long curTime = System.currentTimeMillis();
                    if (myTask.getTime() > curTime) {
                        // 时间未到,放回
                        queue.put(myTask);
                    } else {
                        // 时间已到,执行
                        myTask.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo1 {
    public static void main(String[] args) {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(() -> System.out.println("1111"), 2000);
        myTimer.schedule(() -> System.out.println("2222"), 4000);
        myTimer.schedule(() -> System.out.println("3333"), 6000);
    }
}

但是这段代码有个缺点,我们手动创建的线程,在没有任务时也一直在循环,占用 CPU 资源,相当于“忙等”

解决方式:使用 sleep ?sleep 虽然确实可以降低循环的频率,减少开销,但是损失了定时器的时间精度(任务到时了,却仍在 sleep)

使用 wait 等待,当有新任务到来的时候唤醒:

package thread;

import java.util.concurrent.PriorityBlockingQueue;

class MyTask implements Comparable<MyTask> {
    private final Runnable command;
    private final long time;

    public MyTask(Runnable command, long after) {
        this.command = command;
        this.time = System.currentTimeMillis() + after;
    }

    public void run() {
        command.run();
    }

    public long getTime() {
        return time;
    }

    @Override
    public int compareTo(MyTask o) {
        return (int) (this.time - o.time);
    }
}

class MyTimer {
    // 用来阻塞等待的锁
    private final Object locker = new Object();

    // 使用线程安全的阻塞优先级队列,来保存若干任务
    private final PriorityBlockingQueue<MyTask> queue = new PriorityBlockingQueue<>();
   
    public void schedule(Runnable command, long after) {
        MyTask myTask = new MyTask(command, after);
        synchronized (locker) {
        	queue.put(myTask);
            locker.notify();
        }
    }

    public MyTimer() {
        Thread t = new Thread(() -> {
            // 循环不断尝试从队列中获取元素,然后判断时间是否到
            while (true) {
                try {
                    synchronized (locker) {
                        while (queue.empty()) {
                            locker.wait();
                        }
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.getTime() > curTime) {
                            // 时间未到,放回
                            queue.put(myTask);
                            locker.wait(myTask.getTime() - curTime);
                        } else {
                            // 时间已到,执行
                            myTask.run();
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

public class Demo1 {
    public static void main(String[] args) {

        MyTimer myTimer = new MyTimer();
        myTimer.schedule(() -> System.out.println("1111"), 2000);
        myTimer.schedule(() -> System.out.println("2222"), 4000);
        myTimer.schedule(() -> System.out.println("3333"), 6000);
    }
}

线程池

标准库中的线程池

ExecutorService threadPool = Executors.newFixedThreadPool(10);

这里使用静态方法来创建实例,这样的方法,称为工厂方法,对应的设计模式,就叫做工厂模式

通常情况下,创建对象,是借助 new,调用构造方法来实现的,但是构造方法用诸多限制, 不方便使用。因此就需要给构造方法再包装一层,外面起到包装作用的方法就是工厂方法

向线程池中添加任务:

threadPool.submit(() -> System.out.println("Hello"));

自己实现一个线程池:

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool {
    // 使用阻塞队列来存放任务
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    public void submit(Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    // 构造方法创建 n 个线程,每个线程都在等待执行队列中的任务
    public MyThreadPool(int n) {
        for (int i = 0; i < n; ++i) {
            Thread t = new Thread(() -> {
                while (!Thread.currentThread().isInterrupted()) {
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            });
            t.start();
        }
    }
}

常见锁策略

乐观锁 和 悲观锁

乐观锁:预测接下来锁冲突的概率不大,就要做另一类操作

悲观锁:预测接下来锁冲突的概率很大,就要做一类操作

synchronized 既是一个悲观锁,也是一个乐观锁,即自适应锁。当前锁冲突概率不大,以乐观锁的方式运行,往往是用户态执行。一旦发现锁冲突概率大了,以悲观锁的方式运行,往往要进入内核,对当前线程进行挂起等待


普通的互斥锁 和 读写锁

synchronized 属于普通的互斥锁。

读写锁,把加锁操作细化,分成了读锁和写锁

  • 情况一:线程 A 和 B 都尝试获取写锁

    A、B 产生竞争,和普通的锁没区别

  • 情况二:线程 A 和 B 都尝试获取读锁

    A、B 不产生竞争,和没加锁一样

  • 情况三:线程 A、B 分别尝试获取读锁、写锁

    A、B 产生竞争,和普通的锁没区别

在Java中,ReentrantReadWriteLock 是一个内置的读写锁实现,它实现了 ReadWriteLock 接口。下面是一个简单的使用示例:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteLockExample {
    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private int sharedData = 0;

    public int readData() {
        readWriteLock.readLock().lock();
        try {
            // 读取共享资源
            return sharedData;
        } finally {
            readWriteLock.readLock().unlock();
        }
    }

    public void writeData(int newData) {
        readWriteLock.writeLock().lock();
        try {
            // 写入共享资源
            sharedData = newData;
        } finally {
            readWriteLock.writeLock().unlock();
        }
    }
}

public class Demo1 {
    public static void main(String[] args) {
        ReadWriteLockExample example = new ReadWriteLockExample();

        // 启动多个读线程
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int data = example.readData();
                System.out.println(Thread.currentThread().getName() + "读取数据:" + data);
            }).start();
        }

        // 启动一个写线程
        new Thread(() -> {
            int newData = 42;
            example.writeData(newData);
            System.out.println(Thread.currentThread().getName() + "写入数据:" + newData);
        }).start();
    }
}

重量级锁 和 轻量级锁

重量级锁:锁的开销比较大,做的工作比较多

轻量级锁:锁的开销比较小,做的工作比较少

悲观锁经常是重量级锁,乐观锁经常是轻量级锁

重量级锁:主要依赖操作系统提供的锁,容易产生阻塞等待

轻量级锁:主要尽量避免使用操作系统提供的锁,而是在用户态完成功能,尽量避免用户态和内核态的切换,避免挂起等待

synchronized 是自适应锁,既是轻量级锁,又是重量级锁,根据锁冲突的情况:冲突高则是重量级,冲突不高则是轻量级

自旋锁 和 挂起等待锁

自旋锁是轻量级锁的具体实现,是乐观锁,挂起等待锁是重量级锁的具体实现,是悲观锁

自旋锁:当发现锁冲突的时候,不会挂起等待,而是迅速再来尝试这个锁是否能获取到

自旋锁的伪代码:

while (抢锁(lock) == 失败) {}
  • 优点:一旦锁被释放,就可以第一时间获取到
  • 缺点:如果锁一直,就会消耗大量的 CPU

挂起等待锁:发现锁冲突,就挂起等待:

  • 优点:在锁被其他线程占用的时候,会放弃 CPU 资源,
  • 缺点:锁被释放后,不能第一时间获取到

synchronized 作为轻量级锁的时候,内部是自旋锁,作为重量级锁的时候,内部是挂起等待锁

公平锁 和 非公平锁

符合先来后到的规则,就是公平。

  • 挂起等待锁是非公平锁
  • synchronized 是非公平锁

可重入锁 和 不可重入锁

可重入锁在一个线程中可以获取多次,即使没有释放锁。可重入锁内部会记录锁的获取者是不是同一个线程。

可重入锁内部还有一个计数器,来记录当前加锁的次数,当计数器为 0 才会真正释放锁,避免过早释放锁。

如以下代码,使用可重入锁可解决死锁问题:

synchronized (Demo.class) {
    // 此时锁未释放,又尝试获取锁,如果是不可重入锁,则进入死锁
    synchronized (Demo.class) {
        
    }
}

synchronized 属于可重入锁,所以上述代码执行并不会死锁

CAS

CAS 是 CPU 提供的一个特殊指令——Compare And Swap。是操作系统/硬件,给 JVM 提供的一种更轻量的原子操作机制

其中,比较是指比较内存和寄存器的值,如果相等,则把寄存器和另一个值进行交换,如果不相等,不进行操作。

CAS 伪代码:

boolean CAS(address, expectValue, swapValue) {
    if (&address == expectValue) {
        &address = swapValue;
        return true;
    }
    return false;
}

典型应用

  1. 原子类

    如标准库中的 AtomicInteger,该类的 getAndIncrement() 方法就是基于 CAS 实现的原子的自增操作。其实现方法类似于如下代码

    public int getAndIncrement() {
        int oldValue = value;
        while (!CSA(value, oldValue, oldValue + 1)) {
            oldValue = value;
        }
        return oldValue;
    }
    
  2. 实现自旋锁

    public class SpinLock {
        private Thread owner = null;
        
        public void lock() {
            // 当owner为null,设为当前线程,也就是调用此方法尝试加锁的线程,循环结束
            // 否则,说明其他线程占有了锁,什么也不做,一直循环
            while (!CAS(this.owner, null, Thread.currentThread())) {
            }
        }
        
        public void unlock() {
            this.owner = null;
        }
    }
    

CAS 的 ABA 问题

ABA 问题的情境如下:

  1. 线程 T1 读取变量 V 的值为 A。
  2. 线程 T2 将变量 V 的值从 A 修改为 B,然后又将其修改回 A。
  3. 线程 T1 再次进行 CAS 操作,比较的是当前变量 V 的值(A)与之前读取的值(也是 A),发现相等,于是执行操作。

在这个过程中,T1 看到的 V 的值虽然在两次操作之间没有改变,但实际上已经经历了变化(从A到B再到A),这可能引发一些意外的问题。

为了解决 ABA 问题,一种常见的方式是使用带有版本号的 CAS,即将变量的值与版本号一起进行比较。每次修改变量时,版本号都会增加。这样,即使变量的值从A变成B再变回A,版本号也会发生变化。

synchronized 工作原理

  1. 既是悲观锁,也是乐观锁(自适应)
  2. 既是轻量级锁,也是重量级锁(自适应)
  3. 轻量级锁基于自旋锁实现,重量级锁基于挂起等待锁实现
  4. 不是读写锁
  5. 是非公平锁
  6. 是可重入锁

synchronized 是怎样进行自适应的?(锁膨胀/升级的过程)

synchronized 在加锁的时候经历的几个阶段:

  1. 无锁
  2. 偏向锁(刚开始加锁,未产生竞争的时候)
  3. 轻量级锁(产生锁竞争了)
  4. 重量级锁(锁的竞争更激烈了)

偏向锁不是真正加锁,只是在锁的对象头里做了个标记,表示获取了锁,直到有其他线程来竞争的时候,才真正加锁

其他编译器优化

锁消除:编译器自动判定,如果认为这个代码没有加锁的必要,就不加了。

锁粗化:增加锁的粒度

JUC

java.util.concurrent 包,这个包里放了很多和多线程开发相关的类

Callable

Runnable 类似,不同点在于,Callable 指定的任务是带返回值的,而 Runnable 是不带返回值的。

使用案例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 类型参数就是返回类型
    Callable<Integer> callable = () -> {
        int sum = 0;
        for (int i = 0; i <= 1000; ++i) {
            sum += i;
        }
        return sum;
    };
    // Callable不能直接传入Thread构造方法,需要套上一层FutureTask,而且这一层还能用来获取返回的结果
    FutureTask<Integer> task = new FutureTask<>(callable);
    Thread t = new Thread(task);
    t.start();

    // 使用get获取返回值,并且在线程结束前,get会阻塞
    System.out.println(task.get()); // 500500
}

ReentrantLock

synchronized 也是可重入锁,但是它和 ReentrantLock 有很大的区别。

  1. synchronized 是一个关键字,以代码块为单位进行加锁解锁,而 ReentrantLock 是一个类,使用 lock 和 unlock 方法加锁解锁
  2. ReentrantLock 还提供了一个公平锁的版本,在构造方法中可以指定参数,切换到公平锁模式
  3. ReentrantLock 还提供了一个特殊的加锁操作——tryLock(),该方法不会阻塞,如果申请不到锁就直接往下执行。该方法还提供了一个设定等待时间的重载
  4. ReentrantLock 提供了更强大的等待/唤醒机制,搭配 Condition 类来实现等待唤醒,可以做到随机唤醒一个,也能做到指定线程唤醒

原子类

使用 CAS 实现,除了之前讲过的 AtomicInteger,还有

  • AtomicBoolean
  • AtomicIntegerArray
  • AtomicLong
  • AtomicReference
  • AtomicStampedReference

AtomicInteger 为例:常见方法有:

方法 说明
int addAndGet(int delta) i += delta
int decrementAndGet() --i
int getAndDecrement() i--
int incrementAndGet() ++i
int getAndIncrement() i++

线程池

ExecutorService 和 Executors

  • ExecutorService 是线程池类
  • Executors 是一个工厂类,能够创建出几种不同风格的线程池

上面讲线程池的时候用过,下面列出创建线程池的几种常见方式

方法 说明
ExecutorService newFixedThreadPool(int nThreads) 创建固定线程数的线程池
ExecutorService newCachedThreadPool() 创建线程数动态增长的线程池
ExecutorService newSingleThreadExecutor() 创建只包含单个线程的线程池
ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 设定延迟时间后执行命令,或者定期执行命令,是进阶版的 Timer

上述操作都是基于 ThreadPoolExecutor 类的封装

ThreadPoolExecutor 构造方法参数最多的版本

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
                   ThreadFactory threadFactory,
                   RejectedExecutionHandler handler)
  • corePoolSize 核心线程数
  • maximumPoolSize 最大线程数
    • 线程池中分为核心线程和临时线程两类,核心线程始终存在,临时线程在繁忙的时候创建,空闲的时候销毁
  • keepAliveTime 临时线程的存活时间
  • unit 时间单位
    • 这两个参数组合描述一个时间,临时线程空闲超过这个时间就会被销毁
  • workQueue 任务队列,虽然线程池内部可以内置队列,但是我们也可以自己定义队列来交给线程池使用
  • threadFactory 参与具体的线程创建工作
  • handler 拒绝策略,当任务队列满了的时候,两次尝试添加任务,线程池要怎么做。常见策略:
    • 超过负荷,直接抛异常
    • 交给添加任务的调用者处理
    • 丢弃任务队列中最老的任务
    • 丢弃任务队列中最新的任务

实际工作中,建议使用 ThreadPoolExecutor,显式传参,这样就可以更好地掌控代码

当我们使用线程池的时候,线程数目如何设置?

答:针对当前的程序进行性能测试,分别设置不同的线程数目进行测试。在测试过程中,程序的运行时间,CPU占用,内存占用等指标。根据压测结果,来选择适合当前场景的线程数目。

信号量 Semaphore

信号量就是一个计数器,描述了可用资源的个数。

申请一个可用资源,信号量 -= 1,称为 P 操作,释放一个资源,信号量就 += 1,称为 V 操作

信号量的取值为 0-1 时,就退化成了一个普通的锁。

Java 标准库中的 Semaphore

import java.util.concurrent.Semaphore;

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        // 初始值为 3 的信号量
        Semaphore semaphore = new Semaphore(3);
        // P 操作,申请资源
        semaphore.acquire();
        // V 操作,释放资源
        semaphore.release();
    }
}

CountDownLatch

同时等待多个线程

import java.util.concurrent.CountDownLatch;

public class Demo1 {
    public static void main(String[] args) throws InterruptedException {
        // 模拟跑步比赛
        // 设定有 10 个选手参赛
        CountDownLatch latch = new CountDownLatch(10);
        for (int i = 0; i < 10; ++i) {
            Thread t = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                    System.out.println("到达终点");
                    latch.countDown(); // latch -= 1
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            t.start();
        }
        // 等待latch减为0
        latch.await();
        System.out.println("比赛结束");
    }
}

多线程环境使用集合类

多线程环境使用 ArrayList

  1. 自己使用 synchronizedReentrantLock
  2. Collections.synchronizedList(new ArrayList);
  3. 使用 CopyOnWriteArrayList,写时拷贝,修改的时候先拷贝一份,修改副本,然后用副本替换原有的数据

多线程环境使用队列

  1. ArrayBlockingQueue 基于数组的阻塞队列
  2. LinkedBlockingQueue 基于链表的阻塞队列
  3. PriorityBlockingQueue 优先级阻塞队列
  4. TransferQueue 最多只包含一个元素的阻塞队列

多线程环境使用哈希表

HashMap 线程不安全,HashTable 线程安全,但是不推荐使用

ConcurrentHashMap 是推荐使用的线程安全的哈希表

ConcurrentHashMap 的优化特点:

  1. 把锁的粒度细化,每个哈希桶一把锁(每个链表的头结点),降低了锁冲突的概率
  2. 读不加锁,写加锁
  3. 更充分使用 CAS 特性
  4. 针对扩容进行优化

问:HashMapHashTableConcurrentHashMap 之间的区别

答:

  1. HashMap 线程不安全,HashTableConcurrentHashMap 是线程安全的
  2. HashTable 锁的粒度比较粗,锁冲突概率很高,ConcurrentHashMap 则是每个哈希桶一把锁,锁冲突概率大大降低了
  3. ConcurrentHashMap 其他优化策略。。。
  4. HashMapkey 允许为 null,另外两个不允许

在 Java1.7 中,ConcurrentHashMap 采用分段锁,简单来说就是把若干个哈希桶分成一个段(Segment),针对每个段分别加锁,目的也是为了降低锁竞争的概率,当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争。

死锁

死锁指的是两个或多个进程(或线程)由于彼此等待对方释放资源而无法继续执行的状态。在死锁状态下,每个进程都在等待某个被其他进程占用的资源,同时又不释放自己占用的资源,从而形成了一种相互等待的僵局。

常见的死锁场景:

  1. 一个线程一把锁:如一个线程连续加锁两次,如果是不可重入锁,就死锁了
  2. 两个线程两把锁:两个线程各自持有锁不释放,又互相申请对方的锁
  3. N 个线程 M 把锁:哲学家就餐问题

死锁的四个必要条件:

  1. 互斥使用:线程 1 拿到了锁 A,其他线程无法获取到 A
  2. 不可抢占:线程 1 拿到了锁 A,其他线程只能阻塞等待,等到线程 1 把锁释放,不能强行把锁抢走
  3. 请求和保持:线程 1 拿到锁后,就会一直保持获取到锁的状态,直到主动释放。
  4. 循环等待:线程 1 等待线程 2,线程 2 又尝试等待线程 1

破坏任意一点,就可以避免死锁的情况,但是上述前 3 点都是描述锁的基本特点,无法干预,只有第 4 点,和我们的代码编写密切相关。

打破循环等待的办法:

  • 针对多把锁进行编号
  • 约定在获取多把锁的时候,明确获取锁的顺序是从小到大

在学校操作系统的教科书上,会学到哲学家就餐问题,其中给出一个避免死锁的办法——“银行家算法”。但是这个方法比较复杂,不建议使用。

你可能感兴趣的:(Java,java,开发语言)