当前位置: 首页 > 后端技术 > Java

Java多线程超详解(看这篇文章就够了)

时间:2023-04-01 14:02:41 Java

多线程可以提高程序性能,也属于高薪冰庚的核心技术栈。本文将全面详解Java多线程。@mikechen主要包括以下几点:基本概念很多人对其中的一些概念不是很清楚,比如同步、并发等,我们先来建立一个数据字典,避免误解。进程运行在操作系统中的程序就是一个进程。比如你的QQ、播放器、游戏、IDE等等线程。多线程多线程:多个线程并发执行。Java中的同步是指通过人为的控制和调度,使多线程对共享资源的访问变得线程安全,从而保证结果的准确性。例如:synchronized关键字在保证结果准确的同时提高了性能。线程安全优先于性能。Parallel\Multiplecpuinstances或者多台机器同时执行一段处理逻辑,真正做到了同时。并发使用cpu调度算法,让用户看似同时执行,但实际上从cpu运行层面上并不是真正的同时执行。并发往往在场景中有公共资源,所以这个公共资源往往会出现瓶颈,我们会用TPS或者QPS来体现这个系统的处理能力。线程的生命周期在线程的生命周期中,会经历五个状态:New、Runnable、Running、Blocked、DeadNew状态:当程序使用new关键字创建线程后,线程处于新的状态。这时只有JVM为其分配内存,并初始化其成员变量的值。就绪状态:当线程对象调用start()方法时,线程处于就绪状态。Java虚拟机为其创建方法调用栈和程序计数器,等待调度运行。运行态:如果处于就绪态的线程获得CPU并开始执行run()方法的线程执行体,则线程处于运行态,阻塞态:当处于运行态的线程失去占用的资源后,进入阻塞状态并死亡:线程在run()方法执行结束后进入死亡状态。另外,如果线程执行了interrupt()或stop()方法,也会通过异常退出进入死状态。线程状态控制\具体的方法可以参考上面的线程状态流程图,这样具体的作用就更清楚了:1.start()启动当前线程,并调用当前线程的run()方法2.run()通常需要重启在Thread类中写这个方法,在这个方法中声明创建的线程要执行的操作3.yield()释放当前CPU的执行权4.join()在线程a中调用线程b的join(),这时线程a进入阻塞状态,线程a只有在线程b执行完毕后才结束阻塞状态。5.sleep(longmilitime)使线程休眠指定的毫秒数。在指定的时间内,线程处于阻塞状态6.wait()一旦执行到该方法,当前线程就会进入阻塞状态,一旦执行到wait(),同步监视器就会被释放。7、sleep()和wait()的异同:这两个方法一旦执行,都可以使线程进入阻塞状态。不同点:1)两个方法声明的位置不同:sleep()在Thread类中声明,wait()在Object类中声明2)调用要求不同:sleep()可以在任何类中调用所需场景。wait()必须在同步代码块内调用。2)关于是否释放同步监视器:如果在同步代码块的同步方法中同时使用这两种方式,sleep不会释放锁,wait会释放锁。8.notify()一旦执行该方法,它将唤醒一个被等待的线程。如果有多个线程正在等待,则唤醒优先级最高的线程。9.notifyAll()一旦执行该方法,它将唤醒所有等待的线程。10.LockSupportLockSupport.park()和LockSupport.unpark()实现线程阻塞和唤醒。创建多线程的5种方式1.继承Thread类包com.mikechen.java.multithread;/***多线程创建:InheritThread**@authormikechen*/classMyThreadextendsThread{privateinti=0;@Overridepublicvoidrun(){for(i=0;i<10;i++){System.out.println(Thread.currentThread().getName()+""+i);}}publicstaticvoidmain(String[]args){MyThreadmyThread=newMyThread();我的线程.start();}}2.实现Runnable接口包com.mikechen.java.multithread;/***多线程创建:实现Runnable接口**@authormikechen*/publicclassMyRunnableimplementsRunnable{privateinti=0;@Overridepublicvoidrun(){for(i=0;i<10;i++){System.out.println(Thread.currentThread().getName()+""+i);}}}publicstaticvoidmain(String[]args){RunnablemyRunnable=newMyRunnable();//创建Runnable实现类的对象Threadthread=newThread(myRunnable);//将myRunnable作为线程target创建一个新线程thread.start();}}3.线程池创建线程池:它其实是一个可以容纳多个线程的容器,里面的线程可以重复使用,省去创建线程对象的频繁操作,不用重复创建线程和消费太多许多系统资源packagecom.mikechen.java.multithread;importjava.util.concurrent.Executor;importjava.util.concurrent.Executors;/***多线程创建:线程池**@authormikechen*/publicclassMyThreadPool{publicstaticvoidmain(String[]args){//创建一个线程池,有5个线程//实际上返回的是ExecutorService,ExecutorService是Executor的子接口ExecutorthreadPool=Executors.newFixedThreadPool(5);for(inti=0;I<10;I++){threadpool.execute(newrunnable(){publicvoidrun(){system.out.println(thread.currenThreadRead());getname()+"is跑步”);;}}}}核心核心公共线程PRETHEPOLEXECUTOR(intcorepoolSize,intmaximumpoolsize,longeaberAlivetime,timeunit单元,blockingqueueworkqueue,threadfactorythreadfactory,recubledexecutionhandlerhandler)提交任务后,会先尝试交给核心线程池中的线程要执行,但是核心线程池中的线程数必须是有限的,所以必须通过任务队列做一个缓存,先把任务缓存到队列中,然后等待线程执行最后,由于太多任务,队列也满了,这时候线程池中剩余的线程就会开始帮助核心线程池执行任务。如果还是没办法正常处理新来的任务,线程池只能将新提交的任务交给饱和策略处理。4.匿名内部类适用于创建启动线程较少的环境,编写packagecom.mikechen.java.multithread;/***多线程创建:匿名内部类**@authormikechen*/publicclassMyThreadAnonymous{publicstaticvoidmain(String[]args){匿名内部类创建线程方法1...");};}.start();//方法二:实现Runnable,将Runnable作为匿名内部类实现NewThread(NewRunnable(){PublicvoidRun(){System.out.println("匿名内部类创建线程2...");}}}).start();}}5.Lambda表达式创建包com.mikechen.java.multithread;:lambda表达式**@authormikechen*/publicclassMyThreadLambda{publicstaticvoidmain(String[]args){//匿名内部类创建多线程newThread(){@Override@Overridepublicvoidrun(out){.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程1");}}}}。开始();//使用Lambda表达式实现多线程newThread(()->{System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程2");}).start();//优化LambdanewThread(()->System.out.println(Thread.currentThread().getName()+"mikchen的互联网架构创建新线程3")).start();}}线程同步线程同步是为了防止多个线程访问一个数据对象时数据被破坏。线程同步是保证多线程安全访问竞争资源的一种手段1、常见的同步方法lock是当前实例对象,在进入同步代码之前必须先获取到当前实例的锁。/***在普通方法中使用*/privatesynchronizedvoidsynchronizedMethod(){System.out.println("--synchronizedMethodstart--");try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("--synchronizedMethodend--");}2。静态同步方法锁是当前类的类对象,在进入同步代码之前必须先获取当前类对象的锁。/***在静态方法中使用*/privatesynchronizedstaticvoidsynchronizedStaticMethod(){System.out.println("synchronizedStaticMethodstart");try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("synchronizedStaticMethod结束");}3.同步方法块锁就是括号中的对象,在进入同步代码库之前,对给定对象进行加锁,获得给定对象的锁。/***在类中使用*/privatevoidsynchronizedClass(){synchronized(SynchronizedTest.class){System.out.println("synchronizedClassstart");try{Thread.sleep(2000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("synchronizedClassend");}}4.synchronized的底层实现synchronized的底层实现完全依赖于JVM虚拟机,所以在讲底层实现的时候synchronized,就不得不说到数据在JVM内存中的存储方式:Java对象头,Monitor对象监视器。1、Java对象头在JVM虚拟机中,对象在内存中的存储布局可以分为三个区域:对象头(Header)、实例数据(InstanceData)、对齐填充(Padding)Java对象头主要包括两部分数据:1)类型指针(KlassPointer)是对象指向其类元数据的指针,虚拟机通过这个指针来判断对象是哪个类实例;2)标记字段(MarkWord)用于存储对象自身的运行时间数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,是实现轻量锁和偏向锁的关键。所以,很明显synchronized使用的锁对象是存放在Java对象头中的tag字段中的。2.Monitormonitor被描述为对象监视器,可以类比为一个特殊的房间。这个房间里有一些受保护的数据。监视器保证一次只有一个线程可以进入这个房间来访问受保护的数据。为了握住显示器,离开房间就是释放显示器。下图是synchronized同步代码块的反编译截图,可以清楚的看到monitor的调用。syncrhoized加锁的同步代码块在字节码引擎中执行时,主要是通过锁对象的monitor的获取(monitorenter)和释放(monitorexit)来实现的。多线程的优点很明显,但是多线程的缺点也很明显。线程的使用(滥用)会给系统带来上下文切换的额外负担,线程间共享变量可能会造成死锁。1、线程安全问题1)原子性并发编程中很多操作都不是原子操作,比如:i++;//操作2i=j;//操作3i=i+1;//操作4在单线程环境下这三个操作不会有问题,但是在多线程环境下,如果不进行加锁操作,很可能会出现意想不到的值。原子性可以通过java中的synchronized或者ReentrantLock来保证。2)Visibility可见性:当多个线程访问同一个变量时,一个线程修改变量的值,其他线程可以立即得到修改后的值。如上图所示,每个线程都有自己的工作内存,工作内存和主内存需要通过store和load进行交互。为了解决多线程的可见性问题,java提供了volatile关键字。当一个共享变量被volatile修改时,它会保证修改后的值会立即更新到主存中。当其他线程需要读取它时,会去主存中读取新值,普通共享变量的可见性无法保证,因为变量被修改后刷新回主存的时间不确定。2、线程死锁线程死锁是指由于两个或多个线程持有彼此需要的资源,导致这些线程处于等待状态,无法去执行。当线程持有彼此需要的资源时,它们会等待对方释放资源。如果线程不主动释放占用的资源,就会出现死锁,如图:例子:publicvoidadd(intm){synchronized(lockA){//得到lockA的锁this.value+=米;synchronized(lockB){//获取lockB的锁this.another+=m;}//释放lockB的锁}//释放lockA的锁}publicvoiddec(intm){synchronized(lockB){//获得lockB的锁this.another-=m;synchronized(lockA){//获取lockA的锁this.value-=m;}//释放lockA的锁}//释放lockB的锁}两个线程各自持有不同的锁,然后各自尝试获取对方手中的锁,导致双方无限等待,这就是死锁。3、上下文切换多线程并发会很快吗?其实不一定,因为多线程有线程创建和线程上下文切换的开销。\CPU是一种非常宝贵的资源,它的速度是非常快的。为了保证平衡,时间片通常分配给不同的线程。当CPU从一个线程切换到另一个线程时,CPU需要保存当前线程的本地数据。程序指针等状态,加载下一个要执行的线程的本地数据,程序指针等,这种切换称为上下文切换。一般减少上下文切换的方法有:无锁并发编程、CAS算法、使用协程等,多线程用得好,效率可以成倍提高,但用得不好,可能会比单线程。以上作者简介陈睿|mikechen,10年+大工厂架构经验,《BAT架构技术500期》系列文章作者,分享十余年BAT架构经验和面试心得!阅读mikechen收集的更多互联网架构Java并发|JVM|MySQL|Spring|Redis|分布式|高并发|架构师关注“mikechen的互联网架构”公众号,回复【架构】获取我的原创《300 期 + BAT 架构技术系列与 1000 + 大厂面试题答案》