学习笔记 2021.10.30
2021.10.30
并发
基础知识
生产者和消费者问题
本质即是线程之间的通信问题。
package JUC;//线程之间的通信问题
//生产者消费者,通过num作为媒介,一边加1,一边减1的通信过程
public class Demo02 {
public static void main(String[] args) {
Data dd = new Data();//创造资源对象
//下面即是分别的生产者和消费者的线程定义
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dd.incres();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
try {
dd.decres();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"B").start();
}
}
//基本的消费者生产者模型的模板:判断-等待-业务-通知
class Data//资源类
{
private int num = 0;
public synchronized void incres() throws InterruptedException {
//加一操作
if (num != 0)
{
this.wait();//等待
}
num++;//业务
System.out.println(Thread.currentThread().getName()+"---"+num);
//通知其他加一完毕
this.notifyAll();
}
public synchronized void decres() throws InterruptedException {
//减一操作
if (num == 0)
{
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"---"+num);
//通知其他减一完毕
this.notifyAll();
}
}
基本的细节都在其中了,该类型的问题都通过这么一个交互模式去判断和出发,最新的即是要注意资源类的简洁。反正就是对同一资源的使用,在各自的逻辑下还要保证互相的通信。
但是这种写法可能没法满足大于两个对象的共同使用,即虚假唤醒问题

具体的问题分析和用while来实现的原因:
拿两个加法线程A、B来说,比如A先执行,执行时调用了wait方法,那它会等待,此时会释放锁,那么线程B获得锁并且也会执行wait方法,两个加线程一起等待被唤醒。此时减线程中的某一个线程执行完毕并且唤醒了这俩加线程,那么这俩加线程不会一起执行,其中A获取了锁并且加1,执行完毕之后B再执行。如果是if的话,那么A修改完num后,B不会再去判断num的值,直接会给num+1。如果是while的话,A执行完之后,B还会去判断num的值,因此就不会执行。
改进后的部分和执行结果
class Data//资源类
{
private int num = 0;
public synchronized void incres() throws InterruptedException {
//加一操作
while (num != 0)
{
this.wait();//等待
}
num++;
System.out.println(Thread.currentThread().getName()+"---"+num);
//通知其他加一完毕
this.notifyAll();
}
public synchronized void decres() throws InterruptedException {
//减一操作
while (num == 0)
{
this.wait();
}
num--;
System.out.println(Thread.currentThread().getName()+"---"+num);
//通知其他减一完毕
this.notifyAll();
}
}

Lock版本的生产者消费者问题
基本的语句的区别


注意lock这版本下面两个方法都是通过conditio接口实现的。主要就是资源类里面具体方法的区别:
class Data//资源类
{
private int num = 0;
Lock ll = new ReentrantLock();
Condition cc = ll.newCondition();//Contion接口
public void incres() throws InterruptedException {
ll.lock();
try {
//加一操作
while (num != 0)
{
cc.await();//等待
}
num++;
System.out.println(Thread.currentThread().getName()+"---"+num);
cc.signalAll();//通知其他加一完毕
} finally {
ll.unlock();
}
}
public void decres() throws InterruptedException {
ll.lock();
try {
//加一操作
while (num == 0)
{
cc.await();//等待
}
num--;
System.out.println(Thread.currentThread().getName()+"---"+num);
cc.signalAll();//通知其他加一完毕
} finally {
ll.unlock();
}
}
现在的问题在于线程是一个随机的状态,即执行哪个全看运气,目标为了使其达到一种稳定的状态呢?
condition的精准唤醒
因为condition充当的是一个监视器的作用,所以既可以给所有的线程配同一个,也可以给每一个线程都配一个监视器。
具体实现在于
当num = 1 时,B 和 C 进入各自的方法,分别被 condition2 和 condition3 塞入各自的条件队列 分别是 b 和 c, 即 b 队列有 B 线程, c 队列有 C线程。即通过await方法塞入了方法队列,也就实现了监视器和具体方法的对应。后面再调用signal就是在指定的队列中进行选择了。
package JUC;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo04 {
public static void main(String[] args) {
Data1 dd =new Data1();
new Thread(()->{
for (int i = 0; i < 5; i++) {
dd.printA();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
dd.printB();
}
},"A").start();
new Thread(()->{
for (int i = 0; i < 5; i++) {
dd.printC();
}
},"A").start();
}
}
class Data1
{
private Lock ll =new ReentrantLock();
private final Condition cc1 = ll.newCondition();
private Condition cc2 = ll.newCondition();
private Condition cc3 = ll.newCondition();
private int num =1;
public void printA()
{
ll.lock();
try {//具体业务包括 判断-等待-执行业务-通知
while (num != 1)
{
cc1.await();
}
System.out.println(Thread.currentThread().getName()+"AAAA");
//此时为了体现精确性,就可以唤醒指定的人
num++;
cc2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ll.unlock();
}
}
public void printB()
{
ll.lock();
try {//具体业务包括 判断-等待-执行业务-通知
while (num != 2)
{
cc2.await();
}
System.out.println(Thread.currentThread().getName()+"bbbb");
//此时为了体现精确性,就可以唤醒指定的人
num++;
cc3.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ll.unlock();
}
}
public void printC()
{
ll.lock();
try {//具体业务包括 判断-等待-执行业务-通知
while (num != 3)
{
cc3.await();
}
System.out.println(Thread.currentThread().getName()+"cccc");
//此时为了体现精确性,就可以唤醒指定的人
num=1;
cc1.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
ll.unlock();
}
}
}
八锁现象进而理解锁
例子1

对于上面图这种的情况,无论怎么延时,都是先输出发短信再是打电话,原因也就如上图所示,因为是同一个对象,所以谁先调用同步方法谁就拿到了锁,进而顺序也就固定了。
例子2

当加了一个普通方法后,他不受锁的限制,锁相关的延时他自己的,普通方法也并发执行出去了。
例子3:

多加了一个对象,因为锁的是方法的调用对象,对于这里的两个对象,执行各自的锁里的逻辑。
例子4

加了static 的同步方法,此时直接就锁的是整个类了,包含但不限于锁一个对象的所有情况了。
所以就算这里向前面一样再加一个对象分别调用的话也是先输出发短信,因为对象是属于一个类的。整个类都被锁了。
例子5
此时方法一个锁类和一个所对象,此时不管上面方法调用是用一个对象还是两个对象,都是先执行输出打电话的。
具体理解的话,就理解成,只要锁的东西不一样,是怎么都无法体现的,只有锁一样的东西的情况下,才能去考虑锁的作用。
集合类不安全
List不安全

具体的cow的实现原理?
cow比vector好的方面在于vector实现是通过同步方法的,相对cow的优化来说效率自然低,cow用的就是lock锁来实现。
Set不安全
package JUC;//不安全的set的测试
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
public class Demo05 {
public static void main(String[] args) {
Set<String> ss =new HashSet<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
ss.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(ss);
},String.valueOf(i)).start();
}
}
}
同样的,Set如果这样去定义和操作的话,由于并发操作覆盖的原因,会出现下面的这种异常:
因此针对JUC 的写法,创建安全的类,代码如下图所示:
package JUC;//不安全的set的测试
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
public class Demo05 {
public static void main(String[] args) {
Set<String> ss =new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
ss.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(ss);
},String.valueOf(i)).start();
}
}
}
重点就是创建了cow这么一个并发下安全的集合类。
Map不安全
与前面两个的测试都是一样的,这里就不自己去写了,贴现成的代码有个印象即可

此时改变的即是调用JUC中的安全的map类,即如下图所示

Callable

- 可以有返回值。
- 可以抛出异常。
- 方法不同,这里是call方法。
与线程类和runnable接口之间的关系


代码测试
package JUC;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Demo06 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//new Thread().start();
myth mm =new myth();
FutureTask ff = new FutureTask(mm);
new Thread(ff,"A").start();
//get方法可能会产生阻塞,一般放到最后一行。
String ss = (String)ff.get();
//获取callable的返回结果
System.out.println(ss);
}
}
class myth implements Callable<String>
{
@Override
public String call() throws Exception {
System.out.println("kaishi ");
return "null";
}
}
- 注意这里的结果可能会缓存,后面了解到了再回这里来看
常用辅助类
countdownlatch

代码测试
package JUC;
import java.util.concurrent.CountDownLatch;
//countdownlatch类测试
public class Demo07 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch cc = new CountDownLatch(6);
for (int i = 1; i <= 6; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName());
cc.countDown();
},String.valueOf(i)).start();
}
cc.await();
System.out.println("nongwanle");
}
}
主要就是两个方法,一个是计数器减一,一个是判断计数器的值是否为0,如果不为0,则一直等待到计数器的值减为0为止。等待的在上面这个程序看来就是主线程在等待。
cyclicbarrier

简单来看就可以理解成一个加法计数器。

即可看作当加法计数器达到了给定值的时候,就会执行其中定义的线程。
这里穿插一个对于await方法的理解:
不要看着是通过for循环创建的各个线程就认为他一定是执行完了才跳出执行main方法中的语句,因为线程之间是并行的,就算写到了后面也可能去执行main中的语句,所以在各个线程启动后在后面加入一个await,就会把执行域卡在上面,满足条件后再往下进行。
Semaphore:信号量
package JUC;import java.sql.Time;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public class Demo08 { public static void main(String[] args) { //默认参数为线程数量,理解成能够承载的线程数 //限流的时候可以使用 Semaphore ss =new Semaphore(3); for (int i = 1; i <=6 ; i++) { new Thread(() -> { //acquire可以得到资源 try { ss.acquire(); System.out.println(Thread.currentThread().getName()+"得到了"); TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName()+"zoule "); } catch (InterruptedException e) { e.printStackTrace(); }finally { //relese则是释放资源 ss.release(); } },String.valueOf(i)).start(); } }}

读写锁

代码示例
package JUC;import java.util.HashMap;import java.util.Map;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Demo09 { public static void main(String[] args) { mycah mm = new mycah(); //开启写入线程 for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { mm.put(temp+"",temp+""); },String.valueOf(temp)).start(); } //开启读取线程 for (int i = 0; i < 5; i++) { final int temp = i; new Thread(() -> { mm.get(temp+""); },String.valueOf(temp)).start(); } }}class mycah{ private volatile Map<String,Object> mm = new HashMap<>(); //这就是读写锁,下面的使用也是很类似的 private ReadWriteLock rr = new ReentrantReadWriteLock(); //存入即写入的过程: public void put(String key,Object value) { rr.writeLock().lock(); try { System.out.println(Thread.currentThread().getName()+"写入"+key); mm.put(key,value); System.out.println(Thread.currentThread().getName()+"写入完成"); } catch (Exception e) { e.printStackTrace(); } finally { rr.writeLock().unlock(); } } public void get(String key) { rr.readLock().lock(); try { System.out.println(Thread.currentThread().getName()+"读取"+key); mm.get(key); System.out.println(Thread.currentThread().getName()+"读取完成"); } catch (Exception e) { e.printStackTrace(); } finally { rr.readLock().unlock(); } }}

几点注意的需要说明:
- 这里的读写锁是分种类的,虽然都是读写锁,写锁是独占锁,即只允许一个线程进行,而读锁是共享锁,即可以同时进行读数据。
- 这里还需要加读锁的原因即在于防止在写入的时候就开始去读数据了,要保证写入完毕再开始读取。
阻塞队列

阻塞队列也属于collection的分支,与list和set是同一重量级的。
大概的一个集合相关的包含关系:

用到阻塞队列的场景:
多线程并发处理,线程池中可能都会涉及到阻塞队列的使用。
相关的用法和四种api

超时等待的用法就是参数不同的样子。
package JUC;import java.util.concurrent.ArrayBlockingQueue;public class Demo10 { public static void main(String[] args) { test(); } public static void test() { ArrayBlockingQueue bb =new ArrayBlockingQueue(3); System.out.println(bb.add("a")); System.out.println(bb.add("b")); System.out.println(bb.add("c")); //满的时候再加即会抛出异常 // System.out.println(bb.add("d")); System.out.println(bb.remove()); System.out.println(bb.remove()); System.out.println(bb.remove()); //空的时候再弹就会抛出异常 System.out.println(bb.remove()); }}
public void test2(){ ArrayBlockingQueue bb =new ArrayBlockingQueue(3); System.out.println(bb.offer("a")); System.out.println(bb.offer("c")); System.out.println(bb.offer("b")); //此时就是不抛出异常,有返回值 System.out.println(bb.offer("d")); System.out.println(bb.poll()); System.out.println(bb.poll()); System.out.println(bb.poll()); //同样如上不抛出,返回NUll System.out.println(bb.poll());}
public void test3() throws InterruptedException { ArrayBlockingQueue bb =new ArrayBlockingQueue(3); bb.put("a"); bb.put("b"); bb.put("c"); //再加时没有位置了,但是会一直等待 // bb.put("d"); System.out.println(bb.take()); System.out.println(bb.take()); System.out.println(bb.take()); //同样,空队列再取时也只会等待 System.out.println(bb.take());}
public void test4() throws InterruptedException { ArrayBlockingQueue bb =new ArrayBlockingQueue(3); bb.offer("a"); bb.offer("b"); bb.offer("c"); //也是等待,但不是一直等待,超时就退出。 bb.offer("d", 2,TimeUnit.SECONDS); //同样的取出也是一样的,这里就不写了}
同步队列
即阻塞队列的一种,就理解成只有一个存储空间的阻塞队列即可。体现在具体实现上就是放入了一个元素后不取出是没法进行具体操作的这么一个队列。

对于上面代码的执行结果,就会发现是放入取出这么一个顺序执行了3次的。
浙公网安备 33010602011771号