工程师必备,扒一扒Java并发!!!

java工程师学习路线(必备知识) Java工程师学习路线 第一个阶段:java基础阶段  java语法 面向对象 (java核心) 常用的api  界面编程 (安卓,APP等开发必备) 多线程 (面试必备) 文件io  java网络编程.(需了解) XML解析 这是学习java开发的基础阶段,也是以后的学习基石,这一部分的学习很重要!   第二个阶段:数据库阶段  oracle  mysql  sql s... 阅读详情

先提几个曾经困扰过我的问题啊,看似很简单,而且可能还有很多同学还存在误解,我们来一起看一下。

  • 问题一:经常听说“主内存“,”工作内存“,那它们到底指什么? 或者说它们以何种形式存在?
  • 问题二:我们还经常听说“可见性”,到底什么是可见性?为什么会出现“不可见”的情况?
  • 问题三:你肯定还听说过“原子性”,那什么是原子性?哪些操作可以认为是原子的?
  • 问题四:”有序性“,代码真的按我们写的先后顺序执行吗?背后有什么玄机?

如果这些问题也曾困扰过你,那这篇文章最合适你不过了,接下来我们一起进入Java的世界扒一扒并发。

 

一、什么是主内存,工作内存?

这2个概念是Java内存模型(Java Memory Model)中提出的,关于内存模型的详细介绍猛戳这里,我们目前只需要知道内存模型是帮我们屏蔽底层硬件细节的,程序员只需要按照它的规则来写代码,写的程序就可以实现跨平台运行,很巧妙的设计。

了解了内存模型,我们回到主题,我们知道JVM将内存划分了以下几大块

  • 堆 (进程内所有线程共享)
  • 方法区 (进程内所有线程共享)
  • 虚拟机栈 (每个线程独立)
  • native本地方法栈 (每个线程独立)
  • pc计数器 (每个线程独立)

那主内存,工作内存跟它们的对应关系是怎么样的呢? 这里直接给出结论。

  • 主内存就是堆 + 方法区
  • 工作内存就是虚拟机栈 + native本地方法栈 + pc计数器

这个知识点看似不起眼,但是却很重要,因为只有有了这个结论,才能与我们后面的实际代码例子结合起来,否则就会感觉理论与实际操作脱节了,没法对应起来。

二、举个例子

需求是这样的,有一个Counter计数器类,内部有一个count成员变量int类型,记录当前的总数,具体定义如下。


 
public class Counter { private int count; public void increment() { this.count++; } public int getCount() { return this.count; } } 

我们现在的任务是调用一亿次increment方法,然后打印count的数量,那么显然正确的输出应该是一亿。

public static void main(String[] args) {

// 单线程代码

Counter counter = new Counter();

int loopCount = 100000000;

long startTime = System.currentTimeMillis();

for (int i = 0; i < loopCount; i++) {

counter.increment();

}

System.out.println("count:" + counter.getCount());

System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms");

}

// 输出结果

count:100000000

take time:577ms

so easy!这样的代码我们太熟悉了,但是这次我想从代码在虚拟机栈中的具体执行过程来加深理解程序是怎么运作的。先通过Javac和Javap命令查看Counter类的increment方法的字节码实现。


 
javac Counter.java javap -verbose Counter.class // Counter类 increment方法的字节码 public void increment(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return 

我们知道Java中方法的调用是基于栈帧实现的,每个栈帧中主要包含操作数栈+用到类的运行时常量池引用+本地变量表。我画了一张示意图帮助大家理解整个执行过程,并且将其中一次count++操作的字节码在操作数栈中的执行步骤分解了(以count=10为例),这里要注意,下面这张图的运行是在工作内存中(main方法所在线程的虚拟机栈中)。

 

  • 过程其实比较简单,我们写的代码在底层就这样运行着,是不是一点点兴奋了。 好,到这里,我要总结方法论了!上面的代码之所以在单线程中运行正确,因为满足了以下三个条件!循环从0到一亿,是严格按顺序执行的(有序性)
  • 循环过程中,前一次对count的修改对后面可见(可见性)
  • 因为是严格按顺序执行的,所以count++操作中间不会交叉执行,所以其实在单线程环境中,可以认为满足原子性 (原子性)

上面的条件只要有一个被打破,执行的结果就可能不正确,这也就是为什么Java多线程环境下容易出现并发问题,原因就是没有同时满足这三个条件。

上面已经提到,我们上面的Count类的实例中,需同时满足 有序性,可见性,原子性,其中有序性和原子性,我们比较容易想到因为多个线程交叉执行,如果不加同步控制,有序性和原子性肯定没法保证,但是这里比较难理解的是可见性,骨头先捡难啃的啃,所以接下来我们先谈谈可见性。

三、什么是可见性?为什么会出现”不可见“?

还是以我们上面的示例来说明。 我们已经知道


 
counter.increment(); 

编译成字节码为

getfield #2

iconst_1

iadd

putfield #2

前面已经说过,这里的字节码的执行过程是在工作内存中,但是getField和putField这二条指令其实是跟主内存有交互的,这里还是以Counter类的increment方法为例。

  • getField指令会从主存中读取count的值,但是并不是每次都从主存中读,因为CPU高速cache的存在,我们count值有可能会从cache中读,导致读的并不是最新的
  • putField指令会将count新的值写入主内存,但是也不是立即生效,别的CPU的高速cache中的count不会立即更新,CPU会使用缓存一致性协议来做同步,这个对我们是透明的。

正是因为CPU高速cache的存在,在多核环境中会有可见性的问题。这里额外提一句 ,之所以有高速cache存在,是为提高运行效率,现代CPU的速度比我们的内存快很多,如果每次都锁总线写主存,会导致执行速度下降很多,这是不可以接受的,木桶理论我们都能理解哈。这里我也画了一张图,来帮助大家理解。

 

那有没有办法解决可见性带来的问题呢?当然是有的,对于Java,我们可以使用volatile关键字。

四、volatile

volatile修饰的变量有下面的特性

  • 在写volatile的时候,有monitor release的语义,会刷新各个cpu中该变量的cache,存入最新的值
  • 在读volatile的时候,有monitor acquire的语义,会使当前cpu的cache中该变量的cache失效,从主存中读取最新的值
  • volatile拥有禁止指令重排序的语义

其中monitor可以理解为锁,moniter release就是释放锁,monitor acquire就是获取锁,这样就是volatile变量的读写都是直接对主存操作的,相当于牺牲一部分性能来换取可见性,这一部分牺牲的性能一般是可以忽略不计的,只需要知道有这么回事就行。

volatile实现原理

给count加上volatile修饰符后,查看编译后的字节码后会发现,字节码层面唯一的变化是给count添加了ACC_VOLATILE标识flag,在运行时会根据这个flag会自动插入内存屏障,保证volatile可见性语义,内存屏障一共有四种,分别是:

  • LoadLoad
  • LoadStore
  • StoreStore
  • StoreLoad

这里有个文档,比较权威详细的说明了内存屏障的知识,这一块知识大家可以自己继续深入。这里给出文档中的一个实例,比较形象的说明了内存屏障是怎么插入的。

 

再回到上面的例子,我们给count添加上volatile修饰符之后,是不是就能在多线程中得到正确的累加结果呢?我们试验一下,简单起见,我们只开2个线程,每个线程分配一半的计算量。


 
// Counter.java private volatile int count; // main 方法 Counter counter = new Counter(); int loopCount = 100000000; int halfCount = loopCount / 2; Thread thread1 = new Thread(() -> { for (int i = 0; i < halfCount; i++) { counter.increment(); } }); Thread thread2 = new Thread(() -> { for (int i = halfCount; i < loopCount; i++) { counter.increment(); } }); thread1.start(); thread2.start(); try { thread1.join(); thread2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("count:" + counter.getCount()); System.out.println("take time:" + (System.currentTimeMillis() - startTime) + "ms"); // 运行结果 count:51743664 take time:2335ms 

结果显然还是不对的,而且程序运行的时间长了好几倍了。这是因为volatile只保证了可见性,却没有原子性语义,比如下面这种情况

 

在T1-T6时间内,初始count=0,经过二次++操作,最后count的值还是1,在我们上面的例子中,5千万次的循环会出现大量类似的错误覆盖写入。根据我们上面分析的volatile的语义,在T5时刻,Thread1对count的修改对Thread2是可见的,这里的可见指的是,如果此时调用getfield指令,拿到的值会是Thread1修改的最新的1,但是遗憾的是,Thread2对此一无所知,只是按着自己的步骤将错误的1写入了count中。那我们不妨设想下,如果在putfield之前,检查下当前栈中存储的count是不是最新的,如果不是最新的重新读取count,然后重试,如果是最新的,直接写入更新值,似乎这样就能解决我们上面出现的错误写入的问题。看起来似乎是一个不错的想法,但是一定要注意,整个检查过程要保证原子性,否则仍然会有并发问题。事实上JDK中Unsafe包里面的CAS方法就是这个思路,不断循环尝试,这个过程就是自旋,它的底层实现依赖cmpxchgl 和 cmpxchgq这二个汇编指令,不同平台的cpu有不同的实现,但是代码大同小异,我在这里以opekjdk8为例扒一扒CAS的源码,源码比较多我只会贴出关键代码块。


 
// Unsafe.class中的三个CAS方法,都是native的 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5); public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5); public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6); 

它对应的native实现在hotspot/src/share/vm/prims/unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))

UnsafeWrapper("Unsafe_CompareAndSwapInt");

oop p = JNIHandles::resolve(obj);

jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);

return (jint)(Atomic::cmpxchg(x, addr, e)) == e;

UNSAFE_END

篇幅关系,这里只贴上compareAndSwapInt的实现,可以看到又调用了Atomic::cmpxchg方法,继续跟进去

unsigned Atomic::cmpxchg(unsigned int exchange_value,

volatile unsigned int* dest, unsigned int compare_value) {

assert(sizeof(unsigned int) == sizeof(jint), "more work to do");

return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,

(jint)compare_value);

}

jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {

assert(sizeof(jbyte) == 1, "assumption.");

uintptr_t dest_addr = (uintptr_t)dest;

uintptr_t offset = dest_addr % sizeof(jint);

volatile jint* dest_int = (volatile jint*)(dest_addr - offset);

jint cur = *dest_int;

jbyte* cur_as_bytes = (jbyte*)(&cur);

jint new_val = cur;

jbyte* new_val_as_bytes = (jbyte*)(&new_val);

new_val_as_bytes[offset] = exchange_value;

while (cur_as_bytes[offset] == compare_value) {

jint res = cmpxchg(new_val, dest_int, cur);

if (res == cur) break;

cur = res;

new_val = cur;

new_val_as_bytes[offset] = exchange_value;

}

return cur_as_bytes[offset];

}

我们跟踪到了调用了cmpxchg这个方法,这个方法不是在atomic.cpp中定义的,查看atomic.hpp,看到了cmpxchg对应的内联函数的定义


 
inline static jint cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value); // See comment above about using jlong atomics on 32-bit platforms inline static jlong cmpxchg (jlong exchange_value, volatile jlong* dest, jlong compare_value); 

这里我们以solaris_x86平台为例,cmpxchg对应的内涵函数定义在hotspot/src/os_cpu/solaris_x86/vm/atomic_solaris_x86.inline.hpp


 
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: " inline jint _Atomic_cmpxchg(jint exchange_value, volatile jint* dest, jint compare_value, int mp) { __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)" : "=a" (exchange_value) : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp) : "cc", "memory"); return exchange_value; } 

这个是内嵌汇编代码,实话说,汇编这一块的知识我也还给老师了。根据LOCK_IF_MP这个宏定义判断是不是多核心,如果是多核心需要加锁,但是这个锁是cpu总线锁,它的代价比我们应用层中用的Lock代价小得多。同时我们看到cmpxchgl这个关键的指令。追到这一层,我想对于应用开发工程师已经足够了。了解了底层实现,我们来现学现卖实战一波。

使用CAS改造我们的加法器Counter,使其是线程安全的

要使用CAS,肯定要使用Unsafe类,我们还是通过反射来获取Unsafe对象,先看UnsafeUtil类的实现

// UnsafeUtil.java

public static Unsafe getUnsafeObject() {

Class clazz = AtomicInteger.class;

try {

Field uFiled = clazz.getDeclaredField("unsafe");

uFiled.setAccessible(true);

return (Unsafe) uFiled.get(null);

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

public static long getVariableOffset(Object target, String variableName) {

Object unsafeObject = getUnsafeObject();

if (unsafeObject != null) {

try {

Method method = unsafeObject.getClass().getDeclaredMethod("objectFieldOffset", Field.class);

method.setAccessible(true);

Field targetFiled = target.getClass().getDeclaredField(variableName);

return (long) method.invoke(unsafeObject, targetFiled);

} catch (Exception e) {

e.printStackTrace();

}

}

return -1;

}

再来看Counter类的实现


 
public class Counter { private volatile int count; private Unsafe mUnsafe; private long countOffset; public Counter() { mUnsafe = UnsafeUtil.getUnsafeObject(); countOffset = UnsafeUtil.getVariableOffset(this, "count"); } public void increment() { int cur = getCount(); while (!mUnsafe.compareAndSwapInt(this, countOffset, cur, cur+1)) { cur = getCount(); } } public int getCount() { return this.count; } } 

再次开启二个线程,执行我们的累加程序

// 输出结果

count:100000000

take time:5781ms

可以看到我们得到正确的累加结果,但是运行时长更长了,但是还好,时间复杂度还是在一个数量级上的。这里要注意一点的是,上述示例代码中,我给count变量增加了volatile关键字,其实就算不加volatile关键字,在这里CAS也是能够正确工作的,但是效率会低一点,我测试下来差不多性能会低5%左右,大家可以思考下为什么不加volatile效率会低?

volatile关键字还有一个禁止指令重排序的语义,一个经典的应用就是DCL单例模式,大家应该都很熟了,就不赘述了。

到这里,关于可见性我们已经讨论的差不多了,接下来我们来讨论”原子性“

五、原子性,怎么保证原子性?

其实上面我们已经提及了一些,比如CAS本身就是原子的,那想一想还有哪些是原子的?我这里还是以Counter类的increment方法为例。


 
0: aload_0 1: dup 2: getfield #2 // Field count:I 5: iconst_1 6: iadd 7: putfield #2 // Field count:I 10: return 

这7条字节码指令,都是原子的,没有问题吧,但是我们如果再往深处想的话,还是会有疑问,单个字节码指令都是原子的吗? 如果单条都不是原子的,我想我们前面的所有判断都是错误的,因为我们得出结论的理论基石被打破了。事实是,单条字节码就是原子的,这个原子性由谁来保证呢?由Java内存模型JMM来保证,程序员不需要知道具体的细节。

虽然单条字节码是原子的,但是多条字节码组合起来就不是原子的了。这也是很多并发问题发生的根源。那我们程序员有哪些手段保证原子性呢?大概有以下三种。

  • CAS + 自旋
  • synchronized关键字
  • concurrent包提供的Lock,具体实现类比如ReentrantLock

CAS我们上面已经讨论过,这里不赘述了,我们来看看synchronized

synchronized

synchronized算是我们最常用的同步方式,主要有以下三种使用方式

// 普通类方法同步

synchronized publid void invoke() {}

// 类静态方法同步

synchronized public static void invoke() {}

// 代码块同步

synchronized(object) {

}

这三种方式不同之处在于我们进行同步的对象不同,普通类synchronized同步的就是对象本身,静态方法同步的是类Class本身,代码块同步的是我们在括号内部填入的对象。本质上它们的原理是相同的,都会有一个monitor的对象与我们要进行同步的对象进行关联,当有一个线程持有了monitor的锁后,其他线程必须等待,一直到该线程释放了该monitor才能被别的线程重新获取,hotspot虚拟机中,它在native层对应的实现类是ObjectMonitor.hpp,这个类内部维护了很多同步相关的变量,我们重点关注二个变量


 
void * volatile _owner; // pointer to owning thread OR BasicLock ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor 

_owner代表当前持有锁的线程,—WaitSet代表等待锁的线程队列,通过ObjectWaiter的数据结构可以推断这是一个双向循环链表。

回到我们的实例,这一次我们通过synchronized来实现我们的Counter类。

public void increment() {

synchronized (Counter.class) {

this.count++;

}

}

// 再运行一次,

// 输出结果

count:100000000

take time:4881ms

输出的结果是正确的,而且从我们打印的运行时间结果来看,synchronized加锁后的执行速度比我们上面的CAS还要快,这有一点点反直觉,其实synchronized在Java1.7后引入了偏向锁,轻量级锁后,synchronized性能有了较大提升,所以在使用synchronized的时候不需要有太多的心理负担,一般情况下,对性能不是极度要求高的话,使用synchronized没有问题。

这里还是贴下increment方法加上synchronized同步后的字节码实现。

 

可以看到有一个monitorenter指令,和2个monitorexit指令,其实是因为编译器自动帮我们添加了try-finally,为了确保monitor一定会被释放,就算出现运行时异常,所以会有二个monitorexit指令。// 我们的实现

synchronized (Counter.class) {

this.count++;

}

// 编译后,相当于下面的伪代码

monitorenter Counter.class;

try {

this.count++;

monitorexit Counter.class;

} finally {

monitorexit Counter.class;

}

除了synchronized关键字之外,还有个比较常用的用来做同步类就是ReentrantLock

ReentrantLock

ReentrantLock的使用大家应该都很熟了,篇幅关系,这里只简单提一下用法,更详细的使用文档大家可以自行查阅相关资料。


 
// 参数表示 是否是公平锁,公平锁严格按照等待顺序获取锁,但是吞吐率低性能差 // 非公平锁性能高,但是有可能会出现锁等待饥饿 ReentrantLock reentrantLock = new ReentrantLock(false); // 一个标准的使用方式 reentrantLock.lock(); try { // do something } finally { reentrantLock.unlock(); } 

这里要注意的是加锁的lock方法的调用,一定要在try-catch-finally的前面,不能在内部,因为如果在内部调用lock,如果代码在lock之前就出现异常了,就会出现我们没有加锁就执行了finally里面的释放锁,肯定会有问题。

为了对比,我们还是使用ReentrantLock实现一遍上面Counter累加。

Lock lock = new ReentrantLock(false);

Thread thread1 = new Thread(() -> {

for (int i = 0; i < halfCount; i++) {

lock.lock();

try {

counter.increment();

} finally {

lock.unlock();

}

}

});

Thread thread2 = new Thread(() -> {

for (int i = halfCount; i < loopCount; i++) {

lock.lock();

try {

counter.increment();

} finally {

lock.unlock();

}

}

});

// 运行程序,输出结果

count:100000000

take time:12754ms

输出结果也是正确的,但是运行时间却是最长的,差不多是我们用synchronized的三倍。当然我们这里并不是搞性能测试,运行的时间也没什么参考意义,贴出来只是让大家有个直观认识,那就是CAS未必就一定性能高,synchronized未必就一定性能差,要具体问题具体分析,一定要有质疑的意识。

既然说到了Lock,我们还是扒一扒它的实现源码,这里以非公平锁为例。


 
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } abstract static class Sync extends AbstractQueuedSynchronizer { //...... public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } } 

可以看到,lock方法的实现,首先会用CAS尝试去设置State为1,如果设置成功,将exclusiveOwnerThread设置为当前thread,如果设置不成功调用acquire方法,又调用了tryAcquire,我们可以重写该方法来自定义锁的逻辑,ReentrantLock中的默认实现如下

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

int c = getState();

if (c == 0) {

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

if (nextc < 0) // overflow

throw new Error("Maximum lock count exceeded");

setState(nextc);

return true;

}

return false;

}

首先尝试获取state状态,如果==0尝试用CAS获取锁,如果!=0,检查是否是当前线程拥有锁,如果是,将state+1,返回true,如果不是返回false。所以这也就是为什么是可重入锁的原因,允许锁的嵌套,如果已经获取了锁,state+1然后返回true。


 
// 可重入锁,允许嵌套重复获得锁,伪代码如下 lock.lock(); lock.lock(); // do something ... lock.unlock(); lock.unlock(); 

总结一下,ReentrantLock的实现是基于队列同步器AbstractQueuedSynchronizer(AQS)的,而AQS内部也是封装了CAS来实现的,深究下去还是有很多内容的,可以说整个concurrent包都是建立在CAS+AQS这二块基石上的,篇幅关系,更多的实现细节我们这里就不讨论了,大家可以自行参考相关的源码分析文章加深理解。

到这里,我们讨论了Java中的”原子性“,以及如果保证”原子性“的三种常规手段,原子性的讨论就结束了。接下来我们进入最后一个问题,Java的”有序性“。

六、有序性

为了方便说明,我们首先举一个简单的例子


 
int a = 1; ① int b = 2; ② int c = a; ③ 

按我们的预期,执行的顺序应该是①②③,但是 真实的执行顺序可能是②①③或者①③②,这是因为指令重排序的原因,基本有二种重排序

  • CPU级的指令重排序
  • 编译器级的指令重排序

重排序的目的是优化我们的程序运行速度,但是优化的前提是不能破坏as-if-serial语义,简单来说,以上面的例子为例,③由于有依赖①的结果,所以它需要永远排在①后面执行。 在单线程有序性还比较容易保证,但是在多线程情况就会变得复杂起来。所以JMM中抽象出了一个happen-before原则,这个原则是JMM给我们开发者的承诺,让我们写代码时对多线程情况下的有序性有一个正确的预期。这个原则有下面5条。

  • 同一个线程中,程序中前面的代码happen-before后面的代码
  • 对一个monitor的解锁happen-before对这个monitor的加锁
  • 对一个volatile变量的写happen-before对这个volatile变量的读
  • 线程start方法调用happen-before线程内的所有action
  • 在A线程调用了B线程的join,则B线程内的操作happen-before于A线程后续的操作

当然happen-before具有传递性,如果A happen-before B, B happen-before C,则A 也 happen-before C。 需要注意的是,happen-before并不完全等同于时间意义上的先执行,比如上面的例子中,根据第一条happen-before原则,int a = 1; 这条语句 happen-before int b = 2; 这条语句,但是由于二者之间没有依赖关系,可以指令重排,所以可以是 int b = 2;先执行,这是合法的,并不违背happen-before原则。

理解这几条happen-before原则后,很多我们平时经常写的并发代码就有了理论依据,比如第二条,加锁happen-before解锁,所以保证了锁的同步范围内的代码,具有原子性和有序性,同时加锁和解锁都会插入内存屏障,可见性也得到保障,所以加锁后的代码是线程安全的。再比如第三条,volatile的写happen-before于volatile的读,有了这一条,多线程之间volatile修饰的共享变量的可见性得到保证。

另外几条原则比较好理解,大家可以自行结合实际代码加深理解,这里就不赘述了。

七、总结

Java并发算是一个比较高级的主题,但是这一块的知识又是高级工程师必须掌握的,骨头再难啃也得啃,希望本文的一些总结能帮助到希望深入了解Java并发的同学,哪怕是其中能有一点,能让你在阅读中有豁然开朗的感觉,我的目的就达到了。

最后,来针鸡血,”怕什么真理无穷,进一寸有一寸的欢喜“。

Java工程师面试简历必备知识点大全 简介:Java作为企业级应用开发的主流语言,要求工程师在简历中充分展示其基础知识、框架和库的掌握、Web开发能力、数据库管理技术、并发编程技能、测试与调试方法、设计模式的应用、项目经验、版本控制技能、技术文档编写能力、持续学习以及团队协作精神等。掌握这些关键点有助于工程师在不同级别的职位面试中脱颖而出,推动职业发展。 阅读详情

相关推荐

实现new, getfield, putfield指令

NULL 博文链接:https://simpleman7210.iteye.com/blog/1951523

java字节码文件的特点_java字节码(class)文件深度解析

1.Class文件基础(1)文件格式Class文件的结构不像XML等描述语言那样松散自由。由于它没有任何分隔符号,所以,以上数据项无论是顺序还是数量都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。(2)数据类型仔细观察上面的Class文件格式,可以看出Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数就是...

weixin_39951396的博客 463

小智AI MCP协议实战:从语音指令到设备响应的完整链路拆解

本文深入拆解了小智AI MCP协议在智能语音交互中的完整工作流程。通过一个“调节音量”的真实场景,详细阐述了从语音采集、云端AI意图理解与决策,到MCP协议下的工具调用、设备端执行,最终完成语音反馈的每一个技术环节。文章重点分析了MCP协议如何通过标准化的交互模型,解决AI意图与设备控制间的“最后一公里”通信难题,实现高效可靠的设备响应。

defi6farmer的博客 249

Java代码到字节码——第一部分

原文地址作者:James Bloom 译者:张坤 理解在Java虚拟机中Java代码如何别被编译成字节码并执行是非常重要的,因为这可以帮助你理解你的程序在运行时发生了什么。这种理解不仅能确保你对语言特性有逻辑上的认识而且做具体的讨论时可以理解在语言特性上的妥协和副作用。 这篇文章讲解了在Java虚拟机上Java代码是如何编译成字节码并执行的。想了解JV...

weixin_33755847的博客 242

高速窄脉冲峰值保持设计

一、目的 在激光探测领域,探测器发出的高速窄脉冲,单片机AD进行采样,很难捕捉到窄脉冲的幅值,即使使用高速AD进行采样,也存在一定的难度。因此,本方案设计了一种高速窄脉冲峰值保持电路,有助于单片机AD采样能很好的捕捉到脉冲幅值。 二、方案分析与比较 峰值保持器一般分为两种:电压型和跨导型。 方案一:电压型峰值保持器,由电压型运放、二极管、保持电容、缓冲器组成,优点是元件成本低,缺点是峰值保持存在过...

weixin_43184208的博客 9298

PLC与变频器联动控制:从开关量、模拟量到通讯的接线与编程实战

在工业自动化系统中,电机调速控制是核心需求之一,其关键在于实现可编程逻辑控制器(PLC)与变频器之间的可靠联动。其基本原理是通过PLC输出控制信号,由变频器接收并驱动电机按设定速度运行。这项技术的核心价值在于实现电机的软启停、精确调速与节能控制,广泛应用于风机、水泵、传送带及精密张力控制等场景。其中,开关量控制以其接线简单、抗干扰能力强的特点,成为基础且可靠的方案;而模拟量控制则能实现无级平滑调速,满足更高精度的工艺要求。本文以西门子、三菱等主流品牌为例,深入解析了这两种主流控制方式的硬件接线、参数设置与梯

1271

JVM 学习笔记二十、字节码指令集与解析举例

二十、字节码指令集与解析举例 1、概述 Java字节码对于虚拟机,就好像汇编语言对于计算机,属于基本执行指令。 Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于 Java虚拟机采用面向操作数栈而不是寄存器的结构,所以大多数的指令都不包含操作数,只有一个操作码。 由于限制了Java虚拟机操作码的长度为一个字节(即0~255),这意味着指令集的操作码总数不可能超过256条。 熟悉.

小宇哥x的博客 393

Canoe CAPL TCP通信避坑指南:从OnTcpConnect回调不触发到Socket句柄管理

本文详细解析了Canoe CAPL中TCP通信的常见问题与解决方案,包括OnTcpConnect回调不触发的三大原因、Socket句柄管理的内存泄漏风险、数据发送截断规则等。通过实战案例和代码示例,帮助开发者规避TCP通信中的典型陷阱,提升车载网络测试的稳定性和可靠性。

weixin_26976635的博客 359

JVM指令详解(下)

九、自增减指令 该指令用于对本地(局部)变量进行自增减操作。该指令第一参数为本地变量的编号,第二个参数为自增减的数量。 比如对于代码:                 int d=10; d++; d+=2; d--; 其指令为:    2:  bipush 10    4:  istore_2//在我的程序中是其所在非静态函数的第2个变量(从0开始计数).

Robin Hu的专栏 9346

【Unity】制作简易三维场景

本文介绍如何制作一个三维场景

CiNian_的博客 5333

网格自适应_Comsol案例解析17:自适应网格的使用

本期导读自适应网格细化有许多优势,比如可以不用对网格进行全局加密,增加计算精度,又会降低求解自由度。本期仍然以气泡上升案例为例,在两相流模块中使用自适应网格。【打开案例模型】【修改网格】适当让气泡边界加密,注意,曲率因子越小,曲线上的网格越密【添加自适应网格】使用默认的参数即可,这里的误差指示器用的是水平集phi。可以根据具体求解模型选择适当的自变量作为误差指示器。【口诀】默认的一般是最...

weixin_28726871的博客 5683

线程安全之原子性

一:非原子性的原因 先举个栗子: public class ThreadCount { volatile int a = 0; public void add() { a++; } } public static void main(String[] args) throws InterruptedException { Th...

狐言 398

从OSGB到3DTiles:倾斜摄影数据高效转换实战指南

本文提供了将倾斜摄影OSGB数据高效转换为Web友好的3DTiles格式的完整实战指南。详细介绍了使用开源工具进行转换的步骤,包括环境搭建、核心命令、参数调优及在Cesium中加载与问题排查,旨在帮助GIS开发者和三维应用工程师打通数据到Web展示的最后一公里,实现大规模三维模型的流畅网络发布。

u0v1w2x3的博客 571

2021Java开发工程师必备知识,Java后端学习主流知识学习系列(一)(建议先收藏)

Java基础+操作系统+计算机网络+数据结构+算法+数据库+热门框架技术+分布式架构+服务器中间件+前沿技术分享

菜菜bu菜的博客 1万+

YOLOv8改进:在C2F模块中引入RepGhost Bottleneck模块

C2F类是YOLOv8中的C2F模块的实现,它包含一个RepGhostBottleneck模块、一个卷积层和一个批归一化层。通过在C2F模块中引入RepGhost Bottleneck模块,我们可以在保持计算效率的同时提升模型的表达能力。这是因为RepGhost Bottleneck模块通过引入重复的卷积核来扩展模型的感受野,从而增加了模型对不同尺度目标的感知能力。RepGhost Bottleneck是一种用于卷积神经网络的改进模块,它可以在保持计算效率的同时提升模型的表达能力。希望以上内容对你有帮助!

IlgCrystal的博客 1307

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

Java高并发三部曲

5G、IOT、多屏合一、万物互联时代来了,分布式、高并发、微服务架构已经成为Java后端应用的主流架构,但是对于Java高并发、SpringCloud RPC底层原理、Nginx底层原理等核心知识,广大的Java开发同学们相对欠缺,而且市面上这方面的书也较少。 有着17载开发经验、10年架构经验的架构师尼恩,独创了自己的写作方式——“尼恩笔法”,从设计模式和基础知识入手,抽丝剥茧,将高深莫测、复杂难懂的Java高并发核心知识介绍得娓娓道来、浅显易懂,呕心沥血、历时三年,编写了Java高并发三部曲: 尼恩

架构师尼恩 5293

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

JAVA工程师必备技能——JAVA基础

持续更新积累。

小小博客 1010

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Java工程师必备

Java工程师必备 JAVA基础扎实,熟悉JVM,熟悉网络、多线程、分布式编程及性能调优 精通Java EE相关技术 熟练运用Spring/SpringBoot/MyBatis等基础框架 熟悉分布式系统相关技术 熟悉MySQL及SQL优化 高并发、高可用、微服务、容器化等开发经验 熟悉JVM原理,精通io、多线程,精通分布式、缓存、消息等机制 熟练使用和配置Tomcat应用服务器 掌握Ec...

程序员哆啦A梦,蓝胖子 449

Java高级开发工程师必备能力

十年Java开发的面试之路

u013430575的博客 1045

Java工程师必备什么技能 Spring框架汇总有哪些

Java工程师必备什么技能?Spring框架汇总有哪些?作为历史悠久的编程语言,Java拥有广阔的应用市场和市场需求,Java工程师薪资也一直处于业内较高水平。越来越多的人加入到Java开发行列,无形中抬高了入职门槛,“低端饱和、高端稀缺”的市场现状让追逐高薪的人进一步钻研学习深度知识,下面就给大家讲解一下高薪Java工程师必备技能——Spring框架知识汇总。 一、什么是...

qq_43444478的博客 665

Java高级工程师必备技术栈

1 Java基础知识 包括异常处理、反射、注解、序列化等。 2 Java集合 数据集合框架以及继承关系。 3 JVM原理 包括JVM的架构、内存管理、垃圾回收、字节码、类加载机制等。 学会这本神书 《深入理解Java虚拟机》 你就可以入门JVM并且应付大部分人了。 4 Java多线程高并发 第一层学会Java线程基础知识以及常见的锁的用法 第二层去理解锁机制,线程的底层原理。 建议阅读 《Java并发编程的艺术》,阿里大牛的神作。 5 设计模式 23种设计模式,这个只需要你能够看出是哪种设计模式就好。如果要

Mr_Gu的博客 5558

《Netty Zookeeper Redis 高并发实战》图书目录

《Netty Zookeeper Redis 高并发实战》目录 高并发研习社群 : 疯狂创客圈 【博客园 总入口 】 简介 机械工业出版社出版,尼恩编著的《Netty Zookeeper Redis 高并发实战》一书, 从操作系统底层的IO原理入手,同时提供高性能开发的实战案例,是一本高并发Java编程应用基础图书。 本书共分为12章。 第1~5章为高并发基础,浅显易懂地剖析高并发IO的...

架构师尼恩 6402

java软件工程师的必备技能

java软件工程师的必备技能,加油

技术成长之路 948

Java中如何限制方法访问的并发数(方法中直接限制并发)

原文链接:https://blog.youkuaiyun.com/manzhizhen/article/details/81413014 并发编程一直是Java基础中的高地,但很多只要有两三年Java基础的工程师,会在简历中很明确的标明“熟悉多线程编程、有高并发编程经验”,来凸显自己编程能力有多厉害,但实际上可能只是看了《Java Concurrency in Practice》的几个章节而已。其实对很多...

itwxming的博客 3016

java工程师技能_java工程师必备技能

该楼层疑似违规已被系统折叠隐藏此楼查看此楼Java高级工程师必备知识一、JAVA。要想成为JAVA(高级)工程师肯定要学习JAVA。一般的程序员或许只需知道一些JAVA的语法结构就可以应付了。但要成为JAVA(高级)工程师,您要对JAVA做比较深入的研究。您应该多研究一下JDBC、IO包、Util包、Text包、JMS、EJB、RMI、线程。如果可能,希望您对JAVA的所有包都浏览一下,知道大概...

weixin_33087827的博客 476
上一篇: 面试官一般会怎么考察Java工程师?
下一篇: Go 程序是怎样跑起来的?你知道吗?
加班不秃顶
博客等级 码龄7年 1822粉丝 599原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值