大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
文章目录
lock和synchronized的区别
- synchronized 自动释放所,lock必须手动释放
- synchronized 如果获取不到锁就一直会等待下取,lock可以不用(trylock()方法)
- lock是可中断锁,而synchronized 不是可中断锁(tryLock(long timeout,TimeUnit unit)方法)
- synchronized 是可重入锁,lock也是可重入锁
可重入锁:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 0 时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应的方法;当某一线程请求成功后,JVM会记下锁的持有线程,并且将计数器置为 1;此时其它线程请求该锁,则必须等待;而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增;当线程退出同步代码块时,计数器会递减,如果计数器为 0,则释放该锁。
public class Lock{
boolean isLocked = false;
Thread lockedBy = null;
int lockedCount = 0;
public synchronized void lock()
throws InterruptedException{
Thread thread = Thread.currentThread();
while(isLocked && lockedBy != thread){
wait();
}
isLocked = true;
lockedCount++;
lockedBy = thread; //第一次的时将lockedBy赋值为改线程
}
public synchronized void unlock(){
if(Thread.currentThread() == this.lockedBy){
lockedCount--;
if(lockedCount == 0){
isLocked = false;
notify();
}
}
}
}
注意虚假唤醒现象
虚假唤醒就是一些obj.wait()会在除了obj.notify()和obj.notifyAll()的其他情况被唤醒,而此时是不应该唤醒的。
if()
wait(); //如果被阻塞,那么下一次唤醒后就直接不用等待,直接执行下面的语句
//改为
while()
wait();
Condition
Condition的作用相当于synchronized中的
Condition condition = new Condition();
condition.await();//相当于wait()
condition.signal();//相当于notify()
作用
主要应用于同步机制,比如A事件做完了才能做B事件
集合类的并发问题
List<String> list = new CopyOnWriteArrayList() //这个列表是线程安全的
//同理还有Set和HashMap的线程安全集合
Callable
创建线程的有一种方法
- 可以有返回值
- 可以抛出异常
- 调用call()方法
CountDownLanuch
CountDownLanuch是减法计数器
- countDown 计数器-1
- await()等待计数器归零然后向下执行
- 通常用于某一任务必须被执行才执行其他任务
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(6);
for(int i = 0;i < 6;i ++){
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "go out");
countDownLatch.countDown();
},String.valueOf(i)).start();
}
countDownLatch.await(); //等待计数器归零 然后向下执行
System.out.println("Close door");
}
CyclicBarrier是加法计数器
Semaphere
一般用于只有固定数量的资源
Semaphere semaphere = new Semaphere(3); //停车位数量
semaphere.acquire(); //获取
semaphere.release(); //释放
ReadWriteLock读写锁
写的时候只能有一个去写,当时可以同时有多个去读
ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
readWriteLock.writeLock().unlock();
readWriteLock.readLock().lock();
readWriteLock.readLock().unlock();
BlockQueue四大组件
add,remove如果不正确会抛出异常
put,take如果不正确会等待
SynchronousQueue
和其他的BlockingQueue不一样,SynchronousQueue不存储元素,put了一个元素后,只能先take取出来,才能继续put
线程池
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单一实例
ExecutorService threadPool = Executors.newFixedThreadPool(5); //固定数量
ExecutorService threadPool = Executors.newCachedThreadPool(5); //可伸缩型,遇强则强遇弱则弱
创建线程
threadPool.execute();
Future异步请求
以下线程都是异步方法,这里的异步类似于ajax异步请求
CompletableFuture<Void>completableFutrue = CompletableFuture.runAsync(...) //没有返回值的runAsync
CompletableFuture<Void>completableFutrue = CompletableFuture.supplyAsync(...) //有返回值的supplyAsync()
JMM
Java的并发采用的是共享内存模型
JMM规范:
线程解锁前,必须把共享变量立刻刷回主存
线程加锁前,必须读取主存中的最新值到工作内存中!
加锁和解锁必须是同一把锁
JMM模型
java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中。每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
-
lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
-
read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
-
load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
-
use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
-
assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
-
store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
-
write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
-
unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
-
(1)不允许read和load、store和write操作之一单独出现(即不允许一个变量从主存读取了但是工作内存不接受,或者从工作内存发起会写了但是主存不接受的情况),以上两个操作必须按顺序执行,但没有保证必须连续执行,也就是说,read与load之间、store与write之间是可插入其他指令的。
-
(2)不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
-
(3)不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中。
-
(4)一个新的变量只能从主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
-
(5)一个变量在同一个时刻只允许一条线程对其执行lock操作,但lock操作可以被同一个条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
-
(6)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
-
(7)如果一个变量实现没有被lock操作锁定,则不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定的变量。
-
(8)对一个变量执行unlock操作之前,必须先把此变量同步回主内存(执行store和write操作)。
Volatile
- 保证可见性
- 不保证原子性
- 禁止指令重排
可见性
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
真正实现可见性的是Synchronized的两条规定
1、线程解锁前,必须把共享变量的最新值刷新到主内存中;
2、线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)
线程执行互斥锁代码的过程:
1.获得互斥锁
2.清空工作内存
3.从主内存拷贝最新变量副本到工作内存
4.执行代码块
5.将更改后的共享变量的值刷新到主内存中
6.释放互斥锁
所以synchronized具有原子可见性
不保证原子性
再执行过程中,会被其他进程打断。
问题
如果不加lock和synchronized,如何保证原子性,不被其他进程打断?
使用原子类,解决原子性问题
volatile static AtomicInteger num = new AtomicInteger(0);
指令重排
什么是指令重排: 你写的程序,计算机并不是按照指定的的步骤执行
源代码—>编译器优化源代码–>指令并行也可能会重排—>内存系统也会重排 执行
在一个变量被volatile修饰后,JVM会为我们做两件事:
-
在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障。
-
在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障。
单例模式
package com.czp.single;
import java.lang.reflect.Constructor;
public class LazyManThread {
private static volatile LazyManThread lazyManThread = null;
private static boolean isExist = false;
private LazyManThread() {
synchronized (LazyManThread.class) {
if (!isExist) {
isExist = true;
} else {
throw new RuntimeException("禁止使用反射创建该对象");
}
}
}
//
private LazyManThread(int a){
synchronized (LazyManThread.class){
if(lazyManThread != null){
throw new RuntimeException("禁止使用反射创建该对象");
}
}
}
public static LazyManThread getInstance() {
//if只会判断一次,当两个线程同时判断时一个线程就会在同步代码块中等待
if (lazyManThread == null) {
//不直接使用同步的原因,提高执行效率
synchronized (LazyManThread.class) {
if (lazyManThread == null) {
lazyManThread = new LazyManThread();
}
}
}
/** * 由于对象创建不是原子性操作 * 1. 分配内存空间 * 2. 使用构造器创建对象 * 3. 将对象指向内存空间 */
/** * 可能会发生指令重排 * 123 * * 132 * * 这是就需使用volatile关键字来防止指令重排 */
return lazyManThread;
}
public static void main(String[] args) throws Exception {
// LazyManThread instance = LazyManThread.getInstance();
Constructor<LazyManThread> declaredConstructor = LazyManThread.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
LazyManThread lazyManThread = declaredConstructor.newInstance();
LazyManThread instance = declaredConstructor.newInstance();
System.out.println(instance);
System.out.println(lazyManThread);
}
}
CAS
CAS(Compare and Swap),是比较并替换的意思。
原子操作类
所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicInteger,AtomicLong,它们分别用于Boolean,Integer,Long类型的原子性操作。这里的Atomic操作类的底层正是使用了“CAS机制”。
Java语言CAS底层如何实现?
利用unsafe提供了原子性操作方法。以AtomicInteger举例说明:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/** * Atomically adds the given value to the current value of a field * or array element within the given object <code>o</code> * at the given <code>offset</code>. * * @param o object/array to update the field/element in * @param offset field/element offset * @param delta the value to add * @return the previous value * @since 1.8 */
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta)); //compareAndSwapInt整个比较并替换的操作是一个原子操作。
return v;
}
Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。
至于valueOffset,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。
正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。
CAS缺点
- 并发量较高时CPU开销过大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。
- 只能保证单个变量的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
- ABA问题
ABA问题的根本在于cas在修改变量的时候,无法记录变量的状态,比如修改的次数,否修改过这个变量。这样就很容易在一个线程将A修改成B时,另一个线程又会把B修改成A,造成casd多次执行的问题。
解决ABA问题, 引入原子引用 ! 对应的思想: 乐观锁
package com.czp.CAS;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class CASDemo {
public static void main(String[] args) {
//integer
AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(1, 1);
new Thread(()->{
System.out.println("a1=>" + stamp.getStamp());
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
stamp.compareAndSet(1, 2, stamp.getStamp(), stamp.getStamp()+1);
System.out.println("a2=>" + stamp.getStamp());
stamp.compareAndSet(2, 1, stamp.getStamp(), stamp.getStamp()+1);
System.out.println("a3=>" + stamp.getStamp());
}).start();
new Thread(()->{
System.out.println("b1=>" + stamp.getStamp());
stamp.compareAndSet(1, 2, stamp.getStamp(), stamp.getStamp() + 1);
System.out.println("b2=>" + stamp.getStamp());
}).start();
}
}
公平锁, 非公平锁
公平锁: 非常公平,先来后到,不允许插队
非公平锁: 非常不公平, 允许插队
public ReentrantLock() {
sync = new NonfairSync(); //无参默认非公平锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//传参为true为公平锁
}
自旋锁
自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。
获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。
/** * Date: 2016年1月4日 下午4:41:50 * * @author medusar */
public class SpinLock {
private AtomicReference cas = new AtomicReference();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}
ock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。
缺点
-
如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
-
上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。
优点
自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)
死锁
- 死斥
- 占有等待
- 循环等待
- 不可抢占
案例
package com.czp.lock;
import java.util.concurrent.TimeUnit;
public class KillLock implements Runnable {
private String stringA;
private String stringB;
public KillLock(String stringA, String stringB) {
this.stringA = stringA;
this.stringB = stringB;
}
@Override
public void run() {
synchronized (stringA) {
System.out.println(Thread.currentThread().getName() + "lock" + stringA + "try to lock stringB");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (stringB) {
System.out.println(Thread.currentThread().getName() + "lock" + stringB + "try to lock stringA");
}
}
}
public static void main(String[] args) {
String a = "a";
String b = "b";
new Thread(new KillLock(a, b)).start();
new Thread(new KillLock(b, a)).start();
}
}
- 使用jps -l定位进程号
- 使用jstack 查看进程信息找到死锁问题
Java stack information for the threads listed above:
===================================================
"Thread-1":
at com.czp.lock.KillLock.run(KillLock.java:25)
- waiting to lock <0x00000000d5f169c8> (a java.lang.String)
- locked <0x00000000d5f169f8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at com.czp.lock.KillLock.run(KillLock.java:25)
- waiting to lock <0x00000000d5f169f8> (a java.lang.String)
- locked <0x00000000d5f169c8> (a java.lang.String)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/168798.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...