19、多线程

 在日常生活中,很多事情都是同时进行的。例如,人可以同时进行呼吸、血液循环、思考问题等活动。在使用计算机的过程中,应用程序也可以同时运行,用户可以使用计算机一边听歌,一边打游戏。在应用程序中,不同的程序块也是可以同时运行的,这种多个程序块同时运行的现象被称做并发执行。
 多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一个线程,它们会交替执行,彼此之间可以进行通信。
1.1 进程
 什么是进程?
  在一个操作系统中,每个独立执行的程序都可被称为一个进程,也就是"正在运行的程序"。目前大部分计算机上安装的都是多任务操作系统,即能同时执行多个应用程序。
1.2线程
 通过上面的学习可以知道,每个运行的程序都是一个进程,在一个进程中还可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。操作系统中的每一个进程中都至少存在一个线程。当一个Java程序启动时,就会产生一个进程,该进程会默认创建一个线程,在这个线程上会运行main()方法中的代码。
 在以前的程序学习中,代码都是按照调用顺序依次往下执行,没有出现两段程序代码交替运行的效果,这样的程序称作单线程程序。如果希望程序中实现多段程序代码交替运行的效果,则需要创建多个线程,即多线程程序。多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行。

  如图所示的多条线程,看似是同时执行的,其实不然,它们和进程一样,也是由CPU轮流执行的,只不过CPU运行速度很快,故而给人同时执行的感觉。


2.线程的创建
 在Java程序中如何实现多线程?
  Java提供了两种多线程实现方式,一种是继承java.lang包下的Thread类,覆写Thread类的run()方法,在run()方法中实现运行在线程上的代码;另一种是实现java.lang.Runnable接口,同样是在run()方法中实现运行在线程上的代码。
2.1继承Thread类创建多线程
 public class Test1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //创建线程MyThread的线程对象
        myThread.start(); //开启线程
        while (true) { //通过死循环语句打印输出
            System.out.println("main()方法在运行");
            
        }
    }
}
class MyThread extends Thread{
    public void run() {
        while (true) { //通过死循环语句打印输出
            System.out.println("MyThread类的run()方法在运行");
        }
    }
}
通过继承Thread类,并且重写Thread类中的run()方法便可实现多线程。在Thread类中,提供了一个start()方法用于启动新线程,线程启动后,系统会自动调用run()方法。


运行结果:
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行


2.2实现Runnable接口创建多线程
 上面通过继承Thread类实现了多线程,但是这种方式有一定的局限性。因为Java中只支持单继承,一个类一旦继承了某个父类就无法再继承Thread类。
 为了克服这种弊端,Thread类提供了另外一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。当通过Thread(Runnable target)构造方法创建线程对象时,只需为该方法传递一个实现了Runnable接口的实例对象,这样创建的线程将调用实现了Runnable接口中的run()方法作为运行代码,而不需要调用Thread类中的run()方法。
public class Test1 {
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //创建MyThrad的实例对象
        Thread thread = new Thread(myThread); //创建线程对象
        thread.start(); //开启线程,执行线程中的run()方法
        while (true) {
            System.out.println("main方法在运行");
        }
    }
}
class MyThread implements Runnable{


    public void run() { //线程的代码块,当调用start()方法时,线程从此处开始执行
        while (true) {
            System.out.println("MyThread类的run()方法在运行");
        }
    }
    
}
运行结果:
main方法在运行
main方法在运行
main方法在运行
main方法在运行
main方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
3.多线程同步
 3.1线程安全
public class TestSP2 {
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread(); //创建SaleThread实例对象saleThread
        new Thread(saleThread,"线程 1").start(); //创建线程对象并命名为线程 1,开启线程
        new Thread(saleThread,"线程 2").start(); //创建线程对象并命名为线程 2,开启线程
        new Thread(saleThread,"线程 3").start(); //创建线程对象并命名为线程 3,开启线程
        new Thread(saleThread,"线程 4").start(); //创建线程对象并命名为线程 4,开启线程
    }
}
class SaleThread implements Runnable {
    private int tickets = 10; //10张票
    public void run() {
        while (tickets>0) { 
            
                try {
                    Thread.sleep(10); //经过此处的线程休眠10毫秒
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
            }
        }
    
}
运行结果:
线程 4---卖出的票10
线程 3---卖出的票10
线程 1---卖出的票9
线程 2---卖出的票8
线程 1---卖出的票7
线程 4---卖出的票6
线程 3---卖出的票7
线程 2---卖出的票5
线程 4---卖出的票4
线程 1---卖出的票3
线程 3---卖出的票4
线程 2---卖出的票2
线程 3---卖出的票1
线程 1---卖出的票1
线程 4---卖出的票0
线程 2---卖出的票-1


 最后几行打印售出的票为0和负数,这种现象是不应该出现的,因为在售票程序中做了判断,只有当票号大于0时才会进行售票。运行结果中之所以出现了负数的票号是因为多线程在售票时出现了安全问题。接下来对问题进行简单的分析。
 在售票成绩程序的while循环中添加了sleep()方法,这样就模拟了售票过程中线程的延迟。由于线程有延迟,当票号减为1时,假设线程1此时出售1号票,对票号进行判断后,进入while循环,在售票之前通过sleep()方法让线程休眠,这时线程2会进行售票,由于此时票号仍为1,因此线程2也会进入循环,同理,四个线程都会进入while循环,休眠结束后,四个线程都会进行售票,这样就相当于将票号减了四次,结果中出现了0、-1、-2这样的票号。
 3.2同步代码块
 通过上面的学习了解到,线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决上述代码中的问题,必须保证下面用于处理共享资源的代码在任何时刻只能有一个线程访问。
 为了实现这种限制,Java中提供了同步机制。当多个线程使用同一个共享资源时,可以将处理共享资源的代码放置在一个代码块中,使用synchronized关键字来修饰,被称作同步代码块,其语法格式如下:
  synchronized (lock) {
    操作共享资源代码块
  }
 上面的代码中,lock是一个锁对象,它是同步代码块的关键。当线程执行同步代码块是,首先会检查锁对象的标志位,默认情况下标志位为1,此时线程会执行同步代码块,同时将锁对象的标志位置为0.当一个新的线程执行到这段同步代码块是,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后,锁对象的标志位被置为1,新线程才能激怒同步代码块执行其中的代码。循环往复,直到共享资源被处理完为止。这个过程就好比一个公用电话亭,只有前一个打完电话出来后,后面的人才可以打。
 public class TestSP3 {
    public static void main(String[] args) {
        Ticket1 ticket = new Ticket1(); //创建Ticket1实例对象ticket
        new Thread(ticket,"线程 1").start(); //创建线程对象并命名为线程 1,开启线程
        new Thread(ticket,"线程 2").start(); //创建线程对象并命名为线程 2,开启线程
        new Thread(ticket,"线程 3").start(); //创建线程对象并命名为线程 3,开启线程
        new Thread(ticket,"线程 4").start(); //创建线程对象并命名为线程 4,开启线程
    }
}
class Ticket1 implements Runnable {
    private int tickets = 10; //10张票
    Object lock = new Object(); //定义任意一个对象,用作同步代码块的锁
    public void run() {
        while (true) { 
            synchronized (lock) {
                try {
                    Thread.sleep(10); //经过此处的线程休眠10毫秒
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                if (tickets>0) {
                    System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
                } else {
                    break;
                }
                
            }
        }
    }
}
运行结果:
线程 1---卖出的票10
线程 1---卖出的票9
线程 4---卖出的票8
线程 4---卖出的票7
线程 4---卖出的票6
线程 4---卖出的票5
线程 4---卖出的票4
线程 4---卖出的票3
线程 4---卖出的票2
线程 4---卖出的票1
上诉的代码将有关tickets变量的操作全部都放到同步代码块中。为了保证线程的持续执行,将同步代码块放在死循环中,直到ticket<0时跳出循环。因此,从运行结果中可以看出,售出的票不在出现0和负数的情况,这是因为售票的代码实现了同步,之前出现的线程安全问题得以解决。运行结果中并没有出现 线程2,3 售票的语句,出现这样的现象是很正常的,这是因为线程在获得锁对象时有一定的随机性,在整个程序的运行期间,线程2,3 始终未获得锁对象。
3.3同步方法
 通过3.2的学习,了解到同步代码块可以有效的解决线程的安全问题,当把共享资源的操作放在synvhronized定义的区域内时,便为这些操作加了同步锁。在方法前面同样可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现同步代码块同样的功能,具体语法格式如下:
  synchronized 返回值类型 方法名 ([参数1,.....]){}


  public class TestSP4 {
    public static void main(String[] args) {
        Ticket2 ticket = new Ticket2(); //创建Ticket1对象
        //创建并开启四个线程
        new Thread(ticket,"线程一").start();
        new Thread(ticket,"线程二").start();
        new Thread(ticket,"线程三").start();
        new Thread(ticket,"线程四").start();
    }
}


class Ticket2 implements Runnable {
    private int tickets = 10; // 10张票


    public void run() {
        while (true) {
            saleTicket(); //调用售票方法
            if (tickets <= 0) {
                break;
            }
        }
    }


    private synchronized void saleTicket() {
        if(tickets>0){
            try {
                Thread.sleep(10); //经过此处的线程休眠10毫秒
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
                System.out.println(Thread.currentThread().getName()+"---卖出的票"+tickets--);
        }
    }
}
运行结果:
线程一---卖出的票10
线程一---卖出的票9
线程一---卖出的票8
线程一---卖出的票7
线程一---卖出的票6
线程一---卖出的票5
线程一---卖出的票4
线程一---卖出的票3
线程一---卖出的票2
线程一---卖出的票1
4.多线程通信
 4.1问题引入
 public class Storage1 {
    //数据存储数组
    private int[] cells = new int[10];
    
    //inPos表示存入时数组下标,outPos表示取出时数组下标
    private int inPos,outPos;
    
    //定义一个put()方法向数组中存入数据
    public void put(int num){
        cells[inPos]=num;
        System.out.println("在cells["+inPos+"]中放入数据---"+cells[inPos]);
        inPos++; //存完元素让位置加1
        if(inPos==cells.length)
            inPos=0; //当inPos为数组长度时,将其置为0
    }
    
    //定义一个get() 方法从数组中取出数据
    public void get() {
        int data = cells[outPos];
        System.out.println("从celss["+outPos+"]中取出数据"+data);
        outPos++; //取完元素让位置加1
        if(outPos==cells.length)
            outPos=0;
    }
}


class Input1 implements Runnable{ //输入线程类


    private Storage1 st;
    private int num; //定义一个变量num
    
    Input1(Storage1 st) { //通过构造方法接受一个Storage对象
        this.st = st;
    }
    public void run() {
        while(true){
            st.put(num++); //将num存入数组,每次存入后num自增
        }
    }


}


class Output1 implements Runnable{ //输出线程类


    private Storage1 st;
    private int num; //定义一个变量num
    
    Output1(Storage1 st) { //通过构造方法接受一个Storage对象
        this.st = st;
    }
    public void run() {
        while(true){
            st.get(); //循环取出元素
        }
    }


}


public class TestStor1 {    
    public static void main(String[] args) {
        Storage1 st = new Storage1(); //创建数据存储类对象
        Input1 input = new Input1(st); //创建Input对象传入Storage1对象
        Output1 output = new Output1(st); //创建Output对象传入Storage1对象
        
        new Thread(input).start(); //开启新线程
        new Thread(output).start(); //开启新线程
    }
}
运行结果:
在cells[6]中放入数据---25706
在cells[7]中放入数据---25707
从celss[4]中取出数据25594
从celss[5]中取出数据25705
从celss[6]中取出数据25706
 其中特殊标记的两行运行结果表示在取出数字25594后,紧接着取出的是25705,这样的现象明显是不对的。我们希望出现的运行结果是依次取出递增的自然数。之所以出现这种现象是因为在Input线程存入数字25595时,Output线程并没有及时取出数据,Input线程一直在持续地存入数据,直到将数组放满,又从数组的第一位置开始存入25700,25701,25702,25703,25704,25705...,当Output线程再次取出数据时,取出的不再是25595而是25705。
 4.2问题如何解决
  想解决上述问题,就需要控制多个线程按照一定的顺序轮流执行,此时需要让线程间进行通信。在Object类中提供了wait()、notify()、notifyAll()、方法用于解决线程间的通信问题,由于Java中所有类都是Object类的子类或间接子类,因此任何类的实例对象都可以直接使用这些方法。
 
 public class Storage2 {
    // 数据存储数组
    private int[] cells = new int[10];


    // inPos表示存入时数组下标,outPos表示取出时数组下标
    private int inPos, outPos;
    private int count;


    public synchronized void put(int num) {
        try {
            //如果放入数据等于cells的长度,此线程等待
            while (count == cells.length) {
                this.wait();
            }
            cells[inPos] = num; //向数组中放入数据
            System.out.println("在cells[" + inPos + "]中放入数据---" + cells[inPos]);
            inPos++; // 存完元素让位置加1
            if (inPos == cells.length)
                inPos = 0; // 当inPos为数组长度时,将其置为0
            count++; //每放一个数据count加1
            this.notify();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }


    public synchronized void get() {
        try {
            //如果count为0,此线程等待
            while (count == 0) {
                this.wait();
            }
            int data=cells[outPos]; //向数组中取出数据
            System.out.println("在Cells[" + outPos + "]中取出数据---" + data);
            cells[outPos]=0; //取出后,当前位置的数据置0
            outPos++;
            if (outPos == cells.length)
                outPos = 0; // 当inPos为数组长度时,将其置为0
            count--; //每取出一个数据count减1
            this.notify();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }


    }
}


运行结果:
在Cells[5]中取出数据---41625
在Cells[6]中取出数据---41626
在Cells[7]中取出数据---41627
在Cells[8]中取出数据---41628
在Cells[9]中取出数据---41629
在Cells[0]中取出数据---41630
在cells[1]中放入数据---41631
在cells[2]中放入数据---41632
在cells[3]中放入数据---41633
在cells[4]中放入数据---41634
在cells[5]中放入数据---41635
在cells[6]中放入数据---41636
在cells[7]中放入数据---41637
在cells[8]中放入数据---41638
在cells[9]中放入数据---41639
在cells[0]中放入数据---41640
在Cells[1]中取出数据---41631
在Cells[2]中取出数据---41632
在Cells[3]中取出数据---41633
在Cells[4]中取出数据---41634
在Cells[5]中取出数据---41635
在Cells[6]中取出数据---41636
在Cells[7]中取出数据---41637
在Cells[8]中取出数据---41638
在Cells[9]中取出数据---41639
在Cells[0]中取出数据---41640
在cells[1]中放入数据---41641
在cells[2]中放入数据---41642
在cells[3]中放入数据---41643
在cells[4]中放入数据---41644
在cells[5]中放入数据---41645
在cells[6]中放入数据---41646
在cells[7]中放入数据---41647
在cells[8]中放入数据---41648
在cells[9]中放入数据---41649
在cells[0]中放入数据---41650
在Cells[1]中取出数据---41641
在Cells[2]中取出数据---41642
在Cells[3]中取出数据---41643
在Cells[4]中取出数据---41644
在Cells[5]中取出数据---41645
在Cells[6]中取出数据---41646
在Cells[7]中取出数据---41647
在Cells[8]中取出数据---41648
在Cells[9]中取出数据---41649
在Cells[0]中取出数据---41650
在cells[1]中放入数据---41651
在cells[2]中放入数据---41652
在cells[3]中放入数据---41653
在cells[4]中放入数据---41654
在cells[5]中放入数据---41655
在cells[6]中放入数据---41656
在cells[7]中放入数据---41657
在cells[8]中放入数据---41658
在cells[9]中放入数据---41659
在cells[0]中放入数据---41660
在Cells[1]中取出数据---41651
在Cells[2]中取出数据---41652
在Cells[3]中取出数据---41653
在Cells[4]中取出数据---41654
在Cells[5]中取出数据---41655
在Cells[6]中取出数据---41656
在Cells[7]中取出数据---41657
在Cells[8]中取出数据---41658
在Cells[9]中取出数据---41659
在Cells[0]中取出数据---41660
在cells[1]中放入数据---41661
在cells[2]中放入数据---41662
在cells[3]中放入数据---41663
在cells[4]中放入数据---41664
在cells[5]中放入数据---41665
在cells[6]中放入数据---41666
在cells[7]中放入数据---41667
在cells[8]中放入数据---41668
在cells[9]中放入数据---41669


首先通过使用synchronized关键字将put()方法和get()方法修饰为同步方法,之后每操作一次数据,便调用一次notify()方法唤醒对应同步锁上等待的线程。当存入数据时,如果count的值与cells数组的长度相同,说明数组已经添满,此时就需要调用同步锁的wait()方法使存入数据的线程进入等待状态。同理,当取出数据时如果count的值为0,说明数组已被取空,此时就需要调用同步锁的wait()方法,使取出数据的线程进入等待状态。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值