多线程基础知识
Posted dejavu-f
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程基础知识相关的知识,希望对你有一定的参考价值。
线程与进程的区别
1. 进程是资源分配的最小单元,线程是CPU调度的最小单元。所有与进程相关的资源,均被记录再PCB中。
2. 线程隶属于某一个进程,共享所有进程的资源。线程只由堆栈寄存器、程序计数器和TCB构成。
3. 进程可以看作独立的应用,线程不能看作独立的应用。
4. 进程有独立的地址空间,相互不影响,而线程只是进程的不同执行路径,如果线程挂了,进程也就挂了。所以多进程的程序比多线程程序健壮,但是切换消耗资源多。
Java中进程与线程的关系
1. 运行一个程序会产生一个进程,进程至少包含一个线程。
2. 每个进程对应一个JVM实例,多线线程共享JVM中的堆。
3. Java采用单线程编程模型,程序会自动创建主线程。
4. 主线程可以创建子线程,原则上要后于子线程完成执行。
线程中start方法和run方法的区别
Java中创建线程的方式有两种,不管使用继承Thread的方法是hiRunnable接口的方法,都需要重写run方法。调用start方法会创建一个新的线程并启动,run方法只是启动线程后的回调函数,如果调用run方法,那么执行run方法的线程不会是新创建的线程,而如果使用start方法,那么执行run方法的线程就是我们刚刚启动的那个线程。
public class Main { public static void main(String[] args) { Thread thread = new Thread(new SubThread()); thread.run(); thread.start(); } } class SubThread implements Runnable{ @Override public void run() { // TODO Auto-generated method stub System.out.println("执行本方法的线程:"+Thread.currentThread().getName()); } }
运行结果:
执行本方法的线程:main
执行本方法的线程:Thread-0
Thread和Runnable的关系
区别:
Thread是一个类,而Runnable是一个接口,Runnable接口中只有一个没有实现的run方法,所以Runnable并不能独立开启一个线程,而是依赖Thread类去创建线程,执行自己的run方法,去执行相应的业务逻辑,才能让这个类具有多线程的特性。
使用继承Thread类方法创建子线程
public class Main extends Thread{ public static void main(String[] args) { Main main = new Main(); main.start(); } @Override public void run() { System.out.println("通过继承Thread接口方式创建子线程成功,当前线程名:"+Thread.currentThread().getName()); } }
运行结果:
通过继承Thread接口方式创建子线程成功,当前线程名:Thread-0
使用实现Runnable接口方法创建子线程
public class Main{ public static void main(String[] args) { SubThread subThread = new SubThread(); Thread thread = new Thread(subThread); thread.start(); } } class SubThread implements Runnable{ @Override public void run() { // TODO Auto-generated method stub System.out.println("通过实现Runnable接口创建子线程成功,当前线程名:"+Thread.currentThread().getName()); } }
运行结果:
通过实现Runnable接口创建子线程成功,当前线程名:Thread-0
使用匿名内部类方法创建子线程
public class Main{ public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("使用匿名内部类方式创建线程成功,当前线程名:"+Thread.currentThread().getName()); } }); thread.start(); } }
运行结果:
使用匿名内部类方法创建线程成功,当前线程名:Thread-0
Thread和Runnable关系
1. Thread是实现了Runnable接口的类,使得run支持多线程。
2. 因类的单一继承原则,推荐使用Runnable接口,可以使程序更加灵活。
如何实现处理多线程的返回值
1. 通过让主线程等待,直到子线程运行完毕为止。
public class Main{ static String str; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { str="子线程执行完毕"; } }); thread.start(); //如果子线程还未对str进行赋值,则一直轮转 while(str==null) {} System.out.println(str); } }
2. 使用Thread中的join()方法
//join()方法可以阻塞当前线程以等待子线程处理完毕。
public class Main{ static String str; public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { str="子线程执行完毕"; } }); thread.start(); //如果子线程还未对str进行赋值,则一直轮转 try { thread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(str); } }
join()方法能做到比主线程等待法更精准的控制,但是join方法的控制力度并不够细。比如,我们需要控制子线程将字符串赋一个特定的值时,再执行主线程,这种操作join方法是没办法做到的。
3. 通过Callable接口实现:通过FutureTask或者线程池获取
在JDK1.5之前,线程是没有返回值的,通常程序员需要获取子线程返回值颇费周折,现在Java有了一个的返回值线程,即实现了Callable接口的线程,执行了实现Callable接口的线程之后,可以获得一个Future对象,在该对象上调用一个get方法,就可以执行子线程的逻辑并获取返回的Object。
public class Main implements Callable<String>{ @Override public String call() throws Exception { String str = "我是带返回值的子线程"; return str; } public static void main(String[] args) { Main main = new Main(); try { String str = main.call(); System.out.println(str); } catch (Exception e) { e.printStackTrace(); } } }
运行结果:
我是带返回值的子线程
使用FutureTask
public class Main implements Callable<String>{ @Override public String call() throws Exception { String str = "我是带返回值的子线程"; return str; } public static void main(String[] args) { FutureTask<String> task = new FutureTask<String>(new Main()); new Thread(task).start(); try { if(!task.isDone()) { System.out.println("任务没有执行完成"); } System.out.println("等待中..."); Thread.sleep(3000); System.out.println(task.get()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } }
运行结果:
任务没有执行完成
等待中...
我是带返回值的子线程
使用线程池配合Future获取
public class Main implements Callable<String>{ @Override public String call() throws Exception { String str = "我是带返回值的子线程"; return str; } public static void main(String[] args) throws InterruptedException, ExecutionException { ExecutorService newCacheThreadPool = Executors.newCachedThreadPool(); Future<String> future = newCacheThreadPool.submit(new Main()); if(!future.isDone()) { System.out.println("线程尚未执行结束"); } System.out.println("等待中"); Thread.sleep(300); System.out.println(future.get()); newCacheThreadPool.shutdown(); } }
运行结果:
线程尚未执行结束
等待中
我是带返回值的子线程
线程的状态
Java线程主要分为以下六个状态:新建态(new)、运行态(Runnable)、无限期等待(Waiting)、限期等待(TimeWaiting)、阻塞态(Blocked)、结束(Terminated)
新建(new)
新建态是线程处于已被创建但没有被启动的状态,在该状态下的线程只是被创建出来了,但并没有开始执行其内部逻辑。
运行(Runnable)
运行态分为Ready和Running,当线程调用start方法后,并不会立即执行,而是去争夺CPU,当线程没有开始执行时,其状态就是Ready,而当线程获取CPU时间片后,从Ready
态转为Running态。
等待(Waiting)
处于等待状态的线程不会自动苏醒,而只有等待被其他线程唤醒,在等待状态中该线程不会被CPU分配时间,将一直被阻塞。以下操作会造成线程的等待:
1. 没有设置timeout参数的Object.wait()方法。
2. 没有设置timeout参数的Thread.join()方法。
3. LockSupport.park()方法(实际上park方法并不是LockSupport提供的,而是在Unsafe中,LockSupport只是对其做了一层封装)
限期等待(TimeWaiting)
处于限期等待的线程,CPU同样不会分配时间片,但存在于限期等待的线程无需被其他线程显式唤醒,而是在等待时间结束后,系统自动唤醒。以下操作会造成线程限期等待:
1. Thread.sleep() 方法
2. 设置了timeout参数的Object.wait()方法
3. 设置了timeout参数的Thread.join()方法
4. LockSupport.parkNanos()方法
5. LockSupport.parkUntil()方法
阻塞(Blocked)
当多个线程进入同一块共享区域时,例如Synchronized块,ReentrantLock控制的区域等,会去争夺锁,成功获取锁的线程继续往下执行,而没有获取锁的线程将进入阻塞状态,等待获取锁。
结束(Terminated)
已终止线程的线程状态,执行已结束执行。
Sleep和Wait的区别
1. sleep方法由Thread提供,而wait方法由Object提供。
2. sleep方法可以在任何地方使用,而wait方法只能在synchronized块或synchronized方法中使用(因为必须获wait方法会释放锁,只有获取锁了才能释放锁)。
3. sleep方法指挥让出CPU,不会释放锁,而wait方法不仅会让出CPU,还会释放锁。
public class Main{ public static void main(String[] args) { Thread threadA = new Thread(new ThreadA()); Thread threadB = new Thread(new ThreadB()); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); } public static synchronized void print() { System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Sleep"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行Wait"); try { Main.class.wait(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕"); } } class ThreadA implements Runnable{ @Override public void run() { // TODO Auto-generated method stub Main.print(); } } class ThreadB implements Runnable{ @Override public void run() { // TODO Auto-generated method stub Main.print(); } }
执行结果:
当前线程:threadA执行Sleep
当前线程:threadA执行Wait
当前线程:threadB执行Sleep
当前线程:threadB执行Wait
当前线程:threadA执行完毕
当前线程:threadB执行完毕
从上面的结果可以分析出:当线程A执行sleep后,等待一秒被唤醒后继续持有锁,执行之后的代码,而执行wait之后,立即释放了锁,不仅让出了CPU还让出了锁,而后线程B立即持有锁开始执行,和线程A执行了同样的步骤,当线程B执行了wait方法之后,释放锁,然后线程A拿到了锁打印了第一个执行完毕,然后线程B打印执行完毕。
notify和notifyAll的区别
notify可以唤醒一个处于等待状态的线程
public class Main{ public static void main(String[] args) { Object lock = new Object(); Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } print(); } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { print(); lock.notify(); } } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); } public static void print() { System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕"); } }
运行结果:
当前线程:threadB执行print
当前线程:threadB执行完毕
当前线程:threadA执行print
当前线程:threadA执行完毕
解释:线程A在开始执行时立即调用wait进入无限等待状态,如果没有别的线程来唤醒它,它将一直等待下去,所以此时B持有锁开始执行,并且在执行完毕时调用了notify()方法,该方法可以唤醒wait状态的A线程,于是A线程苏醒,开始执行剩下的代码。
notiftAll可以用于唤醒所有等待的线程,使所有处于等待状态的线程都变为ready状态,并重新争夺锁。
public class Main{ public static void main(String[] args) { Object lock = new Object(); Thread threadA = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { try { lock.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } print(); } } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { synchronized (lock) { print(); lock.notifyAll(); } } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); } public static void print() { System.out.println("当前线程:"+Thread.currentThread().getName()+"执行print"); try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("当前线程:"+Thread.currentThread().getName()+"执行完毕"); } }
运行结果:
当前线程:threadB执行print
当前线程:threadB执行完毕
当前线程:threadA执行print
当前线程:threadA执行完毕
notify和notifyAll的区别
要说清楚他们的区别,首先要简单的说以下Java synchronized的一些原理,在openjdk中查看java的源码可以看到,Java对象中存在monitor锁,monitor对象中包含锁池和等待池。
锁池:假设有多个对象进入synchronized块争夺锁,而此时已经有一个对象获取到了锁,那么剩下争夺锁的对象将直接进入锁池中。
等待池:假设某个线程调用了对象的wait方法,那么这个线程将直接进入等待池,而等待池中的对象不会去争夺锁,而是等待被唤醒。
notifyAll会让所有处于等待池中的线程全部进入锁池去争夺锁,而notify只会随机让其中一个线程去争夺锁。
yield方法
/** * A hint to the scheduler that the current thread is willing to yield * its current use of a processor. The scheduler is free to ignore this * hint. * * <p> Yield is a heuristic attempt to improve relative progression * between threads that would otherwise over-utilise a CPU. Its use * should be combined with detailed profiling and benchmarking to * ensure that it actually has the desired effect. * * <p> It is rarely appropriate to use this method. It may be useful * for debugging or testing purposes, where it may help to reproduce * bugs due to race conditions. It may also be useful when designing * concurrency control constructs such as the ones in the * {@link java.util.concurrent.locks} package. */ public static native void yield();
yield源码上有一段很长的注释,其大意是:当前线程调用yield方法时,会给当前线程调度器一个暗示,当前线程愿意让出CPU的使用,但是它的作用应结合详细的分析和测试来确保已经达到了预期的效果,因为调度器可能会无视这个暗示,使用这个方法是不那么合适的,或许在测试环境中使用它会比较好。
public class Main{ public static void main(String[] args) { Thread threadA = new Thread(new Runnable() { @Override public void run() { System.out.println("ThreadA正在执行yield"); Thread.yield(); System.out.println("ThreadA执行yield方法完成"); } }); Thread threadB = new Thread(new Runnable() { @Override public void run() { System.out.println("ThreadB正在执行yield"); Thread.yield(); System.out.println("ThreadB执行yield方法完成"); } }); threadA.setName("threadA"); threadB.setName("threadB"); threadA.start(); threadB.start(); }
ThreadA正在执行yield
ThreadB正在执行yield
ThreadA执行yield方法完成
ThreadB执行yield方法完成
ThreadA正在执行yield
ThreadB正在执行yield
ThreadB执行yield方法完成
ThreadA执行yield方法完成
可以看出,存在不同的测试结果,这里选出两张。
第一种结果:线程A执行玩yield方法,让出CPU给线程B执行。然后两个线程继续执行剩下的代码。
第二种结果:线程A执行玩yield方法,让出CPU给线程B执行,但是线程B执行yield方法后并没有让出CPU,而是继续往下执行,此时就是系统无视了这个暗示。
interrupt方法
中止线程
interrupt函数可以中断一个线程,在interrupt之前,通常使用stop方法来终止一个线程,但是stop方法过于暴力,它的特带你是,不论被中断的线程之前处于一个什么样的状态,都无条件中断,这回导致被中断的线程后续的一些清理工作无法顺利完成,引发一些不必要的一场和隐患,还有可能引发数据不同步的问题。
温柔的interrupt方法
intterrupt方法的原理与stop方法相比就显得温柔的多,当调用interrupt方法去终止一个线程时,它并不会暴力地强制终止线程,而是通知这个线程应该要被中断了,和yield一样,这也是 一种暗示,至于是否应该中断,由被中断的线程自己去决定。当对一个线程调用interrupt方法时:
1. 如果该线程处于被阻塞状态,则立即退出阻塞状态,抛出InterruptedException异常。
2. 如果该线程处于running状态,则将该线程的中断标志位设置为true,被设置的线程继续执行,不受影响,当运行结束时由线程决定是否被中断。
线程池
线程池的引入是用来解决在日常开发的多线程开发中,如果开发者需要使用到非常多的线程,那么这些线程在被频繁的创建和销毁时,会对系统造成一定的影响,有可能系统在创建和销毁这些线程所消耗的时间会比完成实际需求的时间还要长。
另外,在线程很多的情况下,对线程的管理就形成了一个很大的问题,开发者通常要将注意力从功能上转移到对杂乱无章的线程进行管理上,这项动作实际上是非常消耗精力的。
利用Executors创建不同的线程池满足不同场景的需求
newFixThreadPool( int nThreads )
指定工作线程数量的线程池
newCachedThreadPool( )
处理大量中断事件工作任务的线程池
1. 试图缓存线程并重用,当无缓存线程可用事,就会创建新的工作线程。
2. 如果线程闲置的事件超过阈值,则会被终止并移出缓存。
3. 系统长时间闲置的时候,不会消耗什么资源。
newSingleThreadExecutor()
创建唯一的工作线程来执行任务,如果线程异常结束,会有另一个线程取代它。可保证顺利执行任务。
newSingleThreadScheduledExecutor()
定时或周期性工作调度,两者的区别在于前者是单一工作线程,后者是多线程
newWorkStrelingPool()
内部构建ForkJoinPool,利用working-stealing算法,并行地处理任务,不保证处理顺序。
Fork/Join框架:把大任务分割成若干个小任务并执行,最终汇总每一个小任务后得到大任务结果的框架。
为什么要使用线程池
线程是稀缺资源,如果无限制地创建线程,会消耗系统资源,而线程池可以代替开发者管理线程,一个线程在结束运行后,不会销毁线程,而是将线程归还线程池,由线程池再进行管理,这样就可以对线程进行服用。
所以线程池不但可以降低资源的消耗,还可以提高线程的可管理性。
使用线程池启动线程
public class Main{ public static void main(String[] args) { ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10); newFixThreadPool.execute(new Runnable() { @Override public void run() { // TODO Auto-generated method stub System.out.println("通过线程池启动线程成功"); } }); newFixThreadPool.shutdown(); } }
新任务execute执行后的判断
要知道这个点首先要先说说ThreadPoolExecutor的构造函数,其中有几个参数:
1. corePoolSize:核心线程数量。
2. maximumPoolSize:线程不够用时能创建的最大线程数。
3. workQueue:等待队列。
那么新任务提交后会执行下列判断:
1. 如果运行的线程少于corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的阿。
2. 如果线程池中的数量大于等于corePoolSize且小于maximumPoolSize,则只有当workQueue满时,才创建新的线程去处理任务。
3. 如果设置的corePoolSie和maximumPoolSize相同,则创建的线程池大小是固定的,如果此时有新任务提交,若workQueue未满,则放入wordQueue,等待被处理
4. 如果运行的线程数大于等于maximumPoolSize,maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务
handler线程池饱和策略
- AbortPolicy:直接抛出异常,默认
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃队列中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
- 自定义
线程池的大小如果选的
- CPU密集型:线程数 = 核心数或者核心数+1
- IO密集型:线程数 = CPU核数 * ( 1+平均等待时间/平均工作时间 )
当然这个也不能完全依赖这个公式,更多的是要依赖平时的经验来操作,这个公式也只是仅供参考而已。
以上是关于多线程基础知识的主要内容,如果未能解决你的问题,请参考以下文章