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 目录下面

img

使用 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 的常见属性

属性获取方法
IDlong 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 点,和我们的代码编写密切相关。

打破循环等待的办法:

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值