挖坑记录~并发基础

根据尚硅谷课程整理
尚硅谷Java大厂面试题第二季(java面试必学,周阳主讲)

当一个程序中出现多个进程共享一个变量时,会出现共享变量的并发问题
以下程序
1.主线程启动睡眠待线程启动完毕
2.VolatileExample线程(线程1)进入while循环
3.主线程修改了flag

public class VolatileExample extends Thread {
	// 设置类静态变量,各线程访问这同一共享变量
	private static boolean flag = true;
	// 无限循环,等待flag变为false时才跳出循环
	public void run() {
		while (flag) {
		}
	}

	public static void main(String[] args) throws Exception {
		new VolatileExample().start();
		// sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
		Thread.sleep(1000);
		flag = false;
		System.out.println("主线程执行完毕......");
	}
}

虽然线程1和主线程共享flag变量,主线程修改了flag变量,但是并不会立即影响到线程1. 这是因为java采用了类似cpu的缓存策略.
在计算机中,处理性能依次为硬盘<内存<cpu, 假如cpu处理任务a需要5s, 内存处理此任务需要6s, 此时cpu就需要等待内存空闲时, 才能将下一个任务的结果交给内存.
为了充分使用cpu的性能, 在cpu和缓存中加入了缓存策略
在这里插入图片描述
cpu会优先把处理完的结果交给缓存. 同理, 在java内存模型描述了java类似cpu的处理机制, 所有变量都存在于主内存, 对于不同的线程来说, 同时操作一个共享变量速度太慢, 为了充分利用性能, 每个线程会将该变量复制到线程独有的工作内存中进行操作, 再操作完后, 将结果写回到主内存中.
对于线程1和main线程, main线程将工作内存中的flag副本修改为false, 并写回到主内存, 但是线程1并不知道, 这就是JMM中变量的可见性.

volatile

此关键字特性
1. 可见性
2. 不保证原子性
3. 禁止指令重排

可见性:

当有线程操作完该关键字修饰的变量写会主内存时, 会立即通知其他线程同步主内存的最新值更新回自己的工作内存

public class VolatileExample extends Thread {
	// 设置类静态变量,各线程访问这同一共享变量
	private static volatile boolean flag = true;
	// 无限循环,等待flag变为false时才跳出循环
	public void run() {
		while (flag) {
//			 System.out.println("flag为true......");
		}
	}

	public static void main(String[] args) throws Exception {
		new VolatileExample().start();
		// sleep的目的是等待线程启动完毕,也就是说进入run的无限循环体了
		Thread.sleep(1000);
		flag = false;
		System.out.println("主线程执行完毕......");
	}
}

对于可见性这个问题, jvm已经做出了优化(案例为jdk1.8)
当变量对于其他代码产生影响时, 会立即进行同步

while (flag) {
			 System.out.println("flag为true......");
		}

结果为再打印1s后, false修改并对其他线程可见, 程序终止
在这里插入图片描述

不保证原子性

当不同线程对同一个变量进行操作时, 每个线程都将自己的结果写会主内存, 此时写操作就会出现并发问题

public class VolatileExample2 extends Thread {
	// 设置类静态变量,各线程访问这同一共享变量
	private static volatile int i = 0;
	private CountDownLatch countDownLatch;
	public VolatileExample2(CountDownLatch countDownLatch){
		this.countDownLatch = countDownLatch;
	}
	public void run() {
		for (int j = 0; j <2000; j++) {
			i++;
		}
		countDownLatch.countDown();
	}

	public static void main(String[] args) throws Exception {
		//countdownlatch可以理解为一个计数器,初试为10,
		//每个线程都会用countDownLatch.countDown()对计数器-1,
		//main线程使用countDownLatch.await(),一直阻塞直到计数器为0
		CountDownLatch countDownLatch = new CountDownLatch(10);
		for (int i = 0; i <10; i++) {
			new VolatileExample2(countDownLatch).start();
		}
		countDownLatch.await();
		//或者使用线程数量判断 因为有gc线程和main自己的线程, 所以线程数量为2
		/*while(Thread.activeCount()>2){
			Thread.yield();
		}*/
		System.out.println("主线程执行完毕......i为:"+i);
	}
}

在这里插入图片描述
10个线程, 每个都对i进行1000次++操作, 正常应该为2w
但是因为线程和线程间同时回写时出现了值覆盖情况
针对下图
1.a线程将i修改为2并立即回写主内存
2. b线程也将i修改为2并立即回写
3. 同理1和2
3个线程回写都是在同一时间发生的
3个线程并没有来的及去同步主内存中新的值, 并回写了, 这就bc线程将a线程的值覆盖了, 本来是3个线程操作完结果为4, 但结果为2, 值丢失了
在这里插入图片描述
针对这种并发问题, 可以使用synchronized解决, 但是这样效率低, 同一时刻只有一个线程在运行, 没有充分利用性能, 解决这种并发问题, 一般的处理思想都是通过消耗性能来达到线程安全的效果.
解决方案可以使用原子类

public class VolatileExample2WithAtomic extends Thread {
	// 设置类静态变量,各线程访问这同一共享变量
	private static volatile AtomicInteger atomic = new AtomicInteger();
	private CountDownLatch countDownLatch;
	public VolatileExample2WithAtomic(CountDownLatch countDownLatch){
		this.countDownLatch = countDownLatch;
	}
	public void run() {
		for (int j = 0; j <200; j++) {
			//获取并增加,默认增加的值为1
			atomic.getAndIncrement();
		}
		countDownLatch.countDown();
	}

	public static void main(String[] args) throws Exception {
		//countdownlatch可以理解为一个计数器,初试为10,
		//每个线程都会用countDownLatch.countDown()对计数器-1,
		//main线程使用countDownLatch.await(),一直阻塞直到计数器为0
		CountDownLatch countDownLatch = new CountDownLatch(100);
		for (int i = 0; i <100; i++) {
			new VolatileExample2WithAtomic(countDownLatch).start();
		}
		countDownLatch.await();
		//或者使用线程数量判断 因为有gc线程和main自己的线程, 所以线程数量为2
		/*while(Thread.activeCount()>2){
			Thread.yield();
		}*/
		System.out.println("主线程执行完毕......atomic为:"+atomic.get());
	}
}

aomic为原子类, 内部参考了自旋锁, 对于每个线程的写回时, 都会与主内存中的值进行对比, 与预期结果相同时会进行写回, 不然就读取新的值. 之后会做详细解释.

指令重排

对同一份代码, 编译后的指令是相同的, 但jvm出于充分利用cpu性能考虑, 会对指令执行顺序进行优化
比如

//指令重排
public class InstructionReorder {
	public static boolean flag = false;
	public static int i = 0;
	public static void main(String[] args) {
		if(flag){
			System.out.println(i);
		}
	}
	class ThreadA extends Thread{
		@Override
		public void run() {
			i = 1;
			flag = true;
		}
	}
}

正常来说打印出的结果为1, 但是因为线程2中进行了指令重排, flag优先变为true, main线程进入判断, 此时输出的i仍为0.

volatile对flag进行修饰, 这段代码就不会进行重排, 这是因为对volatile修饰的变量执行时会产生内存屏障 , 区分对普通变量的读写和volatile变量的读写

疑问: 是否需要对同一个栈中多个变量添加volatitle, 还是添加一个就可以保证整段指令不进行重排
如果两个变量之间有依赖关系, 将不进行重排
a =1;
b =2+a;
b依赖了a的值, 所以不会进行优化导致指令重排, 类似的原则请看这篇
happen - before解释
https://www.jianshu.com/p/7a6118b2d794

CAS

compare and swap 比较并交换

Atomic

在使用atomic解决原子性问题时说过, atomic底层使用了比较并交换的方式
getAndAddInt()每次增加1
在这里插入图片描述
另一个方法compareAndset()
在这里插入图片描述
工作内存中atomic的真实值为expect期望值, update为更新值, 期望值与主内存中atomic真实值相同时, 返回true, 将atomic的值改为更新值
在这里插入图片描述

valueoffset

内存偏移量, 通过他指定一个对象的值
compareAndSwapInt()中指定的valueoffset就代表原始值
在这里插入图片描述

 private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

通过valueoffset找到了对象的真实值
在这里插入图片描述
首先do中获取当前主内存中的值i, compareAndSwapInt()如果i 和预期值相同, 就进行swap交换, 如果比较后发现值不相同, 就重新获取一次i ,再进行对比

atomic中没有使用到锁, 就实现了线程安全, 因为底层使用了unsafe类

unsafe

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

在这里插入图片描述
unsafe中的native方法是java底层调用c语言的接口, 编译后按照原指令进行执行,相当于汇编指令, 执行过程中无法被打断, 这样就保证了并发时的原子性
unsafe通过valueoffset可以像C语言的指针一样直接操作内存.

CAS的缺点

1.dowhile中可以看到, 如果和主内存中的值一直不相同, 会一直进行比较, 消耗资源
2.仅能支持一个变量
3.aba问题
假如线程a多次获取到执行权, 在线程b阻塞过程中, 对变量i进行两次写操作, 变量i初始值为0, 第一次修改为2, 第二次修改为0, 对线程b来说, 当他获取主内存中的值并进行比较时, 满足条件, 比较并赋值成功, 线程b不知道这个i已经是a操作后的了.

解决aba问题

AtomicReference原子引用类
原子类中引用了对象泛型, 并保存了一个对象, 再对对象进行cas操作

public class AtomicReferenceTest {
	public static void main(String[] args) {
		AtomicReference<User> atomicReference  =new AtomicReference();
		User jojo = new User();
		jojo.setName("jojo");
		User dio = new User();
		dio.setName("dio");
		atomicReference.set(jojo);
		boolean flag = atomicReference.compareAndSet(jojo,dio);
		System.out.println("flag:"+flag+",username:"+atomicReference.get().getName());
		boolean flag2 = atomicReference.compareAndSet(jojo,dio);
		System.out.println("flag2:"+flag2+",username:"+atomicReference.get().getName());
	}
	
}
class User{
	private String name;

	/**
	 * @return the name
	 */
	public String getName() {
		return name;
	}

	/**
	 * @param name the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}
	
}

AtomicStampedReference原子引用类
原子类中引用了对象泛型, 并保存了一个对象, 并且初试时带有一个版本号, 每次cas操作时, 都对版本号进行操作, 再对对象进行cas操作时, 会比较版本号, 值和版本号均相同时, 代表比较成功, 解决aba问题.

public class AtomicStampedReferenceTest {
	private static AtomicStampedReference<Integer> asr = new AtomicStampedReference(1,1);
	public static void main(String[] args) {
		new Thread(()->{
			try {
				//等待t2线程通过主内存数据,
				TimeUnit.SECONDS.sleep(1);
			} catch (Exception e) {
				e.printStackTrace();
			}
			asr.compareAndSet(1, 2, asr.getStamp(),asr.getStamp()+1);
			System.out.println(Thread.currentThread().getName()+"t1第1次修改的版本号为:"+asr.getStamp());
			asr.compareAndSet(2, 1, asr.getStamp(), asr.getStamp()+1);
			System.out.println(Thread.currentThread().getName()+"t1第2次修改的版本号为:"+asr.getStamp());
		},"t1").start();
		new Thread(()->{
			int stamp = asr.getStamp();
			try {
				//等待线程t1进行aba操作
				TimeUnit.SECONDS.sleep(2);
			} catch (Exception e) {
				e.printStackTrace();
			}
			boolean flag = asr.compareAndSet(1, 2, stamp,stamp+1);
			System.out.println(Thread.currentThread().getName()+"修改结果标示flag:"+flag);
			System.out.println("t2的期望版本号为:"+(stamp+1)+";实际asr版本号为:"+asr.getStamp());
		},"t2").start();
	}
}

集合类不安全

ArrayList

arrayList底层为数组, 扩容时通过复制原数组实现, 扩容因子为0.5, 原长度的一半

arrayList为了保证效率增删改操作都没有加锁
在这里插入图片描述

public class ArrayListDemo {
	public static List<Integer> a = new ArrayList();
	public static void main(String[] args) {
		for (int i = 0; i < 30; i++) {
			new Thread(()->{
				a.add(i);
				System.out.println(a);
			},String.valueOf(i)).start();
		}
	}
}

在多线程并发下, 底层迭代器会比较集合当前长度与预期长度, 如果不同就会抛出异常concurrentModificationException
在这里插入图片描述
在这里插入图片描述
使用vector 或者集合工具类collections 可以保证线程安全, 这两者都是给集合加synchronized锁

public static List<Integer> sycha = Collections.synchronizedList(a);

在这里插入图片描述
juc中提供了一个list的实现类CopyOnWriteArrayList

使用CopyOnWriteArrayList可以通过一种轻锁解决这个问题

public static CopyOnWriteArrayList cwaList = new CopyOnWriteArrayList();

集合中维护了一个数组, 通过volatile修饰, 保持了可见性
在这里插入图片描述
通过重入锁保证安全, 并根据写时复制思想保证效率
首先通过copyof复制出一个新的数组, 当前线程通过对复制出来的新的数组完成写入, 并在写入后替换这个list对象中数组的引用, 指向复制出来的数组.
这样做比直接加synchronized优点是保证写安全的情况下, 允许其他线程读取该对象的数据, 因为写入过程操作的是复制的, 原数组并未更改, 这种方式适合写少读多的情况.
在这里插入图片描述

hashSet

hashset是hashmap, value是一个恒定对象
在这里插入图片描述

add操作同样会出现并发问题
在这里插入图片描述
在这里插入图片描述
同样juc中提供了实现类
在这里插入图片描述
跟hashmap一样, 有copyonwritearraylist创建
在这里插入图片描述
不过set的add操作会先进行一次判断, 判断list是否已包含该元素
在这里插入图片描述

HashMap

HashMap也是采用了迭代器模式, 底层同样会报并发修改异常

使用juc中的ConcurrentHashMap, 底层采用分段锁, 既保证了效率, 又保证了安全, 但是在jdk1.8中, 在多线程中, 当key的hash值相同时, 会有一个死循环的问题, 在1.9之后修复了,
在这里插入图片描述
推荐看一下这篇文章
JDK1.8源码–ConcurrentHashMap锁分段的思考
主要看put的方法操作, 牵扯到hashmap的数据结构问题, hashmap底层使用了Node<k,v>数组, 根据不同情况, 扩容的方式有长度扩容, 节点改为链表, 当满足条件时该节点又会改为红黑树, 所以数组的每个下标位置都会存放大量节点元素, 多线程操作时, 只要将某个下标的元素锁住, 而不是像hashtable一样将整个数组锁住, 同时如果该节点位置为null时, 将通过compareAndSwap不加锁操作, 这种方式是通过对比元素的内存地址, 使用unsafe提供的系统原语操作, 可以保证原子性

 final V putVal(K key, V value, boolean onlyIfAbsent) {
        // 如果key或者value为空,抛异常
        if (key == null || value == null) throw new NullPointerException();
        // 计算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 初始化Node数组
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 根据hash值计算数组的下标,并取出该值,判断是否为空
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 利用cas算法直接插入Node结点,不加锁
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 如果该节点是ForwardingNode结点,
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 利用synchronized锁住tab[i]结点
                // tab数组中下标为i的结点,其中包括链表的头节点,红黑树的根节点,单节点
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        // fh>0表示该结点是链表的头节点
                        if (fh >= 0) {
                            binCount = 1;
                            // 遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 如果key与hash值与当前结点相同,则修改当前节点的value值
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                // 如果当前结点时链表的尾结点,则创建一个新结点,并将该结点插入到链表的尾部
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        // 如果该结点时红黑树的根结点
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            // 向树中插入新结点
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                // 如果binCount不为0,说明put操作对数据产生了影响
                if (binCount != 0) {
                    // 如果链表长度达到临界值8,则将链表树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        // 更新元素个数
        addCount(1L, binCount);
        return null;
    }

公平锁与非公平锁

锁分为公平和非公平, 类似ReentrantLock默认是非公平, 多线程在碰到公平锁时, 未持有锁的线程会进入阻塞队列等待, 但在非公平锁时, 线程会尝试占有锁, 如果获取不到锁, 再去队列等待按序获得锁
synchronized也是非公平锁
在这里插入图片描述
在这里插入图片描述

可重入锁

对同一把锁来说, 当外层方法获取到锁之后, 内层方法也是用同样的锁锁住时, 将会自动获取到锁, 这种设计是为了解决死锁问题,
如果双方线程都持有对方想要获得的锁, 就会造成死锁

public class ReentrantDemo{
	public static void main(String[] args) {
		Phone phone = new Phone();
		new Thread(()->{
			phone.sendMsg();
		},"t1").start();
		new Thread(()->{
			phone.sendMsg();
		},"t2").start();
	}
}
class Phone{
	public synchronized void sendMsg(){
		System.out.println(Thread.currentThread().getName()+"正在发短信");
		type();
	}
	public synchronized void type(){
		System.out.println(Thread.currentThread().getName()+"正在发打字");
	}
}

因为是加了锁, synchronized默认使用类锁, 所以也保证了线程顺序, 执行完sendMsg()方法前t1线程都没有释放锁
在这里插入图片描述
Reentrant字面意思就是可重入的意思, 和synchronized案例一样
声明加锁的地方最后一定要声明解锁, 不然其他线程一直无法获取到锁

	ReentrantLock lock = new ReentrantLock(true);
	lock.lock();
	lock.unlock();

SpinLock自旋锁

Atomic原子类中使用的unsafe的方法getAndAddInt()就是通过不断循环判断cas的结果来进行自旋, 自旋的优点是减少了线程的上下文切换, 但是增加性能消耗

  • 上下文切换简单理解就是线程切换, 因为每次切换都要读取刚刚线程阻塞时的状态, 也会消耗性能
    在这里插入图片描述
    通过原子引用实现自旋锁
public class SpinLockDemo{
	private  AtomicReference<Thread> atomic =  new AtomicReference();
	public void startLock(){
		while(!atomic.compareAndSet(null, Thread.currentThread())){
			
		}
		System.out.println(Thread.currentThread().getName()+":获取到锁");
	}
	public void finishUnlock(){
		atomic.compareAndSet(Thread.currentThread(),null);
		System.out.println(Thread.currentThread().getName()+":释放掉锁");
	}
	public static void main(String[] args) {
		SpinLockDemo spinLockDemo = new SpinLockDemo();
		new Thread(()->{
			spinLockDemo.startLock();
			try {
				System.out.println(Thread.currentThread().getName()+"获取到锁,睡眠3s");
				TimeUnit.SECONDS.sleep(3);
				spinLockDemo.finishUnlock();
			} catch (Exception e) {
				e.printStackTrace();
			}
		},"t1").start();
		new Thread(()->{
			try {
				System.out.println(Thread.currentThread().getName()+"先睡1s,让t1拿到锁");
				TimeUnit.SECONDS.sleep(1);
				System.out.println(Thread.currentThread().getName()+"睡醒尝试获取锁");
				spinLockDemo.startLock();
				spinLockDemo.finishUnlock();
			} catch (Exception e) {
				e.printStackTrace();
			}
		},"t2").start();
	}
}

在这里插入图片描述

读写锁

ReadWriteLock
读写分离, 与copyOnWriteArrayList相同, 对写操作进行上锁, 允许其他线程进行读操作
对比数据库的锁, 适用于不允许线程读取数据出现脏读的场景
出于效率, 适用于读多写少的场景

实现对一个共享缓存的操作

public class ReadWriteLockDemo {
	public static void main(String[] args) {
		Cache cache = new Cache();
		for (int i = 0; i < 4; i++) {
			final int x = i;
			new Thread(()->{
				cache.put(String.valueOf(x), x);
			},String.valueOf(i)).start();
		}
		for (int i = 0; i <4; i++) {
			final int x = i;
			new Thread(()->{
				cache.get(String.valueOf(x));
			},String.valueOf(i)).start();
		}
	}
}
class Cache{
	public volatile Map<String,Object> map = new ConcurrentHashMap();
	public void put(String key,Object value){
		System.out.println(Thread.currentThread().getName()+":开始写入");
		try {
			TimeUnit.MILLISECONDS.sleep(300);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		map.put(key, value);
		System.out.println(Thread.currentThread().getName()+":写入完成");
	}
	public void get(String key){
		System.out.println(Thread.currentThread().getName()+":开始读取");
		try {
			TimeUnit.MILLISECONDS.sleep(300);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		Object object = map.get(key);
		System.out.println(Thread.currentThread().getName()+":读取完成,值为:"+object);
	}
}

发现写操作不能保证原子性, 一个线程在写入的时候, 其他线程会抢占资源
在这里插入图片描述
使用读写锁, 给写时加写锁, 读时加读锁

public class ReadWriteLockDemo {
	public static void main(String[] args) {
		Cache cache = new Cache();
		for (int i = 0; i < 4; i++) {
			final int x = i;
			new Thread(()->{
				cache.put(String.valueOf(x), x);
			},String.valueOf(i)).start();
		}
		for (int i = 0; i <4; i++) {
			final int x = i;
			new Thread(()->{
				cache.get(String.valueOf(x));
			},String.valueOf(i)).start();
		}
	}
}
class Cache{
	public volatile Map<String,Object> map = new ConcurrentHashMap();
	private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
	public void put(String key,Object value){
		try {
			lock.writeLock().lock();
			System.out.println(Thread.currentThread().getName()+":开始写入");
			TimeUnit.MILLISECONDS.sleep(300);
			map.put(key, value);
			System.out.println(Thread.currentThread().getName()+":写入完成");
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			lock.writeLock().unlock();
		}
		
	}
	public void get(String key){
		try {
			lock.readLock().lock();
			System.out.println(Thread.currentThread().getName()+":开始读取");
			TimeUnit.MILLISECONDS.sleep(300);
			Object object = map.get(key);
			System.out.println(Thread.currentThread().getName()+":读取完成,值为:"+object);
		} catch (Exception e) {
			e.printStackTrace();
		}finally{
			lock.readLock().unlock();
		}
	}
}

可以保证线程写时原子性和独占性, 并且允许读取
在这里插入图片描述

*读写锁具有排他性, 同一线程读锁中不能获取写锁, 写锁中可以获取读锁;
*同一种锁中可以再次获取同一种锁;

Condition

condition与Object对象相同, 提供了针对线程的阻塞await(), 唤醒signal();
condition创建需要lock对象, 所以condition的操作需要配合创建他的lock

 public Lock lock = new ReentrantLock();
 public Condition condition = lock.newCondition();

当持有锁的线程阻塞await()时, 他持有的锁将被释放 ,其他尝试获取锁的线程将争夺锁资源 (我觉得这应该是非公平锁创建出来的才会出现争抢的情况, 公平锁会按照阻塞队列顺序获取资源)

The lock associated with this Condition is atomically released and the current thread becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:

因为demo中用了线程池, 所以程序不会停止

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionUseCase {

    public Lock lock = new ReentrantLock();
    public Condition condition = lock.newCondition();

    public static void main(String[] args)  {
        ConditionUseCase useCase = new ConditionUseCase();
        ExecutorService executorService = Executors.newFixedThreadPool (2);
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                useCase.conditionWait();
            }
        });
        executorService.execute(new Runnable() {
            @Override
            public void run() {
                useCase.conditionSignal();
            }
        });
    }

    public void conditionWait()  {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            System.out.println(Thread.currentThread().getName() + "等待信号");
            condition.await();
            System.out.println(Thread.currentThread().getName() + "拿到信号");
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
    public void conditionSignal() {
        lock.lock();
        try {
            Thread.sleep(5000);
            System.out.println(Thread.currentThread().getName() + "拿到锁了");
            condition.signal();
            System.out.println(Thread.currentThread().getName() + "发出信号");
        }catch (Exception e){

        }finally {
            lock.unlock();
        }
    }
}

synchronized 和lock的区别

  1. 构造
    synchronized是jvm层面;
    lock是java类, 使用时需要被实例化;
  2. 使用
    synchronized使用monitor对象, 编译后monitorenter进入锁, monitorexit释放锁, 并且自动处理异常释放锁的情况;
    lock需要显示调用lock()加锁, unlock()解锁; 并且要在finally释放锁
  3. 能否被打断
    synchronized不能被打断;
    lock支持持有锁和阻塞获取锁的线程被打断;
  4. condition
    synchronized没有condition, 需要指明锁对象进行阻塞,唤醒;
    通过lock可以创建多个condition, 通过condition可以指定阻塞, 唤醒特定的线程;
  5. 公平与否
    synchronized只能是非公平;
    lock可以在创建时声明是公平还是非公平;

CountDownLatch

一个计时器, 初试化时赋值,可以调用await()进行阻塞 , 直到其他线程使用countDown做减法, 减到0为时, 被阻塞的线程会继续执行

public class CountDownLatchDemo extends Thread {
	public static volatile CountDownLatch countDownLatch =new CountDownLatch(10);
	public static void main(String[] args) {
		for (int i = 0; i <10; i++) {
			new Thread(()->{
				System.out.println(Thread.currentThread().getName()+":"+countDownLatch.getCount());
				countDownLatch.countDown();
			}).start();
		}
		
		try {
			countDownLatch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("main Thread finish...");
	}
}

以下为一个问题案例

public class CountDownLatchDemo extends Thread {
	private final CountDownLatch countDownLatch;
	public CountDownLatchDemo(CountDownLatch countDownLatch){
		this.countDownLatch = countDownLatch;
	}
	public void run() {
		countDownLatch.countDown();
		System.out.println(Thread.currentThread().getName()+":"+countDownLatch.getCount());
	}

	public static void main(String[] args) throws Exception {
		CountDownLatch countDownLatch = new CountDownLatch(10);
		for (int i = 0; i <10; i++) {
			new CountDownLatchDemo(countDownLatch).start();
		}
		countDownLatch.await();
		System.out.println("main Thread finish...");
	}
}

在这里插入图片描述
这个问题案例加锁以后就好了, 我大概知道问题出在哪, 但是解释不通

public class CountDownLatchProblemDemo extends Thread {
	private final CountDownLatch countDownLatch;
	private final ReentrantReadWriteLock reentrantReadWriteLock;
	public CountDownLatchProblemDemo(CountDownLatch countDownLatch,ReentrantReadWriteLock reentrantReadWriteLock){
		this.countDownLatch = countDownLatch;
		this.reentrantReadWriteLock = reentrantReadWriteLock;
	}
	public void run() {
		reentrantReadWriteLock.writeLock().lock();
		countDownLatch.countDown();
		System.out.println(Thread.currentThread().getName()+":"+countDownLatch.getCount());
		reentrantReadWriteLock.writeLock().unlock();
	}

	public static void main(String[] args) throws Exception {
		ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
		CountDownLatch countDownLatch = new CountDownLatch(10);
		for (int i = 0; i <10; i++) {
			new CountDownLatchProblemDemo(countDownLatch,reentrantReadWriteLock).start();
		}
		countDownLatch.await();
		System.out.println("main Thread finish...");
	}
}

因为countDownLatch底层也是用unsafe类的方法, 本来就可以保证原子性并且多线程安全, 但是不明白为什么声明在局部变量然后传到线程中会出现这种情况=.=
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

CyclicBarrier

直译回环屏障, 与countDownLatch相反, 用来做加法, 所有与cyclicBarrier相关的线程都需要等到屏障值满, 才能执行
创建时, 可以设置参与屏障的数量, 以及屏障满后执行的任务
在这里插入图片描述
其他线程通过await方法, 阻塞自己, 参与屏障规则, 等到屏障消失, 与cyclicBarrier绑定的任务最先执行, 之后其他阻塞的线程任务同时执行

public class CountDownLatchDemo extends Thread {
	public static volatile CountDownLatch countDownLatch =new CountDownLatch(10);
	public static void main(String[] args) {
		for (int i = 0; i <10; i++) {
			new Thread(()->{
				System.out.println(Thread.currentThread().getName()+":"+countDownLatch.getCount());
				countDownLatch.countDown();
			}).start();
		}
		
		try {
			countDownLatch.await();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("main Thread finish...");
	}
}

在这里插入图片描述

Semaphore

直译信号量,信号标;
可以用来多个线程争抢多个有限资源的场景;
比如现在有6台车, 只有3个停车位, 没抢到停车位的要等到其他车开走后才能进入;
创建时可以指定许可证数量, 如果数量为1, 相当于lock或者synchronized, 同一时间仅有一个线程获取许可证在执行;
semaphore.acquire(); 尝试获取许可证permit
semaphore.release(); 释放许可证permit
相当于获取锁释放锁

public class SemaphoreDemo {
	public static void main(String[] args) {
		Semaphore semaphore = new Semaphore(3);
		for (int i = 0; i < 6; i++) {
			final int x = i;
			new Thread(()->{
				try {
					semaphore.acquire();
					System.out.println("Thread:"+x+" 抢到了车位,并停了3s");
					TimeUnit.SECONDS.sleep(3);
				} catch (Exception e) {
					e.printStackTrace();
				}finally{
					semaphore.release();
					System.out.println("Thread:"+x+" 离开了车位..");
				}
			}).start();
		}
	}
}

在这里插入图片描述
可以设置获取到的许可证的总数量, 如果一个线程获取的许可证数量不足, 仍然会阻塞,直到它获取到指定的数量的许可证, 如果不写默认是1个

semaphore.acquire(3);
...
semaphore.release(3);

如果这个线程在阻塞的时候被打断interrupt了, 那么他将释放他获取到的所有许可证

…InterruptedException is thrown and the current thread’s interrupted status is cleared. Any permits that were to be assigned to this thread are instead assigned to other threads trying to acquire permits, as if permits had been made available by a call to release().

在这里插入图片描述

还可以使用不能被打断的方式获取许可, 如果尝试打断使用这种获取方式的线程, 那么线程将会阻塞, 并推迟他获取许可证的时间

… If the current thread is interrupted while waiting for a permit then it will continue to wait, but the time at which the thread is assigned a permit may change compared to the time it would have received the permit had no interruption occurred. When the thread does return from this method its interrupt status will be set.
在这里插入图片描述

countDownLatch , cyclicBarrier , Semaphore 底层都使用了aqs的实现类提供的方法在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值