1:线程安全性
1.1定义
定义:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些进程将如何交替执行,并且在主调代码中不需要任何额外的同步或协同,这个类都能表现出正确的行为,那么就称这个类是线程安全的。
原子性:提供了互斥访问,同一时刻只能有一个线程来对它进行操作。
可见性:一个线程对主内存的修改可及时的被其他线程观察到。
有序性:一个线程观察其他线程中的指令执行顺序,由于指令重排序的存在,该观察结果一般杂乱无序。
1.2 原子性-Atomic
Atomic运用到 比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
1.2.1 AtomicXXX
比如:AtomicInteger count = new AtomicInteger(); 创建一个Integer类型数据,通过incrementAndGet()方法新增。
Demo:
package com.mmall.concurrency.example.count;
import com.alibaba.fastjson.JSON;
import com.mmall.concurrency.annoations.ThreadSafe;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 线程安全
*
* 比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。
*/
@Slf4j
@ThreadSafe
public class ConcurrencyTestSafe {
/**
* 请求总数
*/
public static int clientTotal = 5000;
/**
* 同时并发执行的线程数
*/
public static int threadTotal = 200;
/**
* Atomic
*/
public static AtomicInteger count = new AtomicInteger();
/**
* Semaphore: 允许并发的数量 [Semaphore 是 synchronized 的加强版,作用是控制线程的并发数量。]
* 1):在进行操作的时候,需要先acquire获取到许可,才可以继续执行任务, 如果获取失败,则进入阻塞;处理完成之后需要release释放许可。
* 2): acquire与release之间的关系:在实现中不包含真正的许可对象,并且Semaphore也不会将许可与线程关联起来,
* 因此在一个线程中获得的许可可以在另一个线程中释放。可以将acquire操作视为是消费一个许可,而release操作是创建一个许可,Semaphore并不受限于它在创建时的初始许可数量。
* 也就是说acquire与release并没有强制的一对一关系,release一次就相当于新增一个许可,许可的数量可能会由于没有与acquire操作一对一而导致超出初始化时设置的许可个数。
*/
/**
* CountDownLatch: CountDownLatch是一种java.util.concurrent包下一个同步工具类,它允许一个或多个线程等待直到在其他线程中一组操作执行完成。
* 1、CountDownLatch countDownLatch = new CountDownLatch(N); //构造对象时候 需要传入参数N
* 2、countDownLatch.await() 能够阻塞线程 直到调用N次end.countDown() 方法才释放线程
* 3、countDownLatch.countDown() 可以在多个线程中调用 计算调用次数是所有线程调用次数的总和
*
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
//1定义一个线程池
ExecutorService executorService = Executors.newCachedThreadPool();
//2定义信号量 运行并发的次数
final Semaphore semaphore = new Semaphore(threadTotal);
//3定义计数器
final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
//4执行 【clientTotal】次循环
for (int i = 0; i < clientTotal; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
add();
semaphore.release();
} catch (Exception e) {
log.error("error msg:{};JONSE:{}", e.getMessage(), JSON.toJSONString(e));
}
});
countDownLatch.countDown();
}
countDownLatch.await();
//关闭线程池
executorService.shutdown();
//此时返回的 count结果确定,此是一个安全的线程
log.info("count {}", count);
}
private static void add() {
//线程安全主要是因为 compareAndSwap (CAS)
//compareAndSwapInt 底层方法,不是通过java实现 【不断循环一直到获取想要(正确)的值为止】
count.incrementAndGet();
// count.getAndIncrement();
}
}
1.2.2 AtomicLong和LongAdder的区别
AtomicLong的原理是依靠底层的cas来保障原子性的更新数据,在要添加或者减少的时候,会使用死循环不断地cas到特定的值,从而达到更新数据的目的。【低并发推荐】
LongAdder在AtomicLong的基础上将单点的更新压力分散到各个节点,在低并发的时候通过对base的直接更新可以很好的保障和AtomicLong的性能基本保持一致,而在高并发的时候通过分散提高了性能。
缺点是LongAdder在统计的时候如果有并发更新,可能导致统计的数据有误差。【高并发推荐】
2:原子性-锁
原子性:提供互斥访问,同一时刻只能有一个线程来对其进行操作
除了Atomic包之外,还有锁
2.1 jdk提供锁
synchornized:
依赖JVM 。synchornized关键字作用对象的作用范围内,都是同一时刻只有一个线程可以进行操作。【比较耗费资源,推荐Lock】
Lock:
依赖特殊的CPU指定,代码实现,有代表性的是:ReentrantLock。
2.2 synchornized
修饰代码块: 大括号括起来的代码,<font style="color:red">作用于调用的对象</font>。 不同对象是互不影响的;
修饰方法: 整个方法, 作用于调用的对象 。 不同对象是互不影响的;
修饰静态方法: 整个静态方法, 作用于所有对象 。
修饰类: 括号括起来的部分, 作用于所有对象 。
代码示例:
修饰静态方法和修饰类:
package com.mmall.concurrency.example.syncTs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class synchornizedExampleStatusTs {
public static void main(String[] args) {
synchornizedExampleStatusTs synchornizedExampleTs=new synchornizedExampleStatusTs();
synchornizedExampleStatusTs synchornizedExampleTs2=new synchornizedExampleStatusTs();
//声明线程池
ExecutorService executorService= Executors.newCachedThreadPool();
// //两个同时进行,而不是第二个等到第一个执行完毕才执行
executorService.execute(()->{
test1(1);
});
executorService.execute(()->{
test2(2);
});
// synchornizedExampleTs.test1(1);
// synchornizedExampleTs2.test1(2);
}
/**
* 修饰一个类
*/
public static void test1(int num) {
synchronized (synchornizedExampleStatusTs.class) {
for (int i = 0; i < 10; i++) {
log.info("num:{} this is test1.i:{}",num, i);
}
}
}
/**
* 修饰一个静态 方法
*/
public static synchronized void test2(int num){
for (int i = 0; i < 10; i++) {
log.info("num :{} this is test2.i:{}",num, i);
}
}
}
修饰代码块和方法:
package com.mmall.concurrency.example.syncTs;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
public class synchornizedExampleTs {
public static void main(String[] args) {
synchornizedExampleTs synchornizedExampleTs=new synchornizedExampleTs();
synchornizedExampleTs synchornizedExampleTs2=new synchornizedExampleTs();
//声明线程池
ExecutorService executorService= Executors.newCachedThreadPool();
// //两个同时进行,而不是第二个等到第一个执行完毕才执行
executorService.execute(()->{
synchornizedExampleTs.test1(1);
});
executorService.execute(()->{
synchornizedExampleTs2.test2(2);
});
// synchornizedExampleTs.test1(1);
// synchornizedExampleTs2.test1(2);
}
/**
* 修饰一个代码块
*/
public void test1(int num) {
synchronized (this) {
for (int i = 0; i < 10; i++) {
log.info("num:{} this is test1.i:{}",num, i);
}
}
}
/**
* 修饰一个方法
*/
public synchronized void test2(int num){
for (int i = 0; i < 10; i++) {
log.info("num :{} this is test2.i:{}",num, i);
}
}
}
2.3原子性-对比
Lock:可中断锁,多样化同步,竞争激烈时候能维持常态。
synchronized: 不可中断锁,适合不竞争激烈,可读性好。
Atomic:竞争激烈时候能维持常态,比Lock性能好,只能同步一个值。
2.4可见性
2.4.1导致共享变量在线程间不可见的原因
线程交叉执行
重排序结合线程交叉执行
共享变量更新后的值没有在工作内存与主存间及时更新
2.4.2可见性- synchronized
JMM 关于 synchronized 的两条规定:
线程解锁前,必须把共享变量的最新值刷新到主内存
线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值(注意,加锁与解锁是同一把锁)
2.4.3可见性- volatile
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些情况下性能要优于synchronized,但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1:对变量的写操作不依赖于当前值(就是不能是自增自减等操作,volatile 不具有原子性,适合作为状态标记量(true/false ....))
2:该变量没有包含在具有其他变量的不变式中
通过加入内存屏障和禁止重排序优化来实现
对 volatile 变量写操作时,会在写操作后加入一条 store 屏障指令,将本地内存中的共享变量值刷新到主内存。
对 volatile 变量读操作时,会在读操作前加入一条 load 屏障指令,从主内存中读取共享变量。
2.4.4有序性
Java 内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 、 synchronized 、 Lock
有序性- happens - before 原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个 unLock 操作先行发生于后面对同一个锁的 lock 操作
volatile 变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作 A 先行发生于操作 B ,而操作 B 又先行发生于操作 C ,则可以得出操作 A 先行发生于操作 C
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join(方法结束)、Thread.isAlive (的返回值手段检测到线程已经终止执行)
对象终结规则:一个对象的初始化完成先行发生于他的 finalize ()方法的开始
2.4.5可重入性
synchronized:同一个线程的外层函数获得锁后,内层函数可以直接获取该锁。
Synchronized可重入性的两个优点:
避免死锁
避免死锁的原因: 如果synchronized不具备可重入性,当一个线程想去访问另一个方法时,它自身已经持有一把锁,而且还没有释放锁,又想获取另一个方法的锁,于是造成了永远等待的僵局,就会造成死锁。有了可重入性后,自己持有一把锁,并且可以直接进入到内层函数中,就避免了死锁。
提升封装性
编程人员不需要手动加锁和解锁,统一由JVM管理,提高了可利用性。
关于synchronized 可重入性参考:Java中Synchronized的可重入性和不可中断性的分析和代码验证_五道口-CSDN博客_synchronized 不可重入