基础知识
Hotspot线程创建
参考资料:https://blog.csdn.net/sinat_32873711/article/details/106619963
- 调用
start
方法,最终会进入start0
native方法中,进入JVM过程 - 调用
JavaThread
方法,通过pthread创建本地线程,并且传入入口函数,创建完毕后,暂时挂起 - 回到
JavaThread
方法,最后一步Thread::start(native_thread)
,执行传入的入口方法
1.Java线程创建入口
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
通过上面的代码可以得知,start()方法中做了一些线程状态判断等工作,但是真正启动Java线程的地方是调用了start0(),start0()是一个Native方法。start0()是何处实现的呢?我们先来看看Thread.c 中的一段定义:
static JNINativeMethod methods[] = {
{"start0", "()V", (void *)&JVM_StartThread},
{"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
{"isAlive", "()Z", (void *)&JVM_IsThreadAlive},
{"suspend0", "()V", (void *)&JVM_SuspendThread},
{"resume0", "()V", (void *)&JVM_ResumeThread},
{"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority},
{"yield", "()V", (void *)&JVM_Yield},
{"sleep", "(J)V", (void *)&JVM_Sleep},
{"currentThread", "()" THD, (void *)&JVM_CurrentThread},
{"countStackFrames", "()I", (void *)&JVM_CountStackFrames},
{"interrupt0", "()V", (void *)&JVM_Interrupt},
{"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted},
{"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock},
{"getThreads", "()[" THD, (void *)&JVM_GetAllThreads},
{"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads},
{"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName},
};
2.创建本地线程:深入JVM
JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
JVMWrapper("JVM_StartThread");
JavaThread *native_thread = NULL;
bool throw_illegal_thread_state = false;
{
MutexLocker mu(Threads_lock);
// 1:判断Java线程是否已经启动,如果已经启动过,则会抛异常。
if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {
throw_illegal_thread_state = true;
} else {
//未启动开始创建
jlong size =
java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));
size_t sz = size > 0 ? (size_t) size : 0;
native_thread = new JavaThread(&thread_entry, sz);
if (native_thread->osthread() != NULL) {
native_thread->prepare(jthread);
}
}
}
......
Thread::start(native_thread);
JVM_END
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :
Thread()
{
initialize();
_jni_attach_state = _not_attaching_via_jni;
set_entry_point(entry_point);
os::ThreadType thr_type = os::java_thread;
thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :
os::java_thread;
os::create_thread(this, thr_type, stack_sz);
}
最后一句os::create_thread(this, thr_type, stack_sz) 便开始真正的创建Java线程对应的内核线程。
bool os::create_thread(Thread* thread, ThreadType thr_type,
size_t req_stack_size) {
......
pthread_t tid;
int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread);
......
return true;
}
上面这个方法主要就是利用pthread_create()来创建线程。其中第三个参数thread_native_entry便是新起的线程运行的初始地址,其为定义在os_bsd.cpp中的一个方法指针,而第四个参数thread即thread_native_entry的参数,相当于把要运行的方法也一并传入到了系统的pthread中:
static void *thread_native_entry(Thread *thread) {
......
thread->run();
......
return 0;
}
新线程创建后就会从thread_native_entry()开始运行,thread_native_entry()中调用了thread->run():
// thread.cpp
void JavaThread::run() {
......
thread_main_inner();
}
// thread.cpp
void JavaThread::thread_main_inner() {
if (!this->has_pending_exception() &&
!java_lang_Thread::is_stillborn(this->threadObj())) {
{
ResourceMark rm(this);
this->set_native_thread_name(this->get_thread_name());
}
HandleMark hm(this);
this->entry_point()(this, this);
}
DTRACE_THREAD_PROBE(stop, this);
this->exit(false);
delete this;
}
我们重点关注下this->entry_point()(this, this),entry_point()返回的其实就是在 new JavaThread(&thread_entry, sz) 时传入的thread_entry。这里就相当于调用了thread_entry(this,this)。thread_entry定义在jvm.cpp中:
// jvm.cpp
static void thread_entry(JavaThread* thread, TRAPS) {
HandleMark hm(THREAD);
Handle obj(THREAD, thread->threadObj());
JavaValue result(T_VOID);
JavaCalls::call_virtual(&result,
obj,
KlassHandle(THREAD, SystemDictionary::Thread_klass()),
vmSymbols::run_method_name(),
vmSymbols::void_method_signature(),
THREAD);
}
运行
Thread::start(native_thread);
Java多线程与并发
线程与进程的区别
java线程的几种状态:
线程状态中有几个需要注意的点:
- 就绪态与阻塞态:就绪态代表所有的执行条件都满足(比如i/o、锁)只是等着CPU来调度,而阻塞态戴白不具备一些条件,比如锁或打印机等待等。阻塞的方法有以下四种:sleep()\wait()\join()\io等待
- 就绪态就是线程已经可以运行但不一定运行的状态
- 新生态
Thread t = new Thread();
代表已经有了自己的空间,与就绪态的区别就是是否t,start()
- 死亡状态指的是,线程体的代码执行完毕或者中断执行。一旦进入死亡状态,不能再调用 start() .
线程中start与run方法的区别
private static void attack() {
System.out.println("Fight");
System.out.println("Current Thread is : " + Thread.currentThread().getName());
}
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(){
public void run(){
attack();
}
};
System.out.println("current main thread is : " + Thread.currentThread().getName());
t.run()
t.start();
t.join();
t.start();
}
}
t.run()
t.start();
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
private native void start0();
start0
是native方法
- 调用start()方法会创建新的子线程并启动
- run()方法只是Thread的一个普通方法调用,相当于在主线程里面调用了主线程的方法
Thread和Runnable是什么关系
Thread 是一个类Runnable是一个接口
public class Thread implements Runnable {
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
编写Thread
public class MyThread extends Thread {
private String name;
public MyThread(String name){
this.name = name;
}
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
System.out.println("Thread start : " + this.name + ",i= " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyThread mt1 = new MyThread("Thread1");
MyThread mt2 = new MyThread("Thread2");
MyThread mt3 = new MyThread("Thread3");
mt1.start();
mt2.start();
mt3.start();
}
}
编写Runnable逻辑相同
public class MyRunnable implements Runnable {
private String name;
public MyRunnable(String name){
this.name = name;
}
@Override
public void run(){
for(int i = 0 ; i < 10 ; i ++){
System.out.println("Thread start : " + this.name + ",i= " + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) throws InterruptedException {
MyRunnable mr1 = new MyRunnable("Runnable1");
MyRunnable mr2 = new MyRunnable("Runnable2");
MyRunnable mr3 = new MyRunnable("Runnable3");
Thread t1 = new Thread(mr1);
Thread t2 = new Thread(mr2);
Thread t3 = new Thread(mr3);
t1.start();
t2.start();
t3.start();
}
}
执行结果,同样
Java线程安全问题
Synchronized
为什么会存在线程安全问题:
- 存在共享数据(临界资源:代码,数据结构)
- 存在使用这些数据的多线程
锁机制的特性:
- 互斥性:一段时间只有一个线程持有操作,也称为原子性
- 可见性:对于被锁变量的修改必须是其他线程可见的
锁的分类:对象锁(实例锁):
-
锁住实体里的非静态变量:非静态变量是实例自身变量,不会与其他实例共享,所以锁住实体内声明的非静态变量可以实现对象锁。锁住同一个变量的方法块共享同一把锁。
-
锁住 this 对象,this 指的是当前对象实例本身,所以,所有使用 synchronized(this)方式的方法都共享同一把锁,和上边那个类似,只不过更好解释了。
-
直接锁非静态方法,实际上也是锁住了对象
使用对象锁的情况,只有使用同一实例的线程才会受锁的影响,多个实例调用同一方法也不会受影响。
public class SyncThread implements Runnable {
@Override
public void run() {
String threadName = Thread.currentThread().getName();
if (threadName.startsWith("A")) {
async();
} else if (threadName.startsWith("B")) {
syncObjectBlock1();
} else if (threadName.startsWith("C")) {
syncObjectMethod1();
} else if (threadName.startsWith("D")) {
syncClassBlock1();
} else if (threadName.startsWith("E")) {
syncClassMethod1();
}
}
/**
* 异步方法
*/
private void async() {
try {
System.out.println(Thread.currentThread().getName() + "_Async_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_Async_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 方法中有 synchronized(this|object) {} 同步代码块
*/
private void syncObjectBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized 修饰非静态方法
*/
private synchronized void syncObjectMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void syncClassBlock1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
synchronized (SyncThread.class) {
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private synchronized static void syncClassMethod1() {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
try {
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class SyncDemo {
public static void main(String... args) {
SyncThread syncThread = new SyncThread();
Thread A_thread1 = new Thread(syncThread, "A_thread1");
Thread A_thread2 = new Thread(syncThread, "A_thread2");
Thread B_thread1 = new Thread(syncThread, "B_thread1");
Thread B_thread2 = new Thread(syncThread, "B_thread2");
Thread C_thread1 = new Thread(syncThread, "C_thread1");
Thread C_thread2 = new Thread(syncThread, "C_thread2");
Thread D_thread1 = new Thread(syncThread, "D_thread1");
Thread D_thread2 = new Thread(syncThread, "D_thread2");
Thread E_thread1 = new Thread(syncThread, "E_thread1");
Thread E_thread2 = new Thread(syncThread, "E_thread2");
A_thread1.start();
A_thread2.start();
B_thread1.start();
B_thread2.start();
C_thread1.start();
C_thread2.start();
D_thread1.start();
D_thread2.start();
E_thread1.start();
E_thread2.start();
}
}
- A异步执行,拥有相同的对象锁
- B阻塞执行(同步),需要等待同步锁
- C阻塞执行,一个访问同步方法(S修饰的方法)的线程,另一个访问的会被阻塞,同步方法和同步块锁的是同一个对象,this对象(B和C),两个同步方法也公用一个对象
对程序进行修改,对锁进行new,相当于对于不同的对象加锁
执行结果,互不干扰,同一个类不同对象的对象锁,是互不干扰的
锁的分类:类锁:
类锁是加载类上的,而类信息是存在 JVM 方法区的,并且整个 JVM 只有一份,方法区又是所有线程共享的,所以类锁是所有线程共享的。
- 锁住类中的静态变量:因为静态变量和类信息一样也是存在方法区的并且整个 JVM 只有一份,所以加在静态变量上可以达到类锁的目的。
- 直接在静态方法上加 synchronized:因为静态方法同样也是存在方法区的并且整个 JVM 只有一份,所以加在静态方法上可以达到类锁的目的。
- 锁住 xxx.class: 对当前类的 .class 属性加锁,可以实现类锁。
类锁的特点:
- 每个类只有一个class对象,每个类只有一个类锁(对.class对象上锁)
- 同一个对象的情况下,类锁表现得行为和对象锁表现得行为相同,所有的实例共享一个class对象。
换为不同的实例对象
换为不同的对象,可以看出,A完全不受类锁的影响,D,E进程由于使用了类锁(而类锁只能有一个),因此受到了响应的影响。
- 类锁与对象锁互不干扰,因为拿到的就不是一把锁,这个可以理解为,不同对象锁与类锁只能获取一个,本身也是互相排斥的,资源不可能同时被对象锁与类锁锁定。
C与E不互相干扰,类锁和对象锁互不干扰,因为是两个不同的对象锁
对象锁与类锁的联系与区别
- 有线程访问对象的同步代码块时,另外的线程可以访问该对象的非同步代码块
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象的同步代码块的线程会被阻塞
- 若锁住的是同一个对象,一个线程在访问对象的同步方法时,另一个访问对象同步方法的线程会被阻塞
- 若锁住的是同一个对象,一个线程在访问对象的同步代码块时,另一个访问对象同步方法的线程会被阻塞(因为是同一把锁)
- 同一类的不同对象的对象锁互不干扰
- 类锁是一种特殊的对象锁,具有1234的特性,但不具有5的特性
- 类锁与对象锁互不干扰
Synchronized实现原理:对象头与Monitor
对象头的结构
在HS虚拟机中,对象在内存中存储的布局可以分为三个部分:对象头,实例数据,对齐填充
- mark-word:对象标记字段占 4 个字节,用于存储一些列的标记位,比如:哈希
值、轻量级锁的标记位,偏向锁标记位、分代年龄等,后边的三位标记可以区分无锁、偏向锁、轻量锁、重量锁、GC标记。 - Klass Pointer:Class 对象的类型指针,Jdk1.8 默认开启指针压缩后为 4 字节,关闭指针压缩(-XX:-UseCompressedOops)后,长度为 8 字节。其指向的位置是对象对应的 Class 对象(其对应的元数据对象)的内存地址。
- 对象实际数据:包括对象的所有成员变量,大小由各个成员变量决定,比如:byte占 1 个字节 8 比特位、int 占 4 个字节 32 比特位,就是前文说的那些数据
- 对齐:最后这段空间补全并非必须,仅仅为了起到占位符的作用。由于 HotSpot 虚拟机的内存管理系统要求对象起始地址必须是 8 字节的整数倍,所以对象头正好是8 字节的倍数。因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
Monitor:一种同步机制
引言
参考资料: 作者:jqdywolf
链接:https://www.jianshu.com/p/5c4f441bf142
package jvm;
/**
* Created by ljm on 29/1/2018.
*/
public class ClassCompile {
synchronized void test(){}//锁住方法
void test1(){
synchronized (ClassCompile.class){
//锁住代码块
}
}
}
synchronized void test();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Ljvm/ClassCompile;
void test1();
descriptor: ()V
flags:
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class jvm/ClassCompile
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
- 同步方法,JVM使用ACC_SYNCHRONIZED标识来实现。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能。
- 同步代码块,JVM使用monitorenter和monitorexit两个指令实现同步。即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能的。
Monitor实现原理
在 HotSpot 虚拟机中,monitor 是由 C++中 ObjectMonitor 实现。synchronized 的运行机制,就是当 JVM 监测到对象在不同的竞争状况时,会自动切换到适合的锁实现,这种切换就是锁的升级、降级。那么三种不同的 Monitor 实现,也就是常说的三种不同的锁:偏斜锁(Biased Locking)、轻量级锁和重量级锁。当一个 Monitor 被某个线程持有后,它便处于锁定状态。
class的对象头拥有指向Monitor的指针:
src\share\vm\runtime\ObjectMonitor.hpp
ObjectMonitor() {
_header = NULL;
_count = 0; // 记录个数
_waiters = 0,
_recursions = 0; // 线程重入次数
_object = NULL; // 存储 Monitor 对象
_owner = NULL; // 持有当前线程的 owner
_WaitSet = NULL; // 处于 wait 状态的线程,会被加入到 _WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; // 单向列表
FreeNext = NULL ;
_EntryList = NULL ; // 处于等待锁 block 状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
- ObjectMonitor,有两个队列:_WaitSet、_EntryList,用来保存ObjectWaiter 对象列表。
- _owner,获取 Monitor 对象的线程进入 _owner 区时, _count + 1。如果线程调用了 wait() 方法,此时会释放 Monitor 对象, _owner 恢复为空, _count - 1。同时该等待线程进入 _WaitSet 中,等待被唤醒。
- 如图 ,每个 Java 对象头中都包括 Monitor 对象(存储的指针的指向),synchronized 也 就 是 通 过 这 一 种 方 式 获 取 锁 , 也 就 解 释 了 为 什 么synchronized() 括号里放任何对象都能获得锁🔒,相当于每个对象头都可以被monitor所监控,进而被加锁。
- 如下图,enter与exit分别指向同步代码块的开始与离开位置,当monitor的同步计数器count为0时,线程可以直接获得monitor,上锁相当于在一些位置进入,离开代码块。
具体流程如下:
参考资料:https://www.sohu.com/a/273749069_505779/
- 每个对象都与一个monitor相关联。当且仅当拥有所有者时,才被锁定,执行到monitorenter执行的线程,会尝试获得对应的monitor(获得monitor头指针的指向)
在此步骤中,会调用:`hotspot/src/share/vm/interpreter/synchronizer.cpp::slow_enter`
最终调用:`hotspot/src/share/vm/runtime/objectmonitor.cpp::enter`
void ATTR ObjectMonitor::enter(TRAPS) {
Thread * const Self = THREAD ;
void * cur ;
尝试通过CAS操作尝试把monitor的_owner字段设置为当前线程
cur = Atomic::cmpxchgptr (Self, &owner, NULL) ;
获取失败,这个根本没有
if (cur == NULL) {
assert (_recursions == 0 , "invariant") ;
assert (_owner == Self, "invariant") ;
return ;
}
可重入锁的实现机制:
if (cur == Self) {
_recursions ++ ;
return ;
}
获取成功,第一次进入
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ; //_recursions标记为1
_owner = Self ; //设置owner
OwnerIsThread = 1 ;
return ;
}
竞争失败调用的是
ObjectWaiter node(Self) ;
2 Self->_ParkEvent->reset() ;
3 node._prev = (ObjectWaiter *) 0xBAD ;
4 node.TState = ObjectWaiter::TS_CXQ ;
5
6 // Push "Self" onto the front of the _cxq.
7 // Once on cxq/EntryList, Self stays on-queue until it acquires the lock.
8 // Note that spinning tends to reduce the rate at which threads
9 // enqueue and dequeue on EntryList|cxq.
10 ObjectWaiter * nxt ;
11 for (;;) {
12 node._next = nxt = _cxq ;
13 if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;
14
15 // Interference - the CAS failed because _cxq changed. Just retry.
16 // As an optional optimization we retry the lock.
17 if (TryLock (Self) > 0) {
18 assert (_succ != Self , "invariant") ;
19 assert (_owner == Self , "invariant") ;
20 assert (_Responsible != Self , "invariant") ;
21 return ;
22 }
23 }
以上代码块,本线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TSCXQ;
2) 在for循环中,通过CAS把node节点push到cxq列表中,同一时刻可能有多个线程把自己的node节点push到cxq列表中;
3) node节点push到cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒,实现如下。
- 每个对象维护者一个记录被锁次数的计数器,未被锁定时,该计数器为0,线程进入monitor时,把计数器设为1
- 同一个线程再次获得对象锁时,计数器自增
- 其他线程想获得monitor时,阻塞,直到计数器为0.
底层原理:以wait和notify为例
参考资料:https://www.cnblogs.com/karlMa/p/12273990.html
src\share\vm\runtime\ObjectMonitor.cpp
锁升级:无锁、偏向锁、轻量锁、重量锁
- 偏向锁:只有一个线程进入临界区;
- 轻量级锁:多个线程交替进入临界区;
- 重量级锁:多个线程同时进入临界区。
1. 偏向锁
因为经过HotSpot的作者大量的研究发现,大多数时候是不存在锁竞争的,常常是一个线程多次获得同一个锁,因此如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
-
当线程1访问代码块并获取锁对象时,会在锁对象的java对象头和线程1栈帧中记录偏向的锁的threadID,因为偏向锁不会主动释放锁,因此以后线程1再次获取锁的时候,需要比较当前线程的threadID和Java对象头中的threadID是否一致,如果一致(还是线程1获取锁对象),则无需使用CAS来加锁、解锁;
-
如果不一致(相当于有其他线程要访问线程1的偏向锁,如线程2要竞争锁对象,而偏向锁不会主动释放因此还是存储的线程1的threadID),那么需要查看Java对象头中记录的线程1是否存活,如果没有存活,那么锁对象被重置为无锁状态,其它线程(线程2)可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(线程1)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程。
2.轻量锁
从Java SE 1.6开始,为了减少获得锁和释放锁带来的性能消耗,就引入了轻量级锁。轻量级锁在对象内存布局中 MarkWord 锁标志位为 00,它可以由偏向锁对象因存在多个线程访问而升级成轻量级锁,当然,轻量级锁也可能因多个线程同时访问同步代码块升级成重量级锁。
-
在线程执行同步代码块之前,JVM会现在当前线程的栈桢中创建用于存储锁记录的空间,并将锁对象头中的 markWord 信息复制到锁记录中,这个官方称为 Displaced Mard Word。然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针。
-
将锁对象头中之前的的 markWord 信息复制到线程栈帧的锁记录中,这个官方称为 Displaced Mard Word。然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向本次锁记录的指针。如果替换成功,则进入步骤3,失败则进入步骤4。
-
CAS 替换成功说明当前线程已获得该锁,此时在栈桢中锁标志位信息也更新为轻量级锁状态:00。此时的栈桢与锁对象头的状态如图二所示。
-
如果CAS 替换失败则说明当前时间锁对象已被某个线程占有,那么此时当前线程只有通过自旋的方式去获取锁。如果在自旋一定次数后仍为获得锁,那么轻量级锁将会升级成重量级锁。
解锁
- 通过CAS操作尝试把线程中复制的之前的Displaced Mark Word对象替换当前的Mark Word.
- 如果替换成功了,整个同步完成
- 但如果替换失败了,表示当前线程在执行同步代码块期间,有其他线程也在访问,当前锁资源是存在竞争的,那么锁将会膨胀成重量级锁。图三中重量级锁部分也就眼神了锁膨胀的过程。
3. 重量锁
重量级锁就是前文所述的Monitor管程,每一个对象头中都有指向monitor的指针,相当于使用此monitor专门协调一个线程用于对锁进行控制。在monitor中,保存有当前持有锁的线程,以及等待队列等,显然是用于多线程同步访问临界资源的。
总结
锁的内存语义与可重入
-
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。
-
对比锁释放-获取的内存语义与volatile写-读的内存语义可以看出:锁释放与volatile写有相同的内存语义:锁获取与volatile读有相同的内存语义。
-
线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息。
-
线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息。
-
线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。
-
重入:
当线程再次请求自己所持有锁的临界对象时,叫做重入,如果不支持重入,则会造成死锁,可重入锁也支持在父子类继承的环境中。在子类中,调用父类的synchronized方法,锁住的是子类的对象,而不是父类的对象。
应用举例:在锁的方法中调用另一加锁方法
public synchronized void get() {
System.out.println(Thread.currentThread().getName());
set();
}
public synchronized void set() {
System.out.println(Thread.currentThread().getName());
}
public void run() {
get();
}
public static void main(String[] args) {
ReentrantTest rt = new ReentrantTest();
for(;;){
new Thread(rt).start();
}
}}
自旋锁与自适应自旋锁
自旋锁
锁定状态只会持续很短的时间,为了这段时间放弃CPU资源不值得,从而让进程不断地自旋,等待锁而不放弃CPU资源,用户可通过PreBlockSpin更改
自适应自旋锁
锁消除
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
//StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
//因此sb属于不可能共享的资源,JVM会自动消除内部的锁
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
}
sb是本地使用的,因此不需要在外部使用,因此Jvm会对其自动消除
锁粗化
public static void main(String[] args) {
StringBufferWithoutSync withoutSync = new StringBufferWithoutSync();
for (int i = 0; i < 1000; i++) {
withoutSync.add("aaa", "bbb");
}
}
对于一连串的append操作,只需要加一次锁
Synchronized和ReentrantLock的区别
什么是AQS
AQS是AbustactQueuedSynchronizer的简称,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态。
AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。
- 位于java.util.concurrent.locks包
- 基于AQS实现
- 实现比synchronized更细粒度的控制
- 调用lock后,必须调用unlock释放
- 可重入,通过AQS计数实现
公平锁
ReentrantLock fairLock=new ReentrantLock(true)
;- 参数为true时,倾向于将锁赋予等待时间最久的线程
- 公平锁:获取锁的顺序按先后调用lock方法的顺序
- 非公平锁:随机抢占
ReentrantLock将锁对象化
- 判断是否有线程,或某个特定线程,在排队等待锁
- 带超时的获取锁的尝试
- 感知有没有成功获取锁
ReentrantLock能否将wait\notify\notifyall对象化
使用ReentrantLock可以替代内置锁,当使用内置锁的时候,我们可以使用wait() notify()和notifyAll()来控制线程之间的协作,那么,当我们使用ReentrantLock的时候,我们怎么来处理线程之间的协作呢?
能 通过ArrayBlockingQueue
,
内置锁的话,就只能有一个等待队列,所有的在某个对象上执行wait()方法的线程都会被加入到该对象的等待队列中去(线程会被挂起),需要其他的线程在同一个对象上调用notify()或者是notifyAll()方法来唤醒等待队列中的线程
而使用Condition的话,可以使用不同的等待队列,只需要使用lock.newCondition()即可定义一个Condition对象,每一个Condition对象上都会有一个等待队列(底层使用AQS),调用某个Condition对象的await()方法,就可以把当前线程加入到这个Condition对象的等待队列上
其他的线程调用同一个Condition对象的sinal()或者是signalAll()方法则会唤醒等待队列上的线程,使其能够继续执行
- 数组实现
- 线程安全:互斥锁
- 有界:数组有界
- 阻塞队列:无锁阻塞等待
构造方法
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
从队列中取消息
public E take() throws InterruptedException {
//只有队列有消息才可以触发取走
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
//队列为空,等待直到有消息才返回
return dequeue();
} finally {
lock.unlock();
}
}
向队列中放入消息
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
使用举例:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
private ReentrantLock lock = new ReentrantLock(false);
private Condition condition = lock.newCondition();
public static void main(String[] args) {
Main main = new Main();
main.test();
}
private void test() {
new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("通知等待线程.");
condition.signalAll();
lock.unlock();
}
}).start();
next();
}
private void next() {
try {
lock.lock();
System.out.println("开始等待...");
condition.await();
lock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("等待结束.");
}
}
ReentrantLock相关源码
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
对sync进行判断
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
JMM内存模型
主内存与工作内存
JMM与JVM内存划分是围绕不同方面展开的:
- JVM内存划分是根据功能将内存划分为各种功能区,是一种高层次的抽象
- JMM内存模式描述的是一种内存规则,围绕原子性、有序性、可见性展开
- 联系:都存在共享区域和私有区域,并且有一定程度的耦合
图来源:https://blog.csdn.net/u013887008/article/details/79681609
JMM:虚拟机规范
从抽象的角度看,JMM定义了线程和主内存之间的抽象关系:
- JMM主内存存储JAVA实例对象,属于数据共享区域,多线程并发操作时会引发线程安全问题。成员变量、static变量、类信息存储在主内存中。
- JMM中的工作内存,是CPU对于线程中数据操作的地方,存储当前方法的本地变量信息,本地变量对其他线程不可见:字节码行号治时期,Native方法信息,不存在线程安全问题。引用存储在工作内存中,实例存储在主内存中
- 主内存共享的方式是线程各拷贝一份数据到工作内存,操作完成后刷新回主内存 (包括实例数据,否则就没必要加锁了)。
java虚拟机规范对JMM内存模型定义了8中交互协议操作:
- lock:作用于主内存的变量,把一个变量标识为一条线程独占状态。
- unlock:作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量可以被其他线程锁定。
- read:作用于主内的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load:作用于工作内存的变量,把read读取操作从主内存中得到的变量值放入工作内存的变量拷贝中。
- use:作用于工作内存的变量,把工作内存中一个变量的值传递给java虚拟机执行引擎,每当虚拟机遇到一个需要使用到变量值的字节码指令时将会执行该操作。
- assign:作用于工作内存变量,把一个从执行引擎 (CPU回返) 接收到的变量的值赋值给工作变量,每当虚拟机遇到一个给变量赋值的字节码时将会执行该操作。
- store:作用于工作内存的变量,把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write:作用于主内存的变量,把store操作从工作内存中得到的变量值放入主内存的变量中。
问题: 处理器对某些变量进行修改,可能只是体现在其内核的缓存里,其他线程并不能及时获知,只有刷新到主内存中,才可以在变量之间共享。
A操作的结果对B可见,则A与B存在happens-before关系
JMM在物理机中的实现
以上的内容为java虚拟机规范中所规定的内存如何实现,不代表真实的虚拟机实现
- java语言的特点就是一次编译,到处运行,而忽略底层的硬件实现,这就代表了不同的机器需要自己设计符合规范的运行方法
- 在虚拟机规范中也直接将寄存器、l1、l2、l3等内容抽象成了本地内存,cpu直接对“本地内存”中的副本进行操作,这个本地内存因机器而异,可以为寄存器、l1、l2、l3等。
举例简析
- 线程1首先load主存中的
initflag
变量,变量值为false,将此加载到工作内存中,生成本地副本 - 线程1执行相关代码,陷入while循环
- 线程2读取共享变量
initflag
,并且对其进行修改,置为true - 线程2通过assign将变量放到工作内存之中
以上几步,为多线程变量读取的通行方法,而volatile关键字的作用体现在以下几步,首先说没有关键字的情况:
- 线程2通过
store
指令将工作内存中的变量通过总线刷新到主内存中,主内存中的关键字变为了true - 线程1并不知道变量已经修改,仍然使用本地的副本,在那循环
当使用了volatile关键字时:
- 线程2通过
store
指令通过总线将变量立即刷新到主存中。 - 由于此变量是volatile的,线程1通过总线嗅探技术得知了共享变量已经修改,立刻废除本地副本,从主存中拿最新值,停止循环
缓存一致性协议
可以看出,在上图中,缓存实际上就是JVM定义的工作内存,在冯诺依曼体系下,CPU不能直接对内存进行操控,需要进行取值,写cache,寄存器加,内存回写等步骤,虚拟机规范将此抽象为了工作内存,并且,通过缓存一致性协议来保证并发的可见性一致性,关于一致性协议可以查看博文:https://www.cnblogs.com/ynyhl/p/12119690.html
缓存一致性协议在不同硬件上有不同的实现方式
-
在IA32中使用的MESI协议来实现
-
volatile在底层是通过汇编
lock
前缀指令来实现的, -
会锁定 这块区域的缓存(防止其他的也写,除非unlock) 并回写到内存
-
该写回操作会引起其他CPU里缓存了该内存地址的数据无效,
-
提供基于机器码的内存屏障,防止CPU指令重排,使得lock前后指令不能重排序
-
lock add
由CPU硬件进行实现
内存屏障底层实现:
share/vm/interpreter/bytecodeinterpreter.cpp
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
//判断是不是volatile
if (tos_type == itos) {
//判断类型,并且进行赋值
obj->release_int_field_put(field_offset, STACK_INT(-1));
}
//加入屏障方法
OrederAccess::storeload();
fence方法
根据不同CPU底层加了什么lock前缀指令
happens-before
as-if-serial语义
不管怎么重排(包括编译器和处理器),单线程程序的执行结果不能被改变(多线程不管)。
由于JMM中存在工作内存和主存,存在指令重排序优化,很容易导致多线程环境下的可见性问题,因此jdk1.5之后,对于这种情况设置了h-b原则来保证可见性的问题。
- 在jmm中,如果一个操作的执行结果需要对另一个操作可见,那么这两个操作之间必须存在h-b关系
- 如果 A happens-before B,那么A对内存上的操作对B都是可见的。
- 这些原则实际上是java规范告诉实现虚拟机的人,你造出的虚拟机必须符合这些规范
实际上这些原则就是人为规定了一些有依赖、不能从排序的场景,在这些场景下重排序会造成严重错误
八大原则
-
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作重排序操作不影响一个线程内,只会影响不同的线程。
一段程序的执行,在单个线程中看起来是有序的。程序次序规则看起来是按顺序执行的,因为虚拟机可能会对程序指令进行重排序。虽然进行了重排序但是最终执行的结果是与程序顺序执行的结果是一致的。它只会对不存在数据依赖行的指令进行重排序。该规则是用来保证程序在单线程执行结果的正确性。但是无法保证程序在多线程执行结果的正确性。 -
锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。即无论在单线程还是多线程中,同一个锁如果处于被锁定状态,那么必须先对锁进行释放操作,后面才能继续执行lock操作。
-
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
-
传递规则:如果操作A先行发生于操作B,而操作B先行发生于操作C,则可以得出操作A先行发生于操作C
-
线程启动原则:Thread对象的start()方法先行发生于此线程的每一个动作
-
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
-
线程终结规则:线程中所以的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thhread.isAlive()的返回值手段检测到线程已经终止执行。
-
对象终结规则:一个对象的初始化完成先行于它的finalize(()方法的开始。
举例说明:
转自知乎
作者:海茶依依岛
链接:https://zhuanlan.zhihu.com/p/59039455
-
如果线程 1 解锁了 monitor a,接着线程 2 锁定了 a,那么,线程 1 解锁 a 之前的写操作都对线程 2 可见(线程 1 和线程 2 可以是同一个线程)。
-
如果线程 1 写入了 volatile 变量 v(这里和后续的“变量”都指的是对象的字段、类字段和数组元素),接着线程 2 读取了 v,那么,线程 1 写入 v 及之前的写操作都对线程 2 可见(线程 1 和线程 2 可以是同一个线程)。
-
线程 t1 写入的所有变量(所有 action 都与那个 join 有 hb 关系,当然也包括线程 t1 终止前的最后一个 action 了,最后一个 action 及之前的所有写入操作,所以是所有变量),在任意其它线程 t2 调用 t1.join() 成功返回后,都对 t2 可见。
-
线程中上一个动作及之前的所有写操作在该线程执行下一个动作时对该线程可见(也就是说,同一个线程中前面的所有写操作对后面的操作可见)
指令重排序分类
- CPU指令重排序
- 编译器指令重排序
//优化前
int x = 1;
int y = 2;
int a1 = x * 1;
int b1 = y * 1;
int a2 = x * 2;
int b2 = y * 2;
//优化后
int x = 1;
int y = 2;
int a1 = x * 1;
int a2 = x * 2;
int b1 = y * 1;
int b2 = y * 2;
经过这样的优化,可能对于CPU来说只要读取一次的x和y值。而原来的可能需要反复读取寄存器来交替x和y的值。
作者:地精平方
链接:https://zhuanlan.zhihu.com/p/62249692
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Volatile:JVM提供的轻量级同步机制
Volatile保证变量可见性
把变量使用 volatile 修饰时 public volatile boolean sign = false;线程 01 对变量进行操作时,会把变量变化的值立即强制刷新的到主内存。当线程 02获取值时,会把自己的工作内存里的 sign 值过期掉,之后从主内存中读取。所以添加关键字后程序如预期输出结果。
内存屏障具体实现有以下四种类型:
LoadLoad:保证load1的读操作在load2及后续的读操作之前执行
LoadStore:保证store2及其后的写操作执行前,load1的读操作已经结束
StoreLoad:保证store1的写操作已经刷新到主存之后,load2及其后续的读操作才能执行
StoreStore:在store2及其后的写操作执行前,保证store1的写操作已经刷新到主内存
Volatile为什么不能保证原子性
以i++
的例子为例,进行说明,对于i++操作:
- 先进行load,将变量从主内存读取到工作内存 (而不是使用本地副本)
- 对于i进行i+1操作,在工作内存中修改内存的值
- 使用store将i写回主存,通过jvm给cpu发送lock指令实现
- 使用内存屏障(Memory Lock)
sfence
,让其他线程中的i副本失效,进行线程同步
以上步骤只有第四步是线程安全的,如果前三个步骤发生线程争抢,则会有其他线程进行穿插,load,i+1,store
三步不是原子执行的。
Volatile禁止指令重排序
上回说到了h-b原则,现在我们据此引出指令重排序的相关内容,指令重排序需要满足的条件:
- 单线程环境下不能改变程序运行的结果,单线程中不会对这种相互依赖的情况进行重排序,但是在多线程中则不能保证
- 存在数据依赖关系的不允许重排序
- 总而言之,无法通过happens-before原则推导出来的,才能进行指令的重排序
设置volatile就是为了提供一种人为的、可控的防止重排序机制(其他机制总体上是天然的,难以进行控制)
例2
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; //1
flag = true; //2
}
Public void reader() {
if (flag) { //3
int i = a * a; //4
……
}
}
}
boolean 修改值操作是原子性操作,通过禁止指令重排序操作,即可避免错误
例3
指令重排序引发的问题,多线程引发线程安全问题,使用volatile修饰可以解决这种问题
单例双重检测实现:防止指令重排序优化
public class Single {
private static volatile Single instance = null;
public static Single getInstance(){ //1
if (null == instance){ //2
synchronized (Single .class){ //3
if (null == instance){ //4
instance = new Single (); //5
}
}
}
return instance; //6
}
}
1. 类是否已经加载,没加载就加载类 ; 2. 申请内存; 3 初始化内存(置0,null等等);4. 执行构造方法,初始化对象; 5. 将生成的对象赋给引用
问题就在于这几步指令是可能重排的,比如1 2 3 4 5,变成1 2 3 5 4。
也就是说还没有执行构造方法将这个对象的属性初始化,各个属性都是默认值,就将这个对象赋给了引用instance了。
假设有个线程A发生了上面说的情况,生成对象时执行了1 2 3 5步,这时候有个线程B执行line2发现instance不为null了,于是直接执行line6将还未执行构造方法的对象返回。因此为了万无一失,还是要使用volatile的,防止生成对象时第4步和第5步顺序颠倒。
volatile如何禁止重排优化:
https://www.jianshu.com/p/ef8de88b1343
1、什么是内存屏障
内存屏障其实就是一个CPU指令,在硬件层面上来说可以扥为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。主要有两个作用:
(1)阻止屏障两侧的指令重排序;
(2)强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
内存屏障有三种类型和一种伪类型:
(1)lfence:即读屏障(Load Barrier),在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据,以保证读取的是最新的数据。
(2)sfence:即写屏障(Store Barrier),在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存,以保证写入的数据立刻对其他线程可见。
(3)mfence,即全能屏障,具备ifence和sfence的能力。
(4)Lock前缀:Lock不是一种内存屏障,但是它能完成类似全能型内存屏障的功能。
为什么说Lock是一种伪类型的内存屏障,是因为内存屏障具有happen-before的效果,而Lock在一定程度上保证了先后执行的顺序,因此也叫做伪类型。比如,IO操作的指令,当指令不执行时,就具有了mfence的功能。
V与S的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
- volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化
CAS
悲观锁与乐观锁
- 悲观锁:假定冲突总是发生,尽一切可能屏蔽冲突,例如Synchronized
- 乐观锁:假设不会并发冲突,提交时检查,提交失败,重试,例如CAS
什么是CAS:一种高效实现线程安全性的方法:
- 支持原子更新操作,适用于计数器,序列发生场景
- 属于乐观锁机制,号称lock-free
- CAS操作失败时由开发者决定还是继续尝试,或是执行其他操作
public class CASCase {
public volatile int value;
public synchronized void add() {
value++;
}
}
非原子操作,先取数据,在写回数据
解决方法:AtomicInteger:一种CAS操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
Java线程池
- 降低资源消耗
- 提高线程的可管理性
本线程完成任务后,为防止空闲,从其他线程窃取任务进行
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
线程池基本架构
Executor接口
public interface Executor {
/**
* Executes the given command at some time in the future. The command
* may execute in a new thread, in a pooled thread, or in the calling
* thread, at the discretion of the {@code Executor} implementation.
*
* @param command the runnable task
* @throws RejectedExecutionException if this task cannot be
* accepted for execution
* @throws NullPointerException if command is null
*/
void execute(Runnable command);
}
- Executor接口:运行新任务的简单接口,将任务提交和任务执行细节解耦
Thread t=new Thread();
t.start;
Thread t=new Thread();
executor.execute(t);
- ExecutorService接口:具备管理执行器和任务生命周期的方法,提交任务机制更完善
- ScheduledExecutorService:支持Future和定期任务执行
- 用户提交任务到队列
- 队列接到任务后,可以使用线程池中的Worker对于任务进行处理
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{
/**
* This class will never be serialized, but we provide a
* serialVersionUID to suppress a javac warning.
*/
private static final long serialVersionUID = 6138294804551838833L;
/** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** Per-thread task counter */
volatile long completedTasks;
/**
* Creates with given first task and thread from ThreadFactory.
* @param firstTask the first task (null if none)
*/
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
/** Delegates main run loop to outer runWorker */
public void run() {
runWorker(this);
}
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state.
protected boolean isHeldExclusively() {
return getState() != 0;
}
protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {
}
}
}
}
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
ThreadPoolExecutor构造方法参数的使用规则:
- corePoolSize: 线程池核心线程数
- maximumPoolSize:线程池最大数
- keepAliveTime: 空闲线程存活时间
- unit: 时间单位
- workQueue: 线程池所使用的缓冲队列
- threadFactory:线程池创建线程使用的工厂
- handler: 线程池对拒绝任务的处理策略
线程池的状态与状态转换
常见面试题汇总
并发编程三要素:
- 原子性:操作不能被打断,全部完成或全部不完成
- 可见性:多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立即看到修改的结果。
- 有序性,即程序的执行顺序按照代码的先后顺序来执行。
创建线程的方法:
-
继承Thread类创建线程类
-
通过Runnable接口创建线程类
-
通过Callable和Future创建线程
-
通过线程池创建
三种线程创建方法的对比:
-
采用实现Runnable、Callable接口的方式创建多线程。优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
-
使用继承Thread类的方式创建多线程优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。
-
Runnable和Callable的区别Callable规定(重写)的方法是call(),Runnable规定(重写)的方法是run()。Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。Call方法可以抛出异常,run方法不可以。运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
线程的几种状态:
新建、就绪、运行、阻塞、死亡
几种常见的线程池:
- newCachedThreadPool创建一个可缓存线程池
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
几种线程池的使用场景
https://blog.csdn.net/qq_17045385/article/details/87883101
线程池的优点?
-
重用存在的线程,减少对象创建销毁的开销。
-
可有效的控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞。
-
提供定时执行、定期执行、单线程、并发数控制等功能。
什么是CAS
CAS是compare and swap的缩写,即我们所说的比较交换。
cas是一种基于锁的操作,而且是乐观锁。在java中锁分为乐观锁和悲观锁。悲观锁是将资源锁住,等一个之前获得锁的线程释放锁之后,下一个线程才可以访问。而乐观锁采取了一种宽泛的态度,通过某种方式不加锁来处理资源,比如通过给记录加version来获取数据,性能较悲观锁有很大的提高。
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行
CAS的问题
- CAS容易造成ABA问题:一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference(带有时间戳的对象引用,控制变量值的版本)来解决问题。
- 不能保证代码块的原子性CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
- CAS造成CPU利用率增加之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。通过自旋锁解决
什么是Future?
在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。
FutureTask是什么?
这个其实前面有提到过,FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放入线程池中。
synchronized和ReentrantLock的区别
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
2)ReentrantLock可以获取各种锁的信息
3)ReentrantLock可以灵活地实现多路通知
另外,二者的锁机制其实也是不一样的。ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word,这点我不能确定。
sleep方法和wait方法有什么区别?
这个问题常问,sleep方法和wait方法都可以用来放弃CPU一定的时间,不同点在于如果线程持有某个对象的监视器,sleep方法不会放弃这个对象的监视器,wait方法会放弃这个对象的监视器
首先要说的是单例模式的线程安全意味着:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法,我总结一下:
- 饿汉式单例模式的写法:线程安全
public class SingletonDemo3 {
private static SingletonDemo3 instance = new SingletonDemo3();
private SingletonDemo3(){}
public static SingletonDemo3 getInstance(){
return instance;
}
}
- 懒汉式单例模式的写法:非线程安全
public class SingletonDemo1 {
private static SingletonDemo1 instance;
private SingletonDemo1(){}
public static SingletonDemo1 getInstance(){
if (instance == null) {
instance = new SingletonDemo1();
}
return instance;
}
}
- 双检锁单例模式的写法:线程安全
public class SingletonDemo7 {
private volatile static SingletonDemo7 singletonDemo7;
private SingletonDemo7(){}
public static SingletonDemo7 getSingletonDemo7(){
if (singletonDemo7 == null) {
synchronized (SingletonDemo7.class) {
if (singletonDemo7 == null) {
singletonDemo7 = new SingletonDemo7();
}
}
}
return singletonDemo7;
}
}
ThreadLocal原理
https://www.cnblogs.com/java1024/p/13390538.html
锁升级的过程? (1次)
- 偏向锁: 当一个线程访问同步代码块并获取锁时,会在Mark Word里存储锁偏向的线程ID。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
- 轻量级锁: 当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能。若当前只有一个等待线程,则该线程通过自旋进行等待。针对的是多个线程在不同时间段申请同一把锁的情况
- 重量级锁: 当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁升级为重量级锁。针对的是多个线程同时竞争同一把锁的情况。