并发线程

基础部分1

基础

什么是线程和进程

**进程是程序的一次执行过程,是系统运行程序的基本单位,**因此线程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。在java中,当我们启动main函数时其实就是启动了一个jvm的进程,而main函数所在的线程就是这个进程冲的一个线程,也称为主线程。
线程和进程像是,但是线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
总结: 线程是进程划分成的更小的运行单位。线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。
程序计数器主要有下面两个作用:

程序计数器为什么是私有的?

字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。

所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。

虚拟机栈和本地方法栈为什么是私有的?

虚拟机栈: 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。
所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

一句话简单了解堆和方法区

堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

什么是线程死锁?如何避免死锁

线程死锁描述的是这样一种情况:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
产生死锁必须具备以下四个条件:
互斥条件:该资源任意一个时刻只由一个线程占用。
请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
如何避免线程死锁?
破坏互斥条件 :这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件 :一次性申请所有的资源。
破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

说说 sleep() 方法和 wait() 方法区别和共同点?

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
两者都可以暂停线程的执行。
wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态。调用 start()方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 但是,直接执行 run() 方法,会把 run() 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start() 方法方可启动线程并使线程进入就绪状态,直接执行 run() 方法的话不会以多线程的方式执行。

线程池

池化技术相比大家已经屡见不鲜了,线程池,数据库连接池,Htpp连接池等等都是这个思想的应用。池化技术的思想主要是为了减少可以再次获取资源的消耗,提高对资源的利用率。
线程池提供了一种限制和管理资源。每个线程池还维护一些基本统计信息,例如已完成任务的数量。好处就是降低资源消耗、提高响应速度、提高线程的可管理性。

实现runnable接口和callable接口的区别

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

execute和submit有什么区别?

1.execute()方法用于提交不需要返回值得任务,所以无法判断任务是否被线程池执行成功与否
2.submit()方法用于提交需要返回值的任务。线程池会返回一个Future类型的对象,通过这个Future对象可以判断任务是否执行成功。

JUC常用的工具类

JUC就是java.util .concurrent工具包的简称
CountDownLatch,CyclicBarrier,semaphore

在这里插入图片描述

semaphore 信号量
semaphore.acquire() 获得,假设如果已经满了,等待,等待被释放为止!
semaphore.release(); 释放,会将当前的信号量释放 + 1,然后唤醒等待的线程!
作用: 多个共享资源互斥的使用!并发限流,控制最大的线程数

线程实现的四种方式

1、继承Thread类重写run方法
2、实现runnable实现run方法
3、集成callable接口 实现call方法 使用futureTask调用(有返回值、可处理异常)
4、线程池(线程复用,控制最大并发数,管理线程)—常用
a)低资源消耗:通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗
b)提高响应速度
c)提高线程的可管理性
线程池的业务流程如下图:
在这里插入图片描述

线程的四种类型

1、newcachedThreadPool 创建一个可根据需要创建新线程的线程池
2、NewFixedThreadPool创建一个可重用固定线程数的线程池
3、NewScheduledThreadPool 创建一个线程池,她可以安排在给定延迟后运行命令或定期执行
4、NewSingleThreadExecutor 返回一个线程池(这个线程池只有一个线程),这个线程池在线程死后重新启动一个线程替代原来的线程继续执行下去

如何停止一个正在运行的线程

1)使用退出标志,正常退出
2)使用stop方法强行终止,
3)使用interupt方法中断线程
4)正常运行,线程自动结束

ThreadPoolExecutor

七大参数分析
1)corepoolSize线程中的常驻核心线程数
2)maxmunpoolsize能够容纳同时执行的最大线程数,必须大于1
3)keepaliveTime多余空闲线程的存活时间。当前池中线程数量超过corepoolSize时或者当空闲时间达到keepaliveTime时,多余线程会被直接销毁直到剩下corePoolSize个线程为止(过剩策略)
4)TimeUnit unit ,keepAliveTime的单位
5)BlockingQueue workQueue 任务队列,被提交但是尚未被执行的任务
6)ThreadFactory 标识生成线程池中的工作现场的线程工厂,直接使用默认
7)RejectedExecutionHandler 拒绝策略;表示当前队列满了并且工作线程大于等于线程池的最大线程数时是如何来拒绝请求执行的runnable的策略

拒绝策略:
1)abortPolicy:直接抛出RejectedExcutionException异常阻止系统正常运行
2)callerRunsPolicy:调用者运行 一种调节机制,不会抛出任务也不会抛出异常,而是将某些任务回退到调用者,从而降低新的任务流量
3)DiscardOldesPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
4)DiscardPolicy 直接丢弃任务,不予任何处理也不排除异常。如果任务丢失,这是最好的一种方案

队列的分类
1)arrayBlockingQueue 一个由数组结构组成的有界阻塞队列,先进先出,那么被阻塞在外面的
2)linkedBlockingQueue基于链表结构的队列、有界队列、先进先出
3)synchronousQueue 不存储元素的阻塞队列。每个插入操作必须要等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态 吞吐量要高于linkedBlockingQueue
4)PriorityBlockingQueue 一种具有优先级的无限阻塞队列
5)DelayQueue:一个使用优先级队列实现的无界阻塞队列

Sleep和wait的区别

1、对于sleep方法是属于thread类中的,而wait方法则是属于object方法中的
2、Sleep方法导致线程暂停执行的指定时间,让出cpu给其他线程,但是她的监控状态依然保持者,当指定的时间到了优惠自动恢复运行状态
3、调用sleep()方法的过程中,线程不会释放对象锁
4、调用wait方法的时候,线程会放弃对象所,进入等待此对象的等待锁定池,只有针对此对象调用notify方法后本线程才进入对象锁定池准备获取对象锁进入运行状态

如何保证线程按照指定顺序执行?

1、使用join
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

public class ThreadTest1 {
// T1、T2、T3三个线程顺序执行
public static void main(String[] args) {
Thread t1 = new Thread(new Work(null));
Thread t2 = new Thread(new Work(t1));
Thread t3 = new Thread(new Work(t2));
t1.start();
t2.start();
t3.start();

}
static class Work implements Runnable {
private Thread beforeThread;
public Work(Thread beforeThread) {
this.beforeThread = beforeThread;
}
public void run() {
if (beforeThread != null) {
try {
beforeThread.join();
System.out.println(“thread start:” + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(“thread start:” + Thread.currentThread().getName());
}
}
}
2、使用countDownLatch(闭锁)
是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0。另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒。这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条件之后再执行。

public class ThreadTest2 {

// T1、T2、T3三个线程顺序执行
public static void main(String[] args) {
CountDownLatch c0 = new CountDownLatch(0); //计数器为0
CountDownLatch c1 = new CountDownLatch(1); //计数器为1
CountDownLatch c2 = new CountDownLatch(1); //计数器为1

Thread t1 = new Thread(new Work(c0, c1));
//c0为0,t1可以执行。t1的计数器减1

Thread t2 = new Thread(new Work(c1, c2));
//t1的计数器为0时,t2才能执行。t2的计数器c2减1

Thread t3 = new Thread(new Work(c2, c2));
//t2的计数器c2为0时,t3才能执行

t1.start();
t2.start();
t3.start();

}

//定义Work线程类,需要传入开始和结束的CountDownLatch参数
static class Work implements Runnable {
CountDownLatch c1;
CountDownLatch c2;

Work(CountDownLatch c1, CountDownLatch c2) {
    super();
    this.c1 = c1;
    this.c2 = c2;
}

public void run() {
    try {
        c1.await();//前一线程为0才可以执行
        System.out.println("thread start:" + Thread.currentThread().getName());
        c2.countDown();//本线程计数器减少
    } catch (InterruptedException e) {
    }

}

}
3、CachedThreadPool
4、使用blockingQueue
阻塞队列 (BlockingQueue)是Java util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

public class ThreadTest4 {
// T1、T2、T3三个线程顺序执行
public static void main(String[] args) {
//blockingQueue保证顺序
BlockingQueue blockingQueue = new LinkedBlockingQueue();
Thread t1 = new Thread(new Work());
Thread t2 = new Thread(new Work());
Thread t3 = new Thread(new Work());

blockingQueue.add(t1);
blockingQueue.add(t2);
blockingQueue.add(t3);

for (int i=0;i<3;i++) {
    Thread t = null;
    try {
        t = blockingQueue.take();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t.start();
    //检测线程是否还活着
    while (t.isAlive());
}

}

static class Work implements Runnable {

public void run() {
    System.out.println("thread start:" + Thread.currentThread().getName());
}

}
5、使用单个线程池
newSingleThreadExecutor返回以个包含单线程的Executor,将多个任务交给此Exector时,这个线程处理完一个任务后接着处理下一个任务,若该线程出现异常,将会有一个新的线程来替代。
public class ThreadTest5 {

public static void main(String[] args) throws InterruptedException {
final Thread t1 = new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + " run 1");
}
}, “T1”);
final Thread t2 = new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + " run 2");
try {
t1.join(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “T2”);
final Thread t3 = new Thread(new Runnable() {
public void run() {
System.out.println(Thread.currentThread().getName() + " run 3");
try {
t2.join(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, “T3”);

//使用 单个任务的线程池来实现。保证线程的依次执行
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(t1);
executor.submit(t2);
executor.submit(t3);
executor.shutdown();

}
}

线程中submit()和execute方法有什么区别?

两个方法都可以向线程池提交任务,execute方法返回的类型是void,他定义在executor接口中,而submit方法可以返回持有计算结果的Future对象,它定义在ExecutorService接口中,它扩展了Executor接口,其他线程池类像ThreandPoolExecutor和scheduledThreadPoolExecutor
execute() 参数 Runnable ;submit() 参数 (Runnable) 或 (Runnable 和 结果 T) 或 (Callable)
execute() 没有返回值;而 submit() 有返回值
submit() 的返回值 Future 调用get方法时,可以捕获处理异常

sleep、wait、yield方法区别总结

sleep()
方法sleep()的作用是在指定的毫秒数内让当前“正在执行的线程”休眠(暂停执行)。这个“正在执行的线程”是指this.currentThread()返回的线程。
sleep方法有两个重载版本:
sleep(long millis) //参数为毫秒
sleep(long millis,int nanoseconds) //第一参数为毫秒,第二个参数为纳秒
sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。
但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
注意,如果调用了sleep方法,必须捕获InterruptedException异常或者将该异常向上层抛出。当线程睡眠时间满后,不一定会立即得到执行,因为此时可能CPU正在执行其他的任务。所以说调用sleep方法相当于让线程进入阻塞状态。
wait()
wait()是Object类的方法,调用对象的wait()方法导致当前线程放弃对象的锁(线程暂停执行),进入对象的等待池(wait pool),OS会将执行时间分配给其它线程,同样也是进入阻塞态。只有调用对象的notify()方法(或notifyAll()方法)时才能唤醒等待池中的线程进入等锁池(lock pool),如果线程重新获得对象的锁就可以进入就绪状态。

notify():唤醒一个处于等待状态的线程,在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
yield()
调用yield方法会让当前线程交出CPU权限,让CPU去执行其他的线程。它跟sleep方法类似,同样不会释放锁。但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。而且,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。

Start和run的区别

1、start方法用于启动线程,真正实现了多线程运行,这是无需要等待润方法体执行完毕,可以直接继续执行下面的代码。
2、通过调用Thread的start方法启动一个线程,这是线程处于就绪状态,并没有运行
3、Run方法称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行润函数中代码

Java后台线程

定义:被称为守护线程也叫服务线程,他有一个特性就是用户线程提供公共服务,在没有用户线程可以服务时候会自动离开,JVM的垃圾回收线程就是典型的后台线程。
优先级:他的优先级是比较低的,用于系统中的其他对象和线程提供服务。
设置:可以通过setDaemon(True)来设置线程为守护线程,在线程创建对象之前进行设置
生命周期:独立于控制终端并且周期性的执行某种任务或者等待某些发生的事件。不依赖于终端但是依赖于系统。

Java 中用到的线程调度算法是什么?

计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是 指从宏观上看,各个线程轮流获得 CPU 的使用权,分别执行各自的任务.在运行池中,会有多个处于就绪状态的线程在等待 CPU,JAVA 虚拟机的 一项任务就是负责线程的调度,线程调度是指按照特定机制为多个线程分配 CPU 的使用权.
有两种调度模型:分时调度模型和抢占式调度模型。分时调度模型是指让所有的线程轮流获得 cpu 的使用权,并且平均分配每个线程占用的 CPU 的时间片这个也比较好理解。 java 虚拟机采用抢占式调度模型,是指优先让可运行池中优先级高的线程占用CPU,如果可运行池中的 线程优先级相同,那么就随机选择一个线程,使其占用CPU。处于运行状态的线程会一直运行,直至它不得不放弃 CPU。

种类

1)公平锁、非公平锁
公平锁是按照申请顺序来获取锁,非公平锁顺序打乱,后申请也可以先获取锁。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。
2)可重入锁/不可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。
3)独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。
对于Java

ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。
4)互斥锁/读写锁
面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock
5)乐观锁/悲观锁
乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。
悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。
悲观锁在Java中的使用,就是利用各种锁。
乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。
分段锁分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。
6)偏向锁/轻量级锁/重量级锁
这三种锁是指锁的状态,并且是针对Synchronized。在Java5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。自旋锁在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

Synchronized关键字

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证被它修饰的方法或者代码块在任意时刻只能 有一个线程执行。
另外,在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来 实现的,Java 的线程是映射到操作系统的原生线程之上的。 如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的 转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。
庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对 锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销

对象头

一个对象在内存中分为对象头、实例对象、对其填充(补充)。
实例变量是用来存本对象的属性信息,对其填充使该对象保持占用8字节的整数倍。
对象头分为三部分: MarkWord、指向类的指针,数组对象(只有数组对象才有)
MarkWord里面存了对象跟锁有关的信息。用于存储对象自身的运行时数据, 如HashCode, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程ID等等.
主要是通过jvm的monitor对象实现的,通过持有monitor获取锁,释放锁,

优化后的synchronize锁的分类

级别从低到高依次是:
1、无锁状态
2、偏向锁状态
3、轻量级锁状态(自旋锁)
4、重量级锁状态
锁的比较在这里插入图片描述

锁可以升级,但是不能降级,也就是单向的
细节部分:
1)Synchronized的锁是jvm自动升级的
2)轻量级锁别称:自旋锁
3)重点部分:轻量级锁的实现原理是cas(compare and swap)也也可以说是compare and exchange 就是比较和交换

自己是怎么使用synchronize关键字

1、修饰实例方法
2、修饰静态方法
3、修饰代码块

面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!”

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();
                }
            }
        }
        return uniqueInstance;
    }
}

另外,需要注意 uniqueInstance 采用 volatile 关键字修饰也是很有必要。

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

构造方法可以使用 synchronized 关键字修饰么?

先说结论:构造方法不能使用 synchronized 关键字修饰。

构造方法本身就属于线程安全的,不存在同步的构造方法一说。

讲一下 synchronized 关键字的底层原理

synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。
**synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,**该标识指明了该方法是一个同步方法。
不过两者的本质都是对对象监视器 monitor 的获取。

谈谈 synchronized 和 ReentrantLock 的区别

相似点 1)都是加锁方式同步而且都是阻塞式同步,都是可重入锁
区别:
1)这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需 要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/fifinally语句块来完成。
2)相比Synchronized,ReentrantLock类提供了一些高级功能,
主要有以 下3项:
**1.等待可中断,**持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于 Synchronized来说可以避免出现死锁的情况。 通过 lock.lockInterruptibly() 来实现这个机制。
2.可实现公平锁,ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。
3.可实现选择性通知(锁绑定多个条件), synchronized关键字与wait()和notify()/notifyAll()方法相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

锁概念

**无锁:**我们刚实例化一个对象
**偏向锁:**单个线程的时候,会开启偏向锁。可以使用xx:UseBiasedLocking来禁用偏向锁
轻量级锁:当多个线程来竞争的时候,偏向锁会进行一个升级,升级为轻量级锁(内部是自旋锁),因为轻量级锁认为,因为轻量级锁会认为我马上就会拿到锁,所以自旋的时候,等待线程释放锁
**重量级锁:**由于轻量级锁过于乐观,结果迟迟拿不到锁,所以就会不断的自旋,到一次的次数之后,为了避免资源的浪费就会升级为重量级锁
加锁实际上是改变了我们对象头中对象运行时数据(mark word)那8个字节
hashCode的计算也是放在对象头的mark word中,且需要显式调用才能有变化

1、为什么有自旋锁还需要重量级锁?

自旋会不断的小号CPU资源,如果锁的时间长,或者自旋线程多,cpu会被大量消耗,到了重量级锁之后,由于重量级锁有等待队列,拿不到锁的进入等待队列,不需要小号CPU资源

2、偏向锁是否一定比自旋锁效率高?

不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及到锁撤销,这时候直接使用自旋锁,JVM启动过程,会有很多线程竞争,所以默认情况下不打开偏向锁,过一段时间在打开
3、不同锁的不同标志形式

锁实现流程

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,
    ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争, JVM 会将 一部分线程移动到 EntryList 中作为候选竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
    OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM 中,也把这种选择行为称之为“竞争切换”。
  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList 中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify 或者 notifyAll 唤醒,会重新进去 EntryList 中。
  5. 处于 ContentionList、 EntryList、 WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统 来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时, 等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。参考: https://blog.csdn.net/zqz_zqz/article/details/70233767
  7. 每个对象都有个 monitor 对象, 加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
  8. **synchronized 是一个重量级操作,**需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。
  9. Java1.6, synchronized 进行了很多的优化, 有适应自旋、锁消除、锁粗化、轻量级锁及偏向 锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做 了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
  11. JDK 1.6 中默认是开启偏向锁和轻量级锁

Volatile

先了解下什么是java的内存模型,是java虚拟机所定义的一种抽象规范用来屏蔽不同硬件和不同操作系统的内存访问差异
volatile是轻量级的synchronized。因为他不会引起线程上下文的切换和调度。
原理:产生汇编代码之后,在该位置会有一个lock前置指令,相当于内存屏障,该屏障提供三个功能:
1)确保指令重排序时,不会将屏障前的操作排到屏障后,也不会将后面的操作排到屏障前
2)强制对cpu缓存修改立即写入内存
3)如果是写操作哦,让其他cpu对应的缓存无效
适用的场景:
1、轻量级读写锁策略:一线程写多线程读
2、状态标记:动态修改标记的状态对线程接下来的逻辑进行控制
3、单利模式双检查锁机制(DCL单例)
特性:
1)保证可见性
通俗来说,两个线程,线程A和B共享同一变量,volatile保证当前线程A对共享变量的值修改后,线程B也可以的到最新的结果,为了解决数据一致性问题。
本质上是MESI 使用cpu缓存一致性协议

2)不保证原子性
Volition不保证原子性,对于这一点可以使用juc下的atomic类保证原子性
3)禁止指令重排序(CPU)
指令重排序是指:JVM在编译代码的时候,或者cpu在执行jvm字节码的时候,对现有的指令顺序进行重新排序。
为什么会有指令重排序:为了不改变程序执行结果的前提下,优化程序的运行效率。(单线程下的程序执行结果)
通过内存屏障保证指令的有序性
**内存屏障是一种cpu指令。**它使cpu或者编译器对屏障指令之前和对之后发出的内存操作执行一个排序约束。这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。
还解决了long类型和double类型数据的8字节赋值问题
内存屏障分为四种类型:
1)loadload屏障
抽象场景:load1、loadload,load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
2)StoreStore屏障
场景Store1; StoreStore;
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
Store2
3)loadStore屏障
象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕
4)storeload屏障
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的

  1. 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
  2. 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。

volatile和synchronize的区别

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
禁止进行指令重排序。
synchronized 关键字和 volatile 关键字是两个互补的存在,而不是对立的存在!
1.volatile 关键字是线程同步的轻量级实现,所以volatile 性能肯定比synchronized关键字要好。但是volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块
2.volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。
3.volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。
4. volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的

CAS(乐观锁的典型实现机制)

主要的参数:内存值V,旧的预期值A,即将更新的值B
主要是两个步骤:冲突检查和数据更新
Cas加volatile关键词是实现并发包的基石,没有cas就不会有并发包,synchronized是一种独占锁、悲观锁,JUC中借助了cas指令实现了区别于synchronize的一种乐观锁。

什么是乐观锁和悲观锁?

悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以在每次那数据的时候都会上锁,这样当第二个线程想拿这个数据的时候,第二个线程会一直堵塞,知道第一个释放锁,他拿到锁后才可以访问。传统的数据库里面就用到了这种锁机制。Java中的synchronize就是悲观锁。
乐观锁,每次那数据都认为别的线程不会修改这个数据,US航所,只有在更新的时候会判断在此期间别的线程有没有修改过数据,乐观锁适用于都操作很多的场景
1.ABA问题
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且two进行了一些操作变成了B,然后two又将V位置的数据变成A,这时候线程one进行CAS操作发现内存中仍然是A,然后one操作成功。尽管线程one的CAS操作成功,但可能存在潜藏的问题。
怎么解决?
atomic包里提供了一个类AtomicStampedReference(带有时间错的对象引用)来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

AQS(abstractQueuedSynchronizer)

AQS(AbstractQuenedSynchronizer 抽象队列同步器) 是一个用来构建锁和同步器的框架,使用 AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于 AQS的。AQS是一种提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
CLH队列是一个虚拟的双向队列(仅仅存在结点之间的关联关系)。CLG(FIFO)是自旋锁加粗样式

原理概览

AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。
看个 AQS(AbstractQueuedSynchronizer)原理图:

在这里插入图片描述

AQS 对资源的共享方式

AQS 定义两种资源共享方式

Exclusive(独占):只有一个线程能执行,如 ReentrantLock。又可分为公平锁和非公平锁:
公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
Share(共享):多个线程可同时执行,如 CountDownLatch、Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
ReentrantReadWriteLock 可以看成是组合式,因为 ReentrantReadWriteLock 也就是读写锁允许多个线程同时对某一资源进行读。

Atomic原子类

介绍下原子类

Atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 Atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

所以,所谓原子类说简单点就是具有原子/原子操作特征的类。
并发包 java.util.concurrent 的原子类都存放在java.util.concurrent.atomic下,如下图所示。
在这里插入图片描述

JUC包中的原子类是哪4类?

基本类型
1、atomicInteger 整型原子类
2、atomicLong 长整型原子类
3、atomicBoolean 布尔型原子类

数组类型
使用原子方式更新数组里面的某个元素
1、atomicIntegerArray 整形数组原子类
2、atomicLongArray 长整型数组原子类
3、atomicReferenceArray 引用类型数组原子类

引用类型
1、AtomicReference:引用类型原子类
2、AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
3、AtomicMarkableReference :原子更新带有标记位的引用类型
对象的属性修改类型
1、AtomicIntegerFieldUpdater:原子更新整形字段的更新器
2、AtomicLongFieldUpdater:原子更新长整形字段的更新器
3、AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS 的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个 volatile 变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。


版权声明:本文为m0_56413640原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。