Java并发基础
1、Process与Thread
- 程序是指令和数据的有序集合,其本身没有任何运行的含义,是一个静态的概念
- 进程是程序的一次执行过程,它是一个动态概念。是系统资源分配的单位
- 通常在一个进程中可以包括若干个线程,线程是CPU调度和执行的单位
2、何为线程:
-
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器**、**虚拟机栈 和 本地方法栈。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
3、为什么程序计数器、虚拟机栈和本地方法栈是线程私有的呢?为什么堆和方法区是线程共享的呢?
程序计数器主要有下面两个作用:
- 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
- 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。
所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。
虚拟机栈、本地方法栈
- 虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
- 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。
堆和方法区:
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
4、多线程会带来的问题:
并发编程的目的就是为了能提高程序的执行效率提高程序运行速度,但是并发编程并不总是能提高程序运行速度的,而且并发编程可能会遇到很多问题,比如:内存泄漏、死锁、线程不安全等等。
5、停止线程
- 建议线程正常停止,利用次数,不建议死循环
- 建议使用标志位–>设置一个标志位
- 不要使用stop或destory等过时或者JDK不建议使用的方法
6、线程的生命周期和状态?
-
线程创建之后它将处于**New(新建)**状态,调用
start()
方法后开始运行,线程这时候处于READY(可运行)状态。可运行状态的线程获得了CPU时间片(timeslice)后就处于RUNNING(运行)状态Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
-
当线程执行
wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态 -
而 TIMED_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过
sleep(long millis)
方法或wait(long millis)
方法可以将 Java 线程置于 TIMED_WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态 -
线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态
-
线程在执行 Runnable 的
run()
方法之后将会进入到 TERMINATED(终止) 状态。
7、什么是上下文切换?
线程在执行过程中会有自己的运行条件和状态(也称上下文),比如上文所说到过的程序计数器,栈信息等。当出现如下情况的时候,线程会从占用 CPU 状态中退出。
- 主动让出 CPU,比如调用了
sleep()
,wait()
等。 - 时间片用完,因为操作系统要防止一个线程或者进程长时间占用CPU导致其他线程或者进程饿死。
- 调用了阻塞类型的系统中断,比如请求 IO,线程被阻塞。
- 被终止或结束运行
这其中前三种都会发生线程切换,线程切换意味着需要保存当前线程的上下文,留待线程下次占用 CPU 的时候恢复现场。并加载下一个将要占用 CPU 的线程上下文。这就是所谓的 上下文切换。
8、线程礼让yield
1、礼让线程,让当前线程暂停,但不阻塞
2、将线程从运行状态转为就绪状态
3、让CPU重新调度,礼让不一定成功!看CPU心情
join方法:调用join方法只会使主线程(或者调用t.join的线程)进入等待池并等待t线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态的其他线程。
9、线程优先级:
1、java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程执行
2、线程的优先级用数字表示,范围从1~10
Thread.MIN_PRIORITY=1;
Thread.MAX_PRIORITY=10;
Thread>NORM_PRIORITY=5;
3、使用以下方式改变或获取优先级:getPriority()、setPriority(int xxx)
10、守护线程:
- 线程分为用户线程和守护线程
- 虚拟机必须确保用户线程执行完毕
- 虚拟机不用等待守护线程执行完毕
- 如,后台记录操作日志,监控内存,垃圾回收等待…
11、线程同步:(多个线程同时访问一个资源)
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用
- 一个线程持有锁会导致其他所有需要此锁的线程挂起
- 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题
12、死锁:
多个线程各自占有一些共享资源,并且相互等待其他线程占用的资源才能运行。而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题
死锁必须具备的四个条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已经获得资源保持不放
- 不可剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才能释放资源
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
13、如何预防死锁?
破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件 :一次性申请所有的资源。
- 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种进程推进顺序(P1、P2、P3…Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3…Pn>序列为安全序列。
14、为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?
new
一个Thread
,线程进入新建状态。调用start()
方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。start()
会执行线程的相应准备工作,然后自动执行run()
方法的内容,这是真正的多线程工作。 但是,直接执行run()
方法,会把run()
方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
15、Lock锁
-
从JDK1.5开始,Java提供了更强大的线程同步机制—通过显示定义同步锁对象来实现同步。同步锁使用Lock对象充当
-
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能由一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得lock对象
-
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显示加锁、释放锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D162PP1q-1639370239209)(C:\Users\Zhangyiwei\AppData\Roaming\Typora\typora-user-images\image-20211111161418529.png)]
16、synchronized与lock的对比
- Lock是显示锁(手动开启和关闭锁),synchronized是隐式锁,出了作用域自动释放
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更高的子类)
- 优先性:Lock>同步代码块>同步方法
17、说说sleep方法和wait方法区别和共同点?
- 两者最主要的区别在于:
sleep()
方法没有释放锁,而wait()
方法释放了锁 。 - 两者都可以暂停线程的执行。
wait()
通常被用于线程间交互/通信,sleep()
通常被用于暂停执行。wait()
方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的notify()
或者notifyAll()
方法。sleep()
方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)
超时后线程会自动苏醒。
Java并发进阶
synchronized关键字:synchronized
关键字解决的是多个线程之间访问资源的同步性,可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是是给 Class 类对象上锁。synchronized
关键字加到实例方法上是给对象实例上锁。- 尽量不要使用
synchronized(String a)
因为 JVM 中,字符串常量池具有缓存功能!重校验锁实现对象单例
#单例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
} //构造器私有
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton(); //不是原子性操作
}
}
}
//
123
132 A
B //此时还没有完成构造 但uniqueInstance不为空 执行return uniqueInstance
return uniqueInstance;
}
}
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
synchronized (Singleton.class) {
if(uniqueInstance!=null) {
throw new RuntimeException("不要试图使用反射破坏异常!");
}
}
} //构造器私有
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton(); //不是原子性操作
}
}
}
return uniqueInstance;
}
//试图用反射去破坏
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Singleton uniqueInstance = Singleton.getUniqueInstance();
Constructor<Singleton> declaredConstructor = Singleton.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true); //使私有变公有
Singleton singleton = declaredConstructor.newInstance();
System.out.println(uniqueInstance);
System.out.println(singleton);
}
}
//反射不能破坏枚举的单例
public class StaticSingleton{
private StaticSingleton() {
System.out.println("StaticSingleton is create");
}
private static class SingletonHolder {
private static StaticSingleton instance = new StaticSinglenton();
}
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
}
//首先在getInstance方法中没有锁,这使得在高并发环境下性能优越。其次,只有在getInstance()方法第一次调用时,StaticSingleton的实例才会被创建。巧妙地使用了内部类和类的初始化方式。内部类被声明为private,使得不能在外部访问并且初始化它。
为什么uniqueInstance采用volatile关键字(内存屏障)
源代码---->编译器优化的重排------>指令并行也可能重排-------->内存系统也会重排------->执行
-
uniqueInstance
采用volatile
关键字修饰也是很有必要的,uniqueInstance = new Singleton();
这段代码其实是分为三步执行:- 为
uniqueInstance
分配内存空间 - 初始化
uniqueInstance
- 将
uniqueInstance
指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用
getUniqueInstance
() 后发现uniqueInstance
不为空,因此返回uniqueInstance
,但此时uniqueInstance
还未被初始化。使用
volatile
可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。 - 为
两个判空 什么作用?
-
第一个校验,为了提高代码效率,单例模式只一次创建实例即可,当创建过一个实例后,再次调用
getInstance()
方法就不必再进入同步代码块,不必再竞争锁,直接返回之前创建的实例即可 -
第二个校验,防止二次创建实例,线程1通过第一个if后,进入同步代码块之前,资源被线程2抢占,线程2两个if都通过,创建了
singleton
实例,线程1重新获取资源,进入同步代码块中,如果没有第二个if,则会创建两个singleton
实例
构造方法可以使用
synchronized
关键字修饰吗?
构造方法本身就属于线程安全的,不存在同步的构造方法一说
说说JDK1.6之后的
synchronized
关键字底层作了哪些优化?
- JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销
- 锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率
谈谈
synchronized
和ReentrantLock
的区别:
-
两者都是可重入锁
“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁。
-
synchronized
依赖于JVM
而ReentrantLock
依赖于API
synchronized
是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为synchronized
关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReentrantLock
是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 -
RentrantLock
比synchronized
增加了一些高级功能相比
synchronized
,ReentrantLock
增加了一些高级功能。主要来说主要有三点:-
等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()
来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 -
可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁。而synchronized
只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock
默认情况是非公平的,可以通过ReentrantLock
类的ReentrantLock(boolean fair)
构造方法来制定是否是公平的。 -
可实现选择性通知(锁可以绑定多个条件):
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
类当然也可以实现,但是需要借助于Condition
接口与newCondition()
方法。
-
condition
Condition
是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock
对象中可以创建多个Condition
实例(即对象监视器),线程对象可以注册在指定的Condition
中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify()/notifyAll()
方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock
类结合Condition
实例可以实现“选择性通知” ,这个功能非常重要,而且是 Condition 接口默认提供的。而==synchronized
关键字就相当于整个 Lock 对象中只有一个Condition
实例,所有的线程都注册在它一个身上。如果执行notifyAll()
方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题==,而Condition
实例的signalAll()
方法 只会唤醒注册在该Condition
实例中的所有等待线程。
package thread;
import java.util.PriorityQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConTest2 {
private int queueSize = 10;
private PriorityQueue<Integer> queue = new PriorityQueue<Integer>(queueSize);
private Lock lock = new ReentrantLock();
private Condition notFull = lock.newCondition();
private Condition notEmpty = lock.newCondition(); //用Reentrant类结合condition实例可以实现"选择性通知"
//Reentrantlock的newCondition方法返回与某个lock实例相关的Condition对象
public static void main(String[] args) throws InterruptedException {
ConTest2 test = new ConTest2();
Producer producer = test.new Producer();
Consumer consumer = test.new Consumer();
producer.start();
consumer.start();
Thread.sleep(0);
producer.interrupt();
consumer.interrupt();
}
class Consumer extends Thread{
@Override
public void run() {
consume();
}
volatile boolean flag=true;
private void consume() {
while(flag){
lock.lock();
try {
while(queue.isEmpty()){
try {
System.out.println("队列空,等待数据");
notEmpty.await();
} catch (InterruptedException e) {
flag =false;
}
}
queue.poll(); //每次移走队首元素
notFull.signal();
System.out.println("从队列取走一个元素,队列剩余"+queue.size()+"个元素");
} finally{
lock.unlock();
}
}
}
}
class Producer extends Thread{
@Override
public void run() {
produce();
}
volatile boolean flag=true;
private void produce() {
while(flag){
lock.lock();
try {
while(queue.size() == queueSize){
try {
System.out.println("队列满,等待有空余空间");
notFull.await();
} catch (InterruptedException e) {
flag =false;
}
}
queue.offer(1); //每次插入一个元素
notEmpty.signal();
System.out.println("向队列取中插入一个元素,队列剩余空间:"+(queueSize-queue.size()));
} finally{
lock.unlock();
}
}
}
}
}
condition
可以通俗的理解为条件队列。当一个线程在调用了await
方法以后,直到线程等待的某个条件为真的时候才会被唤醒。这种方式为线程提供了更加简单的等待/通知模式。Condition
必须要配合锁一起使用,因为对共享状态变量的访问发生在多线程环境下。一个Condition
的实例必须与一个Lock
绑定,因此Condition
一般都是作为Lock的内部实现。
等待队列
-
Condition是AQS的内部类。每个Condition对象都包含一个队列(等待队列)。等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。AQS有一个同步队列和多个等待队列,节点都是Node。等待队列的基本结构如下所示
等待分为首节点和尾节点。当一个线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列。新增节点就是将尾部节点指向新增的节点。节点引用更新本来就是在获取锁以后的操作,所以不需要CAS保证。同时也是线程安全的操作。
-
如果从**队列(同步队列和等待队列)的角度去看await()**方法,当调用await()方法时,相当于同步队列的首节点(获取锁的节点)移动到Condition的等待队列中。
-
调用该方法的线程成功的获取锁的线程,也就是同步队列的首节点,该方法会将当前线程构造成节点并加入到等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。
-
当等待队列中的节点被唤醒的时候,则唤醒节点的线程开始尝试获取同步状态。如果不是通过 其他线程调用Condition.signal()方法唤醒,而是对等待线程进行中断,则会抛出InterruptedException异常信息。
整个协作过程是靠结点在AQS的等待队列和Condition的等待队列中来回移动实现的,每个队列的意义不同,Condition作为一个条件类,很好的自己维护了一个等待信号的队列,并在适时的时候将结点加入到AQS的等待队列中来实现的唤醒操作
volatile关键字
-
关于JMM的一些同步规定:
- 线程解锁前,必须把共享变量立刻刷主存
-
线程加锁前,必须读取主存的最新值到工作内存中!
- 加锁和解锁是同一把锁
-
JMM(java内存模型)–同步八种操作:
1:lock: 把主内存变量标识为一条线程独占,此时不允许其他线程对此变量进行读写。
2:unlock:解锁一个主内存变量。
3:read: 把一个主内存变量值读入到线程的工作内存,强调的是读入这个过程。
4:load: 把read到变量值保存到线程工作内存中作为变量副本,强调的是读入的值的保存过程。
5:use: 线程执行期间,把工作内存中的变量值传给字节码执行引擎。
6:assign(赋值):字节码执行引擎把运算结果传回工作内存,赋值给工作内存中的结果变量。
7:store: 把工作内存中的变量值传送到主内存,强调传送的过程。
8:write: 把store传送进来的变量值写入主内存的变量中,强调保存的过程。 -
Java内存模型(JMM):
在当前的 Java 内存模型下,线程可以把变量保存在本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。
要解决这个问题,就需要把变量声明为
volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。所以,
volatile
关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。
并发编程的三个重要特性:
- 原子性:一个的操作或者多次操作,要么所有的操作全部都得到执行并且不会收到任何因素的干扰而中断,要么所有的操作都执行,要么都不执行。
synchronized
可以保证代码片段的原子性。 - 可见性:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。
volatile
关键字可以保证共享变量的可见性。 - 有序性:代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。
volatile
关键字可以禁止指令进行重排序优化。
public class VDemo01 {
private volatile static int num = 0;
public static void add() {
num++; //不是原子操作
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + num);
}
}
如何在不使用sychronized和lock的条件下保证原子性
使用原子类解决原子性问题,多线程环境使用原子类保证线程安全。使用AtomicInteger之后,不需要加锁,也可以使用线程安全
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
public class VDemo01 {
private volatile static AtomicInteger num = new AtomicInteger(0);
public static void add() {
num.getAndIncrement(); //原子类CAS 比锁使用高效 类的底层和操作系统挂钩 在内存中修改值
}
public static void main(String[] args) {
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
add();
}
}).start();
}
while(Thread.activeCount()>2) {
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+ ": " + num);
}
}
说说synchronized关键字和volatile关键字的区别
synchronized关键字和volatile关键字是两个互不存在,而不是对立的存在!
- volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
- volatile关键字只能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证
- volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性
ThreadLocal
多线程访问同一个共享变量的时候容易出现并发问题,ThreadLocal是除了加锁这种同步方法方式之外的一种规避多线程访问出现线程不安全的方法,当我们创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。
如果你创建了一个ThreadLocal
变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal
变量名的由来。他们可以使用 get()
和 set()
方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题
6、集合类不安全
List不安全
**写入时复制(CopyOnWrite,简称COW)**思想是计算机程序设计领域中的一种通用优化策略。其核心思想是,如果有多个调用者(Callers)同时访问相同的资源,他们会共同获取相同的指针指向相同的资源,直到某个调用者修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这个过程对其他的调用者都是透明的。此做法主要的优点是如果调用者没有修改资源,就不会有副本被创建,因此多个调用者只是读取操作时可以共享同一份资源。
JDK 的 CopyOnWriteArrayList/CopyOnWriteArraySet 容器正是采用了 COW 思想,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在更新的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。
ReentrantReadWriteLock
读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 CopyOnWriteArrayList
类比相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList
读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升
CooyOnWriteArrayList源码分析:
-
我们先来看看 CopyOnWriteArrayList 的 add() 方法,其实也非常简单,就是在访问的时候加锁,拷贝出来一个副本,先操作这个副本,再把现有的数据替换为这个副本。
public boolean add(E e) { final ReentrantLock lock = this.lock; lock.lock(); //加锁 try { Object[] elements = getArray(); int len = elements.length; Object[] newElements = Arrays.copyOf(elements, len + 1); //拷贝新数组 newElements[len] = e; setArray(newElements); return true; } finally { lock.unlock(); } }
-
CopyOnWriteArrayList 的 get(int index) 方法就是普通的无锁访问
public E get(int index) { return get(getArray(), index); } @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; } final Object[] getArray() { return array; }
CopyOnWrite优点和缺点
- 优点:
- 对于一些读多写少的数据,写入时复制的做法就很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
- CopyOnWriteArrayList 并发安全且性能比 Vector 好。Vector 是增删改查方法都加了synchronized 来保证同步,但是每个方法执行的时候都要去获得锁,性能就会大大下降,而 CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性能就好于 Vector。
- 缺点:
- 数据一致性问题。这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。
- 内存占用问题。如果对象比较大,频繁地进行替换会消耗内存,从而引发 Java 的 GC 问题,这个时候,我们应该考虑其他的容器,例如 ConcurrentHashMap。
CopyOnWriteArraySet
-
HashSet是不安全的
解决方法1:可以用Collections.synchronizedSet(new HashSet<>());转换成集合安全类
-
HashSet的底层就是HashMap
public HashSet() { map = new HashMap<>(); //add set 本质就是map key是无法重复的! } public boolean add(E e) { return map.put(e, PRESENT)==null; } private static final Object PRESENT = new Object(); //不变的值
HashMap也不安全
- 加载因子(0.75),初始化容量(16)
ConcurrentHashMap并发的HashMap
HashMap
不是线程安全的。也就是说,在多线程环境下,操作HashMap
会导致各种各样的线程安全问题,比如在HashMap
扩容重哈希时出现的死循环问题,脏读问题等。HashMap
的这一缺点往往会造成诸多不便,虽然在并发场景下HashTable
和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )
可以代替HashMap
,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。- 它为
HashMap
提供了一个线程安全的高效版本 ——ConcurrentHashMap
。在ConcurrentHashMap
中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。特别地,在理想状态下,ConcurrentHashMap
可以支持 16 个线程执行并发写操作(如果并发级别设为16),及任意数量线程的读操作。
JDK1.8后的ConcurrentHashMap
-
JDK8中
ConcurrentHashMap
参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作。并发控制使⽤synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构 -
可以理解为,synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,这样只要 hash 不冲突,就不会产⽣并发,效率⼜提升 N 倍。
ConcurrentHashMap和HashTable的区别:
-
Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
-
Hashtable(同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤get,竞争会越来越激烈效率越低;
常用的辅助类
1、CountDownLatch
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
//总数是6 必须要执行任务的时候,再使用!
CountDownLatch countDownLatch = new CountDownLatch(6);
for (int i = 0; i < 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"Go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归零,然后向下执行
System.out.println("Close Door");
}
}
原理
:
countDownLatch.countDown();
//数量-1
countDownLatch.await();
//等待计数器归零,然后向下执行
每次有线程调用countDown()数量-1,假设计数器变为0,countDownLatch.await()就会被唤醒,继续执行!
2、CyclicBarrier
加法计数器:它的作用就是会让所有线程都等待完成后才会继续下一步行动。
public class CyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(7,()->{
System.out.println("召唤神龙成功!");
});
// public CyclicBarrier(int parties) parties 是参与线程的个数
// public CyclicBarrier(int parties, Runnable barrierAction) //第二个构造方法有一个Runnable参数,这个参数的意思是最后一个到达线程要做的任务
for (int i = 0; i < 7; i++) {
final
System.out.println(Thread.currentThread().getName()+"收集"+temp+"个龙珠");
try {
cyclicBarrier.await(); //线程调用await()表示自己已经到达栅栏
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
3、Semaphore 信号量
public class SemaphoreDemo {
public static void main(String[] args) {
//线程数量:停车位!限流
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 6; i++) {
new Thread(()->{
//acquire() 得到
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()+"抢到车位");
TimeUnit.SECONDS.sleep(2);
System.out.println(Thread.currentThread().getName()+"离开车位");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); //释放
}
},String.valueOf(i)).start();
}
}
}
原理:
semaphore.acquire();
获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release();
释放,会将当前的信号量释放+1
作用:多个共享资源互斥使用!并发限流,控制最大线程数
读写锁
ReadWriteLock
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vS7xuwvy-1639370239212)(C:\Users\Zhangyiwei\AppData\Roaming\Typora\typora-user-images\image-20211110103902029.png)]
public class ReadWriteLockDemo {
public static void main(String[] args) {
MyCacheLock myCacheLock = new MyCacheLock();
//写入
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCacheLock.put(temp+"",temp+"");
},String.valueOf(i)).start();
}
for (int i = 1; i <= 5 ; i++) {
final int temp = i;
new Thread(()->{
myCacheLock.get(temp+"");
},String.valueOf(i)).start();
}
}
}
/*
* 自定义缓存
* */
class MyCacheLock {
private volatile Map<String, Object> map = new HashMap<>();
//读写锁,更加细粒度的控制
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
//存,写入的时候,只希望同时只有一个线程写
public void put(String key, Object value) {
//写锁
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"写入"+key);
map.put(key,value);
System.out.println(Thread.currentThread().getName()+"写入ok");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
//取 读,所有人都可以读!
public void get(String key) {
lock.readLock().lock();
try {
System.out.println(Thread.currentThread().getName()+"读取"+key);
Object o = map.get(key);
System.out.println(Thread.currentThread().getName()+"读取OK");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}
读与读 可以共存! 读-写 不能共存! 写-写 不能共存!
阻塞队列
阻塞:写入:如果队列满了,就必须阻塞等待。 取:如果队列是空的,必须阻塞等待
队列:FIFO
什么情况下我们会使用阻塞队列?BlockingQueue
多线程并发处理、线程池
AbstractQueue非阻塞队列
四组API
方式 | 抛出异常 | 有返回值(true\false) null | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查(队首元素) | element() | peek() | 不可用 | 不可用 |
BlockingQueue
不接受null
元素。试图add
、put
或offer
一个null
元素时,某些实现会抛出NullPointerException
。null
被用作指示poll
操作失败的警戒值。
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
用BlockingQueue队列实现的生产者-消费者模式是一个不错的选择,但是在高并发场合,它的性能并不是特别的优越。ConcurrentLinkedQueue是一个高性能的队列,但是BlockingQueue队列知识为了方便数据共享
SynchronousQueue 同步队列
没有容量,没有一个元素,必须等待取出来之后,才能再往里面放一个元素!
put、take
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<String> queue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"put 1");
queue.put("1");
System.out.println(Thread.currentThread().getName()+"put 2");
queue.put("2");
System.out.println(Thread.currentThread().getName()+"put 3");
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T1").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+" "+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"T2").start();
}
}
和其他的BlockingQueue不一样,SynchronizeQueue不存储元素 put了一个元素,必须从里面先take取出来,否则不能再put进去东西
线程池
线程池:三大方法、七大参数、四大拒绝策略
池化技术
池化技术:实现准备好一些资源,有人要用,就来这里拿,用完之后还给我
线程池的好处:
1、降低资源的消耗
2、提高响应的速度
3、方便管理
线程复用,可以控制最大并发数、管理线程
public class Demo1 {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单个线程
ExecutorService threadPool1 = Executors.newFixedThreadPool(5);//固定大小的一个线程池
ExecutorService threadPool2 = Executors.newCachedThreadPool();//可伸缩的线程池 遇强则强
try {
for (int i = 0; i < 10; i++) {
threadPool2.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭程序
threadPool.shutdown();
}
}
}
七大参数
源码分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OhN2rSMB-1639370239214)(C:\Users\Zhangyiwei\AppData\Roaming\Typora\typora-user-images\image-20211110182942968.png)]
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
}
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//七大参数
public ThreadPoolExecutor(int corePoolSize, //核心线程池大小
int maximumPoolSize, //最大核心池大小
long keepAliveTime, //超时了没有人调用就会释放
TimeUnit unit, //超时单位
BlockingQueue<Runnable> workQueue, //阻塞队列
ThreadFactory threadFactory, //线程工厂,创建线程的,一般不用动
RejectedExecutionHandler handler) { //拒绝策略
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
手动创建一个线程池 四大拒绝策略
//new ThreadPoolExecutor.AbortPolicy() 银行满了,没有人进来,不处理这个人,抛出异常
//new ThreadPoolExecutor.CallerRunsPolicy() 哪来的去哪里
//new ThreadPoolExecutor.DiscardPolicy() 队列满了 直接丢弃 不会抛出异常
//new ThreadPoolExecutor.DiscardOldestPolicy() 队列满了 尝试和最早的竞争,如果不成功丢弃 也不会抛出异常
public class Demo1 {
public static void main(String[] args) {
ExecutorService threadPool2 = new ThreadPoolExecutor(2,
5,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),//一般不会动
new ThreadPoolExecutor.DiscardOldestPolicy()); //队列满了 尝试和最早的竞争,如果不成功丢弃
//超时等待 3、4、5 一个小时都没有业务关闭窗口 被释放
try {
//最大承载 queue+max
for (int i = 0; i < 9; i++) {
threadPool2.execute(()->{
System.out.println(Thread.currentThread().getName()+"ok");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//线程池用完,程序结束,关闭程序
threadPool2.shutdown();
}
}
}
CPU密集型和IO密集型 最大线程该如何定义
- CPU密集型
//几核就定义为几 效率最高
Runtime.getRuntime.availableProcessors() //获取CPU的核数
- IO密集型
程序 15个大型程序 IO十分占用资源!> 判断你的程度中十分耗IO的进程
计划任务
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
//方法schedule会在给定时间,对任务进行一次调度
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay,long period, TimeUnit unit);
//它是以上一个任务开始执行时间为起点,在之后的period时间调度下一次任务
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,
long delay,TimeUnit unit);
//FixDelay方式则是在上一个任务结束后,再经过delay时间进行任务调度
public class ScheduledExecutorServiceDemo {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
System.out.println(System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},0,2, TimeUnit.SECONDS);
}
}
自定义线程创建:ThreadFactory
ThreadFactory
是一个接口,它只有一个用来创建线程的方法
Thread newThread(Runable r) //当线程池需要新建线程时,就会调用这个方法
四大函数式接口(必须掌握)
函数式接口:只有一个函数的接口
简化编程模型,在新版本的框架底层大量应用
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
//foreach(消费者类的函数式接口)
四大函数式接口
@FunctionalInterface
public interface Function<T, R> {
R apply(T t); //传入参数T 返回类型R
}
public class Demo01 {
public static void main(String[] args) {
Function function = (str)->{return str;};
System.out.println(function.apply("asd"));
}
}
//断定性接口
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t); //输入参数 返回值是固定的 boolean值
}
public class Demo01 {
public static void main(String[] args) {
Predicate<String> predicate = (str)->{return str.isEmpty();};
System.out.println(predicate.test("sssss"));
}
}
//消费者接口
@FunctionalInterface
public interface Consumer<T> {
void accept(T t); //只有输入,没有返回值
}
//供给型接口 没有参数 有返回值
@FunctionalInterface
public interface Supplier<T> {
T get();
}
Stream流式计算
什么是Stream流计算
大数据:存储和计算,计算都应该交给流来操作!
链式编程
这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
//生成流
stream() --为集合创建串行流
parellelStream() --为集合创建并行流
List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
//Collectors 类实现了很多归约操作,例如将流转换成集合和聚合元素。Collectors 可用于返回列表或字符串:
List<String>strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl");
List<String> filtered = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.toList());
System.out.println("筛选列表: " + filtered);
String mergedString = strings.stream().filter(string -> !string.isEmpty()).collect(Collectors.joining(", "));
System.out.println("合并字符串: " + mergedString);
//另外,一些产生统计结果的收集器也非常有用。它们主要用于int、double、long等基本类型上,它们可以用来产生类似如下的统计结果。
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5);
IntSummaryStatistics stats = numbers.stream().mapToInt((x) -> x).summaryStatistics();
System.out.println("列表中最大的数 : " + stats.getMax());
System.out.println("列表中最小的数 : " + stats.getMin());
System.out.println("所有数之和 : " + stats.getSum());
System.out.println("平均数 : " + stats.getAverage());
ForkJoin(分支合并)
什么是ForkJoin
ForkJoin在JDK1.7,并行执行任务!提高效率,大数据量!
大数据:Map Reduce(把大任务拆分成小任务)
它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出
ForkJoin框架完成两件事情:任务分割、执行任务并合并结果
ForkJoin原理:工作窃取
- 在Java的Fork/Join框架中,使用两个类完成上述操作
- ForkJoinTask:首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
- RecursiveAction:用于没有返回结果的任务
- RecursiveTask : 用于有返回结果的任务
- ForkJoinTask:首先需要创建一个ForkJoin任务。该类提供了在任务中执行fork和join的机制。通常情况下我们不需要直接集成ForkJoinTask类,只需要继承它的子类,Fork/Join框架提供了两个子类:
- ForkJoinPool : ForkJoinTask需要通过ForkJoinPool来执行
- 任务分割出的子任务会添加到当前工作线程所维护的双端队列中,进入队列的头部。当一个工作线程的队列里暂时没有任务时,它会随机从其他工作线程的队列的尾部获取一个任务(工作窃取算法)。
- Fork/Join框架的实现原理:
- ForkJoinPool由ForkJoinTask数组和ForkJoinWorkerThread数组组成,ForkJoinTask数组负责将存放程序提交给ForkJoinPool,而ForkJoinWorkerThread负责执行这些任务。
- ForkJoinTask的Fork方法的实现原理:
当我们调用ForkJoinTask的fork方法时,程序会把任务放在ForkJoinWorkerThread的pushTask的workQueue中,异步地执行这个任务,然后立即返回结果,代码如下:
public class Demo02 {
private static final Integer MAX = 200;
static class MyForkJoinTask extends RecursiveTask<Integer> {
// 子任务开始计算的值
private Integer startValue;
// 子任务结束计算的值
private Integer endValue;
public MyForkJoinTask(Integer startValue , Integer endValue) {
this.startValue = startValue;
this.endValue = endValue;
}
@Override
protected Integer compute() {
// 如果条件成立,说明这个任务所需要计算的数值分为足够小了
// 可以正式进行累加计算了
if(endValue - startValue < MAX) {
System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
Integer totalValue = 0;
for(int index = this.startValue ; index <= this.endValue ; index++) {
totalValue += index;
}
return totalValue;
}
// 否则再进行任务拆分,拆分成两个任务
else { //递归
Demo02.MyForkJoinTask subTask1 = new Demo02.MyForkJoinTask(startValue, (startValue + endValue) / 2);
subTask1.fork();
Demo02.MyForkJoinTask subTask2 = new Demo02.MyForkJoinTask((startValue + endValue) / 2 + 1 , endValue);
subTask2.fork();
return subTask1.join() + subTask2.join();
}
}
}
}
public static void main(String[] args) {
// 这是Fork/Join框架的线程池
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> taskFuture = pool.submit(new MyForkJoinTask(1,1001));
try {
Integer result = taskFuture.get();
System.out.println("result = " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(System.out);
}
}
异步回调
Future模式
Future是多线程开发中非常常见的一种设计模式,它的核心思想是异步调用。有时候调用一个函数方法时,如果这个函数执行地很慢,不着急要结果。可以让被调用者立即返回,让它在后台慢慢处理这个请求。
/**
* 数据接口
*/
public interface Data {
/**
* 获取数据
* @return
*/
String getResult();
}
/**
* RealData类
*/
public class RealData implements Data {
protected final String result;
public RealData(String para) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append(para);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
result = sb.toString();
}
@Override
public String getResult() {
return result;
}
}
/**
* FutureData类
*/
public class FutureData implements Data {
//FutureData是realData的包装
protected RealData realData = null;
protected boolean isReady = false;
public synchronized void setRealData(RealData realData) {
if (isReady) {
return;
}
this.realData = realData;
isReady = true;
notifyAll(); //RealData已经被注入,通知getResult方法
}
@Override
public synchronized String getResult() {
while (!isReady) {
try {
wait(); //一直在等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return realData.getResult();
}
}
public class Client {
public Data request(final String queryStr) {
final FutureData futureData = new FutureData();
//单起个线程进行数据处理
new Thread() {
@Override
public void run() {
RealData realData = new RealData(queryStr); //RealData的构建很慢 所以在单独线程中进行
futureData.setRealData(realData);
}
}.start();
//立即返回
return futureData;
}
public static void main(String[] args) {
Client client = new Client();
Data data = client.request("ljhname");
System.out.println("请求完毕");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据" + data.getResult());
}
}
JDK的Future模式:JDK已经帮我们准备一套完整的实现,我们可以利用其进行非常方便的实现功能:
public class RealData implements Callable<String> {
private String para;
public RealData (String para){
this.para = para;
}
@Override
public String call() throws Exception {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10; i++) {
sb.append(para);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
public class FutureMain {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<String> futureTask = new FutureTask<String>(new RealData("ljh"));
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(futureTask);
System.out.println("请求完毕");
try {
//这里依然可以做额外的数据操作,使用sleep代替其他业务逻辑处理
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//如果此时call()方法没有执行完成,则依然会等待
System.out.println("数据=" + futureTask.get());
}
}
Future接口的其他一些功能:
boolean cancel(boolean mayInterruptIfRunning);
//取消任务
boolean isCancelled()
//任务是否取消
boolean isDone()
//是否完成任务
V get() throws InterruptedException, ExecutionException;
//取得返回对象
V get(long timeout, TimeUnit unit)
//取得返回对象,可以设置超时时间
Guava对Future模式的支持
增强了Future模式,增加了对Future模式完成时的回调接口,使得Future完成时可以自动通知应用程序进行后续处理
深入理解CAS
什么是CAS?
CAS:比较当前工作内存的值和主内存中的值,如果这个值是期望的,那么执行操作!如果不是则一直循环!利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
为什么需要CAS机制
我们经常使用volatile
关键字修饰某一个变量,表明这个变量是全局的一个变量,同时具有了可见性和有序性。但是却没有原子性。比如a++。这个操作其实细分为三个步骤:
1、从内存中读取a
2、对a进行加1
3、将a的值重新写入内存中
单线程状态下这个操作没有一点问题,但是在多线程中就会出现各种各样的问题了。因为可能一个线程对a进行了加1操作,还没来得及写入内存,其他的线程就读取了旧值。造成了线程的不安全现象。
Volatile
关键字可以保证线程间对于共享变量的可见性有序性,可以防止CPU的指令重排序,但无法保证操作的原子性,所以JDK1.5
之后引入了CAS利用CPU原语保证线程操作的原子性。
缺点:
1、循环会耗值
2、一次只能保证一个共享变量的原子性
3、ABA问题
通过代码追溯,JAVA中的CAS操作都是通过sun包下的Unsafe类实现,而Unsafe类中的方法都是native方法,由JVM本地实现,所以最终的实现是基于C、C++在操作系统之上操作的
Unsafe类
CAS:ABA问题(狸猫换太子)
解决ABA问题:
1)添加版本号
2)AtomicStampedReferenced(带有标记的原子引用类)
原子引用
AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等,而AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。也就是它可以保证你在修改对象引用时的线程安全性。
CAS使用时机:
- 线程数较少、等待时间短可以采用自旋锁进行CAS尝试拿锁,较于synchronized高效
- 线程数较大、等待时间长,不建议使用自旋锁,占用CPU较高
各种锁的引用:
1、公平锁、非公平锁:
1、公平锁:非常公平,不能插队,必须先来后到!
2、非公平锁:非常不公平,可以插队(默认都是非公平的)
//ReentrantLock lock = new ReentrantLock(); 默认非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// ReentrantLock lock = new ReentrantLock(true); 可以设置为公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
2、可重入锁
可重复锁:拿到了外面的锁,就可以拿到里面的锁(自动获得)
sychronized //一把锁
lock //两把锁
-
sychronized版本
public class Demo01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone { public synchronized void sms() { System.out.println(Thread.currentThread().getName() + "SMS"); call(); } public synchronized void call() { System.out.println(Thread.currentThread().getName() + "call"); } }
-
ReentrantLock版本
public class Demo01 { public static void main(String[] args) { Phone phone = new Phone(); new Thread(()->{ phone.sms(); },"A").start(); new Thread(()->{ phone.sms(); },"B").start(); } } class Phone { Lock lock = new ReentrantLock(); public void sms() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "SMS"); call(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } public void call() { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "call"); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
3、自旋锁
什么是自旋锁?
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
class spinlock {
private AtomicReference<Thread> cas;
spinlock(AtomicReference<Thread> cas){
this.cas = cas;
}
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) { //为什么预期是null??
// DO nothing
System.out.println("I am spinning");
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
自旋锁的优点:
- 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
- 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。
可重入自旋锁:
为了实现可重入锁,需要引入一个计数器,用来记录获取的线程数:
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
private int count;
public void lock() {
Thread current = Thread.currentThread();
if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
// 如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
if (cur == cas.get()) {
if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}
//可重入自旋锁验证
class Task1 implements Runnable{
private AtomicReference<Thread> cas;
private ReentrantSpinLock slock ;
public Task1(AtomicReference<Thread> cas) {
this.cas = cas;
this.slock = new ReentrantSpinLock(cas);
}
@Override
public void run() {
slock.lock(); //上锁
slock.lock(); //再次获取自己的锁!没问题!
for (int i = 0; i < 10; i++) {
//Thread.yield();
System.out.println(i);
}
slock.unlock(); //释放一层,但此时count为1,不为零,导致另一个线程依然处于忙循环状态,所以加锁和解锁一定要对应上,避免出现另一个线程永远拿不到锁的情况
slock.unlock();
}
}
总结:获取互斥锁的线程,如果锁已经被占用,则该线程进入睡眠状态;获取自旋锁的线程则不会睡眠。而是一直循环等待锁释放。
4、死锁
怎么发现和解决
1、使用jsp -l定位进程号
2、查看进程信息 jstack 进程号 查看堆栈信息
面试,工作中!排查问题: 1、日志 2、堆栈
Guava和RateLimiter限流
Guava是Google下的一个核心库,提供了一大批设计精良、使用方便的工具类。RateLimiter是Guava中的一款限流工具
令牌桶算法
是一种反向的漏桶算法,在令牌桶算法中,存放的是令牌。处理程序只有拿到令牌后,才能对请求进行处理。为了限制流速,该算法在每个单位时间产生一定量的令牌放入桶中。
RateLimiter正是采用了令牌桶算法
static void submitTasks2() {
ExecutorService pool = Executors.newFixedThreadPool(10);
RateLimiter rateLimiter = RateLimiter.create(5); // rate is "5 permits per second"
IntStream.range(0, 10).forEach(i -> pool.submit(() -> {
rateLimiter.acquire();
log.info("start");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
pool.shutdown();
偏向锁
偏向锁是Java6引入的一项多线程优化
偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。
如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
偏向锁的实现:
1、访问Mark Word中偏向锁的标识是否设置成1,锁标志是否为01,确认为可偏向状态
2、如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤5,否则进入步骤3
3、如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程ID,然后执行5;如果竞争失败,执行4
4、如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。(撤销偏向锁的时候会导致stop the word)
5、执行同步代码
偏向锁的释放:
偏向锁的撤销在上述第四步骤中有提到。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态
在Java语言中,不变模式的实现:
- 去除setter方法及所有修改自身属性的方法
- 将所有属性设置为私有,并用final标记,确保其不可修改
- 确保没有子类可以重载修改它的行为
- 有一个可以创建完整对象的构造函数
public final class product{
private final String no;
private final String name;
private final double price;
public Product(String no, String name, double price) {
super();
this.no = no;
this.name = name;
this.price = price;
}
public String getNo() {
return no;
}
public String getNamge{
return name;
}
public double getPrice{
return price;
}
}
//class的final确保了类不会有子类
在JDK中,不变模式的应用非常广泛。其中,最典型的就是java.lang.String类。此外,所有的元数据类、包装类都是使用不变模式是实现的。
CPU cache的优化:解决伪共享问题
什么是伪共享问题:
为了提高CPU的速度,CPU有一个高速缓存Cache,在高速缓存中,读写数据的最小单位为缓存行。当两个变量放在一个缓存行时,在多线程访问中,可能会影响彼此的性能。假设X和Y在同一缓存行,运行在CPU1上的线程更新了变量X,那么CPU2上的缓存行就会失效,同一行的变量Y即使没有修改也会失效,导致cache无法命中。系统的吞吐量会急剧下降。
解决方法:
为了避免这种情况的发生,一种可行的办法就是在变量X的前后空间都先占据一定位置(Padding,用来填充)。这样,当内存被读入缓存时,这个缓存行中,只有变量X一个变量实际是有效的。