引用
https://www.liaoxuefeng.com/wiki/1252599548343744/1255943750561472
https://www.cnblogs.com/tong-yuan/p/StampedLock.html
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html
https://www.cnblogs.com/chenpi/p/5375805.html
https://blog.csdn.net/qq_30379689/article/details/80785650
线程创建
public class Main {
public static void main(String[] args) {
Thread t = new MyThread();//通过扩展Thread
Thread t = new Thread(new MyRunnable());
Thread t = new Thread(() -> {
System.out.println("start new thread!");
});
t.start(); // 启动新线程
}
}
class MyThread extends Thread {
@Override
public void run() {
System.out.println("start new thread!");
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("start new thread!");
}
}
- 通过扩展Thread创建线程
- 通过实现Runable,作为构造函数的参数创建线程
- 通过内部类实现Runable,作为构造函数的参数创建线程
线程中断
通过 interrupt()
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.start();
Thread.sleep(1000);
t.interrupt(); // 中断t线程
t.join(); // 等待t线程结束
System.out.println("end");
}
}
class MyThread extends Thread {
@Override
public void run() {
Thread hello = new HelloThread();
hello.start(); // 启动hello线程
try {
hello.join(); // 等待hello线程结束
} catch (InterruptedException e) {
System.out.println("interrupted!");
}
hello.interrupt();
}
}
class HelloThread extends Thread {
@Override
public void run() {
int n = 0;
while (!isInterrupted()) {
n++;
System.out.println(n + " hello!");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
System.out.println("interrupted!2");
}
}
}
}
//输出
1 hello!
2 hello!
3 hello!
4 hello!
5 hello!
6 hello!
7 hello!
8 hello!
9 hello!
10 hello!
interrupted!
end
interrupted!2
11 hello!
12 hello!
....
我们来详细分析一下这个例子
- Main线程开启了MyThread线程,开始睡眠1000ms
- MyThread开启了HelloThread,并等待其结束,主要此时有一个InterruptedException异常的捕获
- HelloThread每睡眠100ms输出
n+hello!
,也有一个捕获 - Main线程经过1000ms的睡眠,对MyThread线程发出打断指令,并等待其结束
- MyThread收到打断指令,停止对HelloThread线程的等待,并抛出InterruptedException异常,被捕获后输出
interrupted!
- MyThread对HelloThread线程发出打断指令后,MyThread结束后Main先得知并输出
end
- 此时HelloThread正好处于睡眠,于是抛出异常并输出
interrupted!2
,但由于打断指令时在睡眠,且异常被捕获了,于是!isInterrupted()
仍未true,继续输出`n+hello!
我们从这个例子中可以得知
- interrupt()方法仅仅向线程发出了“中断请求”,至于线程是否能立刻响应,要看具体代码。
- join的等待和sleep的睡眠,在收到interrupt()打断时都会抛出InterruptedException异常,抛出异常后会影响到isInterrupted()的监测
通过标志位中断
public class Main {
public static void main(String[] args) throws InterruptedException {
HelloThread t = new HelloThread();
t.start();
Thread.sleep(1);
t.running = false; // 标志位置为false
}
}
class HelloThread extends Thread {
public volatile boolean running = true;
public void run() {
int n = 0;
while (running) {
n ++;
System.out.println(n + " hello!");
}
System.out.println("end!");
}
}
- 通过标准位中断必须使用volatile
因为在Java虚拟机中,变量的值保存在主内存中,但是,当线程访问变量时,它会先获取一个副本,并保存在自己的工作内存中。如果线程修改了变量的值,虚拟机会在某个时刻把修改后的值回写到主内存,但是,这个时间是不确定的!
volatile
- 每次访问变量时,总是获取主内存的最新值;
- 每次修改变量后,立刻回写到主内存。
守护线程
Thread t = new MyThread();
t.setDaemon(true);
JVM在所有非守护线程退出后才会退出,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。
可以得知所以
- 守护线程是为其他线程服务的线程
- 守护线程不能持有任何需要关闭的资源,例如打开文件等,因为虚拟机退出时,守护线程没有任何机会来关闭文件,这会导致数据丢失
线程同步
JAVA通过synchronized对线程进行同步
- 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
public void add(int n) {
synchronized(this) { // 锁住实例
count += n;
} // 解锁
}
//or
public synchronized void add(int n) { // 锁住实例
count += n;
} // 解锁
- 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
public class Counter {
public synchronized static void test(int n) {//锁住Counter类的class实例
...
}// 解锁
}
//or
public class Counter {
public static void test(int n) {
synchronized(Counter.class) {//锁住Counter类的class实例
...
}
}// 解锁
}
synchronized是可重入锁
- JVM允许同一个线程重复获取同一个锁,这种能被同一个线程反复获取的锁,就叫做可重入锁。
- 每获取一次锁,记录+1,每退出synchronized块,记录-1,减到0的时候,才会真正释放锁。
常用类
wait() 等待
class TaskQueue {
Queue<String> queue = new LinkedList<>();
public synchronized void addTask(String s) {
this.queue.add(s);
this.notifyAll();
}
public synchronized String getTask() throws InterruptedException {
while (queue.isEmpty()) {
this.wait();
}
return queue.remove();
}
}
- 定义在Object类的一个native方法
- 必须在synchronized块中才能调用wait()方法
- wait()方法调用时,会释放线程获得的锁,wait()方法返回后,线程又会重新试图获得锁。
notify()、notifyAll() 唤醒
- 定义在Object类的一个native方法
- 随机唤醒一个(全部)正在锁等待的线程
- 唤醒的线程仍需要正常获取到锁才能运行
ReentrantLock 重入锁(Lock实现)
public class Counter {
private final Lock lock = new ReentrantLock();
private int count;
public void add(int n) {
lock.lock();
//lock.tryLock(1, TimeUnit.SECONDS))
try {
count += n;
} finally {
lock.unlock();
}
}
}
- ReentrantLock实现了Lock接口
- ReentrantLock可以替代synchronized进行同步
- 可以使用tryLock()尝试获取锁(例子中尝试1秒时间内获取锁)
Condition 状态(ReentrantLock中的wait,notify等)
class TaskQueue {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private Queue<String> queue = new LinkedList<>();
public void addTask(String s) {
lock.lock();
try {
queue.add(s);
condition.signalAll();
} finally {
lock.unlock();
}
}
public String getTask() {
lock.lock();
try {
while (queue.isEmpty()) {
condition.await();
}
return queue.remove();
} finally {
lock.unlock();
}
}
}
- Condition实例通常由Lock接口的实例创建
- await()会释放当前锁,进入等待状态;
- signal()会唤醒某个等待线程;
- signalAll()会唤醒所有等待线程;
ReentrantReadWriteLock 重入读写锁(ReadWriteLock实现)
public class Counter {
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
private int[] counts = new int[10];
public void inc(int index) {
wlock.lock(); // 加写锁
try {
counts[index] += 1;
} finally {
wlock.unlock(); // 释放写锁
}
}
public int[] get() {
rlock.lock(); // 加读锁
try {
return Arrays.copyOf(counts, counts.length);
} finally {
rlock.unlock(); // 释放读锁
}
}
}
- ReentrantReadWriteLock实现了ReadWriteLock
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
- 只允许一个线程写入(其他线程既不能写入也不能读取)
- 没有写入时,多个线程允许同时读(提高性能)
StampedLock 冲压锁?
public class Point {
private final StampedLock stampedLock = new StampedLock();
private double x;
private double y;
public void move(double deltaX, double deltaY) {
long stamp = stampedLock.writeLock(); // 获取写锁
try {
x += deltaX;
y += deltaY;
} finally {
stampedLock.unlockWrite(stamp); // 释放写锁
}
}
public double distanceFromOrigin() {
long stamp = stampedLock.tryOptimisticRead(); // 获得一个乐观读锁
// 注意下面两行代码不是原子操作
// 假设x,y = (100,200)
double currentX = x;
// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)
double currentY = y;
// 此处已读取到y,如果没有写入,读取是正确的(100,200)
// 如果有写入,读取是错误的(100,400)
if (!stampedLock.validate(stamp)) { // 检查乐观读锁后是否有其他写锁发生
stamp = stampedLock.readLock(); // 获取一个悲观读锁
try {
currentX = x;
currentY = y;
} finally {
stampedLock.unlockRead(stamp); // 释放悲观读锁
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
- StampedLock也是一种读写锁,但不是基于AQS实现的(AQS实现的:Lock、Semaphore、ReentrantLock等)
- StampedLock把读锁细分为乐观读和悲观读,还提供了更复杂的将悲观读锁升级为写锁的功能
- StampedLock的state的高24位存储的是版本号,写锁的释放会增加其版本号,读锁不会
- StampedLock的低7位存储的读锁被获取的次数,第8位存储的是写锁被获取的次数;
- StampedLock不是可重入锁
- StampedLock不能实现条件锁;
JAVA 并发集合包 concurrent
- 使用与集合相同,加锁在方法内部实现
JAVA 原子操作包 concurrent.atomic
- 原子操作实现了无锁的线程安全
- Atomic包里的类基本都是使用Unsafe实现的包装类,核心操作是CAS(Compare and Set)原子操作.现代CPU已广泛支持CAS指令,如果不支持,那么JVM将使用自旋锁,与互斥锁一样,两者都需先获取锁才能访问共享资源,但互斥锁会导致线程进入睡眠,而自旋锁会一直循环等待直到获取锁
- 系统在硬件层面保证了CAS操作的原子性,不会锁住当前线程,它的效率是很高的。但是在并发越高的条件下,失败的次数会越多,CAS如果长时间不成功,会极大的增加CPU的开销,因此CAS不适合竞争十分频繁的场景
自旋锁
-
阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间。状态转换消耗的时间有可能比用户代码执行的时间还要长。
-
当前线程不放弃CPU的执行时间,进行自旋,如果在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就可以不必阻塞而是直接获取同步资源,从而避免切换线程的开销。这就是自旋锁。
-
自旋等待虽然避免了线程切换的开销,但它要占用处理器时间,所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次)没有成功获得锁,就应当挂起线程。