第二十章多线程

概念


有很多工作是可以同时完成的,这种思想放在Java中被称为并发,并发完成每一件事被称为线程。

程序员可以在程序中执行多个线程,每一个线程完成一个功能//与其他线程并发执行,这种机制被称为多线程,并不算所有编程语言都支持多线程。

创建线程


继承Thread类和实现Runnable接口两种方法

继承Thread类


是Java.long包下的一个类,在这个类中实例化对象代表线程,程序员启动一个新线程需要建立一个实例。Thread类常用的两种构造方法如下:

public Thread():创建一个新的线程对象

public Thread(String threadName):创建一个名为threadName的线程对象

继承Thread类创建一个新的线程的语法如下:

public class ThreadTest extends Thread{
}

完成线程真实代码的功能放在run()方法,当继承Thread类后,就可以在该线程中覆盖run()方法,将实现线程功能的代码写入run()方法中,调用run()方法。

Thread对象需要一个任务来执行,任务是指线程在启动时执行的工作,这个代码写在了run()方法中,语法格式如下:

public void run(){

如果start()方法调用一个已经启动的线程,系统将抛出IllegalThreadStateException异常

例题20.1
package lx;
 
public class Demo20_1 extends Thread {
    public void run(){
        for(int i=0;i<=10;i++) {
            System.out.println(i+"");
        }
    }
    public static void main(String[] args) {
        Demo20_1 th=new Demo20_1();
        th.start();
        
    }
 
}
 


实现Runnable接口


线程都是通过扩展 Thread 类来创建的,如果需要继承其他类(非 Thread类),而且还要使当前类实现多线程,那么可以通过 Runnable 接口来实现。实现 Runnable 接口的语法如下:

public class Thread extends Object implements Runnable{
}

 实质上 Thread 类实现了 Runnable 接口,其中的run()方法正是对 Runnable 接口中的 run()方法的具体实现

实现 Runnable 接口的程序会创建一个 Thread 对象,并将 Runnable 对象与 Thread 对象相关联。

Thread 类中有两个构造方法:

public Thread(Runnable target)。

public Thread(Runnable target,String name)。

 这两个构造方法的参数中都存在 Runnable 实例

使用 Runnable 接口启动新的线程的步骤如下:

建立 Runnable 对象。

使用参数为 Runnable 对象的构造方法创建 Thread 实例。

调用 start()方法启动线程。

 线程最引人注目的部分应该是与 Swing 相结合创建GUI程序

例题20.2
package lx;
 
import java.awt.Container;
 
import javax.swing.JFrame;
import javax.swing.*;
 
public class Demo20_2 extends JFrame {
    int c=0;//图标横坐标
    public Demo20_2() {
        setBounds(300,200,250,100);//绝对定位窗体大小和位置
        Container con=getContentPane();//主容器
        con.setLayout(null);//使窗体不使用任何布局管理器
        
        Icon img=new ImageIcon("src/1.gif");//图标对象
        JLabel jl=new JLabel(img);//显示图标的标签
        jl.setBounds(10, 10, 200, 50);//设置标签的位置和大小
        Thread t=new Thread() {//定义匿名线程对象
            public void run() {
                while(true) {
                    jl.setBounds(c, 10, 200, 50);//将标签的横坐标用线程表示
                    try {
                        Thread.sleep(500);//使线程休眠500毫秒
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                    c+=4;//使横坐标每次增加4
                    if(c>=120) {
                        c=10;//当图标到达标签最右边时,使其回到标签最左边
                    }
                }
            }
        };
        t.start();//启动线程
        con.add(jl);//将标签添加到容器中
        setVisible(true);//使窗体可见
        setDefaultCloseOperation(EXIT_ON_CLOSE);//设置窗体的关闭方式
    }
    public static void main(String[] args) {
        new Demo20_2();
    }
 
}


为了使图标具有滚动功能,需要在类的构造方法中创建 Thread 实例。

在创建该实例的同时需要 Runnable 对象作为 Thread 类构造方法的参数,然后使用内部类形式实现 run()方法。

在 run()方法中主要循环图标的横坐标位置,

当图标横坐标到达标签的最右方时,再次将图标的横坐标置于图标滚动的初始位置。

启动一个新的线程,不是直接调用 Thread 子类对象的 run()方法,而是调用 Thread 子类的 start()方法,Thread 类的 start()方法产生一个新的线程,该线程运行 Thread 子类的 run()方法。

线程的生命周期


线程具有生命周期,其中包含 7 种状态,分别为出生状态、就绪状态、运行状态、等待状态、休眠状态、阻塞状态和死亡状态。

出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用start()方法之前线程都处于出生状态.

当用户调用 start()方法后,线程处于就绪状态 

当线程得到系统资源后就进入运行状态。

旦线程进入可执行状态,它会在就绪与运行状态下转换,同时也有可能进入等待、休眠、阻塞或死亡状态。

虽然多线程看起来像同时执行,但事实上在同一时间点上只有一个线程被执行,只是线程之间切换较快,所以才会使人产生线程是同时进行的假象。

操作线程的方法


操作线程有很多方法,这些方法可以使线程从某一种状态过波到另一种状态。

 线程的休眠


一种能控制线程行为的方法是调用 seep()方法,sleep()方法需要一个参数用于指定该线程休眠的时间,该时间以毫秒为单位。slep()方法的语法如下:

try{
Thread.sleep(2000);

}catch(InterruptedException e){
e.printStackTrace();

}

上述代码会使线程在 2 秒之内不会进入就绪状态。

由于 sleep()方法的执行有可能抛InterrupledException 异常,所以将 sleep()方法的调用放在 try-catch 块中。

不能保证线程醒来后进入运行状态,只能保证它进入就绪状态。

例题20.3
package lx;
 
import java.awt.Color;
import java.awt.Graphics;
import java.util.Random;
 
import javax.swing.JFrame;
 
public class Demo20_3 extends JFrame {
    private static Color[]color= {//定义颜色数组
            Color.BLACK,Color.BLUE,Color.CYAN,Color.GREEN,Color.ORANGE,Color.YELLOW
            ,Color.RED,Color.PINK,Color.LIGHT_GRAY
    };
    private static  final Random rand=new Random();
    //创建随机对象
    private static Color getC() {//获取随机颜色值的方法
        return color[rand.nextInt(color.length)];
    }
    public Demo20_3() {
        Thread t=new Thread(new Runnable() {
            //创建匿名线程对象
            int x=70;
            //定义初始坐标
            int y=50;
            public void run() {
                while(true) {//无限循环
                    try {
                        Thread.sleep(100);//线程休眠0.1秒
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
                Graphics g= getGraphics();//获取组件绘图上下文对象
                g.setColor(getC());//设置绘图颜色
                g.drawLine(x, y, 200, y++);//绘制直线并递增垂直坐标
                if(y>=100) {
                    y=50;
                }
            }
        }
            });
        t.start();//启动线程
    }
    
    public static void main(String[] args) {
        init(new Demo20_3(),300,100);
    }
    
    public static void init(JFrame f,int w,int h) {//初始化程序界面的方法
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setSize(w, h);
        f.setVisible(true);
    }
}


在本实例中定义了 getC()方法,该方法用于随机产生 Color 类型的对象并且在产生线程的匿名内部类中使用 getGraphics()方法获取 Graphics 对象,使用该对象调用 setColor()方法为图形设置颜色。调用 drawLine()方法绘制一条线段,同时线段会根据纵坐标的变化自动调整。

线程的加入


如果当前某程序为多线程程序,假如存在一个线程 A,现在需要插入线程 B,并要求线程 B 先执行完毕,然后再继续执行线程 A,此时可以使用 Thread 类中的 join()方法来完成。

例题20.4
package lx;
 
import java.awt.BorderLayout;
 
import javax.swing.*;
 
public class Demo20_4 extends JFrame{
    private Thread A;//定义两个线程
    private Thread B;
    private JProgressBar Bar=new JProgressBar();//定义两个进度条组件
    private JProgressBar Bar2=new JProgressBar();
    
    public static void main(String[] args) {
        Demo20_4 Text=new Demo20_4();
        Text.setVisible(true);
 
    }
    public Demo20_4() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setBounds(200,200,200,100);
        getContentPane().add(Bar,BorderLayout.NORTH);//将进度条设置在窗体最北面
        getContentPane().add(Bar2,BorderLayout.SOUTH);//将进度条设置在窗体最南面
        Bar.setStringPainted(true);//设置进度条显示数字字符
        Bar2.setStringPainted(true);
        A=new Thread(new Runnable() {//使用匿名内部类形式初始化Thread实例
            int c=0;
            public void run() {//重写润()方法
                while(true) {
                    Bar.setValue(c++);//设置进度条当前值
                    try {
                        
                        Thread.sleep(100);//让A线程休眠100毫秒
                        
                        B.join();//让/B调用join()方法
                        if(c==30)//设置当A线程走到了30,B线程才启动
                            B.start();//启动B线程
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        A.start();//启动A线程
        B=new Thread(new Runnable() {
            int c=0;
            public void run() {
                while(true) {
                    Bar2.setValue(++c);//设置进度条当前值
                    try {
                        Thread.sleep(100);//让B线程休眠100毫秒
                        
                    }catch(InterruptedException e) {
                        e.printStackTrace();
                        
                }
                    if(c==100)//当c变量增长为100时
                        break;            //跳出循环    
                }
            }
        });
        
        }
    }


线程的中断


如果线程是因为使用了 sleep()或 wait()方法进入了就绪状态,可以使用 Thread 类中 interrupt()方法使线程离开run()方法,同时结束线程,但程序会抛出 InterruptedException 异常,用户可以在处理该异常时完成线程的中断业务处理,如终止 while 循环。

例题20.5
package lx;
 
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
 
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JProgressBar;
 
public class Demo20_5 extends JFrame{
    
    public Demo20_5(){
        JProgressBar Bar=new JProgressBar();//创建进度条
        getContentPane().add(Bar,BorderLayout.NORTH);//将进度条设置在窗体最北面
        JButton b=new JButton("停止");
        getContentPane().add(b,BorderLayout.SOUTH);//将进度条设置在窗体最南面
        Bar.setStringPainted(true);//设置进度条显示数字字符
        Thread t=new Thread(new Runnable() {
            int c=0;
            public void run() {
                while(true) {
                    Bar.setValue(++c);//设置进度条当前值
                    try {
                        Thread.sleep(100);//让A线程休眠100毫秒
                    }catch(InterruptedException e) {//捕捉InterruptedException异常
                        System.out.println("当前线程程序被中断");
                        break;
                    }
                }
            }
            
        });
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                t.interrupt();//中断线程
            }
        });
        t.start();//启动线程
    }
    public static void init(JFrame frame,int w,int h) {
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(w, h);
        frame.setVisible(true);
    }
    public static void main(String[] args) {
        init(new Demo20_5(),100,100);
 
    }
 
}


线程的礼让


Thread 类中提供了一种礼让方法,使用 yield()方法表示,它只是给当前正处于运行状态的线程一个提醒,告知它可以将资源礼让给其他线程,但这仅是一种暗示,没有任何一种机制保证当前线程会将资源礼让。yicld()方法使具有同样优先级的线程有进入可执行状态的机会,在当前线程放弃执行权时会再度回到就绪状态。

线程的优先级


每个线程都具有各自的优先级,线程的优先级可以表明在程序中该线程的重要性。

线程的优先级可以使用 setPriority()方法调整,如果使用该方法设置的优先级不在 1~10,将产生IllegalArgumentException 异常。

例题20.6
package lx;
 
public class Demo20_6 implements Runnable{
    String name;
    public     Demo20_6(String name) {
        this.name=name;
    }
        public void run() {
            String tmp="";
            for(int i=0;i<50000;i++) {//完成5万次字符串拼接
                tmp+=i;
            }
            System.out.println(name+"线程完成任务");
        }
    
    
    public static void main(String[] args) {
    Thread a=new Thread(new Demo20_6("A"));//A线程优先级最小
    a.setPriority(1);
    Thread b=new Thread(new Demo20_6("B"));
    b.setPriority(3);
    Thread c=new Thread(new Demo20_6("C"));
    c.setPriority(7);
    Thread d=new Thread(new Demo20_6("D"));//D线程优先级最大
    d.setPriority(10);
    a.start();
    b.start();
    c.start();
    d.start();
    //线程的执行顺序由CPU决定,所有可能不一定按优先级排序
    }
 
}

结果


由于线程的执行顺序是由 CPU 决定的,即使线程设定了优先级也是作为 CPU 的参考数据,所以真实的运行结果可能并不一定按照优先级排序

线程同步


在单线程程序中,每次只能做一件事情,后面的事情需要等待前面的事情完成后才可以进行,但是如果使用多线程程序,就会发生两个线程抢占资源的问题,如两个人同时说话、两个人同时过同-个独木桥等。所以,在多线程编程中需要防止这些资源访问的冲突。Java 提供了线程同步的机制来防止资源访问的冲突。

 线程安全


实际开发中,使用多线程程序的情况很多,以火车站售票系统为例,在代码中判断当前票数是否大于 0,如果大于 0 则执行将该票出售给乘客的功能,但当两个线程同时访问这段代码时(假如这时只剩下一张票),第一个线程将票售出,与此同时第二个线程也已经执行完成判断是否有票的操作,并得出票数大于 0 的结论,于是它也执行售出操作,这样就会产生负数。

所以,在编写多线程程序时,应该考虑到线程安全问题。实质上线程安全问题来源于两个线程同时存取单一对象的数据。

实例
package lx;
 
public class Demo20_6_1 implements Runnable {
    int n=10;//设置当前总票数
    public void run() {
        while(true) {//设置无限循环
            if(n>0) {//判断当前票数是否大于 0
                try {
                    Thread.sleep(100);        //使当前线程休眠 100毫秒    
                    }
            catch(InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"————票数"+n--);//票数减1
            }    
        }
    }
    
    
    public static void main(String[] args) {
        Demo20_6_1 t=new Demo20_6_1();//实例化类对象
        Thread tA=new Thread(t,"线程一");//以该类对象分别实例化 4 个线程
        Thread tB=new Thread(t,"线程二");
        Thread tC=new Thread(t,"线程三");
        Thread tD=new Thread(t,"线程四");
        tA.start();//分别启动线程
        tB.start();
        tC.start();
        tD.start();
        
        
    }
 
}


线程同步机制


该如何解决资源共享的问题呢? 所有解决多线程资源冲突问题的方法基本上都是采用给定时间只允许一个线程访问共享资源的方法,这时就需要给共享资源上一道锁。

同步块


Java 中提供了同步机制,可以有效地防止资源冲突。同步机制使用 synchronized 关键字,使用该关键字包含的代码块称为同步块,也称为临界区,语法如下:

synchronized (Object){
}

 通常将共享资源的操作放置在 synchronized 定义的区域内,这样当其他线程获取到这个锁时,就必须等待锁被释放后才可以进入该区域。

例题20.7
package lx;
 
public class Demo20_6_1 implements Runnable {
    int n=10;//设置当前总票数
    public void run() {
        while(true) {//设置无限循环
            synchronized (this) {
                if(n>0) {//判断当前票数是否大于 0
                    try {
                        Thread.sleep(100);        //使当前线程休眠 100毫秒    
                        }
                catch(InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"————票数"+n--);//票数减1
                }    
            }
            
        }
    }
    
    
    public static void main(String[] args) {
        Demo20_6_1 t=new Demo20_6_1();//实例化类对象
        Thread tA=new Thread(t,"线程一");//以该类对象分别实例化 4 个线程
        Thread tB=new Thread(t,"线程二");
        Thread tC=new Thread(t,"线程三");
        Thread tD=new Thread(t,"线程四");
        tA.start();//分别启动线程
        tB.start();
        tC.start();
        tD.start();
        
        
    }
 
}


从这个结果可以看出,打印到最后票数没有出现负数,这是因为将共享资源放置在了同步块中,不管程序如何运行都不会出现负数。

同步方法


同步方法就是在方法前面用 synchronized 关键字修饰的方法,其语法如下:

synchronized void f(){
}

 当某个对象调用了同步方法时,该对象上的其他同步方法必须等待该同步方法执行完毕后才能被执行。必须将每个能访问共享资源的方法修饰为 synchronized,否则就会出错。

修改20.7的代码如下:
package lx;
 
public class Demo20_6_1 implements Runnable {
    int n=10;//设置当前总票数
    
    public  synchronized void du() {
        if(n>0) {//判断当前票数是否大于 0
            try {
                Thread.sleep(100);        //使当前线程休眠 100毫秒    
                }
        catch(InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"————票数"+n--);//票数减1
        }    
    }
    public void run() {
        while(true) {//设置无限循环
            du();
            
        }
    }
    
    
    public static void main(String[] args) {
        Demo20_6_1 t=new Demo20_6_1();//实例化类对象
        Thread tA=new Thread(t,"线程一");//以该类对象分别实例化 4 个线程
        Thread tB=new Thread(t,"线程二");
        Thread tC=new Thread(t,"线程三");
        Thread tD=new Thread(t,"线程四");
        tA.start();//分别启动线程
        tB.start();
        tC.start();
        tD.start();
        
        
    }
 
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值