前言
Java是少数的几种支持“多线程”的语言之一。大多数的程序语言只能循序运行单独一个程序块,但无法同时运行不同的多个程序块。Java的“多线程”恰可弥补这个缺憾,它可以让不同的程序块一起运行,如此一来可让程序运行更为顺畅,同时也可达到多任务处理的目的
一、关于线程
1.1 进程
程序: 是为了完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,
进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片。由于CPU执行速度非常快,使得所有程序好象是在“同时”运行一样。
每个独立执行的程序称为进程
在操作系统中进程是进行系统资源分配、调度和管理的最小单位,进程在执行过程中拥有独立的内存单元。比如:Windows采用进程作为最小隔离单位,每个进程都有自己的数据段、代码段,并且与别的进程没有任何关系。因此进程间进行信息交互比较麻烦
1.2 线程 应用程序的执行路径
线程空间默认最多为1M
为了解决进程调度资源的浪费,为了能够共享资源,出现了线程。线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源,多个线程共享内存,从而极大地提高了程序的运行效率。线程是比进程更小的执行单位,线程是进程内部单一的一个顺序控制流。所谓多线程是指一个进程在执行过程中可以产生多个线程,这些线程可以同时存在、同时运行,形成多条执行线。一个进程可能包含了多个同时执行的线程。
一个或更多的线程构成了一个进程(操作系统是以进程为单位的,而进程是以线程为单位的,进程中必须有一个主线程main)。
如果一个进程没有了,那么线程肯定会消失,如果线程消失了,但是进程未必会消失。只有所有的线程都结束了,进程才会结束!!!而且所有线程都是在进程的基础之上同时运行
1.3 进程与线程的关系
进程的产生,肯定会产生至少一个以上的线程;
进程关闭,该进程内的线程会全部销毁;
线程销毁,进程未必会关闭
1.4 线程与进程的区别
多线程是实现并发机制的一种有效手段。进程和线程一样,都是实现并发的一个基本单位。线程和进程的主要差别体现如下:
进程:每个进程都有自己独立的代码和数据空间,进程间的切换开销大
线程:一个进程内的多个线程,共享代码和数据空间,线程间的切换开销比较小
多线程的应用范围很广。在一般情况下,程序的某些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其它部分的执行,这种情况下,就可以考虑创建一个线程,令它与那个事件或资源关联到一起,并让它独立于主程序运行。通过使用线程,可以避免用户在运行程序和得到结果之间的停顿,还可以让一些任务(如打印任务)在后台运行,而用户则在前台继续完成一些其它的工作。总之,利用多线程技术,可以使编程人员方便地开发出能同时处理多个任务的功能强大的应用程序
1.5 多线程
多线程,指的是一个进程内的多个任务并发执行;
不管计算机上是一个进程还是多个进程,也不管是一个线程还是多个线程,CPU只有一块,要实现多线程,实际上都需要在一个CPU上完成资源的调度。例如:在某一段时间内只允许A线程操作,而在另外一段时间内,CPU就让给了其他线程,此时需要一个时间片的轮转算法,进行资源的调度
多线程的好处:可以更高效地利用CPU资源,同时,让固定流程的程序更加灵活;
注意:多个线程之间,谁先抢占到资源,谁就先执行
二、多线程的实现
在Java之中,如果要想实现多线程的开发,有三种形式:一种是继承Thread类,另外一种实现Runnable接口,最后一种实现Callable接口.那么下面通过代码分析,来观察这三种实现的操作
2.1 继承Thread类
线程的操作首先一定要有一个线程的主体操作类,这个主体操作类就可以通过继承Thread类来完成,而继承Thread类之后还要去覆写Thread类中的run()方法,此方法的功能与main()方法类似,属于线程的启动点.
package cn.sz.gl.no5; public class MyThread extends Thread { @Override public void run() { System.out.println("执行了线程的run方法_"); } }
启动线程:
package cn.sz.gl.no6; public class Test { public static void main(String[] args) { MyThread mta = new MyThread(); MyThread mtb = new MyThread(); MyThread mtc = new MyThread(); MyThread mtd = new MyThread(); MyThread mte = new MyThread(); mta.start(); mtb.start(); mtc.start(); mtd.start(); mte.start(); } }
其中start()方法的实现源码如下:
public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0();
注意:启动多线程必须是通过线程类的对象来调用start()方法。不能直接调用run()方法,如果直接调用run()则仍然是单线程,没有启动多线程。通过调用start()方法,启动一个子线程,子线程会直接运行run()方法内的代码
start()方法执行,首先做一个判断,这里是判断该线程是否已经启动了,如果是已经启动的线程,会报IllegalThreadStateException异常
结论:线程不允许重复启动
调用start0()方法,该方法没有实现,且用native修饰,因为该方法执行,需要调用系统资源,而java有很大的一个特征,就是可移植,那么这时候做法有两种,可以通过JNI技术来实现,但会影响可移植性,我们还有一种选择,把控制权交给JVM来处理,由JVM向底层请求,此时只需要给该方法加上native来修饰就可以了
在多线程使用时,要想启动多线程,必须通过start()方法
2.2 实现Runnable接口
多线程类实现Runnable接口后,还是需要Thread类下的start()方法来启动线程
package cn.sz.gl.no6; public class MyRunnable implements Runnable { @Override public void run() { System.out.println("我的线程..."); } }
启动线程:
package cn.sz.gl.no6; public class Test { public static void main(String[] args) { MyRunnable mra = new MyRunnable(); MyRunnable mrb = new MyRunnable(); MyRunnable mrc = new MyRunnable(); MyRunnable mrd = new MyRunnable(); MyRunnable mre = new MyRunnable(); new Thread(mra).start(); new Thread(mrb).start(); new Thread(mrc).start(); new Thread(mrd).start(); new Thread(mre).start(); } }
2.3 实现Callable接口
(1)创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。
(2)创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。
(3)使用FutureTask对象作为Thread对象的target创建并启动新线程。
(4)调用FutureTask对象的get()方法来获得子线程执行结束后的返回值
package Thread; import java.util.concurrent.*; public class TestThread { public static void main(String[] args) throws Exception { testCallable(); } public static void testCallable() throws Exception { Callable callable = new MyThreadCallable(); FutureTask task = new FutureTask(callable); new Thread(task).start(); System.out.println(task.get()); Thread.sleep(10);//等待线程执行结束 //task.get() 获取call()的返回值。若调用时call()方法未返回,则阻塞线程等待返回值 //get的传入参数为等待时间,超时抛出超时异常;传入参数为空时,则不设超时,一直等待 System.out.println(task.get(100L, TimeUnit.MILLISECONDS)); } } class MyThreadCallable implements Callable { @Override public Object call() throws Exception { System.out.println("通过实现Callable,线程号:" + Thread.currentThread().getName()); return 10; } }
2.4 三种实现方式的区别
- 采用继承Thread类方式:
(1)优点:编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this,即可获得当前线程。
(2)缺点:因为线程类已经继承了Thread类,所以不能再继承其他的父类。
- 采用实现Runnable接口方式:
(1)优点:线程类只是实现了Runable接口,还可以继承其他的类。在这种方式下,可以多个线程共享同一个目标对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
(2)缺点:编程稍微复杂,如果需要访问当前线程,必须使用Thread.currentThread()方法。
- Runnable和Callable的区别:
(1)Callable规定的方法是call(),Runnable规定的方法是run().
(2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
(3)call方法可以抛出异常,run方法不可以,因为run方法本身没有抛出异常,所以自定义的线程类在重写run的时候也无法抛出异常
(4)运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。start()和run()的区别
- start()方法用来,开启线程,但是线程开启后并没有立即执行,他需要获取cpu的执行权才可以执行
- run()方法是由jvm创建完本地操作系统级线程后回调的方法,不可以手动调用(否则就是普通方法)
三.多线程的优势
使用多线程有许多优势,主要包括以下几点:
1.提高程序效率
当一个任务需要执行很长时间时,如果在单个线程中阻塞等待,那么整个程序的执行效率会变得非常低。通过将该任务放入另一个线程中运行,可以使主线程继续执行其它任务,从而提高程序效率。
2.提高响应能力
当用户与程序交互时,如果单个线程中执行大量计算或者IO操作(如网络请求、文件读写等),那么程序可能会出现卡顿现象。通过将这些操作放到另一个线程中执行,可以避免阻塞主线程,从而提高程序的响应能力。
3. 利于资源共享
当多个线程访问共同的资源时(如数据库连接池、缓存等),如果所有线程都采用串行方式来调用这些资源,那么程序的运行效率会变得非常低。通过使用多线程,可以同时访问这些资源,从而提高程序的效率。
4. 实现并发控制
当多个线程对同一个变量或对象进行操作时,可能会出现数据竞争和同步问题。通过使用锁、条件变量等机制,可以实现对共享资源的访问控制,从而避免以上问题。
四.多线程的注意事项
虽然Java多线程可以提高程序效率和响应能力,但在实际开发过程中,需要注意以下几点:
1. 线程安全性
当多个线程同时访问某个变量或资源时,可能会出现数据竞争和同步问题,从而导致程序出现各种错误。因此,在编写多线程程序时,必须考虑到线程安全性,并采用合适的机制(如锁、原子操作等)来保证数据的一致性和正确性。
2. 死锁
当多个线程在执行过程中,由于互相等待对方释放锁而陷入无限等待的状态,就称为死锁。因此,在编写多线程程序时,必须谨慎地选择锁的粒度和加锁顺序,以避免出现死锁问题。
3. 上下文切换
当CPU需要在不同线程之间切换时,需要保存和恢复上下文信息,从而带来一定的开销。因此,在编写多线程程序时,必须谨慎地选择线程数量和任务分配策略,以避免过多的上下文切换带来的性能损失。
总结
Java多线程是Java语言强大的特性之一,它可以让程序更高效、更快速地运行,并且使得开发者能够更好地掌控程序的执行流程和数据共享。在实际开发中,需要注意线程安全性、死锁和上下文切换等问题,以保证程序的正确性和性能。如果你想深入学习Java多线程,可以参考Java编程相关书籍或在线资源,通过实践来提升自己的技能水平。