Java学习笔记4(多线程)

多线程

多个程序块同时运行的现象被称作并发执行。多线程就是指一个应用程序中有多条并发执行的线索,每条线索都被称作一条线程,它们会交替执行,彼此间可以进行通信。
进程:在一个操作系统中,每个独立执行的程序都可称作为一个进程,也就是“正在运行的程序”。在计算机中,所有的程序都是由CPU执行的,CPU在某个时间点只能执行一个进程,在极短的时间下在不同的进程之间进行切换,给人程序在同时进行的感觉。
线程:每个运行的程序都是一个进程,在一个进程中可以有多个执行单元同时运行,这些执行单元可以看作程序执行的一条条线索,被称为线程。多条线程看似同时执行,其实不然,它和进程一样,都是CPU轮流执行的。
继承Thread类创建多线程
通过继承Thread类,并重写Thread类中的run()方法便可以实现多线程,在Thread类中提供了start()方法用于启动新线程。
class MyThread extends Thread{
    //重写run()方法
    public void run(){
        while(true){
            System.out.println("MyThread类的run()方法在运行");
        }
    }
}
public class Test{
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();   //开启线程
        while(true){
            System.out.println("main()方法在运行");
        }
    }
}
运行结果
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
在多线程中,main()方法和MyThread类的run()方法可以同时运行,互不影响。
实现Runnable接口创建多线程
由于Java中只支持单继承,当一个类继承一个类后就无法继承Thread了,所以Thread类提供了一个构造方法Thread(Runnable target),其中Runnable是一个接口,它只有一个run()方法。
class MyThread implements Runnable{
    //重写run()方法
    public void run(){  //线程的代码段,当调用start()方法时,线程从此处开始执行
        while(true){
            System.out.println("MyThread类的run()方法在运行");
        }
    }
}
public class Test{
    public static void main(String[] args) {
        MyThread myThread = new MyThread(); //创建MyThread实例对象
        Thread thread = new Thread(myThread);   //创建线程对象
        thread.start();   //开启线程,执行线程中run()方法
        while(true){
            System.out.println("main()方法在运行");
        }
    }
}
运行结果
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
MyThread类的run()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
main()方法在运行
MyThread类实现了Runnable接口,并重写Runnable接口中的run()方法,通过Thread类的构造方法将MyThread类的实例对象作为参数传入。
两种实现多线程方式的对比分析
举个例子,有四个售票窗口共出售100张票,四个窗口相当于四个线程。
通过Thread的currentThread()方法得到当前线程的实例对象,然后调用getName()可以获取到线程的名字。
下面是继承Thread类的方式实现多线程的创建:
class TicketWindows extends Thread{
    private int tickets = 100;
    public void run(){
        while(true){
            if (tickets>0){
                Thread th = Thread.currentThread(); //获取当前进程(目的是为了下一句能获取当前进程名字)
                String th_name = th.getName();  //获取当前进程的名字
                System.out.println(th_name+"正在发售第"+tickets--+"张票");
            }else
                break;  //跳出死循环
        }
    }
}
public class Test{
    public static void main(String[] args) {
        new TicketWindows().start();    //创建一个线程对象TicketWindows并开启
        new TicketWindows().start();    //再创建一个线程对象TicketWindows并开启
        new TicketWindows().start();    //再创建一个线程对象TicketWindows并开启
        new TicketWindows().start();    //再创建一个线程对象TicketWindows并开启
    }
}
运行结果
Thread-3正在发售第100张票
Thread-3正在发售第99张票
Thread-3正在发售第98张票
Thread-0正在发售第100张票
Thread-0正在发售第99张票
Thread-0正在发售第98张票
Thread-1正在发售第100张票
Thread-2正在发售第100张票
Thread-2正在发售第99张票
为了保证资源共享,在程序中只能创建一个售票对象,然后开启多个线程去运行同一个售票对象的售票方法,这就需要用到多线程的第二种实现方法。
使用构造方法Thread(Runnable target,String name)在创建线程对象的同时指定线程的名称,下面是实现Runnable接口的方法实现多线程:
class TicketWindows implements Runnable{
    private int tickets = 100;
    public void run(){
        while(true){
            if (tickets>0){
                Thread th = Thread.currentThread(); //获取当前进程(目的是为了下一句能获取当前进程名字)
                String th_name = th.getName();  //获取当前进程的名字
//上面两行等同于String th_name = Thread.currentThread().getName();
                System.out.println(th_name+"正在发售第"+tickets--+"张票");
            }else
                break;  //跳出死循环
        }
    }
}
public class Test{
    public static void main(String[] args) {
        TicketWindows tw = new TicketWindows(); //创建TicketWindows实例对象tw
        new Thread(tw,"窗口1").start();   //创建线程对象并命名为窗口1,开启线程
        new Thread(tw,"窗口2").start();   //创建线程对象并命名为窗口2,开启线程
        new Thread(tw,"窗口3").start();   //创建线程对象并命名为窗口3,开启线程
        new Thread(tw,"窗口4").start();   //创建线程对象并命名为窗口4,开启线程
    }
}
运行结果
窗口3正在发售第98张票
窗口3正在发售第96张票
窗口2正在发售第100张票
窗口4正在发售第94张票
窗口3正在发售第93张票
窗口1正在发售第92张票
通过以上两个例子可以看出,Runnable接口相对于继承Thread类来说有两个好处:①适合多个相同程序代码的线程去处理同一个资源,把线程同程序代码、数据有效的分离,很好的体现了面向对象的设计思想②可以避免由于Java的单继承带来的局限性。
后台线程
创建的线程默认都是前台线程,如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成了后台线程。进程中只有后台线程运行时,进程就会结束。
class DamonThread implements Runnable{
    public void run(){
        while(true){
            System.out.println(Thread.currentThread().getName()+"---is running.");
        }
    }
}
public class Test{
    public static void main(String[] args) {
        System.out.println("main线程是后台线程吗?"+Thread.currentThread().isDaemon());  //判断是否为后台线程
        DamonThread dt = new DamonThread(); //创建一个DamonThread对象dt
        Thread t = new Thread(dt,"后台线程");   //创建线程t共享dt资源
        System.out.println("t线程默认是后台线程吗?---"+t.isDaemon());
        t.setDaemon(true);
        System.out.println("调用setDaemo语句后t线程默认是后台线程吗?---"+t.isDaemon());
        t.start();      //开启线程t
        for(int i=0;i<10;i++){
            System.out.println(i);
        }
    }
}
运行结果
main线程是后台线程吗?false
t线程默认是后台线程吗?---false
调用setDaemo语句后t线程默认是后台线程吗?---true
0
1
后台线程---is running.
2
3
4
5
6
7
8
9
后台线程---is running.
后台线程---is running.
后台线程---is running.
开启线程t后会执行死循环中的打印语句,当前台线程死亡后,JVM会通知后台线程,接收到指令需要一定的时间,所以打印了几句“后台线程---is running.
”后,线程就结束了。
注:要将某个线程设置为后台线程,必须在该线程启动之前,也就是说setDaemon()方法必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。
线程的生命周期及状态转换
Thread对象创建完成时,线程的生命周期就开始了,当run()方法中代码正常执行完毕或者线程抛出一个未捕获异常或者错误时,线程的生命周期结束。
线程的生命周期分为五个阶段:
①新建状态(New),创建一个线程对象就处于新建状态。
②就绪状态(Runnable),当线程对象调用start()方法后进入就绪状态。
③运行状态(Running),处于就绪状态的线程获得了CPU的使用权,开始执行run()方法中的线程执行体,则该线程处于运行状态。
④阻塞状态(Blocked),一个正在执行的线程在某些特殊情况下,如执行耗时的输入/输出操作时,会放弃CPU的使用权,进入阻塞状态。
⑤死亡状态(Terminated),当run()方法中代码正常执行完毕或者线程抛出一个未捕获异常或者错误时,线程就进入死亡状态。

线程的调度

Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称为线程的调度。
线程调度有两种模型:分时调度模型(所有线程轮流获得CPU使用权,并平均分配每个线程占用的CPU时间片)和抢占式调度模型(让可运行池中优先级高的线程优先占用CPU,优先级相同的随机选择一个线程占用CPU,当它失去CPU的使用权后,再随机选择其他线程获取CPU使用权)。
Java虚拟机默认使用抢占式调度模型,但也可以通过程序来改变这种调度。
线程的优先级通过1~10表示,数字越大优先级越高。
Thread类优先级常量
Thread类的静态常量
功能描述
static int MAX_PRIORITY
表示线程最高优先级,相当于值10
static int MIN_PRIORITY
表示线程最低优先级,相当于值1
static int NORM_PRIORITY
表示线程普通优先级,相当于值5
可以通过Thread类的setPriority(int newPriority)方法对优先级进行设定,newPriority参数接收的是1~10或者上面的三个静态常量。
//定义MaxPriority实现Runnable接口
class MaxPriority implements Runnable{
    public void run(){
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出: "+i);
        }
    }
}
//定义MinPriority实现Runnable接口
class MinPriority implements Runnable{
    public void run(){
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"正在输出: "+i);
        }
    }
}
public class Test{
    public static void main(String[] args) {
        //创建两个线程
        Thread minPriority = new Thread(new MinPriority(),"优先级较低的线程");
        Thread maxPriority = new Thread(new MaxPriority(),"优先级较高的线程");
        minPriority.setPriority(Thread.MIN_PRIORITY);   //设置线程优先级为1
        maxPriority.setPriority(10);    //设置线程优先级为10
        //开启两个线程
        minPriority.start();
        maxPriority.start();
    }
}
运行结果
优先级较高的线程正在输出: 0
优先级较高的线程正在输出: 1
优先级较高的线程正在输出: 2
优先级较高的线程正在输出: 3
优先级较高的线程正在输出: 4
优先级较低的线程正在输出: 0
优先级较低的线程正在输出: 1
优先级较低的线程正在输出: 2
优先级较低的线程正在输出: 3
优先级较低的线程正在输出: 4
上面的MinPriority类也可以直接继承MaxPriority类,可以省去run()方法的重写。
注:优先级需要操作系统的支持,不同的操作系统对优先级的支持不一样,所以在设计多线程应用程序时,其功能的实现一定不能依赖线程的优先级,而只能把线程的优先级作为一种提高程序效率的手段。
线程休眠
要使正在执行的线程暂停,将CPU让给别的线程,可以使用静态方法sleep(long millis),在指定时间(millis毫秒)内是不会被执行的,这样其他线程就得到了执行的机会了。
sleep(long millis)方法声明抛出InterruptedException异常,因此调用该方法时要捕获异常,或者声明抛出该异常。
//定义SleepThread实现Runnable接口
class SleepThread implements  Runnable{
    public void run(){
        for(int i=0;i<10;i++){
            if(i==3){
                try{
                    Thread.sleep(2000);     //当前线程休眠2秒
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("线程一正在输出:"+i);
            try{
                Thread.sleep(500);      //当前线程休眠500毫秒
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
public class Test{
    public static void main(String[] args) {
        //创建一个线程
        new Thread(new SleepThread()).start();
        for(int i=0;i<10;i++){
            if(i==5){
                try{
                    Thread.sleep(2000);
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            System.out.println("主线程正在输出:"+i);
            try{
                Thread.sleep(500);
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
}
运行结果
主线程正在输出:0
线程一正在输出:0
主线程正在输出:1
线程一正在输出:1
主线程正在输出:2
线程一正在输出:2
主线程正在输出:3
主线程正在输出:4
线程一正在输出:3
线程一正在输出:4
线程一正在输出:5
主线程正在输出:5
主线程正在输出:6
线程一正在输出:6
线程一正在输出:7
主线程正在输出:7
主线程正在输出:8
线程一正在输出:8
线程一正在输出:9
主线程正在输出:9
上面程序运行可以发现,线程一和主线程并发执行,都是每隔500毫秒输出一次,线程一在输出3之前得等待2秒,主线程在输出5之前得等待2秒。
线程让步
线程让步可以通过Thread类提供的yield()方法来实现,可以让正在运行的线程暂停,这个方法并不是阻塞该线程,而是将线程转换成就绪状态。当某个线程调用yield()方法之后,只有与当前线程优先级相同或者更高的线程才能获得执行机会。
//定义YieldThread类继承Thread类
class YieldThread extends Thread{
    //定义一个有参的构造方法
    public YieldThread(String name){
        super(name);
    }
    public void run(){
        for(int i=0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+"---"+i);
            if (i==3){
                System.out.print("线程让步:");
                Thread.yield();     //线程运行到此,做出让步
            }
        }
    }
}
public class Test{
    public static void main(String[] args) {
        //创建两个线程
        Thread t1 = new YieldThread("线程A");
        Thread t2 = new YieldThread("线程B");
        //开启两个线程
        t1.start();
        t2.start();
    }
}
运行结果
线程A---0
线程A---1
线程B---0
线程A---2
线程A---3
线程让步:线程B---1
线程B---2
线程B---3
线程让步:线程A---4
线程B---4
可以看出,当一个线程到3的时候,做出了让步,另一个线程还在继续执行。
线程插队
Thread类中提供的join()方法实现线程插队。
当某个线程中调用其他线程的join()方法时,会被阻塞,直到被join()方法加入的线程执行完成后它才会继续执行。
class EmergencyThread implements Runnable{
    public void run(){
        for(int i=0;i<6;i++){
            System.out.println(Thread.currentThread().getName()+"输入:"+i);
            try{
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
public class Test{
    public static void main(String[] args) {
        Thread t = new Thread(new EmergencyThread(),"线程一");     //创建线程
        t.start();      //开启线程
        for(int i=0;i<6;i++){
            System.out.println(Thread.currentThread().getName()+"输入:"+i);
            if(i==2){
                try {t.join();}catch(Exception e){ e.printStackTrace();}    //捕获异常中调用join()方法
            }
            try{Thread.sleep(200);}catch(InterruptedException
e){e.printStackTrace();} //捕获异常中调用slee()方法
        }
    }
}
运行结果
线程一输入:0
main输入:0
main输入:1
线程一输入:1
main输入:2
线程一输入:2
线程一输入:3
线程一输入:4
线程一输入:5
main输入:3
main输入:4
main输入:5
可以看出,在main里为2的时候,实现线程插队,线程一插队完成线程后main继续执行。
多线程同步
举个例子,10张票四个窗口出售,售票时线程休眠500毫秒。
class SaleThread implements Runnable{
    private int tickets = 10;
    public void run(){
        while(tickets>0){
            try{Thread.sleep(500);}catch(InterruptedException e){e.printStackTrace();}
            System.out.println(Thread.currentThread().getName()+"---正在出售票号"+tickets--);
        }
    }
}
public class Test{
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread();   //创建saleThread对象
        //创建并开启四个线程
        new Thread(saleThread,"窗口一").start();
        new Thread(saleThread,"窗口二").start();
        new Thread(saleThread,"窗口三").start();
        new Thread(saleThread,"窗口四").start();
    }
}
运行结果
窗口二---正在出售票号10
窗口一---正在出售票号9
窗口三---正在出售票号8
窗口四---正在出售票号7
窗口二---正在出售票号6
窗口一---正在出售票号5
窗口三---正在出售票号4
窗口四---正在出售票号3
窗口二---正在出售票号2
窗口一---正在出售票号1
窗口四---正在出售票号0
窗口三---正在出售票号-1
窗口二---正在出售票号-2
可以发现,出现了0、-1和-2,这是不允许的,这就是所谓的安全问题。原因是sleep()方法休眠时,假如此时窗口一出售票号1,对票号判断后进入循环,由于休眠期,其他线程也会进入循环,使得票号被减了四次。
同步代码块
线程安全问题其实就是因为多个线程共享资源造成的,所以当共享资源的代码在任何时刻只有一个线程访问就可以解决当前问题。为此Java提供了同步机制,将共享资源的代码放到一个代码块中,用关键字synchronized来修饰,格式:
synchronized (lock){
  操作共享资源代码块  
}
lock是一个锁对象,是同步代码的关键,当某个线程进入代码块时会上锁,其他线程会发生阻塞,等待当前线程执行完同步代码块后,随机选择一个线程进入。
class SaleThread implements Runnable{
    private int tickets = 10;
    Object lock = new Object();     //定义任意一个对象,用作同步代码块的锁
    public void run(){
        while (true){
            synchronized (lock){    //定义同步代码块
                try{Thread.sleep(500);}catch(InterruptedException e){e.printStackTrace();}
                if (tickets>0)
                    System.out.println(Thread.currentThread().getName()+"---正在出售票号"+tickets--);
                else
                    break;
            }
        }
    }
}
public class Test{
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread();   //创建saleThread对象
        //创建并开启四个线程
        new Thread(saleThread,"窗口一").start();
        new Thread(saleThread,"窗口二").start();
        new Thread(saleThread,"窗口三").start();
        new Thread(saleThread,"窗口四").start();
    }
}
运行结果
窗口二---正在出售票号10
窗口三---正在出售票号9
窗口四---正在出售票号8
窗口二---正在出售票号7
窗口二---正在出售票号6
窗口四---正在出售票号5
窗口四---正在出售票号4
窗口三---正在出售票号3
窗口三---正在出售票号2
窗口二---正在出售票号1
由于线程在获得锁对象时有一定的随机性,所以没有出现窗口一的现象很正常。
注:①锁对象可以是任意类型的对象,但多个共享的锁对象必须是唯一的。②锁对象创建代码不能放到run()方法中,否则线程访问时会创建新的锁对象。
同步方法
方法也可以被synchronized关键字修饰,表示同步方法,格式:
synchronized 返回值类型 方法名([参数1,参数2...){}
synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行该方法。
class SaleThread implements Runnable{
    private int tickets = 10;
    //定义一个同步方法saleTicket()
    private synchronized void saleTicket(){
        if(tickets>0){
            try{Thread.sleep(500);}catch(InterruptedException e){e.printStackTrace();}  //经过的线程休眠500毫秒
            System.out.println(Thread.currentThread().getName()+"---正在出售票号"+tickets--);
        }
    }
    public void run(){
        while (true){
            saleTicket();   //调用售票方法
            if(tickets<=0)
                break;
        }
    }
}
public class Test{
    public static void main(String[] args) {
        SaleThread saleThread = new SaleThread();   //创建saleThread对象
        //创建并开启四个线程
        new Thread(saleThread,"窗口一").start();
        new Thread(saleThread,"窗口二").start();
        new Thread(saleThread,"窗口三").start();
        new Thread(saleThread,"窗口四").start();
    }
}
运行结果
窗口一---正在出售票号10
窗口一---正在出售票号9
窗口二---正在出售票号8
窗口三---正在出售票号7
窗口三---正在出售票号6
窗口三---正在出售票号5
窗口三---正在出售票号4
窗口四---正在出售票号3
窗口四---正在出售票号2
窗口三---正在出售票号1
同步代码块和同步方法的弊端就是每次都会判断锁的状态,非常消耗资源,效率较低。
死锁问题
两个线程在运行时都在等待对方的锁,这样便造成了程序的停滞,这种现象称为死锁。
多线程通信
如果需要多个线程按照一定的顺序轮流执行,此时需要让线程间进行通信。在Object类中提供了wait()、notify()、notifyAll()方法用于解决线程间通信问题。
唤醒线程的方法
方法声明
功能描述
Void wait()
使当前线程放弃同步锁并进入等待,直到其他线程进入此同步锁,并调用notify()方法或notifyAll()方法唤醒该线程为止
Void notify()
唤醒此同步锁上等待的第一个调用wait()方法的线程
Void notifyAll()
唤醒此同步锁上调用wait()方法的所有线程
注:wait()、notify()和notifyAll()方法的调用者必须是同步锁对象,否则Java虚拟机会抛出IllegalMonitorStateException异常。
class Storage{
    private int[] cells = new int[10];      //数据存储数组
    private int inPos,outPos;   //inPos是存入时数组下标,outPos是取出时数组下标
    private int count;  //存入或取出数据的数量
    public synchronized void put(int num){
        try {
            Thread.sleep(500);
            //如果放入的数据等于cells的长度,此线程等待
            while(count==cells.length){
                this.wait();
            }
            cells[inPos] = num; //向数组中放入数据
            System.out.println("在cells["+inPos+"]中放入的数据---"+cells[inPos]);
            inPos++;    //存完元素让位置加1
            if(inPos==cells.length){    //当在cells[9]放完数据后再从cell[0]开始
                inPos = 0;
            }
            count++;    //每放一个数据count加1
            this.notify();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    public synchronized void get(){
        try{
            Thread.sleep(500);
            //如果count为0此线程等待
            while(count==0){
                this.wait();
            }
            int data = cells[outPos];   //从数组中取出数据
            System.out.println("从cells["+outPos+"]中取出数据"+data);
            cells[outPos] = 0;  //取出后,当前位置的数据置0
            outPos++;   //取完元素让位置加1
            if(outPos==cells.length){   //当从cells[9]取完数据后再从cells[0]开始
                outPos = 0;
            }
            count--;    //每取出一个元素count减1
            this.notify();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
//定义 输入 线程类
class Input implements Runnable{
    private Storage st;
    private int num;    //定义一个变量num
    Input(Storage st){  //定义一个构造方法接收Storage对象
        this.st = st;
    }
    public void run(){
        while (true){
            st.put(num++);
        }
    }
}
//定义 输出 线程类
class Output implements Runnable{
    private Storage st;
    private int num;
    Output(Storage st){ //定义一个构造方法接收Storage对象
        this.st = st;
    }
    public void run(){
        while (true){
            st.get();   //循环取出元素
        }
    }
}
public class Test{
    public static void main(String[] args) {
        Storage st = new Storage(); //创建数据存储类对象
        Input input = new Input(st);    //创建Input对象传入Storage对象
        Output output = new Output(st);     //创建Output对象传入Storage对象
        new Thread(input).start();  //开启新线程
        new Thread(output).start(); //开启新线程
    }
}
运行结果
在cells[0]中放入的数据---0
从cells[0]中取出数据0
在cells[1]中放入的数据---1
在cells[2]中放入的数据---2
从cells[1]中取出数据1
从cells[2]中取出数据2
在cells[3]中放入的数据---3
从cells[3]中取出数据3
在cells[4]中放入的数据---4
在cells[5]中放入的数据---5
从cells[4]中取出数据4
从cells[5]中取出数据5
在cells[6]中放入的数据---6
从cells[6]中取出数据6
在cells[7]中放入的数据---7
从cells[7]中取出数据7
在cells[8]中放入的数据---8
从cells[8]中取出数据8
在cells[9]中放入的数据---9
在cells[0]中放入的数据---10
从cells[9]中取出数据9
从cells[0]中取出数据10
在cells[1]中放入的数据---11
在cells[2]中放入的数据---12
从cells[1]中取出数据11
从cells[2]中取出数据12
在cells[3]中放入的数据---13
在cells[4]中放入的数据---14
从cells[3]中取出数据13
首先通过使用synchronized关键字将put()方法和get()方法修饰为同步方法,之后每操作一次数据,便调用一次notify()方法唤醒对应同步锁上等待的线程。
原文地址:https://www.cnblogs.com/xiaochen2715/p/12512711.html