JUC

2JUC并发

image-20210706095226977

JUC中的休息、暂停、sleep是TimeUnit.SECONDS.sleep()

2.进程和线程和协程

image-20210706102943920

java只能去调用本地方法开启线程,自身是开不了的

java无法操作硬件,他是运行在jvm虚拟机之上的

 

 

 

协程:

  1. 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方。在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。进程线程都是同步机制,而协程则是异步。协程不需要多线程的锁机制。

并行与并发

image-20210707095926515

线程有几个状态

image-20210707181559026

java中的sleep方法是thread.sleep(),而Python是time.sleep

但是在企业中一般不使用thread.sleep

而用

image-20210707181727766

线程中wait和sleep的区别

线程状态

image-20220622110714796

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁

两者都可以暂停线程的执行。

wait() 通常被用于线程间交互/通信,sleep()通常被用于暂停执行。

wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify()或者 notifyAll() 方法。sleep()方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

 

image-20210707182340709

现在wait也需要捕获异常;

企业中开发多线程

image-20210707182254149

image-20210707183030264

3 lock锁

模板:

image-20211009145202392

image-20210707183210616

 

这里reentrantlock又分为

image-20210707183734619

如果一个线程要3小时一个,只要3分钟,那么用公平锁就要排队3小时

 

synchronize和lock锁区别

synchronized关键字修饰的代码相当于数据库上的互斥锁。确保多个线程在同一时刻只能由一个线程处于方法或同步块中,确保线程对变量访问的可见和排它,获得锁的对象在代码结束后,会对锁进行释放。

synchronzied使用方法有两个:①加在方法上面锁定方法,②定义synchronized块。

synchronized详解

synchronized是Java中的关键字,是一种同步锁能够保证代码片段的原子性(也就是当同步代码块出现异常的时候,所有的操作都会回滚)和可见性但是不保证有序性。它修饰的对象有以下几种:

  1. 修饰一个代码块,(:指定加锁对象,对给定对象/类加锁。synchronized(this|object) 表示进入同步代码库前要获得给定对象的锁synchronized(类.class) 表示进入同步代码前要获得 当前 class 的锁注意基本数据类型是不可以用的,必须是object,因为基本类型里面没有objectminitor成员对象)被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象也就是这个锁是单个对象拥有的;

  2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;这个锁是单个对象拥有的

  3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象,也就是这个锁是.class类拥有的;

  4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象也就是这个锁是.class类拥有的。

【注意】,synchronized是不能直接修饰变量的,会报错Modifier 'synchronized' not allowed here

image-20220224152030295

但是可以通过同步代码块来修饰,举例:

这里很明显resource1的锁和resource2的锁不是同一把锁(因为得到是指定对象的锁,不管这个对象是不是static的所以这里的resource1的锁和resource2的锁是两个对象各自的锁),否则会出现可重入

//////下面这个演示的不是可重入锁,而是死锁
public class DeadLockDemo {
   private static Object resource1 = new Object();//资源 1
   private static Object resource2 = new Object();//资源 2

   public static void main(String[] args) {
       new Thread(() -> {
           synchronized (resource1) {//这里由于还没有对resource2上锁,所以resource2是可以被其他线程获取的
   
               System.out.println(Thread.currentThread() + "get resource1");
               try {
                   Thread.sleep(1000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println(Thread.currentThread() + "waiting get resource2");
               synchronized (resource2) {//这里如果resource2没有被其他线程上锁,并且可以获取,就会再次上锁,但是这个锁和上面锁resource1的是不同的,因为是对不同对象加锁
                   System.out.println(Thread.currentThread() + "get resource2");
              }
          }
      }, "线程 1").start();

       new Thread(() -> {
           synchronized (resource2) {
               System.out.println(Thread.currentThread() + "get resource2");
               try {
                   Thread.sleep(1000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println(Thread.currentThread() + "waiting get resource1");
               synchronized (resource1) {
                   System.out.println(Thread.currentThread() + "get resource1");
              }
          }
      }, "线程 2").start();
  }
}
  • 尽量不要使用 synchronized(String a) 因为 JVM 中,字符串常量池具有缓存功能!

加入使用了

    private static String resource1 = "abc"
   private static String resource2 = "abc"
       //那么你下面获取resource1和source2就是同一个对象的锁,因为“abc”会被放到常量池中
     public static void main(String[] args) {
       new Thread(() -> {
           synchronized (resource1) {
   
               System.out.println(Thread.currentThread() + "get resource1");
               try {
                   Thread.sleep(1000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println(Thread.currentThread() + "waiting get resource2");
               synchronized (resource2) {
                   System.out.println(Thread.currentThread() + "get resource2");
              }
          }
      }, "线程 1").start();

 

 

 

public void synchronized method(){}

synchronized(mythread.class){},,例如:

image-20211020102152339

synchronized底层

在javaguide->并发编程->进阶篇->1.3

synchronized 同步语句块的实现使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法。

不过两者的本质都是对对象监视器 monitor 的获取。

 

synchronized详解与Lock区别

首先synchronized获得锁的对象是根据修饰的不同而不同,一般分为对象和.class模板

而Lock的锁是来自其新建的new ReentrantLock对象

1.synchronize是内置关键字,lock是java类

2.synchronize无法判断获取锁的状态,lock可以判断是否获得了锁

3.synchronize会自动释放锁(正常执行完程序或者抛异常都会自动释放),lock必须手动释放锁,不释放锁就会死锁

4.synchronize 线程1(获得锁,阻塞)线程2(等待,傻傻的等)因为非公平锁;lock锁就不一定会等待下去,lock.trylock可以尝试获取锁

5.synchronize可重入锁,不可以中断,只能是非公平锁;lock。可重入锁,可以判断锁,非公平锁或者公平锁(可自己设置)

6.synchronize适合锁少量的同步代码,lock适合大量的同步代码。lock更灵活

7.synchronized关键字与wait()notify()/notifyAll()方法(this.wait()和this.notify())相结合可以实现等待/通知机制。ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition()方法。

锁是什么,如何判断锁是谁!

4生产者和消费者synchronized

wait和notifyAll

wait和notifyAll都是通过对象的this调用的

wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放),调用wait方法的一个或多个线程就会解除wait状态,重新参与竞争对象锁,程序如果可以再次得到锁,就可以继续向下运行。

 

1)wait()、notify()和notifyAll()方法是本地方法,并且为final方法,无法被重写。

 

 2)当前线程必须拥有此对象的monitor(即锁),才能调用某个对象的wait()方法能让当前线程阻塞,

(这种阻塞是通过提前释放synchronized锁,重新去请求锁导致的阻塞,这种请求必须有其他线程通过notify()或者notifyAll()唤醒重新竞争获得锁)

 

 3)调用某个对象的notify()方法能够唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;(线程去竞争)

(notify()或者notifyAll()方法并不是真正释放锁,必须等到synchronized方法或者语法块执行完才真正释放锁)

 

 4)调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程,唤醒的线程获得锁的概率是随机的,取决于cpu调度

image-20211009150045056

面试:单例模式、排序、生产者消费者、死锁问题

image-20210707202723673

notifyall就是唤醒等待的线程

如果有ABCD4个线程:用以上方法可能存在虚假唤醒

image-20210707203226261

把if判断改为while判断,因为多个线程都被唤醒了,很可能其中一个唤醒的线程,先一步改变的condition. 此时另一个线程的condition已经不满足,因此需要加Where再次判断,参考下列代码和结果:

if唤醒之后不会再次判断,while唤醒之后会再次判断

 

5 JUC版本生产者消费者

image-20211009144809956

新版:

Lock lock = new ReentrantLock( );
Condition condition = lock.newCondition( );
condition.await();//等待
condition.signalALL()); //唤醒全部

condition优势

目前线程是随机状态执行

image-20211009145522399

如何有序执行呢

image-20211009155023147

 

6 8锁现象

8锁就是关于锁的8个问题

synchronize锁的对象是方法的调用者

1

image-20211009160307410

.由于多线程是共享主线程的堆和方法区的,所以能够使用主线程的对象方法

image-20211009160038338

两个都是先发短信再打电话,因为锁的存在,谁先拿到锁,谁就先执行

2.hello方法没有锁

增加一个普通方法后,由于发短信延时4s,所以先执行hello再执行发短信

 

3.两个对象,先执行打电话,因为发短信延时4s,并且两个锁不一样

image-20211009160431746

4.static synchronize静态方法,锁的对象是class模板,与对象无关。还是发短信优先。两个都是静态锁

image-20211009160711811

image-20211009161908524

5.普通同步和静态同步,一个锁对象,一个锁class模板,是两把锁

image-20211009162102989

image-20211009162134828

小结

普通同步锁,锁的是对象

静态同步锁,锁的是class模板

7 集合类不安全

出现并发修改异常:ConcurrentModificationException

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.Vector;

public class ArrayListNotSafeDemo {
public static void main(String[] args) {
       List<String> list = new ArrayList<>();
       //List<String> list = new Vector<>();
       //List<String> list = Collections.synchronizedList(new ArrayList<>());

       for (int i = 0; i < 30; i++) {
           new Thread(() -> {
               list.add(UUID.randomUUID().toString().substring(0, 8));
               System.out.println(list);
          }, String.valueOf(i)).start();
      }
}
}

 

List不安全

解决方案:1.是用Vector,这是安全的 2.使用collection把集合变安全Collections.synchronizedList( new ArrayList<>());

3.使用JUC下面的CopyOnWriteArrayList(写入时复制),这个比vector牛逼,因为vector使用synchronize而后者用的lock

Copyonwrite 写入时复制COw计算机程序设计领域的一种优化策略;//多个线程调用的时候,List,读取的时候,固定的,写入(覆盖) 在写入的时候避免覆盖,造成数据问题,是读写分离的

image-20211009163205261

image-20211009163805349

方案二:Collections.synchronized() 采用Collections集合工具类,在ArrayList外面包装一层 同步 机制

 

上一节程序导致抛java.util.ConcurrentModificationException的原因解析

综上所述,假设线程A将通过迭代器next()获取下一元素时,从而将其打印出来。但之前,其他某线程添加新元素至list,结构发生了改变,modCount自增。当线程A运行到checkForComodification(),expectedModCount是modCount之前自增的值,判定modCount != expectedModCount为真,继而抛出ConcurrentModificationException。 方案三:

CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想 写时复制,CopyOnWrite容器即写时复制的容器,往一个容器中添加元素的时候,不直接往当前容器Object[]添加,而是先将Object[]进行copy,复制出一个新的容器object[] newElements,然后新的容器Object[] newElements里添加原始,添加元素完后,在将原容器的引用指向新的容器 setArray(newElements);这样做的好处是可以对copyOnWrite容器进行并发的读 ,而不需要加锁,因为当前容器不需要添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器

public boolean add(E e) {
       final ReentrantLock lock = this.lock;
       lock.lock();
       try {
           Object[] elements = getArray();
           int len = elements.length;
           Object[] newElements = Arrays.copyOf(elements, len + 1);
           newElements[len] = e;
           setArray(newElements);//讲原引用指向新的
           return true;
      } finally {
           lock.unlock();
      }

 

 

Set不安全

解决方法: ①Collections.synchronizedSet(new HashSet<>()) ②CopyOnWriteArraySet<>()(推荐,这是JUC提供的)

hashset的底层就是hashmap,就是用了hashmap的key,因为key是不重复的

public HashSet(){
   map=new HashMap<>();
}
//add set本质就是map key是无法重复的,这也就是为什么set是无序的,因为没法对key进行排序存储

public boolean add(E e) {
       return map.put(e, PRESENT)==null;//这里的PRESENT是一个常量new Object
  }
private static final object PRESENT = new object();//不变得值!

与list同理

HashMap不安全

解决方法:

  1. HashTable

  2. Collections.synchronizedMap(new HashMap<>())

  3. ConcurrencyMap<>()(推荐,这是JUC提供的)

重要:加载因子0.75和初始化容量16

 

关于HashMap为什么是线程不安全的原因

原因:

我们知道hashmap的扩容因子是0.75,如果hashmap的数组长度已经使用了75%就会引起扩容,会新申请一个长度为原来两倍的桶数组,

然后将原数组的元素重新映射到新的数组中,原有数据的引用会逐个被置为null。就是在resize()扩容的时候会造成线程不安全。

另外当一个新节点想要插入hashmap的链表时,在jdk1.8之前的版本是插在头部,在1.8后是插在尾部。

那么hashmap什么时候进行扩容呢?当hashmap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,

那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为216=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,

所以如果我们已经预知hashmap中元素的个数,那么预设数组的大小能够有效的提高hashmap的性能。

(1)在put的时候,因为该方法不是同步的,假如有两个线程A,B它们的put的key的hash值相同,不论是从头插入还是从尾插入,假如A获取了插入位置为x,

但是还未插入,此时B也计算出待插入位置为x,则不论AB插入的先后顺序肯定有一个会丢失;

(2)在扩容的时候,jdk1.8之前是采用头插法,当两个线程同时检测到hashmap需要扩容,在进行同时扩容的时候有可能会造成链表的循环,

主要原因就是,采用头插法,新链表与旧链表的顺序是反的,在1.8后采用尾插法就不会出现这种问题,同时1.8的链表长度如果大于8就会转变成红黑树。

 

 

8 callable()

image-20211012105146232

1、可以有返回值 2、可以抛出异常 3、方法不同,run(/ call)

 

线程启动的方式其实只有一个new Thread().start();

 

代码测试

image-20211012105957057

因为FutureTask里面的构造器能够接收callable,并且FutureTask是Runnable的实现类

使用FutureTask.get()获得callable的返回结果 ,get()方法可能会产生阻塞,等待结果的产生,或者使用异步通信,结果产生了才去拿,没产生就先执行后面的代码

image-20211012110142366


细节:

1.有缓存

2.结果可能需要等待,会阻塞

image-20211012110553649

image-20211012110515338

这里其实就是只会有线程A执行,线程B不会执行,因为AB执行的是同一个对象,检测到futuretask已经执行过,就不会再执行了

8 常用辅助工具类

下面的类都是AQS的组件

8.1 CountDownLatch减法计数器(倒计时)

CountDownLatch(倒计时器): CountDownLatch 是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。

image-20211012110855522

image-20211012111258697

如果不使用.await则会出现没有归0就close door的情况,使用之后必须归0才能关门,但是里面线程执行的顺序是不确定的

 

8.2 Cyclicbarrier 加法器

CyclicBarrier(循环栅栏): CyclicBarrierCountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。

image-20211013102134410

很明显线程是在await会执行+1操作,之后线程沉睡,然后,当到达7就会被唤醒,结束线程

也就是所有之前的线程都会等待知道最后一个线程出现才会同时进行

 

 

8.3 Semaphore 信号量

例子:抢车位 6辆车抢3个车位

image-20211013102711293

Semaphore.acquire():获得,假设已经满了,就会等待被释放

Semaphore.release():释放,会将当前信号量+1,然后唤醒等待的线程

作用:多个共享资源互斥的使用!并发限流,控制最大的线程数!

 

9 ReadWriteLock读写锁

读可以被多个线程读,写只能被一个线程,读锁和写锁是互斥的马也就是读的时候不能写

读的时候也需要加锁这样能保证读的时候不能写入,就没有脏数据进来

ReadwriteLock

读-读 可以共存! 读-写 不能共存! 写-写 不能共存!

image-20211013105442329

image-20211013105506869

这里会出现一个问题,某一个写完之后其实是可以允许读的,但是写的过程中不允许读

10 阻塞队列

image-20211013105826004

在java.util中有

image-20211013105939210

blockingqueue是继承collection接口(又继承了iterable接口)的子接口,与list和set并列

Queue的实现类LinkedList

image-20211013110837133

什么情况下我们会使用阻塞队列:多线程并发处理,线程池!

队列四组API

image-20211014152419269

阻塞等待,是如果队列满了会等待,并且不会抛异常,没有返回值,会卡住

image-20211014152623856

image-20211014152638848

image-20211014152548496

 

同步队列synchronousqueue

不存储元素,放入一个元素之后必须要先取出来,才能再放入,相当于等待的阻塞大小为1的队列

 

11 线程池

线程池:三大方法、7大参数,4种拒绝策略

 

池化技术

程序的运行,本质:占用系统的资源!优化资源的使用!=>池化技术 线程池、jdbc连接池、内存池、对象池// 因为创建和销毁十分浪费资源,所以只创建一次,然后将其保存起来,要用的时候就去拿,用完就放回来

池化技术︰事先准备好一些资源,有人要用,就来我这里拿,用完之后还给我。

线程池的好处:

降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

 

线程复用、可以控制最大并发数、管理线程

 

Executors 三大方法

Executors是ThreadPoolExecutor的工具类(一般带有s的都是工具类),三大方法

//执行的时候
threadPool.execute(()->{})

 

使用线程池之后使用线程池进行创建线程

image-20211014154225543

1.newSingleThreadPool: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

image-20211014154149282

2.newFixedThreadPool:该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

3.newCacheThreadPool:该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 量的请求,从而导致 OOM。

  • FixedThreadPoolSingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。

CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。

 

 

同理

源码分析

三个类底层都是调用的ThreadpoolExecutor

image-20211014154607633

注意这里可伸缩线程最大是integer的值,是21亿,但是服务器不可能跑这么多,可能出现资源耗尽OOM,阿里规则建议使用ThreadpoolExecutor来创建

七个参数

其中阻塞队列就是候客区,

image-20211014154750737

正常创建:

image-20211015104018379

    private static final int CORE_POOL_SIZE = 5;
   private static final int MAX_POOL_SIZE = 10;
   private static final int QUEUE_CAPACITY = 100;
   private static final Long KEEP_ALIVE_TIME = 1L;

   public static void main(String[] args) {

       //使用阿里巴巴推荐的创建线程池的方式
       //通过ThreadPoolExecutor构造函数自定义参数创建
       ThreadPoolExecutor executor = new ThreadPoolExecutor(
               CORE_POOL_SIZE,
               MAX_POOL_SIZE,
               KEEP_ALIVE_TIME,
               TimeUnit.SECONDS,
               new ArrayBlockingQueue<>(QUEUE_CAPACITY),
               new ThreadPoolExecutor.CallerRunsPolicy());

       for (int i = 0; i < 10; i++) {
           executor.execute(() -> {
               try {
                   Thread.sleep(2000);
              } catch (InterruptedException e) {
                   e.printStackTrace();
              }
               System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now());
          });
      }
       //终止线程池
       executor.shutdown();
       try {
           executor.awaitTermination(5, TimeUnit.SECONDS);
      } catch (InterruptedException e) {
           e.printStackTrace();
      }
       System.out.println("Finished all threads");
  }

 

分析:核心线程池是永远会开的,其余的最大线程池减去核心线程池是当候客区已经满了,忙不过来的时候会开

所以12是核心线程池,12345是最大线程池,候客区就是阻塞队列,当候客区满了之后才回去开启最大线程里面的

image-20211014155339426

人太多了,红色的也营业,

image-20211014155420858

 

四种拒绝策略

image-20211015104339319

线程池最大承载是最大线程数+阻塞区

abortPolicy:不处理新进入的,并抛出异常

callerrunsPolicy:哪里来的去哪里,队列满了交给main线程处理,不会抛出异常

DiscartPolicy:任务满了就丢掉新的,不抛出异常

DiscartOldPolicy:把最前面的线程丢掉,执行最新的,不抛出异常

总结:拒绝新线程(1.抛出异常 2.不抛出异常) 交给main线程 执行新的,丢掉最前面的

如何设置最大线程数

 

CPU密集型(CPU-bound) CPU密集型也叫计算密集型,指的是系统的硬盘、内存性能相对CPU要好很多,此时,系统运作大部分的状况是CPU Loading 100%,CPU要读/写I/O(硬盘/内存),I/O在很短的时间就可以完成,而CPU还有许多运算要处理,CPU Loading很高。

在多重程序系统中,大部份时间用来做计算、逻辑判断等CPU动作的程序称之CPU bound。例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。

CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。

IO密集型(I/O bound) IO密集型指的是系统的CPU性能相对硬盘、内存要好很多,此时,系统运作,大部分的状况是CPU在等I/O (硬盘/内存) 的读/写操作,此时CPU Loading并不高。

I/O bound的程序一般在达到性能极限时,CPU占用率仍然较低。这可能是因为任务本身需要大量I/O操作,而pipeline做得不是很好,没有充分利用处理器能力。 计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。(因为每个线程都要单独用到cpu

计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。

第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用

IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。

 

cpu密集型(需要CPu运算的):这个就直接吧线程数设置为cpu核数,用这个获取Runtime.getRuntime( ).availableProcessors()

io密集型:io是十分耗时的,比如15个大型任务,设置30个线程

12 4大函数式接口

新时代的程序员:lambda表达式、链式编程、函数式接口、Stream流式计算

Java中超级多FunctionInterface函数式接口

只要是函数式接口都可以用lambda表达式简化

函数式接口:只有一个方法的接口

例如:runnable接口

image-20211015110512729

 

例如:foreach的中consumer参数

 public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
@SuppressWarnings("unchecked")
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}


@FunctionalInterface
public interface Consumer<T> {


void accept(T t);

default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
}

 

Function函数型接口

Function 函数型接口,有一个输入,有一个输出

image-20211015112142148

注意,这里的T和R是指在这个泛型中会用到的两个东西,并不是特指后面的就是返回值

用lambda表达式简化

image-20211015112440011

一个参数str,可以省略()

Predicate 断定型接口

输入值,返回布尔值

image-20211015112659484

 

Consumer 消费型接口

只有输入,没有返回值

@FunctionalInterface
public interface Consumer<T> {

/**
* Performs this operation on the given argument.
*
* @param t the input argument
*/
void accept(T t);


ArrayList<Object> list = new ArrayList<>();
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String o) {
System.out.println('aaa');
}
};

 

 

Supplier供给型接口

只有返回值,没有输入

image-20211015113114628

 

13 Stream流式计算

一定会用到Arrays.asList

大数据:存储+计算

集合、Mysql本质就是存储东西

计算都交给流来做;

image-20211015145032266

理解:list里面的数据转化为流,然后流依次通过filter,只有返回为true的才能通过,并保存起来

这里面的lambda表达式相当于匿名类重写接口的方法

image-20211015152841434

注意这里map应该改为

.map((u)->{u.setname(u.getname().touppercase);return u})这样才能转化为user类

map是映射方法去对流进行操作,但如果流中包含流,map不会将流整合,去将流数据里面的流数据作为基本对象操作

经过map处理后,得到的东西会放在一起,例如这里就是返回的一推大写U的name

image-20211111161042499

这里的map就是得到了一堆字符串

ForkJoin

JDK1.7之后出现的,并行执行任务,提高大数据效率

大数据:Map Reduce(把大任务拆分为小任务)

image-20211015153235478

这就是类似分治算法

 

forkjoin特点:工作窃取

两个线程AB,B的任务做完了之后,可以拿A的任务过来帮忙跑,

image-20211015162845487

image-20211015162743313

这一行小狂神讲的很撇,没听懂

 

这里是用0+到10亿来,区分普通方法和forkjoin以及并行流

image-20211015162800551

这里使用并行流更快,stream

image-20211015162652782

 

 

15 异步回调

异步回调不用等待,我们最初学的异步调用就是ajax,就是我们请求后不需要立即拿到结果,当他拿到之后返回给我们就行了

首先看一下Future接口,是对未来的get方法返回的结果类型

 

异步回调相当于是多线程,在main线程执行到异步的时候会再开一个线程,main会继续往下执行,然后那个线程之后会返回来值

但是异步与多线程区别是,多线程没有返回值,异步回调是有返回值的

image-20211015170134390

 

 

16 JMM

请你谈谈对volatile的理解

volatile是java虚拟机提供的轻量级同步机制

1.保证可见性

2.不保证原子性

3.禁止指令重排(保证有序性)

 

什么是JMM

JMM:java内存模型,是不存在的,是一种概念,约定

关于JMM的一些同步的约定∶ 1、线程解锁前,必须把共享变量立刻刷回主存。(把虚拟栈里面的变量搞到主线程里面的虚拟栈去) 2、线程加锁前,必须读取主存中的最新值复制一份到线程的工作内存中!

3、加锁和解锁是同一把锁

image-20211016104306476

可见性:线程A修改了值之后了,线程B要及时的能够读取到修改后的值

8种操作: image-20211016163443883

volatile

javaGuide->并发编程-》进阶篇-》2

volatile关键字并不是保证多线程安全的,因为他并没有锁,一个线程获取了之后在处理,另一个线程也可以获取,虽然有可见性,但是仅仅一个volatile并不能保证多线程安全,synchronized+volatile可以保证(看下面volatile没有原子性有例子

synchronized 关键字和 volatile 关键字是两个互补的存在(一般一起使用),而不是对立的存在!

volatile 关键字是线程同步的轻量级实现,所以 volatile性能肯定比synchronized关键字要好 。但是 volatile 关键字只能用于变量而 synchronized 关键字可以修饰方法以及代码块

volatile 关键字能保证数据的可见性,但不能保证数据的原子性。synchronized 关键字两者都能保证。

volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。

 

 

主内存所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)

本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

 

原子性 : 一次操作或者多次操作,要么所有的操作全部都得到执行并且不会受到任何因素的干扰而中断,要么都不执行。synchronized 可以保证代码片段的原子性。

可见性 :当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。注意当一个线程更新了值之后会刷进主内存,并且其余使用到这个变量的线程也会更新这个变量

有序性 :代码在执行的过程中的先后顺序,Java 在编译器以及运行期间的优化,代码的执行顺序未必就是编写代码时候的顺序。volatile 关键字可以禁止指令进行重排序优化

 

 

volatile可见性

可见性也就是,当这个值发生改变之后,会通知别的线程,将别的线程里面的值也变为改变后的值

image-20211016163940212

下面这个程序是不会停下来,因为A线程只复制了第一次的值,第二次主线程更改后已经刷新进了主内存,但是A线程并没有复制变化后的值,所以我们需要让线程A知道主内存这个值变化了。

这个volitile就能解决,保证了可见性,加了volitale就可以结束,保证值的可见性

image-20211016164551666

volatile没有原子性

image-20211019105911881

注意看字节码,原子性能保证这3步操作一口气执行完,不可分割,

但是由于add方法没有保证原子性,因此++操作是不能直接执行完的,这里你可以使用CAS原理(其实现类的AtomicInteger)即来实现++操作,通过调用atomicInteger.getAndIncrement()方法实现原子性,具体的看下面CAS里讲的

线程A在执行任务的时候,不能被打扰的,也不能被分割。要么同时成功,要么同时失败。

image-20211016165709388

这个结果不是20000

这个原因是因为可见性

如果加了sychronized之后就是一个线程技术之后还是20000,因为他不仅有可见性,还保证了原子性

image-20211019104830716

如果加了volatile之后还不是20000,因为他不保证原子性因为++操作的时候,有三步,取值,+1,赋值给原变量。所以在+1的操作的时候,可能别的线程已经+1了,最终两个add方法写回的时候只加了1次,这样就会错误

 

如果不加lock和sychronize怎么保证原子性:

1.首先打开测试代码

2.通过javap -c xxx.class,可以将.class文件展示处理,.class文件就是字节码文件,下面这个就是字节码文件,跟汇编操作一样.

  • javap是jdk自带的反解析工具。它的作用就是根据class字节码文件,反解析出当前类对应的code区(字节码指令)、局部变量表、异常表和代码行偏移量映射表、常量池等信息。javap -c会对当前class字节码进行反编译生成汇编代码

  • image-20211019165603364

image-20211019105645296

3.使用java.concurrent.atomic原子包装类,这就能实现原子性

image-20211019164430939

这个原子类的底层是采用了CAS(直接与操作系统挂钩,在内存中修改值,使用Unsafe类),这个比synchronize和lock还要高效

volatile禁止指令重排

什么是指令重排:你写的程序,计算机并不是按照你写的那样执行的,计算机会自己优化,是汇编码的重排

源代码-->编译器优化重排--》指令并行也可能重排--》内存系统也可能重排--》执行

处理器在进行指令重排的时候,考虑∶数据之间的依赖性!

单线程没有影响,指令重排主要影响多线程

image-20211019192319881

加了volatile会在底层加内存屏障,不允许上面的程序与下面的程序顺序颠倒

image-20211019192555529

 

17 单例模式

 

 

18 深入理解CAS

(71条消息) java CAS详解奔跑灬小熊的博客-CSDN博客cas java

CAS是CPU的并发原语(也就是cpu的指令),compareAndSwap:比较并交换

CAS(compare and swap),比较并交换。可以解决多线程并行情况下使用锁造成性能损耗的一种机制.CAS 操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。一个线程从主内存中得到num值,并对num进行操作,写入值的时候,线程会把第一次取到的num值和主内存中num值进行比较,如果相等,就会将改变后的num写入主内存,如果不相等,则一直循环对比,知道成功为止。

CAS∶比较当前工作内存中的值和主内存中的值,如果这个值是期望的,那么则执行操作!如果不是就—直循环!

AtomicInteger a=new AtomicInteger(num/或者2020);//输入需要设置原子性的变量,或者值
a.compareAndSet(2020,2021)//这个里面就用到了,含义是:如果拿到了我期望的2020那么就更新为2021,如果没有拿到就不更新

-----源码---------
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

也就是说atomicInteger调用的是unsafe类的方法
而usafe类基本是native方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

image-20220226145443370

上面这个图中的getAndAddInt是AtomicInteger的方法,里面调用了compareAndSwapInt()这是一个原语方法,底层调用c++

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);//
参数含义:如果var1对象的内存地址偏移里的值var2,等于var4,那么就让这里面的值等于var5
也就是说如果拿到我们的期望值var4,就把内存地址里的值改为var5//有点像我们的乐观锁,会去对比目前的值和之前取到的值

 

Unsafe类:java一般情况下想要操作内存只能调用本地方法c++写的,也就是这个unsafe类也可以封装了本地方法操作内存

image-20211019194855192

compareAndSwap(a,b):如果是传入的是期望值,则交换a、b

缺点∶ 1、循环会耗时 2、一次性只能保证一个共享变量的原子性(这里因为原来的普通方法num++是没有原子性的,因此++方法在执行的时候会被拆分为三步)

比如

-------没有原子性--------------
public void add(int num)
{
num++;
}

-------有原子性--------------
public synchronized void add(int num)
{
num++;
}

-------有原子性--------------

public AtomicInteger atomicInteger = new AtomicInteger(0);//相当于我们定了一个初值为0的num
//---或者
int num=0;
public AtomicInteger atomicInteger = new AtomicInteger(num);//相当于我们定了一个初值为0的num
public void add()
{
atomicInteger.getAndIncrement();//这里又是有原子性的,不会出现volatile没有原子性出现的问题
}

 

3、ABA问题

原子类的使用(Atomic)

为什么要使用原子类(CAS)

AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销执行效率大为提升。 (不会像锁一样多线程效率比较低)

 

class Test {
private volatile int count = 0;
//若要线程安全执行执行count++,需要加锁
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

② 多线程环境使用原子类保证线程安全(基本数据类型)

 

class Test2 {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}
//使用AtomicInteger之后,不需要加锁,也可以实现线程安全。
public int getCount() {
return count.get();
}
}

 

JUC包装的原子类(Atomic):

基本类型(用于对基本类型包装为原子性)

使用原子的方式更新基本类型

  • AtomicInteger:整形原子类

    public final int get() //获取当前的值
    public final int getAndSet(int newValue)//获取当前的值,并设置新的值
    public final int getAndIncrement()//获取当前的值,并自增
    public final int getAndDecrement() //获取当前的值,并自减
    public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
    boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式(原子性)将该值设置为输入值(update)
    public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
  • AtomicLong:长整型原子类

  • AtomicBoolean:布尔型原子类

数组类型(用于对数组包装为原子性)

使用原子的方式更新数组里的某个元素

  • AtomicIntegerArray:整形数组原子类

  • AtomicLongArray:长整形数组原子类

  • AtomicReferenceArray:引用类型数组原子类

引用类型(用于对引用类型包装为原子性)

  • AtomicReference:引用类型原子类

  • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。只有这个原子类能够解决ABA问题

  • AtomicMarkableReference :原子更新带有标记位的引用类型

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {

public static void main(String[] args) {
AtomicReference<Person> ar = new AtomicReference<Person>();
Person person = new Person("SnailClimb", 22);
ar.set(person);
Person updatePerson = new Person("Daisy", 20);
ar.compareAndSet(person, updatePerson);

System.out.println(ar.get().getName());
System.out.println(ar.get().getAge());
}
}

class Person {
private String name;
private int age;

xxxxx..........

}

 

对象的属性修改类型

  • AtomicIntegerFieldUpdater:原子更新整形字段的更新器

  • AtomicLongFieldUpdater:原子更新长整形字段的更新器

  • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器

 

 

CAS中的ABA问题

即是狸猫换太子的问题,是因为没有使用AtomicStampedReference带有版本号的

这是CAS在多线程中遇到的,1 2是两个线程,本身来说我认为也可以用synchronize解决

image-20211019195534170

A不知道B已经对a进行变为3再变为1的操作,A线程只看到了现在a的值还为1.就以为没变

可以用原子引用解决ABA问题(原子引用采用了乐观锁机制,也有版本号)

原子引用

用于解决ABA问题,对应思想是:乐观锁

原子引用是代版本号的原子操作,这个跟乐观锁就是一样的了,也就是每次对使用了原子引用的这个变量进行操作的时候版本号都会+1,每次比较的时候是比较版本号了,不只是变量值

AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。

Integer num=1;
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(num,1);//这里的1就是时间戳或者版本号

image-20220226153135283

这样别人拿到你第三次修改的数字之后,就算是CSA比较了期望值和现在的值是一样的,但是版本号不一样,也不会执行

 

19 AQS

在javaguide-》并发编程-》进阶篇

AQS是各种lock锁实现的原理

 

各种锁的理解

1. 公平锁和非公平锁

所谓的公平锁就是先等待的线程先获得锁

image-20211019200800631

公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。

  • 优点:所有的线程都能得到资源,不会饿死在队列中。(因为)总会轮到他

  • 缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。

非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。

  • 优点:非公平锁的优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁也就说之前有线程因为没获得锁被阻塞了,但此时锁释放的时候,又有新的线程进来想获得这个锁,那么cpu就不会去唤醒原来阻塞的线程,而是直接给新进来的),CPU 不必唤醒所有线程

  • 缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。(也就说有线程一直获取不到锁,一直是别人获取)

我举个例子给他家通俗

NonfairSync 非公平的意思就是管你三七二十一,我先尝试给共享资源加锁,如果加锁成功就阻塞其他线程(因为其他线程都在队列中排队,这个时候就特别的霸道而显得不公平),如果是共享资源上已经被加锁了,这个时候还要再判断下能不能加锁,两次尝试加锁都失败再霸道也没用了,就只能老老实实去队列尾部排队! 还是去超市购物后买单,只有一个收银台,这个收银台也只能服务一个顾客。当买单的人特别多,大家都排着队等着。这个时候来了个壮汉,仗着自己高大枉顾排队的游戏规则,直接跑到收银台看有没有人正在买单,如果没有人正在买单就直接插队买单。如果看了两眼还是有人正在买单,那就规规矩矩到队尾排队。但是对于非公平锁,管理员对打水的人没有要求。即使等待队伍里有排队等待的人,但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时,刚好来了一个插队的人,这个插队的人是可以直接从管理员那里拿到锁去打水,不需要排队,原本排队等待的人只能继续等待。

2.可重入锁

“可重入锁” 指的是自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增 1,所以要等到锁的计数器下降为 0 时才能释放锁

拿到外面门的锁,就可以拿到里面门的锁,这是自动获得的

image-20211019200900322

 

image-20211019202128290

【注意】这里由于在对象内创建了一个lock锁,所以两个方法使用的lock都是同一把,所以在第一个lock中调用第二个lock是可以获取到的,因为两个lock是同一把,所以是可重入的

锁必须配对,加锁几次就要解锁几次

package com.test.reen;

// 演示可重入锁是什么意思,可重入,就是在一个锁住的代码块中可以重复获取与这个锁相同的锁,synchronized和ReentrantLock都是可重入的
// 可重入降低了编程复杂性
public class WhatReentrant {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (this) {
System.out.println("第1次获取锁,这个锁是:" + this);
int index = 1;
while (true) {
synchronized (this) {
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + this);
//可以发现没发生死锁,可以多次获取相同的锁,也就是说我这里是要获取this,但是this在一开始就被上了锁,按照正常情况下一个锁在使用的时候,别人是没办法再获取的,但是由于是可重入锁,因此可以在一个锁中获取到相同的锁
}
if (index == 10) {
break;
}
}
}
}
}).start();
}
}
//这里打印出来的都是同一把锁,是通过同一把锁去上锁的。
//-------------------------------------Lock----------------------------------------------
package com.test.reen;

import java.util.Random;
import java.util.concurrent.locks.ReentrantLock;

// 演示可重入锁是什么意思
public class WhatReentrant2 {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();

new Thread(new Runnable() {
@Override
public void run() {
try {
lock.lock();
System.out.println("第1次获取锁,这个锁是:" + lock);

int index = 1;
while (true) {
try {
lock.lock();
System.out.println("第" + (++index) + "次获取锁,这个锁是:" + lock);

try {
Thread.sleep(new Random().nextInt(200));
} catch (InterruptedException e) {
e.printStackTrace();
}

if (index == 10) {
break;
}
} finally {
lock.unlock();
}

}

} finally {
lock.unlock();
}
}
}).start();
}
}
/*
第1次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第2次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第3次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第4次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第5次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第6次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第7次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第8次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第9次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
第10次获取锁,这个锁是:java.util.concurrent.locks.ReentrantLock@69a5b91a[Locked by thread Thread-0]
*/

 

 

 

3.自旋锁

这个是AtomicInteger类里面的函数,这里面就用到了自旋锁

这个函数的意义,只要对比var2的值和var5的值不一样,就说明有人改了,我们不能用这个值(跟乐观锁差不多),因此我们需要一直循环到没有人改他,知道我们拿到的值和他本身里面的值一样了,那么就可以进行操作

image-20211019202210330

不断尝试,直到成功为止

image-20211019202904350

这是自己写的自旋锁

image-20211019202802060

首先T1获得锁,并吧atomicreference设置为thread

这时候T2也进入lock,但是由于此时不满足条件会一直while循环

T1解锁之后,条件就发生变化,所以T2就拿到锁,之后就会解锁

 

 

4. 死锁

学过操作系统的朋友都知道产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。

  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态 指的是系统能够按照某种进程推进顺序(P1、P2、P3.....Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3.....Pn>序列为安全序列。

也就是所有线程在分配时都采用同一个资源分配顺序

如:线程1是resource1、resource2 那么线程2也应该resource1、resource2

 

//////下面这个演示的不是可重入锁,而是死锁
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2

public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {//这里由于还没有对resource2上锁,所以resource2是可以被其他线程获取的

System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {//这里如果resource2没有被其他线程上锁,并且可以获取,就会再次上锁,但是这个锁和上面锁resource1的是不同的,因为是对不同对象加锁
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();

new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}

 

什么是死锁

image-20211020100607758

image-20211020100535456

1.可以使用java bin文件下的jps命令来查看哪些进程在运行,由于我们在环境变量添加了JAVA_HOME/bin,所以是可以再系统中直接使用jps.exe

jps -l#查看当前运行进程号

2.jstack查看某个进程是否死锁

jstack 进程号 #查看某个进程的堆栈信息,并给出是否死锁

 

3.处理jstack查看堆栈信息,也可以看日志

 

 

 

 

奇怪的知识点

1.在lambda表达式中,是没办法拿到for循环的值的,但是可以使用final关键字,中间变量,来拿到,因为lambda表达式中只能拿到final型的,因为本身lambda表达式就是一个new对象

image-20211013101357833

2.volatile 保证可见性和指令不可重排

 

3.java虚拟机中有两个线程是默认执行的,main和gc

 

 

主内存与线程本地内存

字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动了 Java 堆中。

  • 主内存 :所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量)

  • 本地内存 :每个线程都有一个私有的本地内存来存储共享变量的副本,并且,每个线程只能访问自己的本地内存,无法访问其他线程的本地内存。本地内存是 JMM 抽象出来的一个概念,存储了主内存中的共享变量副本。

也就是说主内存就是堆和方法区,我之前看到的线程更改值就是因为那个num变量是static的,被放在方法区了,所以可以被别的线程读取到,读取到本地内存可能就是本地方法栈里面

要解决这个问题,就需要把变量声明为 volatile ,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取

volatile 关键字 除了防止 JVM 的指令重排 ,还有一个重要的作用就是保证变量的可见性。:当一个线程对共享变量进行了修改,那么另外的线程都是立即可以看到修改后的最新值。volatile 关键字可以保证共享变量的可见性。(就算另一个线程在使用这个变量,也能够立马看到这个变量被别的线程修改后的值

没有volatile需要等线程执行完之后,才会把值给主内存,有了它之后改了就能给主内存

 

关于ThreadLocal

每一个线程里面都有一个ThreadLocalMap

//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;

当你使用ThreadLocal对象的set()方法。那么你的ThreadLocalMap里面就会被添加

public void set(T value) {//注意这里new的时候是传入的副本变量,这里set也是直接更改副本的,而不是原始的
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);//threadLocals最初线程创建的对象,和当前线程设置的值
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public T get() {//直接获取我们最开始创建的默认值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();//也就是说你只使用的get方法,那么就会给你复制一个副本
}

你可以在你的线程ThreadLocalMap中有很多threadLocals对象,每一个threadLocals对象都代表从主内存复制的一个变量

例如下面这个东西,你就可以在线程中set 3个不同的对象了

public class ThreadLocalExample implements Runnable{

// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
private static final ThreadLocal<SimpleDateFormat> formatter2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
private static final ThreadLocal<SimpleDateFormat> formatter3 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

 

 

 

public class Dog{
private static volatile Dog single;//

private Dog(){//这里的构造器必须写成private,

}

public static Dog getSingle(){
if(simgle==null)
{
Synchronized(Dog.class)
{
if(simgle==null)
{
single=new Dog();
}

}

}
return single;

}

}
 

 

posted @ 2022-07-01 22:09  JJJmk  阅读(175)  评论(0)    收藏  举报