方法一:继承 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 目录下面
使用 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
类是 JVM 中用来管理线程的一个类,每个线程都有一个唯一的 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 –如果当前线程无法在指定的线程组中创建线程 |
属性 | 获取方法 |
---|---|
ID | long getId() |
名称 | String getName() |
状态 | State getState() |
优先级 | int getPriority() |
是否后台线程 | boolean isDaemon() |
是否存活 | boolean isAlive() |
是否被中断 | boolean isInterrupted() |
getId()
获取的是 JVM 里的标识。线程的身份标识有几个:PCB上,用户态线程库里(pthread),JVM 里getState()
获取的状态来自于 JVM 里设立的状态体系,这个状态比操作系统内置的状态更丰富一些isDaemon()
, 前台线程:会阻止进程结束,进程会保证所有前台线程都执行完才退出;后台线程:不会阻止进程结束,进程退出不用管后台线程有没有执行完。一个线程创建出来默认是前台线程。通过 setDaemon()
可以设置线程的前后台属性。使用自己创建的标志位来区分线程是否结束
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;
}
}
使用 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()
方法的行为:
interrupt
会修改内置的标志位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
的行为:
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) |
同上,但更精确的版本 |
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种原因:
后两个原因是编译器优化导致的,通过 volatile
关键字解决
我们知道多个线程对一个变量 ++ 会产生不同的结果,这是因为 ++ 操作不是原子的
Java中,使用 synchronized
关键字来实现线程互斥访问
直接修饰普通方法
两个线程对同一个变量进行 ++ 的例子:
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
}
}
// 结果正确了
修饰静态方法
public class SynchronizedDemo {
public synchronized static void method() {
}
}
修饰代码块:
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
,虽然没有加锁,但是因为不可修改,所以也是线程安全的
在 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
调用 wait
的线程,会进入阻塞等待的状态(WAITING)
注:wait
有设置最大等待时间的重载版本
notify
调用 notify
可以把对应的 wait
线程唤醒(从阻塞状态恢复到就绪状态)
o1.notify()
就可以唤醒调用了 o1.wait()
的线程,而使用 o2.notify()
就不能。wait
和 notify
都是 Object
的方法注:与之相关的方法还有 notifyAll
,用来唤醒在此锁上等待的所有线程,然后它们会一起竞争一把锁。notify
则是只随机唤醒一个线程
wait
的执行过程:
释放锁
等待通知
当通知到达之后,就会被唤醒,并且尝试重新获取锁
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
尝试重新获取锁失败,等执行完 notifyTask
,waitTask
获取锁并执行完。
饿汉模式:
对象的实例设置为静态,构造方法设置为私有,只提供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
,所以支持 offer
、poll
等普通队列的方法,但是只有用 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) == 失败) {}
挂起等待锁:发现锁冲突,就挂起等待:
synchronized
作为轻量级锁的时候,内部是自旋锁,作为重量级锁的时候,内部是挂起等待锁
符合先来后到的规则,就是公平。
synchronized
是非公平锁可重入锁在一个线程中可以获取多次,即使没有释放锁。可重入锁内部会记录锁的获取者是不是同一个线程。
可重入锁内部还有一个计数器,来记录当前加锁的次数,当计数器为 0 才会真正释放锁,避免过早释放锁。
如以下代码,使用可重入锁可解决死锁问题:
synchronized (Demo.class) {
// 此时锁未释放,又尝试获取锁,如果是不可重入锁,则进入死锁
synchronized (Demo.class) {
}
}
synchronized 属于可重入锁,所以上述代码执行并不会死锁
CAS 是 CPU 提供的一个特殊指令——Compare And Swap。是操作系统/硬件,给 JVM 提供的一种更轻量的原子操作机制
其中,比较是指比较内存和寄存器的值,如果相等,则把寄存器和另一个值进行交换,如果不相等,不进行操作。
CAS 伪代码:
boolean CAS(address, expectValue, swapValue) {
if (&address == expectValue) {
&address = swapValue;
return true;
}
return false;
}
典型应用:
原子类
如标准库中的 AtomicInteger
,该类的 getAndIncrement()
方法就是基于 CAS 实现的原子的自增操作。其实现方法类似于如下代码
public int getAndIncrement() {
int oldValue = value;
while (!CSA(value, oldValue, oldValue + 1)) {
oldValue = value;
}
return oldValue;
}
实现自旋锁
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 问题的情境如下:
在这个过程中,T1 看到的 V 的值虽然在两次操作之间没有改变,但实际上已经经历了变化(从A到B再到A),这可能引发一些意外的问题。
为了解决 ABA 问题,一种常见的方式是使用带有版本号的 CAS,即将变量的值与版本号一起进行比较。每次修改变量时,版本号都会增加。这样,即使变量的值从A变成B再变回A,版本号也会发生变化。
synchronized 是怎样进行自适应的?(锁膨胀/升级的过程)
synchronized
在加锁的时候经历的几个阶段:
偏向锁不是真正加锁,只是在锁的对象头里做了个标记,表示获取了锁,直到有其他线程来竞争的时候,才真正加锁
其他编译器优化:
锁消除:编译器自动判定,如果认为这个代码没有加锁的必要,就不加了。
锁粗化:增加锁的粒度
指 java.util.concurrent
包,这个包里放了很多和多线程开发相关的类
和 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
}
synchronized
也是可重入锁,但是它和 ReentrantLock
有很大的区别。
synchronized
是一个关键字,以代码块为单位进行加锁解锁,而 ReentrantLock
是一个类,使用 lock 和 unlock 方法加锁解锁ReentrantLock
还提供了一个公平锁的版本,在构造方法中可以指定参数,切换到公平锁模式ReentrantLock
还提供了一个特殊的加锁操作——tryLock()
,该方法不会阻塞,如果申请不到锁就直接往下执行。该方法还提供了一个设定等待时间的重载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占用,内存占用等指标。根据压测结果,来选择适合当前场景的线程数目。
信号量就是一个计数器,描述了可用资源的个数。
申请一个可用资源,信号量 -= 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();
}
}
同时等待多个线程
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("比赛结束");
}
}
synchronized
或 ReentrantLock
Collections.synchronizedList(new ArrayList);
CopyOnWriteArrayList
,写时拷贝,修改的时候先拷贝一份,修改副本,然后用副本替换原有的数据ArrayBlockingQueue
基于数组的阻塞队列LinkedBlockingQueue
基于链表的阻塞队列PriorityBlockingQueue
优先级阻塞队列TransferQueue
最多只包含一个元素的阻塞队列HashMap
线程不安全,HashTable
线程安全,但是不推荐使用
ConcurrentHashMap
是推荐使用的线程安全的哈希表
ConcurrentHashMap
的优化特点:
问:HashMap
、HashTable
、ConcurrentHashMap
之间的区别
答:
HashMap
线程不安全,HashTable
和 ConcurrentHashMap
是线程安全的HashTable
锁的粒度比较粗,锁冲突概率很高,ConcurrentHashMap
则是每个哈希桶一把锁,锁冲突概率大大降低了ConcurrentHashMap
其他优化策略。。。HashMap
的 key
允许为 null
,另外两个不允许在 Java1.7 中,
ConcurrentHashMap
采用分段锁,简单来说就是把若干个哈希桶分成一个段(Segment),针对每个段分别加锁,目的也是为了降低锁竞争的概率,当两个线程访问的数据恰好在同一个段上的时候,才触发锁竞争。
死锁指的是两个或多个进程(或线程)由于彼此等待对方释放资源而无法继续执行的状态。在死锁状态下,每个进程都在等待某个被其他进程占用的资源,同时又不释放自己占用的资源,从而形成了一种相互等待的僵局。
常见的死锁场景:
死锁的四个必要条件:
破坏任意一点,就可以避免死锁的情况,但是上述前 3 点都是描述锁的基本特点,无法干预,只有第 4 点,和我们的代码编写密切相关。
打破循环等待的办法:
在学校操作系统的教科书上,会学到哲学家就餐问题,其中给出一个避免死锁的办法——“银行家算法”。但是这个方法比较复杂,不建议使用。