文章目录
前言
Java是少数的几种支持“多线程”的语言之一。大多数的程序语言只能循序运行单独一个程序块,但无法同时运行不同的多个程序块。Java的“多线程”恰可弥补这个缺憾,它可以让不同的程序块一起运行,如此一来可让程序运行更为顺畅,同时也可达到多任务处理的目的
提示:以下是本篇文章正文内容,下面案例可供参考
一、关于线程
1.1 进程
每个独立执行的程序称为进程
进程是程序的一次动态执行过程,它经历了从代码加载、执行到执行完毕的一个完整过程,这个过程也是进程本身从产生、发展到最终消亡的过程。多进程操作系统能同时运行多个进程(程序),由于CPU具备分时机制,所以每个进程都能循环获得自己的CPU时间片。由于CPU执行速度非常快,使得所有程序好象是在“同时”运行一样。
在操作系统中进程是进行系统资源分配、调度和管理的最小单位,进程在执行过程中拥有独立的内存单元。比如:Windows采用进程作为最小隔离单位,每个进程都有自己的数据段、代码段,并且与别的进程没有任何关系。因此进程间进行信息交互比较麻烦
1.2 线程
为了解决进程调度资源的浪费,为了能够共享资源,出现了线程。线程是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. 就绪(通过start()方法)
3. 执行(通过资源调度,获取资源)
4. 阻塞
5. 死亡
6.
新生状态(新建状态): threadStatus: 0 , 通过new 方法创建线程对象,
就绪状态(可运行状态): 当这个线程对象调用start()方法
运行状态: 当线程抢到cpu资源,执行run() 方法, 该线程位于运行状态
阻塞状态: 同步阻塞: 没有得到对象锁, 等待阻塞: 调用wait() , 其他阻塞: IO阻塞, sleep() 睡眠, join()
死亡状态: 线程的run() 执行完了, 进入死亡状态. (正常死亡), 执行过程中出异常, 没有处理, 非正常死亡(stop())
四、线程的常用方法
线程的优先级:
设置线程优先级:
setPriority(int newPriority)
获取线程的优先级:
int getPriority()
优先级: 1-10
Thread提供了三个优先级常量:
Thread.MAX_PRIORITY=10
NORM_PRIORITY = 5 默认的
MIN_PRIORITY = 1
Main方法的优先级,是5
优先级高的线程,抢到CPU资源的概率大, 不是高优先级一定先于低优先级抢到CPU资源
线程礼让: 当前线程暂停执行,与其他线程同时抢占资源,如果还是自己抢占到,则继续执行后续的代码,如果是其他线程抢占到,则其他线程先执行
static void yield()
join():当前线程暂停执行,新加入的线程开始执行,当新线程执行完之后,再执行当前线程, 使用多
线程睡眠:
Thread.sleep(时间)
单位毫秒时间一到, 线程自动醒来, 进入到就绪状态, 与其他线程抢占cpu资源
线程睡眠:
Object的wait(时间) 单位毫秒
无参方法: wait()
时间一到, 线程不会自动醒来, 一定要其他线程调用notify(),notifyAll() 唤醒线程
五、线程同步
同时启动多个线程, 如果多个线程共享同一个数据, 并且都修改这个数据, 会出现什么问题?
买票: 模拟多个窗口 卖同一趟车的票(总共有10张票) 剩余票减少
2个窗口: 就是两个线程, 同时修改 票的数量
问题:
超卖, 同一张票可能卖多次
在实际生活中一定不能出现
原因:
当线程A修改共享数据时, 同时线程B也在修改共享数据, 某个线程修改的结果被覆盖了
解决方案:
加锁, 共享方法,或者是共享数据加锁
java提供了synchronized关键字, 互斥锁, 排他锁 jdk优化: 锁升级
锁对象
同步方法:
public 返回值类型
synchronized
方法名(){}锁在该方法的所在类对象,也就是 this, 不是锁方法
如果多个线程调用本类同一个对象的同步方法, 需要前面的线程释放锁, 后面线程才能执行, 需要等待,阻塞
但是其他线程调用是本类非同步方法, 不需要等待,直接执行
public synchronized void shellTicket(){ //先判断使用有票 if(count > 0){ //有票 System.out.println(Thread.currentThread().getName()+":正在买第"+count+"张票...."); //模拟卖票需要时间, 使用线程睡眠,模拟 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"出第"+count+"张票成功"); //数量减1 count--; } }
同步代码块:
方法内部(){
//…
synchronized(对象){
//锁住的代码
}
}
public void shellTicket(){
System.out.println("你好,欢迎光临!!");
//....
//先判断使用有票
// 锁对象,只要是一个共享对象
synchronized (obj) {
if(count > 0){ //有票
System.out.println(Thread.currentThread().getName()+":正在买第"+count+"张票....");
//模拟卖票需要时间, 使用线程睡眠,模拟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"出第"+count+"张票成功");
//数量减1
count--;
}
}
}
同步方法与同步代码块的区别:
同步方法锁整个方法, 锁住的this
同步代码块, 锁的方法某部分代码, 锁住的对象,只要是共享对象就可以,不一定是this
同步: 让线程排队一个一个执行, 效率低, 容易造成死锁. 线程安全, 如果有多个线程共享数据, 并且多个线程同时修改共享数据,这个时候,一定需要线程同步
非同步: 并发执行, 效率高, 线程不安全