大家好,又见面了,我是你们的朋友全栈君。
可重入锁:当线程请求一个由其它线程持有的对象锁时,该线程会阻塞,而当线程请求由自己持有的对象锁时,如果该锁是重入锁,请求就会成功,否则阻塞。
重入锁实现可重入性原理或机制是:每一个锁关联一个线程持有者和计数器,当计数器为 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的作用相当于synchronized中的
Condition condition = new Condition();
condition.await();//相当于wait()
condition.signal();//相当于notify()
作用 主要应用于同步机制,比如A事件做完了才能做B事件
List<String> list = new CopyOnWriteArrayList() //这个列表是线程安全的
//同理还有Set和HashMap的线程安全集合
创建线程的有一种方法
CountDownLanuch是减法计数器
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 = new Semaphere(3); //停车位数量
semaphere.acquire(); //获取
semaphere.release(); //释放
写的时候只能有一个去写,当时可以同时有多个去读
ReadWriteLock readWriteLock= new ReentrantReadWriteLock();
readWriteLock.writeLock().lock();
readWriteLock.writeLock().unlock();
readWriteLock.readLock().lock();
readWriteLock.readLock().unlock();
add,remove如果不正确会抛出异常 put,take如果不正确会等待
和其他的BlockingQueue不一样,SynchronousQueue不存储元素,put了一个元素后,只能先take取出来,才能继续put
ExecutorService threadPool = Executors.newSingleThreadExecutor(); //单一实例
ExecutorService threadPool = Executors.newFixedThreadPool(5); //固定数量
ExecutorService threadPool = Executors.newCachedThreadPool(5); //可伸缩型,遇强则强遇弱则弱
创建线程
threadPool.execute();
以下线程都是异步方法,这里的异步类似于ajax异步请求
CompletableFuture<Void>completableFutrue = CompletableFuture.runAsync(...) //没有返回值的runAsync
CompletableFuture<Void>completableFutrue = CompletableFuture.supplyAsync(...) //有返回值的supplyAsync()
Java的并发采用的是共享内存模型
JMM规范: 线程解锁前,必须把共享变量立刻刷回主存 线程加锁前,必须读取主存中的最新值到工作内存中! 加锁和解锁必须是同一把锁 JMM模型 java内存模型中规定了所有变量都存贮到主内存(如虚拟机物理内存中的一部分)中。每一个线程都有一个自己的工作内存(如cpu中的高速缓存)。线程中的工作内存保存了该线程使用到的变量的主内存的副本拷贝。线程对变量的所有操作(读取、赋值等)必须在该线程的工作内存中进行。不同线程之间无法直接访问对方工作内存中变量。线程间变量的值传递均需要通过主内存来完成。
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存。如何从工作内存同步到主内存中的实现细节。java内存模型定义了8种操作来完成。这8种操作每一种都是原子操作。8种操作如下:
Java内存模型还规定了执行上述8种基本操作时必须满足如下规则:
可见性 被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
真正实现可见性的是Synchronized的两条规定 1、线程解锁前,必须把共享变量的最新值刷新到主内存中; 2、线程加锁时,讲清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)
线程执行互斥锁代码的过程: 1.获得互斥锁 2.清空工作内存 3.从主内存拷贝最新变量副本到工作内存 4.执行代码块 5.将更改后的共享变量的值刷新到主内存中 6.释放互斥锁
所以synchronized具有原子可见性
不保证原子性 再执行过程中,会被其他进程打断。 问题 如果不加lock和synchronized,如何保证原子性,不被其他进程打断? 使用原子类,解决原子性问题
volatile static AtomicInteger num = new AtomicInteger(0);
指令重排 什么是指令重排: 你写的程序,计算机并不是按照指定的的步骤执行 源代码—>编译器优化源代码–>指令并行也可能会重排—>内存系统也会重排 执行 在一个变量被volatile修饰后,JVM会为我们做两件事:
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(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带来很到的压力。
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用synchronized了。
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方法释放了该锁。 缺点
优点 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是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();
}
}
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.
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/168798.html原文链接:https://javaforall.cn