这种实现方式是jdk1.0便已经存在了,实现也很简单,直接继承Thread,重写run方法,然后我们通过Thread对象的start方法启动即可。
示例代码如下:
public class Test1 extends Thread{
@Override
public void run() {
while(true){
System.out.println("线程1");
}
}
public static void main(String[] args) {
new Test1().start();
}
}
实现Runnable接口,其实与第一种实现方式本质上还是一样的,因为Thread就是实现了Runnable接口。当使用实现Runnable接口的方式实现多线程时,应该重写run方法。启动线程时使用Thread的start方法,示例代码如下:
public class TestThread {
public static void main(String[] args) {
new Thread(() -> {
while(true)
System.out.println("Runnable多线程1");
}).start();
new Thread(() -> {
while(true)
System.out.println("Runnable多线程2");
}).start();
}
}
Callable接口是JDK1.5加入的,位置在java并发包里面,他的功能比较强大支持异常的抛出与线程执行结果的返回。这些都是前面两种实现方式所不具备的。前面两种能完成的实现Callable都可以完成,前面两种完不成的Callable也可以完成,所以没有理由不使用Callbale。示例代码如下:
public class TestThread {
public static void main(String[] args) throws Exception{
FutureTask<String> futureTask = new FutureTask<>(()->{
int i =0 ;
while(i<100)
System.out.println("Callable线程1在执行:"+i++);
return "线程1执行完了";
});
FutureTask<String> futureTask2 = new FutureTask<>(()->{
int i =0 ;
while(i<100)
System.out.println("Callable线程2在执行:"+i++);
return "线程2执行完了";
});
new Thread(futureTask).start();
new Thread(futureTask2).start();
System.out.println(futureTask.get());
System.out.println(futureTask2.get());
}
}
以上是三种多线程的实现方式,有人会把线程池也算成一种多线程的实现方式,笔者认为线程池并不能算是一种方式,他只是一种池化技术,这种技术在编程中有很多场景,底层还是使用的一样的技术而已。总结以上三种多线程的实现方式,
不过当下全是使用线程池来实现多线程技术,基本没有手动去创建线程的,所以线程池的使用和手动实现线程池才是应该注意的地方。
创建出Thread的对象后就是new的状态了。
Thread的start方法才是创建一个线程的根本,start方法调用native的实现去开启一个线程(可以发现无论哪种多线程的实现方式都需要Thread的start方法来开启线程),当这个start方法执行后线程就是new的状态了。
运行状态就是线程在运行run方法或者call方法体中的内容了,线程在运行状态中可以进入到阻塞状态,也可以进入到就绪状态,比如yield就会让线程进入就绪状态,wait、sleep会让线程进入到阻塞状态。
当现场争抢cpu时间片失败后就会进入到阻塞状态,阻塞状态中的线程需要等他其他线程释放cpu资源然后再一次和其他线程去争抢cpu的时间片,争抢成功则进入到运行状态,否则再次进入到阻塞状态。
我们知道Thread已经提供了stop等方法用于停止一个线程或者说让一个线程死亡,但是stop方法是Dreprected的,是不推荐使用的,那我们该如何正确的停止一个线程呢?答案是在主线程中维护一个boolean变量,用这个变量控制线程的运行和死亡,比如如下代码:我们可以在适当的时候将flag置为false,这样线程也就结束了。此处有一点需要注意,volatile关键字修饰了flag,被volatile关键字修饰的变量一旦改变,会强制子线程中的该变量失效,子线程需要从主线程从新获取(可见性问题)。
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception{
FutureTask<String> futureTask = new FutureTask<>(()->{
int i =0 ;
while(flag)
System.out.println("Callable线程1在执行:"+i++);
return "线程1执行完了";
});
new Thread(futureTask).start();
System.out.println(futureTask.get());
}
}
我们都知道
设置线程优先级
需要注意的是线程的优先级高也不一定就肯定先执行,只是优先级高的线程先执行的概率更大而已,优先级从1-10,10为最高,不设置默认为5,到底谁先执行与操作系统有关系,主要看操作系统对cpu的调度,示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(() -> {
System.out.println("优先级为1的线程");
});
Thread thread2 = new Thread(() -> {
System.out.println("优先级为3的线程");
});
Thread thread3 = new Thread(() -> {
System.out.println("优先级为5的线程");
});
Thread thread4 = new Thread(() -> {
System.out.println("优先级为7的线程");
});
Thread thread5 = new Thread(() -> {
System.out.println("优先级为10的线程");
});
thread1.setPriority(1);
thread2.setPriority(3);
thread3.setPriority(5);
thread4.setPriority(7);
thread5.setPriority(10);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
让线程进入阻塞状态这个方法用过的人应该是非常多了,传入的是一个int型整数,代表毫秒。目的是让线程睡一会。该方法结束后线程进入就绪状态。值得注意的是每个线程都有一把锁,而sleep并不会释放这把锁。
这是个插队的方法,他不是静态方法,需要使用Thread的对象来调用,当一个线程调用join方法时,该线程会强制抢占cpu资源,直到该线程执行完毕其他线程才会继续执行,示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) throws Exception{
Thread thread = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
System.out.println("vip线程1:"+i);
}
});
for (int i = 0; i < 500; i++) {
System.out.println("主线程:"+i);
if(i==200)
thread.join();
}
thread.start();
}
}
释放cpu资源,让当前线程进入就绪状态,礼让操作,重新和其他线程竞争cpu资源。需要说的是礼让并不一定成功。笔者多次测试都礼让成功了,看来礼让还是有效果的。示例代码如下:
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
int i = 0;
while(i<100){
if(i==50)
Thread.yield();
System.out.println("线程1:"+i++);
}
}).start();
new Thread(() -> {
int i = 0;
while(i<100)
System.out.println("线程2:"+i++);
}).start();
}
}
中断线程有四种放方式,方式一线程正常执行结束终止,方式二使用stop可以终止,方式三使用volatile修饰的标志符控制线程的终止,方式四便可以使用interrupt方法。必须要注意的是单纯使用该方法是不能达到阻断线程的目的的,该方法只是改变线程的阻断标志而已,只是改变了阻断标志,并不会做其他事情,我们可以判断阻断标志的变化来停止线程(默认是false,执行interrupt方法后是true)。代码如下(这里必须使用继承Thread的方式实现多线程):
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) {
Thread thread1 = new Thread(){
@Override
public void run() {
while(!isInterrupted()){
System.out.println("线程1在运行");
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i==50){
System.out.println("################### 线程2试图阻断线程1 ###################");
thread1.interrupt();
}else{
System.out.println("线程2在运行:"+i);
}
}
}
};
thread1.start();
thread2.start();
}
}
也可以使用interrupt配合sleep方法来一起阻断线程,sleep方法有机会抛出InterruptedException异常,我们捕获该异常并break,也是可以退出线程的。示例代码如下(与上一部分代码相比,只改动了第一个线程的代码):
public class TestThread {
volatile static Boolean flag = true;
public static void main(String[] args) {
Thread thread1 = new Thread(){
@Override
public void run() {
while(true){
System.out.println("线程1在运行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("发生了阻断异常。。。。");
break;
}
}
}
};
Thread thread2 = new Thread(){
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i==50){
System.out.println("################### 线程2试图阻断线程1 ###################");
thread1.interrupt();
}else{
System.out.println("线程2在运行:"+i);
}
}
}
};
thread1.start();
thread2.start();
}
}
做下总结,我们阻断线程的操作都是在其他线程中进行的,在实际的场景中肯定也都是在其他线程阻断,在自己线程中的阻断我们称之为线程的正常运行结束。在其他线程中阻断,然后一种是在线程中判断阻断标志停止线程,另一种就是使用sleep然后捕获InterruptedException异常来break线程的运行。
判断当前线程是否是活性的
这些线程停止方法不推荐使用,stop方法是暴力停止线程,使用该方法后线程会立即停止,但是使用stop可能会导致数据不同步、资源未正常释放等结果。所以是不推荐使用的,此外destroy方法也是不推荐使用的,目前destroy方法体中只有一行代码,直接抛出了一个异常,destroy方法如下:
@Deprecated
public void destroy() {
throw new NoSuchMethodError();
}
所以总结stop、destroy这两个方法都不适合用于停止线程,真正用于停止线程的操作还是interrupt中讲述的四种停止方法。
可以通过Thread的setDaemon,将线程设置为守护线程,最典型的守护线程是垃圾收集线程。守护线程的作用就是用来守护用户线程的,虚拟机只会保证用户线程的执行完毕,守护线程会随着虚拟机的关闭而关闭。
多线程必然伴随着并发修改的风险,多个线程同时修改一个变量就会有线程安全问题,线程安全问题需要从三个方面考虑:
private synchronized static void addT(List list,Integer i){
list.add(i);
}
修饰方法时,谁调用这个方法那么持有的就是谁的锁。 private static void add(List list,Integer i){
synchronized(TestThread.class){
list.add(i);
}
}
上面的代码中持有的锁是TestThread.class的锁,需要声明的是所有的类在被虚拟机加载时都会同时生成一个他对应的class对象,且这个对象是唯一的,后期无论我们怎么去创建class对象其实都是只有这最初创建的一个。这里持有的就是它的锁。加锁时我们应该保证锁是唯一的,不能让每个线程持有的锁都不一样,这样就会没有任何意义,也达不到同步的效果。import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
static final Lock lock = new ReentrantLock();
static List list = new ArrayList();
public static void main(String[] args) throws InterruptedException{
new Thread( () -> {
add(list);
}).start();
new Thread( () -> {
add(list);
}).start();
Thread.sleep(1000);
System.out.println(list.size());
}
public static void add(List list){
for (int i = 0; i < 1000; i++) {
lock.lock();
try {
list.add(i);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
}
对比Lock和Synchronized我们知道,lock优于synchronized,synchronized又分为代码块和方法,使用代码块性能要更好于方法(因为方法锁住的范围更广,同步操作的时间就会更长,性能自然就更低)。因此在开发中我们应该这么使用:Lock优于 synchronized代码块优于 synchronized方法。
无论是synchronized还是Lock都是锁机制实现的同步操作,在JDK1.5的并发包下还提供了另外一种不使用锁机制来实现的并发安全操作机制,那就是使用CAS+失败重试机制的Atomic原子类系列。我们知道多线程情况下去对基本数据类型进行增删改操作是不安全的,而并发包下提供了AtomicInteger、AtomicLong、 AtomicBoolean等类用于支持并发场景下的数据操作,这些类的实现机制都是依赖于CAS+失败重试,而不是使用锁机制实现的同步操作,那CAS是什么呢?
什么是CAS
CAS(compare and swap)从字面上翻译CAS就是比较然后交换的意思,其实他的工作原理也就是这个样子,锁机制都是让多线程的操作变成串行化,而CAS却不是,他是先获取变量值,在需要执行变更操作时先去拿这个值与主内存的值进行比较,若是相等再将执行当前线程的操作,不等则需要重新获取然后再执行当前线程的操作,这就是CAS。必须要说的是CAS是一个原子操作,也就是说比较然后设置这个操作是不会被其他线程中断的,它线程安全,此外这种不使用锁来实现的同步机制也被称为乐观锁。相反的synchronized就是悲观锁了。
CAS的工作机制
了解了CAS,还必须要知道CAS是如何保证线程安全的,我们做个场景假设来模拟下CAS的工作流程,需要说的是这个流程是JDK8之前的,JDK8之后对CAS做了优化,但是这套机制还是适用的,JDK8只是将CAS操作变成了分段处理,每段的处理还是现在这个流程,JDK8具体的修改往下看会有介绍。下面先来假设下场景:假设有两个线程:线程一、线程二,正在同时修改AtomicInteger的值。主内存中AtomicInteger值是1。则会有如下场景发生:
①.线程一和线程二都拿到了主内存中的AtomicInteger的值是1。
②线程一想要修改AtomicInteger的值为2,修改之前先拿到自己工作内存中的1与主内存的1对比,发现相等后,将工作内存和主内存的AtomicInteger都改为了2.
③线程二此时却想要将AtomicInteger的值改为3,线程2则先拿着自己工作内存中存储的1去与工作内存中的2对比,发现不相等,不相等则不能设置,而是从新从主内存获取,获取后再次比较发现相等了,然后设置工作内存和主内存的值为3
这就是CAS的工作机制的流程,因为CAS是原子操作故而保证了线程的安全。只要有一个线程在做CAS操作,那其他线程是不能进行打断的。
JDK8对CAS机制的优化 和 LongAdder
根据CAS的机制,我们可以发现当线程量十分多的时候,CAS的性能就会越来越低,因为CAS是原子操作,就会导致其他线程在不停的获取值,然后比较后发现不相等,再接着从新获取,就会陷入这样的恶性循环。因此在JDK8时对CAS机制进行了优化推出了LongAdder类,该类就是基于优化后的CAS实现的。那JDK8对CAS进行了怎样的优化呢?JDK8针对高并发场景提出了分段CAS和自动分段迁移的方式来提升高并发执行CAS时的性能。那这个分段CAS是个什么意思,自动分段迁移又是什么?来看下LongAdder的工作机制就会清楚了。先来做个场景假设有很多个线程在同时修改LongAdder的值,那么就会有如下场景发生。
①当发现有很多线程在进行CAS操作,致使很多线程出现空旋转的情况时,此时会保存一个已经计算出来的值作为base值,并且此时会创建一个cell数组,让一部分线程的计算结果存入一个cell中,这样就可以将所有线程分成好几部分来分开计算(分段CAS)。
②当有cell计算失败时,会将线程的操作自动迁移到其他cell中计算(自动分段迁移)。
③当所有线程都计算完毕后对base和cell进行合并计算得出最终结果。
这样就会提升了CAS在高并发下的效率。
为什么要对CAS进行优化
根据前面假设的场景可以发现CAS在高并发的场景下会让大量线程出现空旋转的情况,从而出现影响性能的情况。因而在JDK8时才对CAS进行优化,新增了LongAdder类。LongAdder的实现机制就是分段CAS+自动分段迁移。这样就大大提高了在多线程场景下的效率,当然了若是线程量比较小的场景我们还是使用原子类AtomicInteger等类即可。无需使用LongAdder。若是了解JDK8中提供的流式操作的同学可能会比较熟悉这个场景,流式操作的底层也是会对流进行分段处理,这其实是一种很常见的并发处理思想,同时也多处用于提升处理效率。
已经有锁了,为什么还要CAS机制
前面已经说过,synchronized是一种悲观锁,CAS机制则被认为一种乐观锁。悲观锁可以支持代码块、方法级别的同步,自然也是可以在包装的情况下修改字段的值,而乐观锁主要强调的是对单一变量的修改,他们的侧重点不一样,并且在单一变量的修改场景使用悲观锁的代价太高,悲观锁所耗费的虚拟机性能要高出很多。所以才有了CAS的生存空间。
AtomicInteger使用示例
下面只是一个假设的场景,主要是为了验证AtomicInteger的安全性,代码如下:
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException{
for (int i1 = 0; i1 < 20000; i1++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"线程正在操作+1");
atomicInteger.addAndGet(1);
}).start();
}
Thread.sleep(1000);
System.out.println(atomicInteger.toString());
}
}
前面介绍过并发编程下必须考虑的三个问题,原子性问题、可见性问题、顺序性问题。我们已经知道synchronized、Lock是可以解决原子性问题和可见性问题的。那顺序性问题如何解决呢?事实上Java提供了Volatile关键字用来修饰变量,就是用来解决顺序性问题的,此外Volatile还可以解决可见性问题。
ThreadLocal为甚么要放在这里说呢,他好像和线程同步完全是反着的,之说以放在这里说,一是不值得为ThreadLocal拉开单独讲,二是因为他和Volatile正好也是反着的,就一起说了,可以相互印证,加深记忆。
ThreadLocal是什么
直白的说ThreadLocal就是被用来在多线程场景中做数据隔离的数据载体,通过ThreadLocal我们可以实现每个线程中操作的数据互不影响,使用ThreadLocal的set和get方法,得到的永远都是自己线程内部设置的信息,其他线程设置的信息对当前线程不可见。ThreadLocal的作用常见的有三种
①.存储单个线程的上下文信息,我们可以利用他来存储上下文信息,这样我们就可以在线程运行的任何地方都可以拿到这些信息了。
②.存储在ThreadLocal的变量线程安全,所以我们可以利用他存储一些线程所必须独有的信息。
③.可以使用ThreadLocal来存储引用链比较长的参数信息,当引用链比较长时,尤其是需要通过jdk原有的方法传递,我们无法更改原有方法时,都可以将参数放置在ThreadLocal中,这样我们就可以在需要的地方随时获取了。
ThreadLocal的使用
ThreadLocal是多线程中比较偏门的一个小东西,使用起来也很简单,如下一段简单的代码,在主线程中声明了ThreadLocal的变量,然后就可以在子线程中使用,子线程中都使用ThreadLocal,但是却彼此都不影响,感觉很神奇是吧,其实原理很简单,稍后笔者会介绍ThreadLocal实现线程隔离的原理。
import java.util.ArrayList;
import java.util.List;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
public static ThreadLocal<List<String>> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("值"+i);
threadLocal.set(list);
System.out.println(threadLocal.get().get(threadLocal.get().size()-1));
}
}).start();
new Thread(() ->{
List<String> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add("zhi"+i);
threadLocal.set(list);
System.out.println(threadLocal.get().get(threadLocal.get().size()-1));
}
}).start();
//验证:主线程可能会在子线程结束之前结束(验证通过)
System.out.println("************************************大小:"+threadLocal.get().size());
}
}
ThreadLocalMap是什么
在说ThreadLocal线程隔离的原理之前,必须要提ThreadLocalMap,他是ThreadLocal的一个静态内部类,并且在Thread中维护了两个如下成员变量,那为什么要在Thread类中维护ThreadLocalMap 的引用呢,其实就是为了实现线程的数据隔离(需要注意inheritableThreadLocals 不是为了实现线程隔离,而是为了实现共享)。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
ThreadLocal怎么实现数据隔离
在不知道原理之前,感觉哇竟然可以让单个变量实现线程间的数据隔离,肯定使用了类似Volatile这样的关键字吧,我们都知道Volatile能让变量在线程间变得可见,那么是不是也有一个关键字可以让变量在线程间变得不可见呢,笔者在未了解这块时就是这么想的,然而发现我错了,JDK使用ThreadLocal实现数据隔离根本不需要什么关键字,比如上面的例子中,先是在主线程中创建了一个ThreadLocal的变量,这里使用的是空参构造,我们点进去这个空参构造,发现里面啥也没有,说明真正的操作不在这里,再然后在子线程中分别使用这个变量设置值,那我们点进set方法看下,set方法的源码如下所示:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
这是我们就会发现先是拿到了当前线程,然后调用getMap方法传入当前线程,这里其实拿到的就是当前线程的ThreadLocalMap了,那有人可能会有这样的疑问,我没创建过ThreadLocalMap的对象啊,往下看,若是map==null的场景会去调用createMap,传入的是当前线程和我们使用set方法往ThreadLocal中设入的值,那我们在看下createMap的源码,如下所示:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
这个方法只有一行代码,就是创建一个ThreadLocalMap,将键和值给他。然后并且将这个新创建的ThreadLocalMap对象交给当前线程的threadLocals变量,在上面介绍ThreadLocalMap时,我们说过每个线程对象都会有这个变量。就是在这里用的。这样是不是就会发现ThreadLocal实现线程间数据隔离的原因了呢,总结一句话就是:每个线程实例都会有一个ThreadLocalMap对象,这个map中键存的是ThreadLocal,值存的是我们开发时,往ThreadLocal中设入的值。所以我们表面上是用ThreadLocal存的,其实是用每个线程独有的变量来存的,ThreadLocal只充当了map中的键,所以对数值的存取没有影响,从而实现了线程间的数据共享。
ThreadLocalMap底层是什么结构
ThreadLocal底层是一个Entry数组,每一个Entry都有键和值,键用来存放ThreadLocal,且键都是弱引用的,值就是通过ThreadLocal的set方法存入的值。这里需要说的是Entry的键都被设置成了弱引用,这种操作一般认为会导致value的内存泄露。
ThreadLocalMap怎么做到不使用链表来解决hash冲突的
前面说了,ThreadLocalMap的底层其实是数组,我们都知道HashMap的底层是数组+链表+红黑树,当没有hash冲突时使用数组存储键值对,当冲突时就会引入链表,当链表长度大于8数组长度大于64就会转化红黑树。那ThreadLocalMap只有数组怎么解决hash冲突呢,ThreadLocal其实是这么设计,当有冲突时就判断下他的后一位是不是空,是的话就存到后一位,若是不为空则继续往后判断直到有空的位置就存进来。这种解决方法简单粗暴,效率不高,当数据量大时存取的效率都非常低,但是ThreadLocalMap并不会存放很多东西,这么设计其实也无可厚非,笔者认为也没有什么问题。
ThreadLocal存在什么问题
如下图所示,当创建ThreadLcoal的线程执行完毕时,ThreadLocalRef这个引用就会失效,那么此时当前线程中存储的ThreadLocal就只有Entry中的key指向他了,而这个引用指向又是一个弱引用。所以下次GC时就会回收掉ThreadLocal,但是因为当前线程未结束那么线程中的Map(ThreadLocalMap)的引用就还存在,他存在entry就存在,所以value就不会回收,但此时key已经没有了,所以就导致了内存泄露。
ThreadLocal导致的内存泄露的真实原因是什么,怎么解决
键被回收了,但是Entry的引用还在,所以value就会一直存在,所以导致了内存泄露,具体原因上面也已经说了。解决就是我们使用完ThreadLocal之后就必须使用remove方法。这样就可以解决内存泄漏的问题,尤其在使用线程池时更为必要使用ThreadLocal的remove方法了,因为线程不会被销毁,所以不使用remove方法,下次线程启用时,里面存的其实是上一次线程执行时的内容。但是笔者这里要说一点自己的看法,其实我感觉这个内存泄露并不严重, 为什么呢,因为ThreadLocal的get方法中会去清理掉为空的键值对,但是这可能会有一个延时,当然也有清理不掉的场景,但是造成的内存泄露不会多严重,不过最保险的办法还是使用remove方法。
首先明确一点所有的这些都是用来描述synchronized实现机制的,JDK1.6开始synchronized实现的锁有这几种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。那自旋锁、适应性自旋锁,还有偶尔会听到的消除锁、锁粗化又是什么呢?他们这四个并不是锁的状态,而是虚拟机为了提升锁效率在适当的场景对锁机制的优化。synchronized是内置锁,他的锁获取都在对象的头部中,对象头主要存储了两类信息,一类是标记字段,一类是类型指针,我们所说的偏向锁、轻量级锁、重量级锁就在这里,重量级锁持有的是Monitor锁,而偏向锁和轻量级锁都是使用的CAS机制。 下面我们就来一起看下这些对锁机制的优化以及synchronized的各种状态。
介绍了偏向锁、轻量级锁、重量级锁,做个小小的总结,偏向锁使用CAS机制更新对象头中的是否持有偏向锁、偏向线程ID,他的工作机制与AQS类似。当有其他线程进来,偏向锁就会膨胀微为轻量级锁,轻量级锁也是基于CAS实现的,他更新的是锁记录,锁记录存储在栈中,并发线程不是很多时JVM会使用轻量级锁来实现,当线程量比较巨大,锁竞争太过激烈是轻量级锁会膨胀为重量级锁。
锁也可以划分为互斥锁和共享锁,当有线程持有锁时是否允许其他线程继续访问来划分的。那就有疑问了?如果一个线程都持有了锁还允许其他线程允许访问,那么还加锁干什么呢?直接不加锁不就完事了吗?事实上还是略有不同。共享锁便是在有线程获得锁之后仍然支持其他线程的访问,但是这个访问并不是支持所有,只是支持部分,怎么说呢,相当于限流了吧,此时只允许部分安全的请求进来。我们说的读锁其实就是共享锁,写锁就是互斥锁。当一个线程加了写锁时,其他线程就只能排队等着了,此时线程执行就是串行化的。当一个线程加了读锁时,那么其他线程都可以读,但是写不支持,加了读锁相当于是只拦截其他线程的写操作,而不拦截其他线程的读操作。这么做的原因也很好理解,要是我正在读你进来把数据改了,不就造成数据脏读了吗,要是我正在写你进来读也是一样的情况。所以只有读的时候允许其他线程进来读,写的时候是不允许其他线程进来写和读的。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
static ReentrantReadWriteLock lock = new ReentrantReadWriteLock(true);
static List<Integer> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException{
new Thread(() -> {
for (int i = 0; i < 100; i++) {
//加读锁
lock.readLock().lock();
list.add(i);
lock.readLock().unlock();
}
}).start();
new Thread(() -> {
for (int i = 0; i < 100; i++) {
//加写锁
lock.writeLock().lock();
list.add(i);
lock.writeLock().unlock();
}
}).start();
}
}
公平锁和非公平锁从字面上看就可以看出他描述的是加锁这个操作是否是公平的,所谓公平就是先来先得,后来的排队等,相反的随便插队那肯定是不公平的了,下面就来一起看下:
非公平锁
synchronized实现的加锁机制就是非公平锁,此外ReentrantLock默认也是非公平锁,那非公平锁是怎么工作的呢,我们就以ReentrantLock来举例吧:
①线程一先执行了加锁操作,将AQS中的state通过CAS操作设为了1,持有锁线程设置为了自己,此时线程二过来发现加不了锁只能进入到了等待队列。
②当线程一释放锁时,又来了个线程三,此时线程二和线程三都是就绪状态,他们都可以获取锁,不好意思,由于巧合线程三拿到了锁,将state设为了1,持有锁线程也就变成了线程三。线程二就得继续进入等待队列。
在这个过程中我们发现,明明是线程二先来的,最后线程三却完成了插队优先执行了,这种就是非公平锁,非公平锁中,所有线程的加锁操作机会都是均等的。
公平锁
上面已经说了非公平锁的工作流程,其实公平锁的工作机制是从第二步才可使不一样的,也就是有其他线程尝试插队时才会不一样。
①线程一先执行了加锁操作,将AQS中的state通过CAS操作设为了1,持有锁线程设置为了自己,此时线程二过来发现加不了锁只能进入到了等待队列。
②当线程一释放锁时,又来了个线程三,这是若是等待队列不为空,则应该让等待队列中的线程执行,让线程三进入等待线程继续等待。
这样来说对于线程二才是公平的,那怎么实现公平锁呢,我们只需要在创建ReentrantLock时传入true即可。这样加锁的机制就是公平的了。
ReentrantLock lock = new ReentrantLock(true);
对比公平锁和非公平锁
知道了公平锁和非公平锁的区别,我们还需要知道他们的优缺点这样才能确定在什么场景下使用他们:
①公平锁的优点是等待线程不会出现饿死的情况,但是公平锁的效率相对于非公平锁会低一些,等待线程中除了第一个线程以外其他线程都会阻塞,因此cpu需要唤醒阻塞线程,而非公平锁中是有机会线程不阻塞直接执行的。
②非公平锁的优点是效率相对较高,非公平锁可能会省去很多唤醒阻塞线程的操作。但是与之相对的,非公平锁可能会造成线程饥饿,可能会有线程要等非常久才能加上锁。
所以总结公平锁和非公屏锁的优缺点我们可以发现,若是要求效率而对线程的执行没有什么必然的先后顺序我们应该使用非公平锁。笔者认为需要考虑线程的执行顺序的场景应该很少,所以我们正常中使用非公平锁即可。
这些概念也是比较常见的,锁这里也是总结下,这些都是在概念层面对锁的划分,并不是一种技术的实现,下面就来总结下这几个概念分别是从什么场景对锁进行划分的。
对于线程并发这块,笔者写了14000多字,也耗费了很长时间,最近工作之余一直都在整理,也耗费了很长时间。关于这一部分的内容大部分都是在说锁有关的问题,其实锁并不多只有synchronized、Lock两种,然后根据Lock又有很多实现比如可重入锁ReentrantLock、读写锁ReadWriteLock(并不是直接继承),又根据Lock的原理介绍了AQS和CAS机制,而说到了CAS就必须得说下Atomic原子类,原子类是基于CAS实现的安全数据类型有AtomicInteger、AtomicLong等,介绍了Lock的底层原理又介绍了synchronized的工作原理:synchronized在jdk1.6后会如何从偏向锁到轻量级锁再到重量级锁的。锁的工作机制和原理都介绍完了以后,又对锁的各种划分做了介绍,比如锁有重量级锁和轻量级锁划分这是根据实现机制来划分的;锁有显示锁和隐式锁的划分这是由锁在书写时加锁解锁的可见性来划分的;锁有公平锁和非公平锁的划分这是根据锁竞争时是否遵循了先来后到的原则来划分的;锁有乐观锁和悲观锁这是根据是否认为操作数据的线程一直可能是一个的场景来划分的,同一个线程操作没有并发问题线程安全是乐观锁(这里有个二八定律的问题)。等等这些都是对锁在不同层面上的一个划分。此外还介绍了JMM和Volitale,这块是提到锁必须要说的事情,因为锁解决的是并发中的原子性、可见性问题,而Volatile解决的是可见性和顺序性问题,他们一起才可以保证多线程情况下的绝对安全,整个篇幅总结下来可以说很长,但是讲的并不是特别深入,都是原理上来进行论述,好了继续来总结线程中的其他问题。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.FutureTask;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
public static void main(String[] args) {
new Thread(() -> {
synchronized (A.class){
System.out.println("我是线程一,我持有了A锁");
try {
Thread.sleep(1000);
synchronized (B.class){
System.out.println("我是线程一,我持有了B锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
new Thread(() ->{
synchronized (B.class){
System.out.println("我是线程二,我持有了B锁");
try {
Thread.sleep(1000);
synchronized (A.class){
System.out.println("我是线程二,我持有了A锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
class A{}
class B{}
}
注册中心就会伴随生产者和消费者的问题,一个人生产完了,另一个想要即时拿走,我就得告诉其他线程你可以拿走了,但是zookeeper是通过加读写锁来实现的啊,好像又和生产者消费者又不像,生产者消费者好像是一个阻塞等着另一个完成。zookeeper应该不是生产者和消费者了。消息队列好像是生产者和消费者的问题,每当有消息进来,就要立马监听到,然后处理消息。嗯这肯定是个生产者消费者问题,注册中心也可能会有这个问题。先待定。
public class TestThread {
public static void main(String[] args) {
RepositoryInn repositoryInn = new RepositoryInn();
new Thread(new Productor(repositoryInn)).start();
new Thread(new Consumer(repositoryInn)).start();
}
}
class Productor implements Runnable{
RepositoryInn repositoryInn;
public Productor(RepositoryInn repositoryInn){
this.repositoryInn = repositoryInn;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
repositoryInn.product("手机"+i);
}
}
}
class Consumer implements Runnable{
RepositoryInn repositoryInn;
public Consumer(RepositoryInn repositoryInn){
this.repositoryInn = repositoryInn;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
repositoryInn.consumer();
}
}
}
class RepositoryInn{
List<String> list = new ArrayList<>();
//生产
public synchronized void product(String str){
if(list.size()==5){
//满了,停止生产等待消费者消费
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产了:"+str);
list.add(str);
this.notifyAll();
}
//消费
public synchronized void consumer(){
//判断仓库是否为空,为空等待生产
if(list.size()==0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费了:"+list.get(list.size()-1));
list.remove(list.size()-1);
this.notifyAll();
}
}
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
public static void main(String[] args) {
Light light = new Light();
new Thread(new Productor(light)).start();
new Thread(new Consumer(light)).start();
}
}
class Productor implements Runnable{
Light light;
public Productor(Light light){
this.light = light;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
light.product("手机"+i);
}
}
}
class Consumer implements Runnable{
Light light;
public Consumer(Light light){
this.light = light;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
light.consum("手机"+i);
}
}
}
class Light{
//信号灯,true:进入生产,false:进入消费
volatile static Boolean flag = true;
//生产方法
public synchronized void product(String str){
if(!flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产了:"+str);
flag = false;
this.notifyAll();
}
//消费方法
public synchronized void consum(String str){
if(flag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("消费了:"+str);
flag = true;
this.notifyAll();
}
}
在实际的开发中是不可能让我们直接写线程的,都是使用线程池来管理线程,什么是线程池呢?线程池是一种线程的池化技术,这种池化思想在实际开发中还是很常见的,比如字符串常量池、数据库连接池等都是这种思想。都是随用随取的思想,而不是用的时候再去创建。因为用的时候再去创建一来增加了程序的响应,二来cpu压力增大都是不利于程序更好的运行的。说到这里就总结下使用线程池的优点吧:
①提升响应速度。
②提升系统性能,减少cpu压力,线程是十分珍贵的资源,来回的创建销毁很耗费性能。
③便于管理线程,使用线程池可以对线程进行统一管理。
介绍线程池之前必须要先介绍这个东西,因为像被大多数人经常提起的线程池中的定长线程池(newFixedThreadPool)、单例线程池(newSingleThreadPool)、缓存线程池(newCachedThreadPool)都是基于ThreadPoolExecutor来实现的,其实他们的本质都是TheadPoolExecutor,只是被传入了不同的参数而已,阿里开发手册也有规定,不允许直接使用jdk提供的线程池,而是去使用ThreadPoolExecutor。为什么要禁止使用jdk提供的线程池呢,最主要的原因还是怕开发者不清楚各个线程池实现的优缺点而造成了性能上的损失,说完ThreadPoolExecutor之后,笔者会卓一介绍常见的线程池并总结他们的优缺点,以及可能造成的问题,ThreadPoolExecutor最长可以有七个构造参数,下面分别来解读下这七个参数:
先展示下定长线程池的实现,如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
上面已经说过了ThreadPoolExecutor这个类了,他才是真正实现线程池的类,定长线程池的实现是通过将核,心线程数和最大线程数都绑定为nThreads来实现定长的,同时活性时间为0,并传入一个链式阻塞队列。这样构成了一个线程池,不过这里只传入了5个参数,还有一个线程工厂和一个拒绝策略没有传,当不穿时,使用的都是默认值,一个使用默认工程(Executors.defaultThreadFactory()),一个使用默认策略(直接抛弃,抛出异常:AbortPolicy)。这样就构成了定长线程池,知道了所有参数的意义,再来看定长线程池就感觉很简单了。
先展示下单利线程池的实现,如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
看到上面的代码,其实就很简单明了了,他的核心线程数和最大线程数都是1,这样自始至终就都只会有一个线程用来执行任务,所以叫单例线程池。
先展示下缓存线程池的实现,如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
为什么叫缓存线程池呢,因为每一个线程使用完以后还会缓存60秒来等待有无任务可以分配到他,若是有则会继续工作,没有的话,在60s以后该线程就会被销毁。从传入的参数我们可以看出缓存线程池是没有核心线程的,但是他的最大线程是int的最大值231-1,这代表着若是使用缓存线程池最大可以创建出231-1个线程。
先展示下定时线程池的实现,如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
这里只能看到我们是传入了一个核心线程数来构建一个线程池,但是这个线程池并不是使用ThreadPoolExecutor,而是ScheduledThreadPoolExecutor。而光看这里看不到定时线程池的实现原理,那我们继续看下ScheduledThreadPoolExecutor的实现,如下:
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
这里就有些我们想要看到的信息了,这里可以看到,核心线程数是我们传入的,最大线程数是int的最大值231-1个,然后最后还传入了一个延时队列,看到延时感觉就找到了定时线程池的实现了。那这是就会有疑问了,我也没传入时间之类的参数啊,其实时间的参数是在执行时传入的而不是创建线程池是传入的,这和其他几个线程池略有区别,下面展示下延时一秒执行的代码。
import java.util.concurrent.*;
import java.util.concurrent.Executors;
/**
* @author pcc
* @version 1.0.0
* @className TestThread
* @date 2021-06-28 16:33
*/
public class TestThread {
public static void main(String[] args) {
ScheduledExecutorService executorService3 = Executors.newScheduledThreadPool(3);
executorService3.schedule(() ->{
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
},5, TimeUnit.SECONDS);
}
}
看到这里相信大家已经知道为什么不推荐使用jdk提供的线程池了,因为大部分每种都有弊端,都做不到业务上的百分百契合,当然了还有很多线程池笔者没有列出来,而且jdk1.8以后也增加了一部分线程池,但是无论是哪种都没有我们使用ThreadPoolExecutor来自己实现线程池来的更好,我们可以根据自己的实际业务,来判断核心线程数应该给与多少,最大线程数应该定到多少合适,并且根据实际场景来设置非核心线程的存活时间,设置自己的的拒绝策略等。
说到多线程就必须得提集合,集合是任何一个语言都会有的数据结构,java中自然也是存在的,而且集合是使用最频繁的数据存储结构可以说没有之一,在多线程的程序中使用集合就必须要考虑线程的安全问题,那么我们就必须要清楚怎么使用集合才是正确的,因为有的集合线程安全我们可以直接在多线程使用比如Vector、HashTable等,而有的线程不是线程安全的我们不可以直接使用,若是使用必须配上加锁处理,比如ArrayList、set、HashMap等。而每次都为ArrayList、HashMap等线程不安全的集合进行加锁还是很繁琐的操作,因此JUC包下面还提供了很多线程安全的集合可以 供我们直接使用比如ConcurrentHashMap等,下面我们就来一起看看JDK提供的这些集合。
先上一张图来理解下List与Set,该图来源网络,图总结的比较好这里就直接用了,原作者已不可考究,通过这个图看下来就很明了了,List的特点就是存入和取出顺序一致(因为底层是数组和链表所以可以一致)、元素可重复(同样因为底层是数组和链表所以可以重复),而set则是存入和取出不一致(因为底层是hash表),元素不可重复(hash表重复会覆盖)。当然了这也只是总结的大概,具体到每一个集合的实现还略有不同,下面我们就来一个个看下他们的特点。
List
ArrayList:
ArrayList是使用频率最高的List集合,ArrayList的最典型特征是线程不安全,元素的存取有序,数据可重复放插入。ArrayList底层使用数组实现,因为使用数组实现所以他的插入删除比较慢,插叙比较快,所以它比较适合查询操作比较频繁的场景。ArrayList有两个构造方法,一个无参构造一个是有参构造,无参构造构造一个空的数组,有参构造构造一个指定参数的数组(还有一个直接将集合转化为ArrayList的构造),不过即使我们使用指定大小的有参构造创建一个ArrayList,若是我们声明的大小小于10的话,在第一次调用add方法后,ArrayList底层会默认进行扩容到10,所以还是建议在使用ArrayList时不要设置10以下的容量,因为扩容需要调用本地方法,并将原来数组中的所有值复制到新的数组中,还是很消耗性能的,另外当数组容量满了以后,ArrayList也会扩容,不过这个扩容只会将数组的长度扩大到原先的1.5倍,具体操作时原先的数组长度加上原先长度的右移一位的值(相当于除以2)。
LinkedList:
使用链表实现,也是比较常见的List集合,他同样具有线程不安全,元素的存取有序,数据可重复插入等特点。但是Linked底层使用链表实现,所以他的插入删除比较快,查询就会比较慢,所以他更适合插入删除比较频繁的业务场景。
Vector:
该集合是List集合中的唯一线程安全的集合,元素的存取有序,数据可重复插入,因为List中只有它是线程安全的,因此若是想要线程安全时就必须使用它。但是我们也必须清楚,只要是数组实现那么他的查询就会很快,插入和删除较慢。
Set
TreeSet:
所有set都是不安全的,想要使用安全的set必须使用JUC提供的set,同时Set集合具有存取无续性,不可重复等特点,不允许插入null。ThreeSet底层是二叉树,因此元素保存好以后就是按大小排序的,当然这个排序不是天然就支持的需要存入的元素实现Comparable接口重写compareTo方法,或者创建TreeSet时传入一个Comparator接口的对象,这样在数据存放时就会根据实现的比较器来排序。TreeSet底层是TreeMap,因此它的元素不仅需要实现Comparable还需要重写hashcode方法和equals方法。
HashSet:
HashSet同样是不安全的,同样具有存取无序性、元素同样不支持重复,允许插入null,HashSet底层是HashMap,HashMap底层是Hash表+链表+红黑树。一般不使用排序功能都使用HashSet,不排序时HashSet的优于TreeSet。
LinkedHashSet:
该Set线程不安全,元素不可重复,允许插入null,但是他存取有序,若是一定要用Set而又想使存取有序就可以使用LinkedHashSet,它的底层是链表+hash表,链表保证了存取有序,但是却也导致了查询慢,若是没有强制要求不建议使用LinkedHashSet,正常情况下都使用HashSet。
Map
HashMap:Map不同于List和Set,Map是以键值对的方式来存取数据结构的,Hash具有存取无续,键不可重复,键值均可为null等特点,他的底层是数组+链表+红黑树,无论哪里的面试HashMap都是重中之重,笔者写过一篇专门总结HashMap的文章,该文章总结了HashMap的所有知识场景,有意愿的可以在这里详细了解HashMap。HashMap一文彻底搞懂
Hashtable:Hashtable线程安全,同样具有存取无续,键不可重复,但是Hashtable键值均不可以为null,Hashtable底层与HashMap基本一致,最大的区别就是方法加了锁操作。
TreeMap:底层是红黑树(二叉树的一种),与前面两种一样,存入TreeMap中的键必须实现hashcode和equals方法,值得注意的是TreeMap是TreeSet的底层实现,因此TreeSet具有的特点TreeMap也是拥有的,即:键值均可以为null,且存取无续,但是元素有序,当然元素有序需要传入比较器,或者元素实现Comparable接口。
总结Collection **
├——-List 接口:元素按进入先后有序保存,可重复
│—————-├ LinkedList :允许值重复,存取有序,底层链表,查询慢,增删快,线程不安全,增删效率最高
│—————-├ ArrayList :允许重复,存取有序,底层数组,查询快,增删慢,线程不安全,查询效率最高
│—————-└ Vector :允许重复,存取有序,底层数组,查询快,增删慢,线程安全
│ ———————-└ Stack 是Vector类的实现类
└——-Set 接口: 仅接收一次,不可重复,并做内部排序
├—————-└HashSet** :不允许重复,存取无续,元素无续,底层是HashMap,线程不安全
│————————└ LinkedHashSet :不允许重复,元素无续,底层是LinkedHashMap,线程不安全
└ —————-TreeSet :不允许重复,底层是TreeMap,元素有序,底层是TreeMap,线程不安全
总结Map
├———Hashtable :存取无续,元素无续,不可重复,键值不可为null,线程安全
├———HashMap :存取无续,元素无续,不可重复,键值可为null,线程不安全
│—————–├ LinkedHashMap 双向链表和哈希表实现
│—————–└ WeakHashMap
├ ——–TreeMap :存取无续,元素有序,不可重复,键值可为null,线程不安全,效率低于HashMap
└———IdentifyHashMap
因为java提供的集合架构很多不支持并发,所以在JUC包下面才提供了一系列的支持并发的集合,其中包括了List、Set、Map等都有,而且JUC包下的集合,虽然支持并发,却都不是基于synchronized的机制来实现的,他们都是基于CAS、CopyOnWrite机制等来实现的并发安全,这样极大的提高了多线程情况下的效率,所以若是开发多线程,JUC下的集合类则是必须掌握的。