1、线程安全

多个线程对同一个共享变量进行读写操作时可能产生不可预见的结果,这就是线程安全问题。

线程安全的核心点就是共享变量,只有在共享变量的情况下才会有线程安全问题。这里说的共享变量,是指多个线程都能访问的变量,一般包括成员变量和静态变量,方法内定义的局部变量不属于共享变量的范围。

线程安全问题示例:

import lombok.extern.slf4j.Slf4j;

/**
* @Author FengJian
* @Date 2021/1/27 10:59
* @Version 1.0
*/
@Slf4j(topic = "c.ThreadSafeTest")
public class ThreadSafeTest {
static int count = 0; public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1"){
@Override
public void run() {
for (int i = 0;i < 5000;i++){
count++;
}
}
}; Thread t2 = new Thread("t2"){
@Override
public void run() {
for (int i = 0;i < 5000;i++){
count--;
}
}
}; t1.start();
t2.start(); /**
* join方法:使main线程与t1、t2线程同步执行,即t1、t2线程都执行完,main线程才会继续执行(但t1、t2之间依然是并行执行的)
* 主要是为了等待两个线程执行完后,在main线程打印count的值
*/
t1.join();
t2.join(); log.debug("count的值为:{}",count);
}
}

运行上述代码三次的结果如下:

[main] DEBUG c.ThreadSafeTest - count的值为:-904
[main] DEBUG c.ThreadSafeTest - count的值为:-2206
[main] DEBUG c.ThreadSafeTest - count的值为:73

在上述代码中,线程t1中count进行5000次自增操作,而线程t2中count则进行5000次自减操作。在两个线程都运行结束后,按照预期结果,count的值应为0。但由打印结果可知,count的值并不为0,且每次运行的结果都不一样。这就是多线程对共享变量进行操作出现的不可预见的结果,即常说的线程安全问题。

而线程安全,则指的是在多线程环境下,程序可以始终执行正确的行为,符合预期的逻辑。具体到上述代码,就是不论执行多少次,在t1、t2线程执行完毕后,count的值都应该始终符合预期的结果0。上述代码明显是线程不安全的。

2、出现线程安全的原因

线程安全是使用多线程必定会面临的问题,导致线程不安全的主要原因有以下三点:

①原子性:一个或者多个操作在 CPU 执行的过程中被中断

②可见性:一个线程对共享变量的修改,另外一个线程不能立刻看到

③有序性:序执行的顺序没有按照代码的先后顺序执行

2.1、原子性

2.1.1 什么是原子性问题

原子性问题,其实说的是原子性操作。即一个或多个操作,应该是一个不可分的整体,这些操作要么全部执行并且不被打断,要么就都不执行。

以上述代码中的count的自增(count++)和自减(count--)为例。

count++count--看似只有一行代码,但实际上这一行代码在编译后的字节码指令以及在JVM执行的对应操作如下:

count++:

getstatic count  //获取静态变量count的值
iconst_1 //准备常量1
iadd //自增
putstatic count //将修改后的值存入静态变量count

count--:

getstatic count  //获取静态变量count的值
iconst_1 //准备常量1
isub //自减
putstatic count //将修改后的值存入静态变量count

由此可知,count自增或自减的操作,并不是一个原子操作,即中间过程是有可能被打断的。

count自增自减操作需要四个步骤(指令)才能完成,这意味着如果这执行这四个步骤的某一步时,线程发生了上下文切换,那么自增自减操作将被打断暂停。

如果使用单线程来执行自增自减操作,这实际上并无问题:

上图为单线程执行count自增自减的一次过程,可以看出在没有线程上下文切换的情况下,即使自增自减不是原子操作,count的最后结果都会是0。

但在多线程环境下,就会出现问题了:



可以看到由于自增自减不是原子操作,因此在线程t1执行自增过程中,如果进行上下文切换,则将导致线程t1还没来得及把count = 1 写入主存,count的值就被t2线程读取,所以在最后,线程t2自减得出的值-1写入主存后,会被线程t1覆盖,变为1。

这结果明显是不符合我们的预期的,实际上,上述图片展示的只是一种可能的结果。还有可能是t2写入count的步骤是最后执行的,那么最后count的值将为-1。

这就是由于非原子操作带来的多线程访问共享变量出现不符合预期的结果,即由于原子性带来的线程安全问题。

上面示例中两个线程t1、t2分别执行count++和count--出现的问题,就是由于原子性带来的线程安全问题。

2.1.2、原子性问题解决办法

解决办法就是将count++和count--的操作变为原子操作,Java中的实现方法是:

①上锁:使用synchronized

只需要创建一个对象作为锁,并在访问count时用synchronized进行加锁即可。

 static int count = 0;
static Object lock = new Object(); //锁对象 synchronized(lock){
count++;
} synchronized(lock){
count--;
}

上锁后,执行自增自减的示意图如下:



由于锁的存在,则保证了不持有锁的t2线程会被阻塞,直到t1线程执行自增完毕,并释放锁。在这一过程中,虽然依旧存在线程的上下文切换,但是t2线程是无法对共享变量count进行操作的,因此保证了t1线程中count++操作的原子性。

因此使用synchronized锁可以解决原子性带来的线程安全问题。

②、循环CAS操作

其基本思路就是循环进行CAS操作(compare and swap,比较并交换)。即对共享变量进行计算前,线程会先将该共享变量保存一份旧值a,计算完毕后得出结果值b。在将b从线程的本地内存刷新回主内存前,会先比较主内存中的值是否和a一致。如果一致,则将b刷新回主内存。若不一致,则一直循环比较,直到主内存中的值与a一致,才把共享变量的值设为b,操作才结束。

在Java中,使用CAS操作保证原子性的具体实现就是Lock和原子类(AtomicInteger)。它们都是通过使用unsafe的compareAndSwap方法实现CAS操作保证原子性的。

Lock的使用:

static int count = 0;
static Lock lock = new Lock (); //锁对象 lock.lock(); //加锁
count++;
lock.unlock(); //解锁 lock.lock(); //加锁
count--;
lock.unlock(); //解锁

原子类的使用:

static AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet(); //自增

count.decrementAndGet(); //自减

以上都是Java中可以保证原子操作的具体方法,它们各有优缺点,要看具体的场景来选择最佳的使用,以此来解决原子性带来的线程安全问题。

2.2、可见性

2.2.1、什么是可见性问题

可见性实际上指的是内存可见性问题。总的来说就是一个线程对共享变量的修改,另外一个线程不能立刻看到,从而产生的线程安全问题。

在上一篇笔记【JAVA并发第三篇】线程间通信 中的通过共享内存进行通信实际上讲的就是内存可见性问题。这里再从线程安全的角度讲述一遍。

我们知道,CPU要从内存中读取出数据来进行计算,但实际上CPU并不总是直接从内存中读取数据。由于CPU和内存间(常称之为主存)的速度不匹配(CPU的速度比主存快得多),为了有效利用CPU,使用多级cache的机制,如图



上图所示是一个双核心的CPU系统架构,每个核心都有自己的控制器和运算器,也都有自己的一级缓存,还有可能有所有CPU核心共享的二级缓存,每个核心都可以独立运行线程。

因此,CPU读取数据的顺序是:寄存器-高速缓存-主存。主存中的部分数据,会先拷贝一份放到cache中,当CPU计算时,会直接从cache中读取数据,计算完毕后再将计算结果放置到cache中,最后在主存中刷新计算结果。所以每个CPU都会拥有一份拷贝。

以上只是CPU访问内存,进行计算的基本方式。实际上,不同的硬件,访问过程会存在不同程度的差异。比如,不同的计算机,CPU和主存间可能会存在三级缓存、四级缓存、五级缓存等等的情况。

为了屏蔽掉各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果,定义了Java的内存模型(Java Memory Model,JMM)。

JMM 的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到主存和从主存中取出变量这样的底层细节。这里的变量指的是能够被多个线程共享的变量,它包括了实例字段、静态字段和构成数组对象的元素,方法内的局部变量和方法的参数为线程私有,不受JMM的影响。

Java的内存模型如下:



Java内存模型中的本地内存,对应的就是CPU结构图中的cache1或者cache2。它实际上并不真实存在,其包含了缓存、写缓冲区、寄存器以及其他的硬件和编译器的优化。

JMM规定:将所有共享变量放到主内存中,当线程使用变量时,会把其中的变量复制到自己的本地内存,线程读写时操作的是本地内存中的变量副本。一个线程不能访问其他线程的本地内存。

这样的情况下,如果有一个变量i在线程A、B的本地内存中都有一份副本。此时,若线程A想修改i的值,在线程A将修改后的值放入到本地内存,但又未刷新回主内存时,如果线程B读取变量i的值,则读到的是未修改时的值,这就造成了读写共享变量出现不可预期的结果,产生线程安全问题。

有代码如下:

/**
* @Author FengJian
* @Date 2021/2/21 23:47
* @Version 1.0
*/
@Slf4j(topic = "c.ThreadSafeTest")
public class ThreadSafe02 {
private static boolean run = true; public static void main(String[] args) throws InterruptedException {
Thread My_Thread = new Thread(new Runnable() {
@Override
public void run() {
while (run) { }
}
}, "My_Thread");
My_Thread.start(); //启动My_Thread线程
log.debug(Thread.currentThread().getName()+"正在休眠@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run);
Thread.sleep(1000); //主线程休眠1s
run = false; //改变My_Thread线程运行条件
log.debug(Thread.currentThread().getName()+"正在运行@"+new SimpleDateFormat("hh:mm:ss").format(new Date())+"--"+run);
}
}

从运行结果发现,即使在主线程中修改了共享变量run的值,My_Thread线程依然在循环并不会停止:



其原因就是main线程对共享变量run的修改,另外一个线程My_Thread并不能立刻看到:



这就是由于内存可见性带来的多线程访问共享变量出现不符合预期的结果,即由于可见性带来的线程安全问题。

2.2.2、可见性问题解决办法

解决办法就是保证共享变量的可见性,具体实现就是任何对共享变量的访问都要从共享内存(主内存)中获取。在Java中的实现方法是:

①加锁,synchronized和Lock都可以保证

线程在加锁时,会清空本地内存中共享变量的值,共享变量的使用需要从主内存中重新获取。而在释放锁资源时,则必须先把此共享变量同步回主内存中。

由于锁的存在,未持有锁的线程并不能操作共享变量,而当阻塞的线程获得锁时,主内存中共享变量的值已经刷新过了,因此线程修改共享变量对其他线程是可见的。这保证了共享变量的可见性,可以解决内存可见性产生的线程安全问题。

②使用volatile修饰共享变量

当一个变量被声明为volitale时,线程在写入变量时,不会把值缓存本地内存,而是会立即把值刷新回主存,而当要读取该共享变量时,线程则会先清空本地内存中的副本值,从主存中重新获取。这些也都保证了内存的可见性。

优先使用volatile关键字来解决可见性问题,加锁消耗的资源更多。

2.3、有序性

2.3.1、什么是有序性问题

有序性,实际上是指令的重排序问题。

我们知道,CPU的执行速度是比内存要快出很多个数量级的。CPU为了执行效率,会把CPU指令进行重新排序。即我们编写的Java代码并不一定按照顺序一行一行的往下执行,处理器会根据需要重新排序这些指令,称为指令并行重排序。

同时,JIT编译器也会在代码编译的时候对代码进行重新整理,最大限度的去优化代码的执行效率,称为编译器的重排序。

而又由于处理器与主存之间会使用缓存和读/写缓冲机制,因此从主存加载和存储操作也有可能是经过指令重排序的,称为内存系统重排序。

综上所述,在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排序,再加上主内存和处理器的缓存,Java源码经过层层的重排序,最后才得出最终结果。

由图可知,从Java源码到最后的执行指令,会经历3种重排序的优化。若有ava代码如下:

int a = 2; //A
int b = 3; //B
int c = a*b; //C

经过上述3种重排序后,语句A和语句B的执行顺序是可能互换的,并且这种互换并不影响代码的正确性。但是我们发现语句C则不能和A、B互换,否则得出的结果将不正确,因为他们之间存在着数据依赖关系,即语句C的数据依赖A和B得出。

由此,我们可以发现,以上3种指令的重排序并不能随意排序,他们需要遵守一定的规则,以保证程序的正确性。

①as-if-serial语义

as-if-serial语义是指:不管怎么样重排序,单线程程序的执行结果都不能被改变。即不会对存在数据依赖关系的操作进行重排序。

编译器、处理器进行指令重排序优化时都必须遵守as-if-serial语义。即在单线程的情况下,指令重排序只能对不影响处理结果的部分进行重排序。

以上述语句A、B、C为例,存在数据依赖关系的语句C和A或B不能被重排序:



as-if-serial语义把单线程程序保护起来了,遵守该语义的编译器、处理器等使我们编写单线程有一个错觉:单线程程序是按照源代码的顺序来执行的。实际上在由于as-if-serial语义的存在,我们编写单线程时,完全可以认为源代码是按照顺序执行的,因为即使代码被进行了重排序,其结果也不会改变,同时单线程中也无需担心内存可见性问题。

as-if-serial语义的核心思想是:不会对存在数据依赖关系的操作进行重排序。

那么数据依赖类型有哪些呢?如下表所示:

类型 示例 说明
写后读 a = 1; b = a 写一个变量后再读该变量
写后写 a = 1; a = 2 写一个变量后再写该变量
读后写 a = b; b = 2 读一个变量后再写该变量

以上三种依赖关系,一旦重排序两个操作的执行顺序,其结果就会改变,所以依照as-if-serial语义,Java在单线程的情况下不会对这三种依赖关系进行重排序(多线程情况不符合此情况)。

as-if-serial语义是基于数据依赖关系的,但它无法保证多线程环境下,重排序之后程序执行结果的正确性。

有代码如下:

/**
* @Author FengJian
* @Date 2021/2/24 16:44
* @Version 1.0
*/
@Slf4j(topic = "c.HappensBeforeTest")
public class HappensBeforeTest {
static int a = 0;
static boolean finish = false; public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
if(finish){
log.debug("a*a:"+a*a);
}
}
}; Thread t2 = new Thread("t2"){
@Override
public void run() {
a = 2;
finish = true;
}
}; t2.start();
t1.start();
}
}

关于上述代码,我们先忽略内存可见性的问题(即线程t2修改了a和finish,但t1可能看不到的缓存问题)。在此前提下如果成功打印a*a的值,那么结果应该为4。

但实际上a*a打印的结果还可能为0,这是由于指令重排序的存在导致的

在线程t2中,由于a = 2;finish = true;没有数据依赖关系,依照as-if-serial语义,可以对这两条语句进行重排序,因此会出现finish = true;的指令比a = 2;先执行的情况。

如果在先执行finish = true;,而a = 2;没有执行时发生线程上下文切换,轮到线程t1执行,那么t1线程中的if语句条件为真,而a的值依然为初始值0,则a*a的结果为0。

可以看出,即使在假设没有内存可见性问题的前提下,上述代码的结果也是不可预期的,因此上述代码也是线程不安全的,其原因就是重排序破坏了多线程程序的语义。

②happens-before规则

既然是重排序出现问题,那么解决思路就是禁止重排序。但是也要注意不能全部禁用重排序,重排序的目的是为了提升执行效率,如果全部禁用那么Java程序的性能将会很差。所以,应该做到的是部分禁用,Java的内存模型提供了一个可用于多线程环境,也适用于单线程环境的规则:happens-before规则。

happens-before规则的定义如下:A happens-before B,那么操作A的执行结果对操作B是可见的,且操作A的执行顺序排在操作B之前。这里的操作A和操作B可以在同一个线程中,也可以在不同线程中。

注意:执行顺序只是happens-before向开发人员做的保证,实际上在处理器和编译器上执行时并不一定按照操作A排在操作B之前执行。

如果重排序之后,依然可以保证与先A后B的执行结果一样,那么进行重排序也是可以的。也就是说,符合happens-before的操作,只要不改变执行结果,处理器和编译器怎么优化(重排序)都行。

只是我们开发人员可以直接认为操作A的执行顺序排在操作B之前。

happens-before保证操作A的执行结果对B可见,依靠这个原则,可以解决多线程环境下内存可见性和有序性问题。

回到代码:

/**线程t1**/
if(finish){
a*a;
} /**线程t2**/
a = 2;
finish = true;

一共有四个操作a = 2;finish = true;if(finish)a*a;,想要上述代码达到线程安全(即打印都正确输出4),只需要:



即在t2线程计算a*a;if(finish);之前,需要知道t1线程中a = 2;finish = true;(t2线程对t1线程的结果可见)。

要达到这一目的,就需要上图中,①和②所示的happens-before关系。

那要如何达到呢?这就需要了解happens-before的六大具体规则了(两个操作,只需要符合其中任何一条就可以认为是happens-before关系):

  • ①程序顺序规则:一个线程中的每个操作,按照程序顺序,前面的操作 happens-before 于该线程中的任意后续操作。
以上述代码为例:
/**线程t2**/
a = 2; //操作1
finish = true; //操作2 /**线程t1**/
if(finish ); //操作3
a*a; //操作4 操作1 happens-before 操作2
操作3 happens-before 操作4
  • ②监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
synchronized (lock) { //加锁
// x是共享变量,初始值=10
if (x < 12) {
x = 12;
}
} //解锁 若有两个线程A、B,先后执行这段代码。则线程A执行完毕后X = 12并释放锁。而线程B获得锁后,进入代码块,在if中取X值判断是否小于12。 此时 线程A中X=12的操作 happens-before 线程B中取X值判断的操作(即线程B能看到线程A中执行的X=12的结果)
  • ③volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
volatile int x = 10;

/**线程t1**/
x = 11; //操作1 /**线程t2**/
int y = x; //操作2 操作1 happens-before 操作2
  • ④传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。

  • ⑤start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

  • ⑥join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

以上就是happens-before的六大常用规则(全部有八种,但后面两种应该很少用到)

2.3.2、有序性问题解决办法

解决有序性问题,实际上就是要运用以上提到的两种规则,as-if-serial语义解决了单线程程序的有序性问题,而happens-before关系则能解决多线程程序的有序性问题。

再回顾一下原始代码,这是一段存在有序性问题线程不安全的代码,我们要利用happens-before关系解决有序性问题:

public class HappensBeforeTest {
static int a = 0;
static boolean finish = false; public static void main(String[] args) {
Thread t1 = new Thread("t1"){
@Override
public void run() {
if(finish){
log.debug("a*a:"+a*a);
}
}
}; Thread t2 = new Thread("t2"){
@Override
public void run() {
a = 2;
finish = true;
}
}; t2.start();
t1.start();
}
}

提取一下关键的操作,如下嗷:

/**线程t1**/
if(finish){
a*a;
} /**线程t2**/
a = 2;
finish = true;

我们的目标是运用happens-before的六大常用规则达到如下图的happens-before关系,以实现上诉代码的线程安全



解决办法如下:

①、方法一:运用volatile修饰变量

使用到happens-before规则中的程序顺序规则、volatile变量规则和传递性。

首先,按照程序顺序规则,可以知道如下的happens-before关系:

线程t1 线程t2
if(finish) happens-before a*a; a = 2; happens-before finish = true;

这由线程中的代码很容易就能得出。接下来运用volatile变量规则,需要用volatile修饰一个变量,我们选变量finish。即初始化时代码改为为volatile static boolean finish = false;

那么根据volatile变量规则,可知对finish的写要happens-before于对finish的读。

因此给finish加上volatile关键字后,就可以达到如下效果:



volatile关键字不仅可以保证内存可见性问题,同时依照happens-before的volatile变量规则,对于volatile修饰的变量,要保证对该变量写的结果要对读的操作可见,因此volatile禁止对有读写操作的volatile修饰的变量进行重排序。

也就是说,volatile关键字不仅可以解决可见性问题,还可以解决有序性问题。

最后,通过传递性。可知:

可知,图示的三和五,就是我们的目标。到此,我们利用happens-before关系保证了代码的可见性和有序性问题。

虽然分析的过程比较长,但是在原代码中,我们实际上只改动了一行代码。即将static boolean finish = false;改为volatile static boolean finish = false;而已,就可以使我们的代码改变线程安全的。

这就是运用volatile修饰变量来解决线程安全的办法。volatile直接通过禁止相关的重排序来达到有序性的目的。

②、方法二:加锁,synchronized

这个应该比较容易理解,对相关代码加锁后,同一时刻就只有一个线程在执行,也就相当于对相关变量的操作,是保证有序的。

不过synchronized并不像volatile一样禁止指令重排序,实际上synchronized块内部的代码指令依然是可以进行重排序优化的。

3、小结

  1. 多个线程对同一个共享变量进行读写操作时就可能产生不可预见的结果,就是线程安全问题。其重点是多线程对共享变量进行读和写,如果只有读,并不会有线程安全问题。
  2. 线程安全的原因有:①线程切换带来的原子性问题②缓存带来的可见性问题③指令重排序带来的原子性问题。
  3. 线程安全的解决办法:①对于原子性问题,使用锁synchronized和Lock、或者使用原子类(AtomicInteger等)②对于可见性问题:使用锁synchronized和Lock,或者使用volatile关键字③对于有序性问题:使用锁synchronized和Lock,或者使用volatile关键字

点个赞吧彦祖,(◕ᴗ◕✿)

由于能力有限,可能存在错误,感谢并恳请老铁们指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:

[1]方腾飞,魏鹏,程晓明. Java并发编程的艺术[M].机械工业出版社.

[2]霍陆续,薛宾田. Java并发编程之美[M].电子工业出版社.

[3]mg驿站. 多线程篇-线程安全-原子性、可见性、有序性解析.知乎.https://zhuanlan.zhihu.com/p/142929863

[4]JAVA bx.Java并发的原子性、可见性、有序性.知乎.https://zhuanlan.zhihu.com/p/205335197

[5]程序员七哥.happens-before是什么?JMM最最核心的概念,看完你就懂了.知乎.https://zhuanlan.zhihu.com/p/126275344

【JAVA并发第四篇】线程安全的更多相关文章

  1. Java并发(四)线程池使用

    上一篇博文介绍了线程池的实现原理,现在介绍如何使用线程池. 目录 一.创建线程池 二.向线程池提交任务 三.关闭线程池 四.合理配置线程池 五.线程池的监控 线程池创建规范 一.创建线程池 我们可以通 ...

  2. Java 1.ExecutorService四种线程池的例子与说明

    1.new Thread的弊端 执行一个异步任务你还只是如下new Thread吗? new Thread(new Runnable() { @Override public void run() { ...

  3. Java并发 - (无锁)篇6

    , 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 本文主要介绍了死锁的概念与一些相关的基础类, 摘录自葛一鸣与郭超的 [Java高并发程序设计]. 无锁是一种乐观的策略, 它假设对资源的访问是没 ...

  4. Java并发-同步容器篇

    作者:汤圆 个人博客:javalover.cc 前言 官人们好啊,我是汤圆,今天给大家带来的是<Java并发-同步容器篇>,希望有所帮助,谢谢 文章如果有问题,欢迎大家批评指正,在此谢过啦 ...

  5. Java并发编程系列-(2) 线程的并发工具类

    2.线程的并发工具类 2.1 Fork-Join JDK 7中引入了fork-join框架,专门来解决计算密集型的任务.可以将一个大任务,拆分成若干个小任务,如下图所示: Fork-Join框架利用了 ...

  6. Java 并发编程(四):如何保证对象的线程安全性

    01.前言 先让我吐一句肺腑之言吧,不说出来会憋出内伤的.<Java 并发编程实战>这本书太特么枯燥了,尽管它被奉为并发编程当中的经典之作,但我还是忍不住.因为第四章"对象的组合 ...

  7. Java并发编程 (四) 线程安全性

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.线程安全性-原子性-atomic-1 1.线程安全性 定义: 当某个线程访问某个类时,不管运行时环境 ...

  8. 【JAVA并发第三篇】线程间通信

    线程间的通信 JVM在运行时会将自己管理的内存区域,划分为不同的数据区,称为运行时数据区.每个线程都有自己私有的内存空间,如下图示: Java线程按照自己虚拟机栈中的方法代码一步一步的执行下去,在这一 ...

  9. Java多线程学习(四)---控制线程

    控制线程 摘要: Java的线程支持提供了一些便捷的工具方法,通过这些便捷的工具方法可以很好地控制线程的执行 1. join线程控制,让一个线程等待另一个线程完成的方法 2. 后台线程,又称为守护线程 ...

随机推荐

  1. pytest内核测试平台落地初体验

    测试平台,有人说它鸡肋,有人说它有用,有人说它轮子,众说纷纭,不如从自身出发,考虑是否要做测试平台: 第1阶段,用Python+requests写接口自动化. 第2阶段,选择unitttest或pyt ...

  2. 深入理解nodejs的HTTP处理流程

    目录 简介 使用nodejs创建HTTP服务 解构request 处理Request Body 处理异常 解构response 简介 我们已经知道如何使用nodejs搭建一个HTTP服务,今天我们会详 ...

  3. 浅谈Webpack模块打包工具一

    为什么要使用模块打包工具 1.模块化开发ES Modules存在兼容性问题 打包之后成产阶段编译为ES5 解决兼容性问题 2.模块文件过多 网络请求频繁  开发阶段把散的模块打包成一个模块 解决网络请 ...

  4. Could not transfer artifact org.apache.maven.plugins:maven-clean-plugin:pom:2.5 from 这类问题的解决方法

    最近换了台电脑,所以重新在IEDA上搭建Spring Boot环境,遇到一个问题,网上查了很久,又实践了一通,终于解决,这里把步骤详细记录下来. 问题描述:创建IDEA的Maven项目后,出现 Mav ...

  5. 【poj 2891】Strange Way to Express Integers(数论--拓展欧几里德 求解同余方程组 模版题)

    题意:Elina看一本刘汝佳的书(O_O*),里面介绍了一种奇怪的方法表示一个非负整数 m .也就是有 k 对 ( ai , ri ) 可以这样表示--m%ai=ri.问 m 的最小值. 解法:拓展欧 ...

  6. bnuoj24252 Divide

    Alice and Bob has found a island of treasure in byteland! They find N kinds of treasures on the isla ...

  7. Codeforces Round #547 (Div. 3) F1/2. Same Sum Blocks (Easy/Hard) (贪心,模拟)

    题意:有一长度为\(n\)的数组,求最多的区间和相同的不相交的区间的个数. 题解:我们可以先求一个前缀和,然后第一层循环遍历区间的右端点,第二层循环枚举左端点,用前缀和来\(O(1)\)求出区间和,\ ...

  8. 牛客练习赛63 C.牛牛的揠苗助长

    题意:有一个长度为\(n\)的数组,从第一天开始,第\(i\)天可以使\(i\)位置上的数\(+1\),当\(i=n\)时,下次从\(i=1\)再开始,另外,在每天结束时,你可以使任意一个位置上的数\ ...

  9. BIM轻量化——浏览器展示

    此篇博客仅为记录,记录钻研过程的零碎思路.         之前考虑过很多可能性,对rvt文件转换格式:.obj.JSON..gltf等等.这些可能性前人一般都尝试过,而且也都做出来了东西.     ...

  10. spring再学习之AOP事务

    spring中的事务 spring怎么操作事务的: 事务的转播行为: 事务代码转账操作如下: 接口: public interface AccountDao { //加钱 void addMoney( ...