一、锁
1.synchronized基础知识
1.1.synchronized简单介绍
synchronized中文意思是同步,也称之为”同步锁“。
synchronized的作用是保证在同一时刻, 被修饰的代码块或方法只会有一个线程执行,以达到保证并发安全的效果。
synchronized是Java中解决并发问题的一种最常用的方法,也是最简单的一种方法。
在JDK1.5之前synchronized是一个重量级锁,相对于j.u.c.Lock,它会显得那么笨重,随着Javs SE 1.6对synchronized进行的各种优化后,synchronized并不会显得那么重了。
synchronized的作用主要有三个:
原子性:确保线程互斥地访问同步代码;
可见性:保证共享变量的修改能够及时可见,其实是通过Java内存模型中的“对一个变量unlock操作之前,必须要同步到主内存中;如果对一个变量进行lock操作,则将会清空工作内存中此变量的值,在执行引擎使用此变量前,需要重新从主内存中load操作或assign操作初始化变量值” 来保证的;
有序性:有效解决重排序问题,即 “一个unlock操作先行发生(happen-before)于后面对同一个锁的lock操作”;
1.2.synchronized的使用
synchronized的3种使用方式:
修饰实例方法:作用于当前实例加锁
修饰静态方法:作用于当前类对象加锁
修饰代码块:指定加锁对象,对给定对象加锁
———————————————— 版权声明:本文为CSDN博主「codedot」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/m0_53474063/article/details/112389756
1.3.synchronized 方式的问题
1.同步代码块的阻塞 无法中断 (interruptibly)
2.同步块的阻塞 无法控制超时 (不能自动解锁)
3.同步块 无法异步处理锁(不能立即知道是否可以拿到锁)
4.同步块 无法根据条件灵活的加锁解锁 (只能跟同步块范围一致)
1.4.synchronized的四种锁状态:
级别由低到高依次为:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。 这几个状态会随着竞争情况逐渐升级。
无锁:
无锁是指没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁:
-偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,
即提高性能。
-即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。
如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。
-偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
轻量级锁:
轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,
线程不会阻塞,从而提高性能。
轻量级锁的获取主要由两种情况:① 当关闭偏向锁功能时;② 由于多个线程竞争偏向锁导致偏向锁升级为轻量级锁。
重量级锁:
重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,
操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。
https://www.jianshu.com/p/d61f294ac1a6
2.更自由的锁:Lock
2.1.Lock是什么
Lock 是 java.util.concurrent.locks 包 下的接口,Lock 实现提供了比 synchronized 关键字 更广泛的锁操作,它能以更优雅的方式处理线程同步问题。Lock提供了比synchronized更多的功能。
1.Lock和ReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock。
2.Lock 接口支持那些语义不同(重入、公平等)的锁规则,可以在非阻塞式结构的上下文(包括 hand-over-hand 和锁重排算法)中使用这些规则。
3.ReadWriteLock 接口以类似方式定义了一些读取者可以共享而写入者独占的锁。此包只提供了一个实现 ReentrantReadWriteLock。
4 .Lock是可重入锁,可中断锁,可以实现公平锁和读写锁,写锁为排它锁,读锁为共享锁。ReentrantLock也是一种排他锁
2.2.synchronized 与 Lock 的区别
1.synchronized是关键字,是JVM层面的,而Lock是一个接口,是JDK提供的API。
2.当一个线程获取了synchronized锁,其他线程便只能一直等待直至占有锁的线程释放锁。当发生以下情况之一线程才会释放锁: a.占有锁的线程执行完了该代码,然后释放对锁的占有。 b.占有锁线程执行发生异常,此时JVM会让线程自动释放锁。 c.占有锁线程进入 WAITING 状态从而释放锁,例如在该线程中调用wait()方法等。
但是如果占有锁的线程由于要等待IO或者因为其他原因(比如调用sleep方法)而使线程阻塞了,但是又没有释放锁,那么线程就只能一直等待,那么这时我们可能需要一种可以不让线程无期限的等待下去的方法,比如只等待一定的时间(tryLock(long time, TimeUnit unit)或者能被人为中断lockInterrup0tibly(),这种情况我们需要Lock。
3.当多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作也会发生冲突现象,但是读操作和读操作不会发生冲突现象,但是如果采用synchronized进行同步的话,就会导致当多个线程都只是进行读操作时也只有获取锁的线程才能进行读操作,其他线程只能等待锁释放后才能读,Lock则可以实现当多个线程都只是进行读操作时,线程之间不会发生冲突,例如:ReentrantReadWriteLock()。
4.可以通过Lock得知线程有没有成功获取到锁 (例如:ReentrantLock) ,但这个是synchronized无法办到的。
5.锁属性上的区别:synchronized是不可中断锁和非公平锁,ReentrantLock可以进行中断操作并别可以控制是否是公平锁。
6.synchronized能锁住方法和代码块,而Lock只能锁住代码块。
7.synchronized无法判断锁的状态,而Lock可以知道线程有没有拿到锁。
8.在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时,此时Lock的性能要远远优于synchronized。 ———————————————— 版权声明:本文为CSDN博主「纯洁的小魔鬼」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/xyy1028/article/details/107333451
2.3.Lock接口提供的方法
1、使用方式灵活可控, 2、性能开销小
//支持中断的API
void lockInterruptibly() throws InterruptedException;
//支持超时的API,设置某个时间去获取锁,超过这个时间则不去获取锁
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
//支持非阻塞获取锁的API,就是尝试去获取锁,
boolean tryLock();
//获取锁 ,等价于 synchronized(lock)
void lock();
//释放锁
void unlock();
//绑定条件
Condition newCondition();
2.4.一个Lock锁的简单案例 ReentrantLock
package com.nj.learn.demo.concurrent;
import com.nj.learn.demo.thread.DeamoThreadFactory;
import java.util.concurrent.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.IntStream;
public class LockCounter {
private int sum = 0;
/**
* 可重入锁 + 公平锁
* 什么是可重入锁: 第二次该对象去调用这个同步代码块 不会被阻塞,叫可重入锁
* 什么是公平锁: 排队考前的线程优先获取锁,非公平锁都是同样的机会
*/
private static Lock lock = new ReentrantLock(true);
private int addAndGet(){
return ++sum;
}
public static void main(String[] args) throws InterruptedException {
LockCounter lockCounter = new LockCounter();
int coreSize = Runtime.getRuntime().availableProcessors();
ForkJoinPool forkJoinPool = new ForkJoinPool(coreSize);
forkJoinPool.execute(()->{
run(lockCounter);
});
forkJoinPool.execute(()->{
run(lockCounter);
});
//等待所有任务完成
forkJoinPool.shutdown();
forkJoinPool.awaitTermination(1, TimeUnit.HOURS);
}
public static void run( LockCounter lockCounter){
try{
lock.tryLock(1,TimeUnit.MINUTES);
IntStream.range(0, 1000).forEach(i -> lockCounter.addAndGet());
System.out.println(lockCounter.sum);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static ThreadPoolExecutor initThreadPool(){
int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = Runtime.getRuntime().availableProcessors()*2;
LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue(5000);
ThreadFactory threadFactory = new DeamoThreadFactory();
return new ThreadPoolExecutor(coreSize, maxSize, 100, TimeUnit.SECONDS, linkedBlockingQueue, threadFactory);
}
}
2.5.可重入锁,公平锁的概念区分
- 什么是可重入锁: 第二次该对象去调用这个同步代码块 不会被阻塞,叫可重入锁,一般都用可重入锁
- 什么是公平锁: 排队考前的线程优先获取锁,非公平锁都是同样的机会
2.6.juc.locks.Condition基础条件接口
几个重要的方法:
void await() throws InterruptedException; // 等待信号,类似与Object.wait()
void awaitUninterruptibly(); // 等待信号
boolean await(long time, TimeUnit unit) throws InterruptedException; //等待信号,超时则返回false
boolean awaitUntil(Date deadline) throws InterruptedException; //等待信号,超时则返回false
void signal(); // 唤醒
void signalAll(); //唤醒所有等待线程
2.7.juc.locks.LockSupport -- 锁当前线程,里面有很多静态方法
public static void park(Object blocker); //暂停当前线程
public static void parkNanos(Object blocker, long nanos) //暂停当前线程,不过有个超时时间的限制
public static void park(); //无期限暂停当前线程
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t) //获取锁对象
2.8.如何使用锁,使用锁的原则:
- 降低锁的范围:锁定代码的范围/作用域
- 细分锁粒度: 将一个大锁,拆分多个小锁
二、Atomic 工具类(原子类工具包java.util.concurrent.atomic)
1.一个简单的使用
Atomic工具的作用,在并发当中保持原子一致性。
AtomicInteger a = new AtomicInteger(0);
a.incrementAndGet();//计数+1
a.get();返回当前值
2.LongAdder 对 AtomicLong 的改进
2.1.通过分段思想计数,没有并发,快排
//通过分段计数,没有并发,快排
LongAdder counter = new LongAdder();
counter.increment();
System.out.println("执行结束!共生成对象次数:" + counter.longValue());
2.2.LongAdder 对 AtomicLong的 改进思路:
- AtomicInteger 和AtomicLong 里面的value 是所有线程竞争读写的热点数据
- 将单个value拆分成线程一样多的数组Cell[ ];
- 每个线程写自己的Cell[i]++,最后数组求和。
2.3.类似的多路归并思想:
- 快排
- G1 GC
- ConcurrentHashMap
3.无锁技术 - Atomic 底层原理
(CAS + Volatitle)无锁技术底层是使用Unsafe API - CompareAndSwap,native关键字,意味着实现代码是在jdk这层,在jvm内部调用的CAS指令,本质是用的是CPU硬件指令支持 CAS指令,乐观锁,赋值的时候 会去cas查询值是否已经变了,如果变了,则自旋重新执行计数。通过 volatitle 关键字读到 写之后的值。
- 并发压力不大,可以用CAS+Volatitle 原子计数。性能好
- 并发压力大,则使用锁好一些。
核心原理:
- volatile 保证读写操作都可见(注意不保证原子);
- 使用CAS指令,座位乐观锁实现,通过自旋重试保证写入;
三、AQS - AbstractQueuedSynchronizer并发工具类相关组件
AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础 (如 Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock),是 JUC 并发 包中的核心基础组件,抽象了竞争的资源和线程队列
1.Semaphore -信号量 ,可以设置并发数,和公平锁
1.1.Semaphore常用场景:限流
举个例子:
比如有个停车场,有5个空位,门口有个门卫,手中5把钥匙分别对应5个车位上面的锁,来一辆车,门卫会给司机一把钥匙,然后进去找到对应的车位停下来,出去的时候司机将钥匙归还给门卫。停车场生意比较好,同时来了100辆车,门卫手中只有5把钥匙,同时只能放5辆车进入,其他车只能等待,等有人将钥匙归还给门卫之后,才能让其他车辆进入。
上面的例子中门卫就相当于Semaphore,车钥匙就相当于许可证,车就相当于线程。 ———————————————— 版权声明:本文为CSDN博主「TyuIn」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/qq_43605444/article/details/121548978
1.2.直接上代码:
private int sum = 0;
/**
* 设置100个并发,开启公平锁
*/
private Semaphore readSemaphore = new Semaphore(100,true);
/**
* 设置一个并发,相当于一个synchronized
*/
private Semaphore writerSemaphore = new Semaphore(1);
public int inrAndGet(){
try {
writerSemaphore.acquireUninterruptibly();
return ++sum;
}finally {
//放锁
writerSemaphore.release();
}
}
public int getSum(){
try {
readSemaphore.acquireUninterruptibly();
return sum;
}finally {
//放锁
readSemaphore.release();
}
}
2.CountdownLatch-监听线程数
可以设置监听线程数。每完成一个线程可以发送一个指令减一latch.countDown();,等到减到0则主线程被唤醒 countDownLatch.await();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for(int i=0;i<10;i++){
new Thread(new readNum(i,countDownLatch)).start();
}
//等到线程数完成减一,等减到0则开始主线程
countDownLatch.await(); // 注意跟CyclicBarrier不同,这里在主线程await
System.out.println("==>各个子线程执行结束。。。。");
System.out.println("==>主线程执行结束。。。。");
}
static class readNum implements Runnable{
private int id;
private CountDownLatch latch;
public readNum(int id,CountDownLatch latch){
this.id = id;
this.latch = latch;
}
@Override
public void run() {
synchronized (this){
System.out.println("id:"+id+","+Thread.currentThread().getName());
//latch.countDown();
System.out.println("线程组任务"+id+"结束,其他任务继续");
latch.countDown();
}
}
}
3.CyclicBarrier -循环屏障
每个线程开始等待阻塞,等所有线程都执行到cyc.await(),则触发聚合点,开始执行各个线程cyc.await()之后的代码;
CyclicBarrier cyclicBarrier = new CyclicBarrier(5, new Runnable() {
@Override
public void run() {
//到了聚合点,就开始执行
System.out.println("回调>>"+Thread.currentThread().getName());
System.out.println("回调>>线程组执行结束");
System.out.println("==>各个子线程执行结束。。。。");
}
});
三、常用的线程安全
1.jdk基础数据与集合类
ArrayList: 基于数组,超过数组需要扩容,扩容成本高 用途:大部分情况下操作一组数据都可以用 ArrayList 原理:使用数组模拟列表,默认大小10,扩容 x1.5,newCapacity = oldCapacity + (oldCapacity >> 1),下取整 线程不安全。
LinkedList: 使用链表实现,无需扩容 用途:不知道容量,插入变动多的情况 原理:使用双向指针将所有节点连起来 线程不安全。
HashMap: 空间换时间,哈希冲突不大的时候性能高 原理:使用hash原理,存k-v数据,初始容量16,扩容*2,负载因子0.75,jdk8以后,链表长度到8 & 数组长度到64时,使用红黑树 线程不安全
ListedHashMap: 继承HashMap,对Entry集合添加一个双向链表 用途:保证有序,stream中 toMap的使用 线程不安全
**concurrentHashMap:**分段保证安全 原理:jdk7默认16个Segment,降低力度,concurrentLevel = 16, ,把Segment去掉了
CopyOnWriteArrayList 读多,写少的场景 线程安全(每次写的时候都会去copy一份)
2.list线程安全的方法
线程不安全的主要原因就是读写重读和写冲突导致的,最简单的思路就是读写加锁
例如:
- ArryList的方法上都加上 synchronized ->Vector
- Collections.sychronizedList,强制将List的操作加上同步
- Arrays.asList,不允许添加删除,但是可以set替换元素
- Collections.unmodifiableList,不允许修改内容,包括添加删除和set
3.线程安全操作利器 - ThreadLocal
ThreadLocal-线程本地变量,每个线程一个副本
4.并行stream
5.防止重复提交-从业务上
给每个提交的表单设置一个值fromkey,只有第一次发送请求的表单的fromkey=01234k,同时添加到session中,,后端验证的时候可以比对session中的值是否相等,相等则执行逻辑,并把session中的fromkey移除掉。
6.线程间的协作和通信
线程共享:
- static/实例变量
- lock
- synchronized
线程协作:
- Thread # join() //唤醒一个线程
- t.join()/t.join(long millis) : 当前线程进入 WAITNG 状态,当前线程不会释放锁,调用t线程的join,让t线程进入就绪状态。由于t线程内部调用的是t.wait,所以会释放锁。
- Object# wait/notify/notifyAll //线程等待,释放锁, 唤醒线程
- Future/Callable //有返回值的线程任务
- CountdownLatch //各个线程执行downlatch计数,到一定数量,主线程awit被唤醒
- CyclicBarrier //各个线程到awit 等待,等到一定数量,集合点被唤醒,各个线程开始执行awit之后的代码
四、运用小结
1.思考
有多少种方式,在 main 函数启动一个新线程,运行一个方法,拿到这个方法的返回值后,退出主线程?
//第一种 FutureTask+callable
System.out.println("-----------------第1种:----------------");
long start = System.currentTimeMillis();
Callable callable =()-> {
return worker();
};
FutureTask<String> future = new FutureTask<>(callable);
Thread t = new Thread(future);
t.start();
String result = future.get();
System.out.println(Thread.currentThread().getName()+"-->主线程获取返回值:--"+result);
System.out.println(Thread.currentThread().getName()+"-->退出主线程,使用时间:"+(System.currentTimeMillis()-start));
//第二种 Future+callable+ExecutorService
System.out.println("-----------------第2种:----------------");
start = System.currentTimeMillis();
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future future2 = executorService.submit(callable);
result = (String) future2.get();
executorService.shutdown();
System.out.println(Thread.currentThread().getName()+"-->主线程获取返回值:--"+result);
System.out.println(Thread.currentThread().getName()+"-->退出主线程,使用时间:"+(System.currentTimeMillis()-start));
//第3种 伪通知
System.out.println("-----------------第3种:----------------");
flag=false;
start = System.currentTimeMillis();
new Thread(()->{
worker();
}).start();
result = getResult();
System.out.println(Thread.currentThread().getName()+"-->主线程获取返回值:--"+result);
System.out.println(Thread.currentThread().getName()+"-->退出主线程,使用时间:"+(System.currentTimeMillis()-start));
//第4种 CompletableFuture
System.out.println("-----------------第4种:----------------");
start = System.currentTimeMillis();
result = CompletableFuture.supplyAsync(()->worker()).get();
System.out.println(Thread.currentThread().getName()+"-->主线程获取返回值:--"+result);
System.out.println(Thread.currentThread().getName()+"-->退出主线程,使用时间:"+(System.currentTimeMillis()-start));
//第5种 notify 唤醒 wait
System.out.println("-----------------第5种:----------------");
start = System.currentTimeMillis();
new Thread(()->{
synchronized (object){
Homework.result=worker();
object.notify();
}
}).start();
synchronized (object){
object.wait();
}
System.out.println(Thread.currentThread().getName()+"-->主线程获取返回值:--"+Homework.result);
System.out.println(Thread.currentThread().getName()+"-->退出主线程,使用时间:"+(System.currentTimeMillis()-start));
1 条评论
回复