Java之多线程讲解
Posted hdc520
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java之多线程讲解相关的知识,希望对你有一定的参考价值。
1、线程的创建
(1)继承Thread类创建线程类,代码如下:
//1、定义一个继承Thread类的子类,并重写该类的run()方法; //2、创建Thread子类的实例,即创建了线程对象; //3、调用该线程对象的start()方法启动线程。 class SomeThead extends Thraad { public void run() { System.out.println("必须要重写Thread方法"); } } public static void main(String[] args){ SomeThread oneThread = new SomeThread(); 步骤3:启动线程: oneThread.start(); }
通过继承Thread实现的线程类,多个线程间无法共享线程类的实例变量
(2)实现Runnable接口创建线程类
//1、定义Runnable接口的实现类,并重写该接口的run()方法; //2、创建Runnable实现类的实例,并以此实例作为Thread的target对象,即该Thread对象才是真正的线程对象。 class SomeRunnable implements Runnable { public void run() { System.out.println("必须要重写run方法"); } } Runnable oneRunnable = new SomeRunnable(); Thread oneThread = new Thread(oneRunnable); oneThread.start(); // 或者 // Thread t1 = new Thread(new Runnable() { // @Override // public void run() { // System.out.println("test"); // } // }); // // 或者 // t1.start(); // new Thread(()-> { // print.printNum(); // }).start();
覆写Runnable接口实现多线程可以避免单继承局限
当子类实现Runnable接口;子类负责业务的操作,thread负责资源调度与线程创建辅助真实业务
(3)实现Callable接口实现多线程
1)与继承Thread类和实现Runnable接口区别:上述两种方法都不能有返回值,且不能声明抛出异常。而Callable接口则实现了此两点,Callable接口如同Runable接口的升级版,其提供的call()方法将作为线程的执行体,同时允许有返回值。
2)Callable对象不能直接作为Thread对象的target,因为Callable接口是 Java 5 新增的接口,不是Runnable接口的子接口。
3)Callable接口实现多线程:利用 Future接口,此接口可以接受call() 的返回值,RunnableFuture接口是Future接口和Runnable接口的子接口,可以作为Thread对象的target 。其中Future 接口提供了一个实现类:FutureTask 。FutureTask实现了RunnableFuture接口,可以作为 Thread对象的target。
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class ThreadCall implements Callable<String> { @Override public String call() throws Exception { // TODO Auto-generated method stub System.out.println("====="); return "9999"; } } public class TestThread { public static void main(String[] args) { FutureTask<String> ft = new FutureTask<>(new ThreadCall()); new Thread(ft).start(); } }
总结:通过上述三种方式,其实可以归为两类:继承类和实现接口两种方式。相比继承, 接口实现可以更加灵活,不会受限于Java的单继承机制。并且通过实现接口的方式可以共享资源,适合多线程处理同一资源的情况。
(4)ThreadLocal:ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。这些副本变量不同之处在于每一个线程读取的变量是对应的互相独立的。实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。
1)应用场景
a、线程间数据隔离
b、数据库连接:如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,此时尚能解决,但是如果有一万个客户端,那么服务器大概率吃不消。此时若有ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
2、线程的生命周期
(1)线程的状态
1)新建状态:用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新建状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态
2)就绪状态:处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于可运行池之中。
3)运行状态:处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
4)阻塞状态:处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待。
5)死亡状态:当线程的run()方法执行完,或者被强制性地终止,例如出现异常,调用了stop()方法等等,就会从运行状态转变为死亡状态。
(2)线程状态的转换
1)运行状态变就绪状态:如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。
2)运行状态变阻塞状态:线程调用sleep方法主动放弃所占用的系统资源;线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁;线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
3)运行状态变死亡状态:当线程的run()方法执行完,或者被强制性地终止,例如出现异常,调用了stop()方法等等,就会从运行状态转变为死亡状态。
(3)线程管理
1)线程睡眠——sleep()方法:
a、使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
b、而sleep(),join()方法是在线程类中实现的,sleep()的作用是将当前线程暂停一定的时间,但在这期间并不释放锁。
c、sleep是静态方法最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下:
public class Test1 { public static void main(String[] args) throws InterruptedException { System.out.println(Thread.currentThread().getName()); MyThread myThread=new MyThread(); myThread.start(); myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程 Thread.sleep(10); for(int i=0;i<100;i++){ System.out.println("main"+i); } } }
2)线程睡眠--yield()方法
yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。
3)sleep()和yield()方法的区别:
a、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
b、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
c、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
4)线程合并--join方法
t.join()方法只会调用t.join()的线程进入等待池并等待t线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态的其他线程。
(4)线程中的概念和ObjectThread类中的方法
1)等待池:当锁对象调用wait方法时,则持有该对象锁的线程进入等待池中,进入等待池中的线程对象不具备持有锁的资格。
2)锁池:假设某对象锁当前被线程A所持有,而其他想持有该锁的线程就会先放进锁池中,待线程A释放所持有的锁时,锁池中线程可以竞争持有该锁。也就是说,锁池中的线程对象具备竞争锁的资格。
3)Object中线程中的方法:notify(),notifyAll(),wait()
a、notify()或notifyAll():notify()用于随机唤醒一个等待该锁对象的线程,notifyAll()用于唤醒所有等待该锁对象的线程。被唤醒的该线程具备了竞争锁的资格)即:notify()会在等待池中随机选择一个线程对象放入锁池中。而notifyAll会将所有等待该锁的线程放入锁池中。
b、wait():持有该锁对象的线程对象进入wait状态:释放锁,然后将当前线程放入等待池中。
4)Thread的方法:sleep(),join(),yield()
(5)如何中止一个正在运行的线程:
1)使用退出标志位来终止线程,当run方法执行完后,线程就会推出,但是有时run方法是不会永远结束的如循环处理请求,此时需要设置退出标志位来跳出循环来.完成run()的执行.
2)使用interrupt()来中断线程.调用 interrupt() 方法仅仅是在当前线程中打一个停止的标记,并不是真的停止线程。想要是实现调用interrupt()方法真正的终止线程,则可以在线程的run方法中做处理即可,比如直接跳出run()方法使线程结束,视具体情况而定。
3)利用stop(),resume()、suspend()、destory()方法来结束线程.但是调用这些方法会立刻停止run()方法中剩余的全部工作.会导致某些工作还未完成就已经结束如文件数据库的关闭.另外,调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
(6)设置线程的优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间
public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("高级", 10).start(); new MyThread("低级", 1).start(); } } class MyThread extends Thread { public MyThread(String name,int pro) { super(name);//设置线程的名称 setPriority(pro);//设置线程的优先级 } @Override public void run() { for (int i = 0; i < 100; i++) { System.out.println(this.getName() + "线程第" + i + "次执行!"); } } }
(7)守护/后台线程
1)守护进程是一个在后台运行并且不受任何终端控制的进程,生存周期比较长。它们独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。他们常常在系统引导装入时启动,在系统关闭时终止。比喻Linux在启动时需要启动很多系统服务, 它们向本地和网络用户提供了Linux 的系统功能接口, 直接面向应用程序和用户。提供这些服务的程序是由运行在后台的守护进程来执行的
2)守护线程使用的情况较少,JVM的垃圾回收、内存管理等线程都是守护线程。setDaemon(true)设置线程为守护线程;JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态。
3、线程同步:可以利用内置锁(synchronized)和显示锁(java.util.concurrent.locks.ReentrantLock)
(1)synchronized关键字:可以充当内置锁,同步锁,悲观锁。
1)synchronized实现原理
a、synchronized是用java的monitor机制来实现的,就是synchronized关键字在经过javac编译后,会在同步块的前后分别形成monitorEnter和monitorExit这两个字节码指令。
b、在执行monitorEnter指令时,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经持有了那个对象的锁,就把锁的计数器的值加1,而在执行monitorExit指令时会将锁计数器的值减1,一旦计数器的值为0,锁随机就会被释放。
c、如果持有对象锁失败,那么当前线程就应当被阻塞等待,直到此线程获取到锁为止。
注意:monitor机制是跟java对象结构相关的。HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:对象头(存储对象的hashCode、锁信息或分代年龄或GC标志等信息),实例数据跟对齐填充。
(2)synchronized修饰类,方法,代码块,对象。
1)修饰类:作用的范围是synchronized后面括号括起来的部分,作用的对象是这个类的所有对象即类的所有对象用的是同一把锁。
class ClassName { //其作用的范围是synchronized后面括号括起来的部分 public void method() { synchronized(ClassName.class) { // todo } } }
2)synchronized修饰实例方法:由于java的每个对象都有一个内置锁,synchronized修饰方法时锁定的是调用该方法的对象。当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。
3)synchronized关键字也可以修饰静态方法:此时如果调用该静态方法,将会锁住整个类。
4)修饰代码块:被修饰的代码块称为同步语句块,其作用范围是大括号{}括起来的代码块,作用的对象是调用这个代码块的对象;
总结:被synchronized修饰的代码段可以防止被多个线程同时执行,必须一个线程把synchronized修饰的代码段都执行完毕了,其他的线程才能开始执行这段代码。因为synchronized保证了在同一时刻,只能有一个线程执行同步代码块,所以执行同步代码块的时候相当于是单线程操作了,那么线程的可见性、原子性、有序性(线程之间的执行顺序)它都能保证了。
(3)使用重入锁实现线程同步
1)java.util.concurrent.locks.ReentrantLock来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和块具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有:
ReentrantLock() : 创建一个ReentrantLock实例
lock() : 获得锁
unlock() : 释放锁
2)synchronized与和reentrantlock的区别:
a、ReentrantLock在等待锁时可以使用lockInterruptibly()方法选择中断, 改为处理其他事情,而synchronized关键字,线程需要一直等待下去。同样的,tryLock()方法可以设置超时时间,用于在超时时间内一直获取不到锁时进行中断。
b、ReentrantLock默认情况下是非公平的,但可以通过带布尔值的构造函数要求使用公平锁,而synchronized的锁是非公平的。reentrantlock缺点:ReentrantLock的主要缺点是方法需要置于try-finally块中,开发人员需要负责获取和释放锁,而开发人员常常忘记在finally中释放锁。
(4)乐观锁是一种思想,而CAS是乐观锁的实现方式,synchronized是实现悲观锁的方式
1)CSA实现原理:
a、定义3个操作数,变量的内存地址address,旧的预期值pre_value和准备的设置的新值new_value。
b、CAS指令执行时,当且仅当adderss符合pre_value时,处理器才会用new_value更新address的值,如果adderss不符合pre_value时,则说明已经有其他的线程做了这两个操作,当前线程则什么都不会去做。
c、但是不管是否更新了address值,都会返回address的旧值即pre_value。
生动形象的例子:比如说给你儿子订婚。你儿子就是内存位置,你原本以为你儿子是和杨贵妃在一起了,结果在订婚的时候发现儿子身边是西施。这时候该怎么办呢?你一气之下不做任何操作。如果儿子身边是你预想的杨贵妃,你一看很开心就给他们订婚了,也叫作执行操作。
(5)自旋锁:自旋锁的实现基于共享变量。一个线程通过给共享变量设置一个值来获取锁;其他等待线程查询共享变量是否为0来确定锁是否可用,然后再等待循环中自旋直到锁可用为止。
1)实现方法:TicketLock和CLHLock,它们都会实现Lock接口。
a、TicketLock:锁拥有一个服务号,表示正在服务的线程,还有一个排队号;每个线程尝试获取锁之前先拿一个排队号,然后不断轮询锁的当前服务号是否是自己的排队号,如果是,则表示自己拥有了锁,不是则继续轮询。当线程释放锁时,将服务号加1,这样下一个线程看到这个变化,就退出自旋。缺点:多处理器系统上,每个进程/线程占用的处理器都在读写同一个变量serviceNum ,每次读写操作都必须在多个处理器缓存之间进行缓存同步,这会导致繁重的系统总线和内存的流量,大大降低系统整体的性能。
b、CLHLock:是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。结点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些结点之间没有明显的next指针,而是通过preNode所指向的结点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个结点。
a、volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
b、volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的
c、volatile不会造成线程阻塞。synchronized可能会造成线程阻塞。
d、volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
e、volatile标记的变量不会被编译器优化即禁止指令重排序优化;synchronized标记的变量可以被编译器优化
5、线程池:Java中已经提供了创建线程池的一个类:Executor,一般使用它的子类:ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
(1)参数解析:
1)corePoolSize :核心池的大小,如果调用了prestartAllCoreThreads()或者prestartCoreThread()方法,会直接预先创建corePoolSize的线程,否则当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;这样做的好处是,如果任务量很小,那么甚至就不需要缓存任务,corePoolSize的线程就可以应对;
2)maximumPoolSize:线程池最大线程数,表示在线程池中最多能创建多少个线程,如果运行中的线程超过了这个数字,那么相当于线程池已满,新来的任务会使用RejectedExecutionHandler 进行处理;
3)keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止,然后线程池的数目维持在corePoolSize 大小;
4)unit:参数keepAliveTime的时间单位;
5)workQueue:一个阻塞队列,用来存储等待执行的任务,如果当前对线程的需求超过了corePoolSize大小,才会放在这里;
6)threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等
7)handler:表示当拒绝处理任务时的策略(线程池已满或线程数量大于maximumPoolSize,),有以下三种取值:
1、直接抛出异常
2、丢弃队列最近的一个任务,并执行当前任务
3、不处理,丢弃掉。
(2)线程池执行流程图
由图可知:
1)线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。
2)当调用 execute() 方法添加一个任务时,线程池会做如下判断:
a、如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
b、如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列。
c、如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程运行这个任务;
d、如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常,告诉调用者“我不能再接受任务了”。
3)当一个线程完成任务时,它会从队列中取下一个任务来执行。
4)当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。
这样的过程说明,并不是先加入任务就一定会先执行。假设队列大小为 10,corePoolSize 为 3,maximumPoolSize 为 6,那么当加入 20 个任务时,执行的顺序就是这样的:首先执行任务 1、2、3,然后任务 4~13 被放入队列。这时候队列满了,任务 14、15、16 会被马上执行,而任务 17~20 则会抛出异常。最终顺序是:1、2、3、14、15、16、4、5、6、7、8、9、10、11、12、13。
public class ThreadPool { private static ExecutorService pool; public static void main( String[] args ) { //自定义拒绝策略 pool = new ThreadPoolExecutor(1, 2, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5), Executors.defaultThreadFactory(), new RejectedExecutionHandler() { public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { System.out.println(r.toString()+"执行了拒绝策略"); } }); for(int i=0;i<10;i++) { pool.execute(new ThreadTask()); } } } public class ThreadTask implements Runnable{ public void run() { try { //让线程阻塞,使后续任务进入缓存队列 Thread.sleep(1000); System.out.println("ThreadName:"+Thread.currentThread().getName()); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } //结果为: com.hhxx.test.ThreadTask@33909752执行了拒绝策略 com.hhxx.test.ThreadTask@55f96302执行了拒绝策略 com.hhxx.test.ThreadTask@3d4eac69执行了拒绝策略 ThreadName:pool-1-thread-2 ThreadName:pool-1-thread-1 ThreadName:pool-1-thread-1 ThreadName:pool-1-thread-2 ThreadName:pool-1-thread-1 ThreadName:pool-1-thread-2 ThreadName:pool-1-thread-1
(3)常见的线程池
1)CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
2)SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
3)FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程
4)SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
(4)线程池中常见的阻塞队列
1)ArrayBlockingQueue是一个有边界的阻塞队列,它的内部实现是一个数组。有边界的意思是它的容量是有限的,我们必须在其初始化的时候指定它的容量大小,容量大小一旦指定就不可改变;此队列按 FIFO(先进先出)原则对元素进行排序。
2)LinkedBlockingQueue阻塞队列大小的配置是可选的,如果我们初始化时指定一个大小,它就是有边界的,如果不指定,它就是无边界的。说是无边界,其实是采用了默认大小为Integer.MAX_VALUE的容量 。它的内部实现是一个链表。此队列按FIFO (先进先出) 排序元素
3)PriorityBlockingQueue是一个没有边界的队列,它的排序规则和 java.util.PriorityQueue一样。需要注意,PriorityBlockingQueue中允许插入null对象。所有插入PriorityBlockingQueue的对象必须实现 java.lang.Comparable接口,队列优先级的排序规则就是按照我们对这个接口的实现来定义的。
(5)线程池的大小应该如何设置?其中N为CPU个数。
1)如果是CPU密集型应用:大部分时间磁盘IO闲着,等着CPU的计算操作;则线程池大小设置为N+1,尽量使用较小的线程池因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,只能增加上下文切换的次数,因此会带来额外的开销。
2)如果是IO密集型应用:大部分时间CPU闲着,在等待磁盘的IO操作;此时则线程池大小设置为2N+1,IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。
以上是关于Java之多线程讲解的主要内容,如果未能解决你的问题,请参考以下文章