JAVASE第十章(线程)


前言

Java 给多线程编程提供了内置的支持。一个进程中可以并发多个线程,每条线程并行执行不同的任务。多线程能满足程序员编写高效率的程序来达到充分利用CPU的目的。


一、程序 线程 进程

1.程序(program)是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码。
2.线程(thread)进程可进一步细化为线程,是一个进程内部的最小执行单元,是操作系统进行任务调度最小单元,隶属于进程。
3.进程(process)就是正在执行的程序,从Windows角度讲,进程是操作系统进行资源分配的最小单位。

线程和进程的关系

1.一个进程可以包含多个线程,一个线程只能属于一个进程,线程不能脱离进程而独立运行;

2.每一个进程至少包含一个线程(称为主线程);在主线程中开始执行程序
 java 程序的入口main()方法就是在主线程中被执行的。 
 
3.在主线程中可以创建并启动其它的线程; 

4.一个进程内的所有线程共享该进程的内存资源。

下面用一张图展示程序、线程、进程的关系。
在这里插入图片描述


二、线程

2.1 创建线程的方式

创建线程的常用方法有两种,一种是继承Thread类,重写run()方法,一种是实现Runnable接口,实现run()方法。

1.继承Thread类,重写run()方法。

public class ThreadDemo1 extends Thread{
    //在Java中要实现线程,最简单的方式就是扩展Thread类,重写其中的run方法
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("执行了我的线程"+i);
        }
    }
}
public class Test1 {
    public static void main(String[] args) {
        ThreadDemo1 myThread=new ThreadDemo1();
        myThread.start();   //启动线程   Thread类中的run方法本身并不执行任何操作,如果我们重写了run方法,当线程启动时,它将执行 run方法
        //myThread.run();  这是调用方法,线程还没启动,不能算是多线程
        for (int i = 0; i < 1000; i++) {
            System.out.println("这是main线程里面的方法"+i);
        }
    }
}

2.实现Runnable接口,实现run()方法。

public class ThreadDemo2 implements Runnable{
    //也可以通过实现Runnable接口的方式来实现线程,只需要实现其中的run方法即可;
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            System.out.println("这是我的线程"+i);
        }
    }
}
public class Test2 {
    public static void main(String[] args) {
        ThreadDemo2 myThread=new ThreadDemo2(); //创建一个任务
        Thread t1=new Thread(myThread);  //创建一个线程作为外壳,将任务包起来,
        t1.start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("这是main线程里面的方法"+i);
        }
    }
}

实现Runnable的好处:

1.避免了单继承的局限性 
2.多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。

3.两种创建线程方式的区别。
1.当要进行多个线程同时访问一个共享资源时,用继承Thread类方法实现线程时,因为继承Thread类中每个对象都有自己的资源,所以要在共享资源加上static修饰,使共享资源只有一份(类所有)。
2.实现Runnable接口的好处就是多个线程可以共享一个对象,共享一个共享资源,非常适合多个相同线程来处理同一份资源。

2.2 Thread类中方法

java.lang包提供了Thread类,Thread类用于操作线程,是所以涉及到线程操作(如并发)的基础。现在我们介绍Thread类中的方法。
1.构造方法

Thread()  创建一个线程。  
Thread(Runnable target)  利用Runable对象创建一个线程。  
Thread(Runnable target, String name)  利用Runable对象创建一个线程同时指定名称。
Thread(String name)  创建一个线程并指定名称。  

2.常用方法

void start()  启动线程
final void setName(String name)   设置线程名称
final String getName()   返回线程名称
final void join()   线程插队
static void sleep(long millis) 让当前线程休眠
final void setPriority(int newPriority)  设置线程优先级
static Thread currentThread() 返回对当前正在执行的线程对象的引用
public class Test3 {
    public static void main(String[] args) {
        ThreadDemo3 myThread=new ThreadDemo3();   //创建一个任务
        Thread t1=new Thread(myThread,"李四");  //利用Runnable对象创建一个线程,并指定该线程的名称
        Thread t2=new Thread(myThread,"张三");
        //优先级较高的线程有更多获得CPU的机会,反之亦然;
        // 优先级用整数表示,取值范围是1~10,一般情况下,线程的默认优先级 都是5
        // 但是也可以通过setPriority和getPriority方法来设置或返回优先级;
        /*同优先级线程组成先进先出队列,使用时间片策略
          对高优先级,使用优先调度的抢占式策略*/
        t1.setPriority(10);
        t2.setPriority(5);
        t1.start();
        t2.start();
        System.out.println(t1.getPriority());
        System.out.println(t1.getPriority());
    }
}

2.3 线程优先级

线程优先级:优先级高的线程有更多获取CPU的机会,优先级用整数表示取值范围为1-10,默认优先级为5,也可以通过setPriority方法设置优先级。

Thread类有如下3个静态常量来表示优先级

1.MAX_PRIORITY:取值为10,表示最高优先级。 
2.MIN_PRIORITY:取值为1,表示最底优先级。 
3.NORM_PRIORITY:取值为5,表示默认的优先级。

调度策略:即依据什么原则挑选程。

1.时间片策略:为每个任务分配一个时间片
2.抢占式:高优先级的线程抢占CPU

Java的调度方法

1.同优先级线程组成先进先出队列,使用时间片策略 
2.对高优先级,使用优先调度的抢占式策略

三、线程状态和分类

3.1线程状态

线程状态:线程是有生命周期的,在不同的周期中处于不同的状态。
在这里插入图片描述

线程的状态:
1.新建:当一个线程被创建时处于新建状态。
2.就绪:新建的线程被start()后,进入了就绪状态。
3.运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态。
4.阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态 。
5.死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。

注意:
1.当线程调用start()方法后,没有马上进入运行状态,而是进入就绪状态,当获得cpu资源时才能进入运行状态。
2.myThread.run() 这是调用方法,线程还没开始启动,不能算是多线程。

3.2线程分类

Java中的线程分为两类:用户线程和守护线程

例如:任何一个守护线程都是整个JVM中所有非守护线程的保姆: 只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

public class Defend extends Thread{
    @Override
    public void run() {
        while(true){
            System.out.println("我是守护线程我在守护着你");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class DefendTest {
    public static void main(String[] args) {
        //只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;
        // 只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。
        Defend d1=new Defend();
        d1.setDaemon(true);  //变成守护线程
        DefendedThread d2=new DefendedThread();
        d2.start();
        d1.start();
    }
}

守护线程的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者

注意:设置线程为守护线程必须在启动线程之前,否则会跑出一个 IllegalThreadStateException异常


四、多线程的概念

多线程:一个程序中可以同时运行多个不同的线程来执行不同的任务。
何时需要多线程:

1.程序需要同时执行两个或多个任务。
2.程序需要实现一些需要等待的任务时,如用户输入、文件读写操作、 网络操作、搜索等。 
3.需要一些后台运行的程序时。 

多线程的优缺点

优点:
1.提高程序的响应。
2.提高CPU的利用率。
3.改善程序结构,将复杂任务分为对个线程,独立运行。
缺点:
1.线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 
2.多线程需要协调和管理,所以需要CPU时间跟踪线程; 
3.线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;

注意:我们说的多线程,一定是同时开启了多个线程,例如在main线程中开启了另外一个线程。下面这个线程只有一个主线程(main),并没有创建多线程,只是进行了基本的函数调用。
在这里插入图片描述


五、线程同步

在学习线程同步之前,我们先来学习一下串行,并行和并发的概念。下面用一张图来说明三者的区别。
在这里插入图片描述
举个通俗的例子:例如我们现在在吃饭,突然来了个电话。
串行:必须等吃完饭,在去接电话,即必须按顺序完成任务。
并发:可以先去接个电话,接完电话后回来接着吃饭。
并行:边吃饭边接电话。

线程同步:多个线程同时读写同一份共享资源时,可能会引起冲突。要确保一个时间点只有一个线程访问共享资源,我们可以给共享资源加一把锁,哪个线程获取这把锁就有权利访问该共享资源。常用的方法是synchronize,常用于同步方法和同步方法块。

下面用一个模拟卖票的例子实现多线程下的同步机制。
两个窗口分别售票,票数为10张
分别使用继承Thread和实现Runnable两种方式实现

1.继承Thread实现

public class SynchronizedThread1 extends Thread {
    static int number=10;
   // static Object obj=new Object();
    public void run(){
        while(true){
            robTicket();
            if (number<=0){
                break;
            }
        }
    }
    public static synchronized  void robTicket(){
        //synchronized(同步监视器)关键字同步方法时,默认是this,一旦创建多个线程对象(不加static的情况),同步对象有多个,相当于没锁住
        //加static修饰后,同步对象变为线程类(类只有一个)
        if(number>0){
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"正在出售:"+number);
            number--;
        }
    }    /*//方法2:写在代码块中
    public void run() {
        while(true){
            synchronized (obj){   //使用synchronized(同步监视器)关键字同步方法或代码块。
                //同步监视器可以是任何对象,必须唯一,保证多个线程获得是同一个对象
                if(number>0){
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+"正在出售:"+number);
                    number--;
                }else{
                    break;
                }
            }
        }
    }*/
}

public class SynchronizedDemo1 {
    public static void main(String[] args) {
        //同步监视器的执行过程
        // 1.第一个线程访问,锁定同步监视器,执行其中代码.
        // 2.第二个线程访问,发现同步监视器被锁定,无法访问.
        // 3.第一个线程访问完毕,解锁同步监视器.
        // 4.第二个线程访问,发现同步监视器没有锁,然后锁定并访问
        SynchronizedThread1 myThread1=new SynchronizedThread1();
        SynchronizedThread1 myThread2=new SynchronizedThread1();
        myThread1.setName("窗口1");
        myThread2.setName("窗口2");
        myThread1.start();
        myThread2.start();
    }
}

注意:
1.synchronized(同步监视器)关键字同步方法时,默认是this,一旦创建多个线程对象(不加static的情况),同步对象有多个,相当于没锁住,方法加static修饰后,同步对象变为线程类(类只有一个)。
2.使用synchronized(同步监视器)关键字同步方法或代码块。同步监视器可以是任何对象,必须唯一,保证多个线程获得是同一个对象。

2.实现Runnable接口

public class SynchronizedThread2 implements Runnable {
    //多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
    int number = 10;
    Object obj = new Object();
    @Override
    public void run() {
        while (true) {
            robTicket();
            if (number <= 0) {
              break;
            }
        }
    }
    public synchronized void robTicket() {  //ynchronized(同步监视器)关键字同步方法时,默认是this,
        // 实现Runnable接口,只有一个类的对象,所以不用加static
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "正在售出:" + number);
            number--;
        }
    }
/*    public void run() {
        while(true){
            synchronized (obj){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if(number>0){
                    System.out.println(Thread.currentThread().getName()+"正在售出:"+number);
                    number--;
                }else{
                    break;
                }
            }
        }
    }*/
}
public class SynchronizedDemo2 {
    public static void main(String[] args) {
        SynchronizedThread2 t1=new SynchronizedThread2();
        //多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源
        Thread myT1=new Thread(t1,"窗口1");
        Thread myT2=new Thread(t1,"窗口2");
        myT1.start();
        myT2.start();
    }
}

注意:
1.多个线程可以共享同一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。
2.实现Runnable接口,只有一个类的对象,共享资源不用加static。


六、Lock(锁)

Lock:JDK 5.0开始,Java提供了更强大的线程同步机制-通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁, 线程开始访问共享资源之前应先获得Lock对象,比较常用的是ReentrantLock。

public class LockThread {
    public static void main(String[] args) throws InterruptedException {
        //面的代码通过lock()方法先获取锁三次,然后通过unlock()方法释放锁3次,程序可以正常退出。
        ReentrantLock lock = new ReentrantLock();
        for (int i = 1; i <= 3; i++) {
            lock.lock();
        }
        for(int i=1;i<=3;i++){
            try {
            } finally {
                lock.unlock();
            }
        }
    }
}

线程死锁:不同的线程分别占用对方需要的同步资源不放弃,就形成了线程的死锁。
例如:张三拿了李四家的钥匙,李四拿了张三家的钥匙,双方都不愿意放弃,于是就卡bug了。
下面一段代码演示是死锁是怎么发生的。

public class DieSynchronizedThread extends Thread{
    static Object A=new Object();
    static Object B=new Object();
    boolean flag;
    @Override
    public void run() {
            if(flag){
                synchronized (A){
                    System.out.println(Thread.currentThread().getName()+"拿到了A");
                    synchronized (B){
                        System.out.println(Thread.currentThread().getName()+"拿到了B");
                    }
                }
            }else{
                synchronized (B){
                    System.out.println(Thread.currentThread().getName()+"拿到了B");
                    synchronized (A){
                        System.out.println(Thread.currentThread().getName()+"拿到了A");
                    }
                }

            }
    }
}
public class DieSynchronizedTest {
    public static void main(String[] args) {
        DieSynchronizedThread t1=new DieSynchronizedThread();
        DieSynchronizedThread t2=new DieSynchronizedThread();
        t1.setName("小黑");
        t2.setName("小白");
        t1.flag=true;
        t2.flag=false;
        t1.setPriority(10);
        t2.setPriority(1);
        t1.start();
        t2.start();
    }
}

七、线程通信

线程通信:多个线程通过消息传递实现相互牵制,相互调度,即线程间的相互作用。
线程通信涉及三个方法:

1.wait一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。 
2.notify一旦执行此方法,就会唤醒被wait的一个线程。
  如果有多个线程被wait,就唤醒优先级高的那个。
3.notifyAll一旦执行此方法,就会唤醒所有被wait的线程。

举例:两个线程交替打印1-100之间的数字

public class CommunicationThread extends Thread{
    static Object obj=new Object();
    static int number=0;
    @Override
    public void run() {
        while(true){
            synchronized (obj){
                //wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
                //第一次先进入一个线程,等进行完该进行的操作后调用wait()阻塞自己,然后释放同步锁,下一个线程就可以进来,进来之后由下一个进程
                //进来唤醒它,虽然已经被唤醒但无法得到同步锁,所以也只能在外面等待,这样就可以实现两个线程交互通讯并保证一次只有一个进入
                obj.notify(); //notify一旦执行此方法,就会唤醒被wait的一个线程。如果有多个线程被wait, 就唤醒优先级高的那个
                if(number<100){
                    System.out.println(Thread.currentThread().getName()+":"+(++number));
                }
                try {
                    obj.wait();  //一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器。
           } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class CommunicationTest {
    public static void main(String[] args) {
        CommunicationThread t1=new CommunicationThread();
        CommunicationThread t2=new CommunicationThread();
        t1.setName("小黑");
        t2.setName("小白");
        t1.start();
        t2.start();
    }
}

注意:
1.wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。
2.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。

生产者/消费者问题

public class Count {
    int number=0;
    public synchronized void add() throws InterruptedException {
        //同步时this是一个对象,要保证一个对象这样才能保证是一把一样的锁
        if (number==0){
          this.notify();
          number++;
          Thread.sleep(2000);
            System.out.println("生产者生产了一个产品");
            System.out.println("当前柜台产品数量:"+number);
        }
        this.wait();
    }
    public synchronized  void sub() throws InterruptedException {
        if(number==1){
            this.notify();
            number--;
            Thread.sleep(2000);
            System.out.println("消费者消费了一个产品");
            System.out.println("当前柜台产品数量:"+number);
        }
        this.wait();
    }
}

public class Consumer extends Thread{
    Count c;
    public Consumer(Count c) {
        this.c = c;
    }
    @Override
    public void run() {
        while(true){
            try {
                c.sub();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }
}

public class Product extends Thread{
    Count c;
    public Product(Count c) {
        this.c = c;
    }
    public void run() {
        while(true){
            try {
                c.add();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class CountTest {
    public static void main(String[] args) {
        Count count=new Count();
        Consumer c=new Consumer(count); //两个线程共享一个对象,这样保证两个线程操作同一个变量
        Product p=new Product(count);
        c.start();
        p.start();
    }
}

即可实现生产者和消费者的有序生产消费。
在这里插入图片描述

注意:两个线程共享一个对象,这样保证两个线程操作同一个变量


八、新增创建线程方式

实现Callable接口:相比使用Runnable接口,功能更加强大,需要重写里面的call方法。
实现Callable接口优点:

1.call()方法可以有返回值,并且返回值支持泛型。
2.可以抛出异常。
3,需要借助FutureTask类,获取返回结果。
public class FutureThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception { // //相比run()方法,1.可以有返回值 2.方法可以抛出异常 3.支持泛型的返回值
        int i=0;
        for (int j = 1; j <=1000; j++) {
            if(j%2==1){
                i+=j;
            }
        }
        return i;
    }
}
public class FutureThread2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int i=0;
        for (int j = 1; j <=1000; j++) {
            if(j%2==0){
                i+=j;
            }
        }
        return i;
    }
}
public class FutureTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //实现Callable接口与使用Runnable相比,Callable功能更强大些.
        FutureThread f1=new FutureThread();
        FutureTask<Integer>t1=new FutureTask<>(f1);
        Thread myThread1=new Thread(t1);  //FutureTask继承了Runnable的类,所以可以当做Thread的参数
        FutureThread2 f2=new FutureThread2();
        FutureTask<Integer>t2=new FutureTask<>(f2);
        Thread myThread2=new Thread(t2);  //Thread需要传进去一个实现了Runnable的类,FutureTask实现了Runnable所以可以传进去
        myThread1.start();
        myThread2.start();
        System.out.println("1-1000之内的奇数和是"+t1.get());//需要借助FutureTask类,获取返回结果
        System.out.println("1-1000之内的偶数和是"+t2.get());
    }
}

注意:
1.FutureTask继承了Runnable的类。
2.Thread需要传进去一个实现了Runnable的类的参数,FutureTask实现了Runnable所以可以传进去。
3.实现Callable接口需要借助FutureTask类,获取返回结果。


总结

本章内容较多,重点掌握线程的三种创建方式以及它们之间的区别。线程的状态是线程这章的重中之重,应该熟练掌握线程各种状态之间的切换以及几个改变线程状态的方法。有效利用多线程的关键是理解程序是并发执行而不是串行执行的。例如:程序中有两个子系统需要并发执行,这时候就需要利用多线程编程。通过对多线程的使用,可以编写出非常高效的程序。


  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JinziH Never Give Up

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值