目录
1. 进程和多线程的概念及线程的优点
1.1 那什么是线程呢?
1.2 那么为什么要使用多线程呢?
2. 使用多线程
2.1 继承Thread类
2.2 实现Runnable接口
2.3 实例变量与线程安全
提到多线程这个技术就不得不提及“进程”这个概念,在“百度百科”中对进程的解释如下:
进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
进程是受操作系统管理的基本运行单元。
线程可以理解成是在进程中独立运行的子任务。线程是指程序在执行过程中,能够执行程序代码的一个执行单元。在Java语言中,线程有4种状态:运行、就绪、挂起和结束。
在操作系统级别上,程序的执行都是以进程为单位的,而每个进程种都会有多个线程互不影响地并发执行。
(1)使用多线程可以减少程序地响应时间。在单线程地情况下,如果某个操作很耗时,或者长时间地等待,此时程序将不会响应鼠标和键盘等操作,使用多线程后,可以把这个耗时地线程分配到一个单独地线程去执行,从而使程序具备了更好地交互性。
(2)与进程相比,线程地创建和切换开销更小。由于启动一个新的进程必须给这个线程分配独立的地址空间,建立许多数据结构来维护线程代码段、数据段等信息,而运行于同一进程内的线程共享代码段、数据段,线程的启动或切换的开销比进程要少很多。同时多线程在数据共享方面效率非常高。
(3)多CPU或多核计算机本身具有执行多线程的能力,如果使用单个线程,将无法重复利用计算机资源,造成资源的巨大浪费。因此在多CPU计算机上使用多线程能提高CPU的利用率。
(4)使用多线程能简化程序的结构,使程序便于理解和维护。一个非常复杂的进程可以分成多个线程来执行。
【注】:多线程是异步的,所以千万不要把IDEA种的代码的顺序当成线程执行的顺序,线程被调用的时机是随机的。
在Java中,实现多线程编程的方式主要有两种,
public class Thread implements Runnable
从上面的源代码中可以发现,Thread类实现了Runnable接口,它们之间具有多态关系。
其实,使用继承Thread类的方式创建新线程时,最大的局限就是不支持多继承,因为Java语言的特点就是单根继承,所以为了支持多继承,完全可以实现Runnable接口的方式,一边实现一边继承。但用这两种方式创建的线程在工作时的性质是一样的,没有本质的区别。
例:创建一个自定义的线程类MyThread.java,此类继承自Thread,并重写run方法。
public class MyThread extends Thread { @override public void run() { super.run(); System.out.println("MyThread"); } }
运行类代码如下:
public class Run { public static void main(String args) { MyThread mythread = new MyThread(); mythread.start(); System.out.println("运行结束!"); } }
在使用多线程技术时,代码的运行结果与代码执行顺序或调用顺序是无关的。
【注】:如果多次调用start()方法,则会出现Exception in thread “main” java.lang.IlleagelThreadStateException。
Thread.java类中的start() 方法通知“线程规划器”此线程已经准备就绪,等待调用线程对象的run()方法。这个过程其实就是让系统安排一个时间来调用Thread中的run()方法,也就是使线程得到运行,启动线程,具有异步执行的效果。
如果调用代码thread.run()就不是异步执行了,而是同步,那么此线程对象并不交给“线程规划器”来进行处理,而是由main主线程来调用run()方法,也就是必须等run()方法中的代码执行完后才可以执行后面的代码。
【注】:执行start()方法的顺序不代表线程启动的顺序。
使用继承Thread类的方式来开发多线程应用程序在设计上是有局限性的,因为Java是单根继承,不支持多继承,所以为了改变这种限制,可以使用实现Runnable接口的方式来实现多线程技术。
构造函数Thread(Runnable target)不光可以传入Runnable接口的对象,还可以传入一个Thread类的对象,这样做完全可以将一个Thread对象中的run()方法交由其他的线程进行调用。
例:创建一个实现Runnable接口的类MyRunnable,代码如下:
public class MyRunnable implements Runnable { @override public void run() { System.out.println("运行中!"); } }
自定义线程类中的实例变量针对其他线程可以有共享和不共享之分,这在多个线程之间进行交互时是很重要的一个技术点。
下面是一个“非线程安全”的例子:
public class MyThread extends Thread { private int count = 5; @override public void run() { super.run(); count--; System.out.println("由 " + this.currentThread().getName() + " 计算,count = " + count); } }
运行类Run.java代码如下:
public class Run { public static void main(String[] args) { MyThread mythread = new MyThread(); Thread a = new Thread(mythread, "A"); Thread b = new Thread(mythread, "B"); Thread c = new Thread(mythread, "C"); Thread d = new Thread(mythread, "D"); Thread e = new Thread(mythread, "E"); a.start(); b.start(); c.start(); d.start(); e.start(); } }
上述代码,会出想多个线程同时处理count的情况,产生”非线程安全“问题。
其实这个示例就是典型的销售场景:5个销售员,每个销售员卖出一个货品后不可以得出相同的剩余数量,必须在每一个销售员卖完一个货品后其他销售员才可以在新的剩余物品数上继续减1操作。这时就需要使多个线程之间进行同步,也就是用按顺序排队的方式进行减1操作。更改代码如下:
public class MyThread extends Thread { private int count = 5; @override synchronized public void run() { super.run(); count--; System.out.println("由 " + this.currentThread().getName() + " 计算,count = " + count); } }
通过在run方法前加入synchronized关键字,使多个线程在执行run方法时,以排队的方式进行处理。当-一个线程调用run前,先判断run方法有没有被上锁,如果上锁,说明有其他线程正在调用run方法,必须等其他线程对run方法调用结束后才可以执行run方法。这样也就实现了排队调用run
方法的目的,也就达到了按顺序对count变量减1的效果了。synchronized 可以在任意对象及方法上加锁,而加锁的这段代码称为“互斥区”或“临界区”。
当一个线程想要执行同步方法里面的代码时,线程首先尝试去拿这把锁,如果能够拿到这把锁,那么这个线程就可以执行synchronize里面的代码。如果不能拿到这把锁,那么这个线程就会不断地尝试拿这把锁,直到能够拿到为止,而且是有多个线程同时去争抢这把锁。