Java基础---多线程

一 多线程

1 进程

定义:在一个操作系统中,每个独立执行的程序都可称之为一个进程,也就是“正在运行的程序”。例如同时运行的QQ、360安全卫士、IDEA开发工具等

说明:
    在多任务操作系统中,表面上看是支持进程并发执行的,例如可以一边听音乐一边聊天,但实际上这些进程并不是在同一时刻运行的。
    在计算机中,所有的应用程序都是由CPU执行的,对于一个CPU而言,在某个时间点只能运行一个程序,也就是说只能执行一个进程,操作系统会为每一个进程分配一段有限的CPU使用时间,CPU在这段时间中执行某个进程,然后会在下一段时间切换到另一个进程中去执行。
    由于CPU运行速度非常快,能在极短的时间内在不同的进程之间进行切换,所以给人以同时执行多个程序的感觉。

2 线程

定义:
    在多任务操作系统中,每个运行的程序都是一个进程,用来执行不同的任务,而在一个进程中还可以有多个执行单元同时运行,来同时完成一个或多个程序任务,这些执行单元可以看做程序执行的一条条线索,被称为线程。
注意:
    操作系统中的每一个进程中都至少存在一个线程,当一个Java程序启动时,就会产生一个进程,该进程中会默认创建一个线程,在这个线程上会运行main()方法中的代码。

(1)单线程

    单线程都是按照调用顺序依次往下执行,没有出现多段程序代码交替运行的效果

(2)多线程

    多线程程序在运行时,每个线程之间都是独立的,它们可以并发执行
    多线程可以充分利用CUP资源,进一步提升程序执行效率。
    多线程看似是同时并发执行的,其实不然,它们和进程一样,也是由CPU控制并轮流执行的,只不过CPU运行速度非常快,故而给人同时执行的感觉

3 线程的创建

创建:Java为多线程开发提供了非常优秀的技术支持,在Java中,可以通过三种方式来实现多线程。
方式一:继承Thread类,重写run()方法
方式二:实现Runnable接口,重写run()方法
方式三:实现Callable接口,重写call()方法,并使用Futrue来获取call()方法的返回结果

(1)Thread类实现多线程

说明:Thread类是java.lang包下的一个线程类,用来实现Java多线程
步骤:
第一步:创建一个Thread线程类的子类(子线程),同时重写Thread类的run()方法;
第二步:创建该子类的实例对象,并通过调用start()方法启动线程。

第一步:

package thread;
//创建一个类继承Thread
public class MyThread extends Thread{
    @Override
    public void run() {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}

第二步:

        //创建线程MyThread线程类对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        //设置线程的名字
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        //启动3个线程
        t1.start();
        t2.start();
        t3.start();

结果:

(2)Runnable实现多线程

说明:
    Java只支持类的单继承,如果某个类已经继承了其他父类,就无法再继承Thread类来实现多线程。在这种情况下,就可以考虑通过实现Runnable接口的方式来实现多线程。
步骤:
第一步:创建一个Runnable接口的实现类,同时重写接口中的run()方法;
第二步:创建Runnable接口的实现类对象;
第三步:使用Thread有参构造方法创建线程实例,并将Runnable接口的实现类的实例对象作为参数传入;
第四步:调用线程实例的start()方法启动线程。

代码:

//第一步:创建Runnable接口实现类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
}
        //第二步:创建Runnable接口实现类对象
        MyRunnable runnable = new MyRunnable();
        //创建Thread类对象,并传入Runnable接口实现类对象
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);
        //第三步:设置线程名字
        t1.setName("线程1");
        t2.setName("线程2");
        t3.setName("线程3");
        //第四步:启动线程
        t1.start();
        t2.start();
        t3.start();

结果:

(3)Callable接口实现多线程

说明:
    通过Thread类和Runnable接口实现多线程时,需要重写run()方法,但是由于该方法没有返回值,因此无法从多个线程中获取返回结果。
    为了解决这个问题,从JDK 5开始,Java提供了一个新的Callable接口,来满足这种既能创建多线程又可以有返回值的需求。
使用:
	Callable接口实现多线程是通过Thread类的有参构造方法传入Runnable接口类型的参数来实现多线程,不同的是,这里传入的是Runnable接口的子类FutureTask对象作为参数,而FutureTask对象中则封装带有返回值的Callable接口实现类。
步骤:
第一步:创建一个Callable接口的实现类,同时重写Callable接口的call()方法;
第二步:创建Callable接口的实现类对象;
第三步:通过FutureTask线程结果处理类的有参构造方法来封装Callable接口实现类对象;
第四步:使用参数为FutureTask类对象的Thread有参构造方法创建Thread线程实例;
第五步:调用线程实例的start()方法启动线程。

代码:

import java.util.concurrent.Callable;
//第一步:创建一个Callable接口的实现类
public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
        return Thread.currentThread().getName()+"执行完毕!";//线程返回值
    }
}
        //第二步:创建Callable接口实现类对象
        MyCallable myCallable = new MyCallable();
        //第三步:创建FutureTask对象
        FutureTask<String> futureTask1 = new FutureTask<>(myCallable);
        FutureTask<String> futureTask2 = new FutureTask<>(myCallable);
        FutureTask<String> futureTask3 = new FutureTask<>(myCallable);
        //第四步:创建Thread线程对象,设置线程名字
        Thread t1 = new Thread(futureTask1,"线程1");
        Thread t2 = new Thread(futureTask2,"线程2");
        Thread t3 = new Thread(futureTask3,"线程3");
        //第五步:开启线程
        t1.start();
        t2.start();
        t3.start();
        //获取线程返回值
        String s1 = futureTask1.get();
        String s2 = futureTask2.get();
        String s3 = futureTask3.get();
        System.out.println("线程返回值是:"+s1);
        System.out.println("线程返回值是:"+s2);
        System.out.println("线程返回值是:"+s3);

结果:

4 后台线程

说明:
    对Java程序来说,只要还有一个前台线程在运行,这个进程就不会结束,如果一个进程中只有后台线程运行,这个进程就会结束。
    这里提到的前台线程和后台线程是一种相对的概念,新创建的线程默认都是前台线程。
使用:
    如果某个线程对象在启动之前调用了setDaemon(true)语句,这个线程就变成一个后台线程。

代码:

        //创建Runnble接口实现类
        MyRunnable runnable = new MyRunnable();
        //创建3个线程
        Thread t1 = new Thread(runnable,"线程1");
        Thread t2 = new Thread(runnable,"线程2");
        Thread t3 = new Thread(runnable,"后台线程");
        //设置t3为后台线程
        t3.setDaemon(true);
        //设置线程的优先级
        t1.setPriority(9);
        t2.setPriority(9);
        t3.setPriority(1);
        //启动线程
        t1.start();
        t2.start();
        t3.start();

结果:

5 线程的生命周期

说明:
	在Java中,任何对象都有生命周期,线程也不例外,它也有自己的生命周期。
	当Thread对象创建完成时,线程的生命周期便开始了。
	当线程任务中代码正常执行完毕或者线程抛出一个未捕获的异常(Exception)或者错误(Error)时,线程的生命周期便会结束。

线程状态图:

新建:
	创建一个线程对象后,该线程对象就处于新建状态,此时它不能运行,和其他Java对象一样,仅仅由JVM为其分配了内存,没有表现出任何线程的动态特征。
RUNNABLE(可运行状态):
	新建状态的线程调用start()方法,就会进入可运行状态。
	在RUNNABLE状态内部又可细分成两种状态:READY(就绪状态)和RUNNING(运行状态),并且线程可以在这两个状态之间相互转换。
RUNNABLE内部状态转换:
	就绪状态:线程对象调用start()方法之后,等待JVM的调度,此时线程并没有运行;
	运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。
BLOCKED(阻塞状态)
	运行状态的线程因为某些原因失去CPU的执行权,会进入阻塞状态。阻塞状态的线程只能先进入就绪状态,不能直接进入运行状态。
线程进入阻塞状态的两种情况:
	当线程A运行过程中,试图获取同步锁时,却被线程B获取;
	当线程运行过程中,发出IO请求时。
WAITING(等待状态)
    运行状态的线程调用了无时间参数限制的方法后,如wait()、join()等方法,就会转换为等待状态。
    等待状态中的线程不能立即争夺CPU使用权,必须等待其他线程执行特定的操作后,才有机会争夺CPU使用权。
例如:
    调用wait()方法而处于等待状态中的线程,必须等待其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程;
    调用join()方法而处于等待状态中的线程,必须等待其他加入的线程终止
TIMED_WAITING(定时等待状态)
    运行状态中的线程调用了有时间参数限制的方法,如sleep(long millis)、wait(long timeout)、join(long millis)等方法,就会转换为定时等待状态。
    定时等待状态中的线程不能立即争夺CPU使用权,必须等待其他相关线程执行完特定的操作或者限时时间结束后,才有机会再次争夺CPU使用权。
例如:
    调用了wait(long timeout) 方法而处于等待状态中的线程,需要通过其他线程调用notify()或者notifyAll()方法唤醒当前等待中的线程,或者等待限时时间结束后也可以进行状态转换。
TERMINATED(终止状态)
    线程的run()方法、call()方法正常执行完毕或者线程抛出一个未捕获的异常(Exception)、错误(Error),线程就进入终止状态。
    一旦进入终止状态,线程将不再拥有运行的资格,也不能再转换到其他状态,生命周期结束。

6 线程的调度

    程序中的多个线程是并发执行的,但并不是同一时刻执行,某个线程若想被执行必须要得到CPU的使用权。
    Java虚拟机会按照特定的机制为程序中的每个线程分配CPU的使用权,这种机制被称作线程的调度。

(1)线程的优先级

    在应用程序中,要对线程进行调度,最直接的方式就是设置线程的优先级。
    优先级越高的线程获得CPU执行的机会越大,而优先级越低的线程获得CPU执行的机会越小。
    线程的优先级用1~10之间的整数来表示,数字越大优先级越高。
    除了可以直接使用数字表示线程的优先级,还可以使用Thread类中提供的三个静态常量表示线程的优先级
Thread**类的静态常量**功能描述
static int MAX_PRIORITY表示线程的最高优先级,相当于值10
static int MIN_PRIORITY表示线程的最低优先级,相当于值1
static int NORM_PRIORIY表示线程的普通优先级,相当于值5
说明:
	程序在运行期间,处于就绪状态的每个线程都有自己的优先级,例如main线程具有普通优先级。
使用:
	可以通过Thread类的setPriority(int newPriority)方法对其进行设置,该方法中的参数newPriority接收的是1~10之间的整数或者Thread类的三个静态常量
        //创建Runnble接口实现类
        MyRunnable runnable = new MyRunnable();
        //创建3个线程
        Thread t1 = new Thread(runnable,"线程1");
        Thread t2 = new Thread(runnable,"线程2");
        Thread t3 = new Thread(runnable,"线程3");
        //设置线程的优先级
        t1.setPriority(10);
        t2.setPriority(5);
        t3.setPriority(1);
        //启动线程
        t1.start();
        t2.start();
        t3.start();

(2)线程的休眠

    如果想要人为地控制线程执行顺序,使正在执行的线程暂停,将CPU使用权让给其他线程,这时可以使用静态方法sleep(long millis)。
    该方法可以让当前正在执行的线程暂停一段时间,进入休眠等待状态,这样其他的线程就可以得到执行的机会。
    sleep(long millis)方法会声明抛出InterruptedException异常,因此在调用该方法时应该捕获异常,或者声明抛出该异常。
//创建Runnable接口实现类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
            //线程1休眠
            if(Thread.currentThread().getName().equals("线程1")){
                try {
                    Thread.sleep(1);//休眠1毫秒
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

结果:

(3)线程让步

    线程让步可以通过yield()方法来实现,该方法和sleep(long millis)方法有点类似,都可以让当前正在运行的线程暂停,区别在于yield()方法不会阻塞该线程,它只是将线程转换成就绪状态,让系统的调度器重新调度一次。
    当某个线程调用yield()方法之后,与当前线程优先级相同或者更高的线程可以获得执行的机会。
//创建Runnable接口实现类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
            //线程2让步
            if(Thread.currentThread().getName().equals("线程2")){
                if(i==50){
                    Thread.yield();//当i=50,线程2让步
                }
            }
        }
    }
}

结果:

(4)线程插队

    在Thread类中也提供了一个join()方法来实现线程插队功能。
    当在某个线程中调用其他线程的join()方法时,调用的线程将被阻塞,直到被join()方法加入的线程执行完成后它才会继续运行。
    Thread类中还提供了带有时间参数的线程插队方法join(long millis)。
    当执行带有时间参数的join(long millis)进行线程插队时,必须等待插入的线程指定时间过后才会继续执行其他线程。
package thread;
//创建Runnable接口实现类
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        //线程执行内容
        for (int i=1;i<=100;i++){
            //Thread.currentThread().getName():获取当前线程名字
            System.out.println(Thread.currentThread().getName()+":"+i);
            //线程插队
            if(i==50){
                try {
                    //当i=50,插入线程2
                    MyThread myThread = new MyThread();
                    myThread.setName("线程2");
                    myThread.start();
                    myThread.join();//线程2插队
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
    public static void main(String[] args) {
        //创建Runnble接口实现类
        MyRunnable runnable = new MyRunnable();
        //创建线程
        Thread t1 = new Thread(runnable,"线程1");
        t1.start();
    }

结果;

7 线程同步

    多线程的并发执行可以提高程序的效率,但是,当多个线程去访问同一个资源时,也会引发一些安全问题。
    例如,当统计一个班级的学生数目时,如果有同学进进出出,则很难统计正确。
    为了解决这样的问题,需要实现多线程的同步,即限制某个资源在同一时刻只能被一个线程访问。

(1)线程安全

    线程安全问题其实就是由多个线程同时处理共享资源所导致的。
    要想解决线程安全问题,必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。
    为此,Java中提供了线程同步机制。
        同步代码块
        同步方法

(2)同步代码块

    synchronized(lock){
    // 操作共享资源代码块
    ...
    }

注意:

	Synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块
	lock锁对象可以是任意类型的对象,但多个线程共享的锁对象必须是相同的。
	锁对象的创建代码不能放到run()方法中,否则每个线程运行到run()方法都会创建一个新对象,这样每个线程都会有一个不同的锁

执行原理:

	当线程执行同步代码块时,首先会检查lock锁对象的标志位;
	默认情况下标志位为1,此时线程会执行Synchronized同步代码块,同时将锁对象的标志位置为0;
	当一个新的线程执行到这段同步代码块时,由于锁对象的标志位为0,新线程会发生阻塞,等待当前线程执行完同步代码块后;
	锁对象的标志位被置为1,新线程才能进入同步代码块执行其中的代码,这样循环往复,直到共享资源被处理完为止。
public class SaleTicketRunnable implements Runnable{
    private int num = 50;
    @Override
    public void run() {
        while(true){
            synchronized (this){//同步代码块
                if(num>0){
                    System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张火车票!");
                    num--;
                }else{
                    System.out.println("火车票卖完了!");
                    break;
                }
            }
        }
    }
}
    public static void main(String[] args) {
        SaleTicketRunnable saleTicketRunnable = new SaleTicketRunnable();
        Thread t1 = new Thread(saleTicketRunnable,"窗口1");
        Thread t2 = new Thread(saleTicketRunnable,"窗口2");
        Thread t3 = new Thread(saleTicketRunnable,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }

(3)同步方法

[修饰符] synchronized 返回值类型 方法名([参数1,……]){}

注意:

	在方法前面也可以使用synchronized关键字来修饰,被修饰的方法为同步方法,它能实现和同步代码块同样的功能。
	被synchronized修饰的方法在某一时刻只允许一个线程访问,访问该方法的其他线程都会发生阻塞,直到当前线程访问完毕后,其他线程才有机会执行。
	同步方法也有锁,它的锁就是当前调用该方法的对象,就是this指向的对象。
	Java中静态方法的锁是该方法所在类的class对象,该对象可以直接类名.class的方式获取。
	同步代码块和同步方法解决多线程问题有好处也有弊端。
	同步解决了多个线程同时访问共享数据时的线程安全问题,只要加上同一个锁,在同一时间内只能有一条线程执行,但是线程在执行同步代码时每次都会判断锁的状态,非常消耗资源,效率较低。
public class SaleTicketRunnable implements Runnable{
    private int num = 50;
    @Override
    public synchronized void run() {//同步方法
        while(true){
            if(num>0){
                System.out.println(Thread.currentThread().getName()+"正在卖第"+num+"张火车票!");
                num--;
            }else{
                System.out.println("火车票卖完了!");
                break;
            }
        }
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值