1.1 CPU底层知识
多线程
什么是进程和线程 一个程序进入内存,被称为进程?一个QQ可以运行多分吗? 同一个进程内部,有多个任务并发执行的需求(比如,一边计算,一边接受网络,一边刷新界面) 能不能使用多进程?可以但是毛病多,最严重的毛病是,我可以轻易搞死别的进程 线程的概念横空出世:共享空间,不共享计算 进程是静态的概念:程序进入内存分配对应资源:内存空间,程序进入内存,同时产生一个主线程 进程就是资源 线程是动态的概念:是可以执行的计算单元 ,线程共享进程空间 线程就是一条条指令
-
一个ALU(逻辑计算单元)同一时间只能执行一个线程
-
同一个代码可以被多个线程执行
线程的切换 保存上下文,保存现场 问题:是不是线程线程数量越多,执行效率越高? 肯定不是 问题:单核cpu多线程执行有没有意义 有 问题:对于一个程序,设置多少个线程合适?(线程池设定多少核心线程) cup应该有多少线程=cpu*cpu利用率*(1+wait/c) wait/等待时间 c/计算时间
-
内存到cpu的缓存
缓存一致性协议 CPU的速度和内存速度(100:1) 现在工业实践中,多用三级缓存架构 缓存行:一次性读取的数据块 程序的局部性原来:空间局部性 时间局部性 如果缓存行大:命中率搞,但是读取效率低,命中率低,但读取效率高 工业实践的妥协结果,目前(2021)的计算机多采用64bytis(64*8bit)为一行 由于缓存行的存在,我们必须有一种机制,来保证缓存数据的一致性,这种机制被称为缓存一致性协议。 MESI MOSI 等一些缓存一致性协议
-
Disruptor消息队列
Disruptor是一种环形的数据结构,只需要一个指针 而其他的消息队列是数组结构,需要两个指针
-
什么是缓存行
cpu寄存器从内存中读取数据或指令。一但出现多线程运行的模式,这写线程会抢夺cpu时间片,当A线程抢夺到时间片,B线程就会等待,那么B线程抢夺到时间片,A线程就会等待,如何A线程执行到了一般时间片就被B线程抢走了,那么当A线程重新抢到时间会继续执行尚未完成的,还是会重新执行。答案当然是会继续执行,因为当A线程的时间片被抢走之后,线程执行完成的部分会放到缓存中(内存区域),工业实践妥协的结果,计算机采用三级缓存机制。 在三级缓存机制中,我们是如何读取数据,如何一个一个读,虽然读取很快,但是返回结果慢。一次读取很多,效率会高的很多,但是在读取的过程中会很慢。工业实践的妥协结果,目前(2021)的计算机多采用64bytis(64*8bit)为一行
-
缓存行的代码的演示
这是没开启缓存行式
public class Demo01 { private static long COUNT=10_0000_0000L; private static class T{ // private long l1,l2,l4,l5,l6,l7; public long x=0L; // private long p1,p2,p3,p4,p5,p6,p7,p8; } public static T[] arr=new T[2]; static { arr[0]=new T(); arr[1]=new T(); } public static void main(String[] args)throws Exception { CountDownLatch latch=new CountDownLatch(2); Thread t1=new Thread(()->{ for (long i=0;i<COUNT;i++){ arr[0].x=i; } latch.countDown(); }); Thread t2=new Thread(()->{ for (long i=0;i<COUNT;i++){ arr[1].x=i; } latch.countDown(); }); final long l = System.nanoTime(); t1.start(); t2.start(); latch.await(); System.out.println(System.nanoTime()-l); } }
开始缓存行之后
public class Demo01 { private static long COUNT=10_0000_0000L; private static class T{ private long l1,l2,l4,l5,l6,l7; public long x=0L; private long p1,p2,p3,p4,p5,p6,p7,p8; } public static T[] arr=new T[2]; static { arr[0]=new T(); arr[1]=new T(); } public static void main(String[] args)throws Exception { CountDownLatch latch=new CountDownLatch(2); Thread t1=new Thread(()->{ for (long i=0;i<COUNT;i++){ arr[0].x=i; } latch.countDown(); }); Thread t2=new Thread(()->{ for (long i=0;i<COUNT;i++){ arr[1].x=i; } latch.countDown(); }); final long l = System.nanoTime(); t1.start(); t2.start(); latch.await(); System.out.println(System.nanoTime()-l); }
可以看出来在我们开启缓存行时,代码的执行效率提高了一倍以上
-
多线程的单例模式
饿汉单例
public class Singleton01 { private static final Singleton01 SINGLETON_01=new Singleton01(); private Singleton01(){} public static Singleton01 getSingleton01(){ return SINGLETON_01; } public static void main(String[] args) { Singleton01 a1 = Singleton01.getSingleton01(); Singleton01 a2 = Singleton01.getSingleton01(); System.out.println(a1==a2); } }
懒汉单例
public class Singleton02 { private static Singleton02 SINGLETON_02; private Singleton02(){} public static Singleton02 getInstance(){ if(SINGLETON_02==null){ synchronized (Singleton02.class){ if(SINGLETON_02==null){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } SINGLETON_02=new Singleton02(); } } } return SINGLETON_02; } public static void main(String[] args) { for (int i=0;i<1000;i++){ new Thread(()->{ System.out.println(Singleton02.getInstance().hashCode()); }).start(); } } }
懒汉单例模式在多线程环境下会出现许多错误,所以我们会加锁,保证程序的原子性
DCl单例有没有bug
//当然时有的,在创建了一半对象,这时候对象已经有地址了,当另一个线程过来这就会检测出对象不为空,就会返回一个对象,但是这个对象只有初始值,因为发生了指令重排的问题,才会导致这个bug的出现 以下8中情况下时不准换顺序 hanppens-before原则(JVM规定重排序必须遵守的原则 )
-
CPU乱序执行的概念
CPU乱序执行: cpu的运行速度时内存的100倍 有一条指令把数据读入寄存器,还有一条数据把是把这个数据加1,但是因为CPU的速度比较快,当在等待内存响应数据时,会有一部分空挡时间,这时会执行下一条指令,这就是CPU的乱序执行 为什么会乱序,主要提高效率
-
CPU乱序执行的证明
public class DemoTest01 { private static int x=0,y=0; private static int a=0,b=0; public static void main(String[] args) throws InterruptedException { for(long i=0;i<Long.MAX_VALUE;i++){ x=0; y=0; a=0; b=0; CountDownLatch latch=new CountDownLatch(2); Thread one =new Thread(()->{ a=1; x=b; latch.countDown(); }); Thread other =new Thread(()->{ b=1; y=a; latch.countDown(); }); one.start(); other.start(); latch.await(); String result="第"+i+"次("+x+","+y+")"; if(x==0&&y==0){ System.err.println(result); break; } } } }
-
程序真的时顺序按顺序运行吗?
线程的as-if-serial: 单线程,两条语句,未必是俺顺序运行 单线程的重排序,必须保证最终一致性 as-if-serial:开上去像是序列化(单线程) 会产生的后果 多线程会产生意向不到的结果
-
禁止编译器乱序
-
内存屏障防止乱序执行
内存屏障是特殊指令,看到这种指令,前面必须执行完,后面才能执行
-
JVM的内存屏障
所以实现JVm规范的虚拟机,必须实现四个屏障 LoadLoadBarrier LoadStore SL SS LoadLoad屏障 LoadStore屏障 CPU 如何完成? hotsport:的volatile细节
-
volatile的底层实现
/*本质两大作用: 保证可见性,一个CPU改变的内容,另一个CPU马上可见:一个线程改变内容,则另一个就必须马上可见 禁止重排序:禁止指令重排 保证可见性,但是不保证原子性(多线程操作进行的读写,不保证线程安全);而synchronized是一种排它(互斥)的机制 在多线程的环境下还是要使用锁机制 */ public class CounThread implements Runnable{ private volatile int count=0; private static final Object o=new Object(); @Override public void run() { for(int i=0;i<100;i++) { synchronized (o) { count++; System.out.println("count =========>>>" + count); } } } } /* 变量使用了volatile关键字,可以保证多线程下数据的可见性,但是不保证原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。在多线程环境下还是要使用锁机制 */
-
volatile的使用场景
-
开关控制
利用可见性地特点,控制某一段代码的执行和关闭()
-
多个线程操作共享变量,但是只用一个线程对其进行写操作,其他线程都是读
-
-
解决问题
使用锁机制,给count++加上一把锁,那么count++就是一个临界区,临界区只有一个线程进行操作,这样Count++就变成原子性的操作了。
public class CounThread implements Runnable{ private volatile int count=0; private static final Object o=new Object(); @Override public void run() { for(int i=0;i<100;i++) { synchronized (o) { count++; System.out.println("count =========>>>" + count); } } } }
LOCk用于在多线程中执行指令时对共享内存的独占用
它的作用时能够将当前处理对应缓存的内容刷新,并使用器处理器对应的缓存失效
另外还提供了有序的指令无法越过这个内存屏障的作用
LOCk并非只是锁总线
-
CPU的并发控制:关中断,缓存一致性
JUC编程入门
1.2 线程状态
线程由生到死的完整过程
当线程被创建时,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,有几种状态?在API中java.lang.Thread.State
这个枚举中给出了六中状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法) |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
什么是并行并发
A学生(线程)和B学生(线程)在进入两个门(CPU),不管先后性,这就可以被乘坐是并行。 如果两个学生不管以方式进入,在路人甲眼里,他两个都在办公室,这就是并发
并发: 并发是一种现象,同时运行多个程序或多个被程序处理的对象 并行: 可以通过多进程/多线程的方式取得多任务,并以多进程/多线程的方式执行这些任务
并发与并行的区别 所以,并发和并行的区别就很明显了。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个进程在运行(处于running),并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。
1.3 原子类
概述:java从JDK1.5开始提供了java.util.concurrent.atomic包(简称Atomic包),这个包中的原子操作类提供了一种用法简单,性能高效,线程安全地更新一个变量的方式。
对原有的代码进行改进
public class CounThread02 implements Runnable{ //原子类,定义一个int类型的变量 private AtomicInteger atomicInteger=new AtomicInteger(); @Override public void run() { for(int i=0;i<100;i++) { //以原子方式将当前值加1,注意,这里返回的是自增前的值。 int j = atomicInteger.getAndIncrement(); System.out.println("count =========>>>" +j); } } }
1.4 原子类 CAS机制介绍
CAS原理:比较在交换:
线程A在读取到变量a,并器增强1,这时候线程B也读取到变量B,此时变量B为0,在要把数据写会时,要读取重新读取一下变量a,这时候在进行复制,这就是CAS的比较在交换。
乐观锁和悲观锁
-
悲观锁(synchronized)
synchronized总是从悲观的角度的出发的,每次拿数据都会认为别人会修改,所以在每次拿数据时都会进行上锁,这样别的线程就会阻塞,知道拿到锁。
共享资源只给一个线程使用,其他线程阻塞,用完再把资源转让给其他线程,因此称为synchronized为悲观锁
-
乐观锁(CAS锁)
CAS是从乐观的情况下出发的:
总是假设最好的情况,每次拿数据搜认为别人不会修改,所以不会上锁,但是在更新的时候回去判断一下在此期间有没有人去更新这个数据。
CAS这种机制我们称为乐观锁。综合性能好
1.5 并发包ConcurrentHashMap
并发包的来历: 在实际开发中如果不需要考虑线程安全问题,大家不需要线程安全,因为如果做了反而性能会降低, 但是开发中有很多业务是需要考虑线程安全问题,此时就考虑。否则业务出现问题。 java为很多业务场景提供了性能优异,且线程安全的并发包,程序员可以选择 (原子类)
Map集合中的经典集合:HashMap他是线程不安全的,到那时性能好 -- 如果在要求线程安全的业务情况下就不能使用Map集合,否则业务会崩溃 为了保证线程安全,可以使用HashTable,注意,线程中加入了计时 为了保证线程安全,在看看ConcurrentHashMap(不止线程安全,而且效率高,但是性能好,最新最好用的) --ConcurrentHashMap保证了线程安全,综合性能好
-
HashMap
public class Conset { public static ConcurrentHashMap<String,String> hashMap=new ConcurrentHashMap <>(); }
@Override public void run() { for(int i=0;i<50000;i++){ Conset.hashMap.put(this.getName()+(i+1),this.getName()+i+1); } System.out.println(this.getName()+"结束"); }
进行测试
public static void main(String[] args) throws InterruptedException { Thread t1=new MyThread(); Thread t2=new MyThread(); t1.setName("线程一"); t2.setName("线程二"); t1.start(); t2.start(); Thread.sleep(1000*5); System.out.println("Map的大小"+Conset.hashMap.size()); }
HashMap线程不安全
Hash是一个Hash表,两个线程都往一个数组里存储数据,但是最后只能留下一个,所以线程是不安全的
HashTable线程安全
HashTable的每个方法都加了synchronized锁了
ConcurrentHashMap 实例
新方法
t1.jion //让t1先跑完 t2.jion //让t2跑完 //主线程能抢其他的CPU
小结
HashMap 它是线程不安全,但是性能好 HashTable 锁整个集合线程安全,性能较差!!!淘汰了 ConcurrentHashMap 线程安全,性能得到了极大的提升了!!分段锁!只锁自己操作的元素位置!综合性能最好
1.6 并发包CountDownLath
CountDownLath 允许一个或多个线程等待其他线程完成操作,在执行自己 例如:线程1要执行打印:A和C,线程2要执行打印:B,但线程1在打印A后 要线程2打印B之后才能打印c,所以:线程1在打印A后 必须等待2打印B之后才能继续执行 需求: 提供A线程,打印A,C 提供B线程,打印B public countDownLath(int count) //初始化唤醒需要几步
代码演示
public class MyTest { public static void main(String[] args) { CountDownLatch latch=new CountDownLatch(1); new MyThread1(latch).start(); new MThread2(latch).start(); } } class MyThread1 extends Thread{ private CountDownLatch c; public MyThread1(CountDownLatch c) { this.c = c; } @Override public void run() { System.out.println("A"); //等待自己 try { c.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("C"); } } class MThread2 extends Thread{ private CountDownLatch c; public MThread2(CountDownLatch c) { this.c = c; } @Override public void run() { System.out.println("B"); c.countDown(); } }
1.7 并发包 CycliBarrier
CycliBarrier (循环屏障)作用 某个线程任务必须等待其他线程执行完毕后才能触发执行。 例如:公司召集5名员工开会,等5名员工都到了,会议开始 使用CycliBarrier线程保证5名员工全部执行后,在执行开会线程
-
小结
可以实现多线程中,某个任务等待其他线程执行完毕以后触发
循环屏障可以实现到达一组屏障,就触发一个任务!
代码演示:
public class MTest02 { public static void main(String[] args) { CyclicBarrier cyclicBarrier=new CyclicBarrier(5,new Messing()); for(int i=1;i<=5;i++){ new EmployeeThread("员工"+i,cyclicBarrier).start(); } } } class Messing implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+"开始会议"); } } class EmployeeThread extends Thread{ private CyclicBarrier cyclicBarrier; public EmployeeThread(String name,CyclicBarrier cyclicBarrier){ super(name); this.cyclicBarrier=cyclicBarrier; } @Override public void run() { System.out.println(Thread.currentThread().getName()+"进入会议"); try { cyclicBarrier.await();//每个线程调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前线程放回到线程池里 } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } } }
构造方法 public CyclicBarrier(5,new Messing()); 第一个参数:是会议人数 第二个参数:实现了Ruanble接口,执行完毕会议之后,在进行这个任务 CyclicBarrier 的底层使用的线程池实现的 public void await()方法不是阻塞,只是把线程放回线程池里。
代码演示
1.8 并发包 Semaphore
Semaphore(发信号)的主要作用就是控制线程的并发数量。
synchronize可以起到“锁的”作用,但是在某个时间内,只允许一个线程执行
Semaphore可以设置同时允许几个线程执行。
Semaphore字面意思是信号量的意思,他的作用是控制特地资源的线程数目
Semaphore构造方法
public Semaphore(int permoits) public semaphor (int permits,boolean fair) fair 表示公平性,
代码演示
servie: public class Servie { private Semaphore semaphore=new Semaphore(2); public void testMode(){ try { semaphore.acquire(); System.out.println(Thread.currentThread().getName()+"进入 时间="+System.currentTimeMillis()); Thread.sleep(1000); System.out.println(Thread.currentThread().getName()+"结束 时间="+System.currentTimeMillis()); semaphore.release(); } catch (InterruptedException e) { e.printStackTrace(); } } } Thread: public class MyThread extends Thread{ private Servie servie; public MyThread(Servie servie){ super(); this.servie=servie; } @Override public void run() { servie.testMode(); } } DemoTest: public class DemoTest { public static void main(String[] args) { Servie servie=new Servie(); for(int i=0;i<5;i++){ Thread t1=new MyThread(servie); t1.setName("线程"+i); t1.start(); } } }
允许一个线程执行:
允许两个线程执行
这个并发包可以用在秒杀环节上,设置多长时间进入的线程的个数
1.9 并发包 Exchanger(交换者)
作用: Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger用于进行线程间的数据交换。 这两个线程通过Exchanger 方法交换数据,如果第一个线程先执行exchanger()方法,她会一直等待第二个线程也会执行Exchanger方法,当两个线程都到达同步点时,这两个线程就交换数据,将本线程生产出来的数据 传递给对方
代码演示
public class Test01 { public static void main(String[] args) { Exchanger<String> exchanger=new Exchanger<>(); new Boy(exchanger).start(); new Griy(exchanger).start(); } } class Boy extends Thread{ private Exchanger<String> exchanger; public Boy(Exchanger<String> exchanger){ this.exchanger=exchanger; } @Override public void run() { System.out.println("男孩正在做🔒"); String rs= null; try { rs = exchanger.exchange("同心锁🔒"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("男孩收到礼物:"+rs); } } class Griy extends Thread{ private Exchanger<String> exchanger; public Griy(Exchanger<String> exchanger){ this.exchanger=exchanger; } @Override public void run() { System.out.println("女孩正在做🔑"); try { String rs = exchanger.exchange("钥匙🔑"); System.out.println("女孩收到礼物:"+rs); } catch (InterruptedException e) { e.printStackTrace(); } } } public class Test01 { public static void main(String[] args) { Exchanger<String> exchanger=new Exchanger<>(); new Boy(exchanger).start(); new Griy(exchanger).start(); } } class Boy extends Thread{ private Exchanger<String> exchanger; public Boy(Exchanger<String> exchanger){ this.exchanger=exchanger; } @Override public void run() { System.out.println("男孩正在做🔒"); String rs= null; try { rs = exchanger.exchange("同心锁🔒"); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("男孩收到礼物:"+rs); } } class Griy extends Thread{ private Exchanger<String> exchanger; public Griy(Exchanger<String> exchanger){ this.exchanger=exchanger; } @Override public void run() { System.out.println("女孩正在做🔑"); try { String rs = exchanger.exchange("钥匙🔑"); System.out.println("女孩收到礼物:"+rs); } catch (InterruptedException e) { e.printStackTrace(); } } }
Exchanger交换者的主要用途,是用于银行的报表的进行校对是使用的