首页 / JAVA / Java基础之多线程
Java基础之多线程
内容导读
互联网集市收集整理的这篇技术教程文章主要介绍了Java基础之多线程,小编现在分享给大家,供广大互联网技能从业者学习和参考。文章包含10625字,纯文字阅读大概需要16分钟。
内容图文
多线程
多线程是指在软件或硬件上实现多个线程并发执行的技术,具有多线程能力的计算机因为有硬件支持而使其能够在同一时间执行多个线程,进而提升整体的处理性能.本章将针对多线程进行详细讲解.
线程概述
在学习线程之前,需要先了解一下什么是进程.在一个操作系统中,每个独立执行的将程序都可以成为一个进程(正在运行的程序),目前几乎所有的操作系统都支持多任务,即能够同时执行多个程序,例如Windows,Linux,UNIX等.在多任务操作系统中,表面上支持进程并发执行,例如可以一边听音乐,一边看小说,但实际上这些程序并不是同时执行的.因此CPU具备分时机制,每个时间点只能执行一个程序,只不过由于CPU运行速度非常快,能够在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个进程的感觉.
每个运行中的程序都是一个进程,在这个进程的内部包含了多个执行单元,而每个执行单元就是一个线程,而且每个进程中至少包含一个线程,当一个Java程序启动时,就会产生一个进程,该进程会默认创建一个线程,称为主线程,在主线程上会运行main()方法中的代码.而在前面学习的程序中,代码都是按照顺序依次调用的,这种程序称为单线程程序.如果希望程序中实现多端程序代码交替执行的效果,则需要创建多线程程序.多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行,这里的并发是相对的,也就是说看起来是同时执行的,而实际上和进程一样,也是由CPU轮流调度的.
线程的创建
在Java中要想实现多线程操作有两种方式,一种是继承Thread类,另一种是实现Runnable接口.接下来针对这两种创建多线程的方式分别进行讲解,并比较他们的优缺点.
1.继承Thread类
Thread类是在java.lang包中定义的,一个类只要继承了Thread类,此类就是多线程的子类.在Thread的子类中,必须重写该类的run()方法,此方法为线程的主体.接下来通过案例演示通过继承Thread类的方式实现多线程.
创建ThreadDemo类
创建的ThreadDemo类需要继承Thread类,并重写Thread类中的run()方法,在run()方法中通过for循环输出五次"ThreadDemo类的run方法执行了"
public class ThreadDemo extends Thread{
public void run() {
for (int i =0;i<5;i++)
System.out.println("ThreadDemo执行了");
}
}
创建测试类,在该类中创建一个线程对象,并调用start()方法启动子线程,然后通过for循环在main()方法(主线程)中输出五次"主方法main()执行了"
public static void main(String[] args) {
ThreadDemo threadDemo=new ThreadDemo();
threadDemo.start();
for (int i=0;i<5;i++)
{
System.out.println("主方法main()执行了");
}
}
如图可以看出,两个for循环中的输出语句交错执行了,说明该程序实现了多线程. 从上图可以看出Test程序是一个多线程程序,在main()方法执行主线程的for循环语句前,单独开启了子线程执行ThreadDemo中的for循环语句,两个线程相互独立,互不影响.
实现Runnable接口
上代码通过继承Thread类实现了多线程,但是这种方式有一定的局限性,因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类,例如猫类Cat继承了动物类Animal,就无法通过继承Thread实现多线程.
为了克服这种弊端,在Thread类中提供了public Thread(Runnable target)和public Thread(Runnable target,String name) 两个构造方法,这两个构造方法都可以接受Runnable的子类实例对象,这样创建线程将调用实现了Runnable接口类中的run()方法作为运行代码,而不需要调用Thread类的run()方法,所以就可以依靠Runnable接口的实现类启动多线程.下面演示如何通过实现Runnable接口的方式创建多线程.
public class ThreadDemo implements Runnable{
public void run() {
for (int i =0;i<5;i++)
System.out.println("ThreadDemo执行了");
}
}
public static void main(String[] args) {
Thread thread=new Thread(new ThreadDemo());
thread.start();
for (int i=0;i<5;i++)
{
System.out.println("主方法main()执行了");
}
}
Thread类和Runnable接口对比分析
既然直接继承Thread类和实现Runnable接口都能实现多线程,那么这两种实现多线程的方式在实际应用中又有什么区别呢?接下来通过一种应用场景进行分析.
假设售票厅有四个窗口可发售某日某次列车的100张车票,这时,100张车票可以看作共享资源,四个售票窗口需要创建四个线程.下面分别通过继承Thread类的方式和实现Runnable接口的方式实现多线程的创建.
package com.thread;
public class Ticket extends Thread {
private int tickets=100;
private String name;
public Ticket(String name)
{
this.name =name;
}
public void run()
{
while (true)
{
if (tickets>0)
{
System.out.println(name+"正在卖第"+tickets--+"张票");
}
else break;
}
}
}
public static void main(String[] args) {
Thread thread1=new Ticket("窗口1");
Thread thread2=new Ticket("窗口2");
Thread thread3=new Ticket("窗口3");
Thread thread4=new Ticket("窗口4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
窗口1正在卖第100张票
窗口1正在卖第99张票
窗口2正在卖第100张票
窗口2正在卖第99张票
窗口4正在卖第100张票
窗口2正在卖第98张票
窗口3正在卖第100张票
由运行结果可以看出,100张票在每个窗口都输出了一次.出现这种情况的原因是四个线程没有共享100张票,而是各自出售了100张票.在程序中创建了四个Ticket对象,就相当于创建了四个售票程序,每个程序中都有100张票,每个线程独立处理各自的资源.
由于现实中的铁路系统的火车票资源是共享的,因此上面的运行结果显然不合理.为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程运行同一个售票对象的售票方法.下面通过实现Runnable接口的方法实现多线程的创建.
package com.thread;
public class Ticket implements Runnable {
private int tickets=100;
public void run()
{
while (true)
{
String name=Thread.currentThread().getName();
if (tickets>0)
{
System.out.println(name+"正在卖第"+tickets--+"张票");
}
else break;
}
}
}
Ticket ticket=new Ticket();
Thread thread1=new Thread(ticket,"窗口1");
Thread thread2=new Thread(ticket,"窗口2");
Thread thread3=new Thread(ticket,"窗口3");
Thread thread4=new Thread(ticket,"窗口4");
thread1.start();
thread2.start();
thread3.start();
thread4.start();
四个线程访问的是同一个tickets变量,共享了100张车票.由此可知,实现Runnable接口相对于继承Thread类来说,有如下显著的优势.
-
适合多个程序代码相同的线程处理同一资源的情况.
-
可以避免由于Java的单继承特性带来的局限.
所以在实际开发中建议使用第二种方式,即实现Runnable接口方式创建多线程.
线程的状态与转换
在Java中,要想实现多线程,就必须在主线程中创建新的线程对象.当线程对象创建完成时,线程的生命周期也就开始了,当run()方法正常执行完毕,或者出现未捕捉的异常或错误时,线程的生命周期也就结束了.线程的生命周期中包含五种状态,分别是新建状态,就绪状态,运行状态,阻塞状态和死亡状态,线程的不同状态表明了线程当前正在进行的活动.在程序中,通过一些操作可以使线程在不同状态之间进行转换.
下图展示了线程各种状态的转换关系,箭头表示可转换的方向,其中单箭头表示状态只能单项转换,双箭头表示表示两种状态可以互相转换,下面针对线程的五种状态分别进行详细讲解.
1.新建状态
当线程对象创建成功后,线程就处于新建状态,处于新建状态的线程仅仅是在Java虚拟机中分配了内容空间,此时还不能运行.
2.就绪状态
当线程对象调用了start()方法后,就进入了就绪状态,处于就绪状态的线程位于可运行池中,具备运行的条件,能否获得CPU的执行权需要等待系统的调度.
3.运行状态
当就绪状态的线程获得了CPU的执行权,并开始执行run()方法时,线程处于运行状态.一个线程启动后,它可能不会一直处于运行状态,当运行状态的线程使用完系统分配的时间后,系统就会剥夺该线程占用的CPU资源,让其他线程获得执行的机会.需要注意的是,只有处于就绪状态的线程才可能转换到就绪状态.
4.阻塞状态
一个正在执行的线程在某些特殊情况下,如果被人为挂起或需要执行耗时的输入/输出操作时,会让出CPU的执行权进入阻塞状态.进入阻塞状态的线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态.
5.死亡状态
线程调用stop()方法时或run()方法执行结束后,即处于死亡状态.处于死亡状态的线程不具有继续运行的能力,也不能转换到其他状态.
多线程同步
一个多线程的程序如果是通过Runnable接口实现的,则意味着类中的属性将被多个线程共享,这样一来也会引发一些安全问题,例如统计车间工人数量时,经常有人来回出入没很难统计正确.为了解决这一的问题,就需要实现多线程同步,限制某个资源在某一时刻只能被一个线程访问,接下来针对多线程同步进行详细讲解.
-
线程安全
针对上两种售票案例很可能出现一张票被打印多次或者出现负数的情况,这些情况都是由多线程操作共享资源tickets所导致的线程安全问题,为了能清楚地看出问题所在,接下来对上述代码进行更改,模拟四个窗口出售100张票,在售票的代码中使用sleep()方法,使每次售票时线程休眠10ms.
1.创建Ticket类
public class Ticket implements Runnable {
private int tickets=100;
public void run()
{
while (true)
{
try {
if (tickets>0)
{
Thread.sleep(1000);
String name=Thread.currentThread().getName();
System.out.println(name+"正在卖第"+tickets--+"张票");
}
else break;
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();}
}
}
}
可以看出,在一个线程还没有对票数进行减操作之前,其他线程就已经将票数减少了,这样一来就会出现票数为负的情况.
同步代码块
要解决上面的线程安全问题,就必须使用同步.同步是指多个操作在同一个时间段内只能有一个线程进行,其他线程要等待此线程完成后才能继续执行.Java为线程的同步操作提供了synchronized关键字,使用该关键字修饰的代码块被称为同步代码块,其语法格式如下:
synchronized(同步对象){
需要同步的代码;}
从上面的代码中可以看出,在使用同步代码块时必须指定一个需要同步的对象,也称为锁对象.一般情况下都将当前对象this设置为同步对象.
synchronized (this) {
try {
if (tickets>0)
{
Thread.sleep(10);
String name=Thread.currentThread().getName();
System.out.println(name+"正在卖第"+tickets--+"张票");
}
else break;
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
售出的票不再出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得到了解决.
同步方法
除了可以将需要的代码设置成同步代码块以外,也可以使用synchronized关键字将一个方法修饰成同步方法,它能实现和同步代码块同样的功能,具体语法格式如下:
权限修饰符 synchronized 返回值类型 方法名 ([参数1,...])
{
需要同步的代码;}
被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法.
接下来再对代码进行修改,使用synchronized关键字把saleTicket()修饰为同步方法,使用同步方法的方式解决线程安全问题.
private synchronized void saleTicket()
{
try {
if (tickets>0)
{
Thread.sleep(10);
String name=Thread.currentThread().getName();
System.out.println(name+"正在卖第"+tickets--+"张票");
}
}
catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
此代码完成了与之前同步代码块同样的功能.需要注意的时,同步方法的锁是当前调用该方法的对象,也就是this指向的对象.
内容总结
以上是互联网集市为您收集整理的Java基础之多线程全部内容,希望文章能够帮你解决Java基础之多线程所遇到的程序开发问题。 如果觉得互联网集市技术教程内容还不错,欢迎将互联网集市网站推荐给程序员好友。
内容备注
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 gblab@vip.qq.com 举报,一经查实,本站将立刻删除。
内容手机端
扫描二维码推送至手机访问。