多线程的基础知识

1、基本概念

1、程序:为完成特定任务、用某种语言编写的一组指令的集合。即一段静态的代码
2、进程:是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程
  • 如:运行中的QQ,运行中的MP3播放器
  • 程序是静态的,进程是动态的
  • 进程作为资源分配的基本单位,系统在运行时会为每个进程分配不同的内存区域
3、线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
  • 若一个进程同一时间并行执行多个线程,就是支持多线程的
  • 线程作为调度和执行的基本单位,每个线程拥有独立的虚拟机栈和程序计数器,线程切换开销小
  • 它们从同一堆中分配对象,可以访问相同的变量和对象
4、单核CPU和多核CPU的理解
  • 单核CPU,其实是一个假的多线程,因为在同一个时间单元内,也只能执行一个线程的任务。例如:虽然有多车道,但是收费的人只有一个,只有收了费才能通过,你们CPU就好比收费人员,如果某个人不想交钱,你们收钱人员可以把他“挂起”(等他相通了,再去收钱)。但是因为CPU时间单元特别短,因此感受不出来
  • 如果是多核的话,才能更好的发挥多线程的效率。(现在的服务器都是多核的)
  • 一个Java引用程序java.exe,其实至少3个线程:main()主线程,gc()垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程
5、并行和并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。
并发:一个CPU(采用时间片)同时执行多个任务。比如:秒杀、多个人做同一件事

6、使用多线程的优点

背景:以单核CPU为例,只使用单个线程先后完成多个任务(调用多个方法),肯定比用多个线程来完成用的时间更短,为何仍需多线程呢?

优点:
1、提高应用程序的响应。对图形化界面更有意义,可增强用户体验
2、提高计算机系统CPU的利用率
3、改善程序结构。将既长又复杂的进程分为多个线程,独立运行,利于理解和修改

7、何时需要多线程

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

8、线程的分类

Java中的线程分为两类:一种是守护线程,一种是用户线程
守护线程:任何一个守护线程都是整个JVM中所有非守护线程的保姆:只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。 例如GC线程
用户线程:用户自定义的线程

2、线程的创建和使用

方式一:继承于Thread类

1、创建一个继承于Thread类的子类
2、重写Thread类的run() --> 此线程执行的操作声明在run()中
3、创建Thread类的子类的对象
4、通过此对象调用start()方法:①启动当前线程 ②调用当前线程的run()

问题一:我们不能通过直接调用run()的方式启动线程
如果这里此对象调用的是run(),而不是start(),其实是主线程调用run()方法,并没有开新的线程
问题二:再对用过的线程重新启动,调用start()方法,即myThread.start();
不可以让已经start()的线程去执行,会报IllegalThreadStateException错误

理由
threadStatus一开始是0,调用完后就不是0了

    public synchronized void start() {
        /**
         * This method is not invoked for the main method thread or "system"
         * group threads created/set up by the VM. Any new functionality added
         * to this method in the future may have to also be added to the VM.
         *
         * A zero status value corresponds to state "NEW".
         */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        /* Notify the group that this thread is about to be started
         * so that it can be added to the group's list of threads
         * and the group's unstarted count can be decremented. */
        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
            }
        }
    }

例子:遍历100以内的所有偶数

package com.test;

//1、创建一个继承于Thread类的子类
class MyThread extends Thread {
    //2、重写Thread类的run()
    @Override
    public void run() {
        for(int i = 0;i < 100;i ++) {
            if(i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        //3、创建Thread类的子类的对象
        MyThread myThread = new MyThread();
        //4、通过此对象调用start()方法
        myThread.start();

        System.out.println("hello");
    }
}

方式二:实现Runnable接口
1、创建一个实现了Runnable接口的类
2、实现类去实现Runnable中的抽象方法:run()
3、创建实现类的对象
4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
5、通过Thread类的对象调用start()方法:①启动当前线程 ②调用当前线程的run() -> 调用了Runnable类型的target的run()

package com.test;

//1、创建一个实现了Runnable接口的类
class MyThread implements Runnable {
    //2、实现类去实现Runnable中的抽象方法:run()
    @Override
    public void run() {
        for(int i = 0;i < 100;i ++) {
            if(i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}
public class ThreadTest {
    public static void main(String[] args) {
        //3、创建实现类的对象
        MyThread myThread = new MyThread();
        //4、将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(myThread);
        //5、通过Thread类的对象调用start()
        t1.start();

        //再启动一个线程,遍历100以内的偶数
        Thread t2 = new Thread(myThread);
        t2.start();
    }
}
比较创建线程的两种方式

开发中:优先选择实现Runnable接口的方式
原因:
1、实现的方式没有类的单继承性的局限性
2、实现的方式更适合来处理多个线程共享数据的情况

联系:Thread本身也实现了Runnable的接口,两种方式都需要重写run()

3、线程的生命周期

新建:当一个Thread类或其子类被声明并创建时,新生的线程对象处于新建状态
就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的状态,只是没分配到CPU资源
运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能
阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态
死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束

image.png

4、线程的同步

问题的引出

例子:创建个窗口卖票,总票数为100张.使用实现Runnable接口的方式

问题:卖票过程中,出现了重票、错票 –>出现了线程的安全问题

问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

如何解决:当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以开始操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。

抽象化
  • 问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有 执行完,另一个线程参与进来执行。导致共享数据的错误。
  • 解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以 参与执行。

同步机制

方式一:同步代码块
synchronized (对象){
    // 需要被同步的代码;
}

说明:

  • 操作共享数据的代码,即为需要被同步的代码。 –>不能包含代码多了,也不能包含代码少了。
  • 共享数据:多个线程共同操作的变量。比如:ticket就是共享数据。
  • 同步监视器,俗称:锁。任何一个类的对象,都可以充当锁。要求:多个线程必须要共用同一把锁。

补充:

  • 在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器(锁)。
  • 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。
1、继承于Thread类的方式
package com.test;

class Window1 extends Thread{
    private static int ticket = 100;
    @Override
    public void run() {

        while(true){
            synchronized (Window1.class){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
                    ticket --;
                }else{
                    break;
                }
            }
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();
        Window1 w2 = new Window1();
        Window1 w3 = new Window1();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}
2、实现Runnable接口的方式
package com.test;

class Window2 implements Runnable {
    private int ticket = 100;

    @Override
    public void run(){

        while(true){
            synchronized (this){
                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
                    ticket --;
                }else{
                    break;
                }
            }
        }
    }
}

public class Window2Test {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
方式二:同步方法

如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的。

public synchronized void show (String name){
    ….
}

关于同步方法的总结

  • 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
  • 非静态的同步方法,同步监视器是:this
  • 静态的同步方法,同步监视器是:当前类本身
1、继承于Thread类的方式
package com.test;

class Window1 extends Thread{
    private static int ticket = 100;
    @Override
    public void run(){

        while(true){
            show();
        }
    }

    private synchronized void show(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
            ticket --;
        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();
        Window1 w2 = new Window1();
        Window1 w3 = new Window1();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}
2、实现Runnable接口的方式
package com.test;

class Window2 implements Runnable {
    private int ticket = 100;

    @Override
    public void run(){

        while(true){
            show();
        }
    }

    private synchronized void show(){
        if(ticket > 0){
            System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
            ticket --;
        }
    }
}

public class Window2Test {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
方式三:Lock锁 — JDK5.0新增

synchronized 与 Lock的异同?

相同:二者都可以解决线程安全问题

不同:synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器

Lock需要手动的启动同步(lock(),同时结束同步也需要手动的实现(unlock())

Lock —> 同步代码块(已经进入了方法体,分配了相应资源 ) —> 同步方法(在方法体之外)

1、继承于Thread类的方式(代码有问题,待修改)
package com.test;

import java.util.concurrent.locks.ReentrantLock;

class Window1 extends Thread{
    private static int ticket = 100;
    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run(){

        while(true){
            try {

                lock.lock();

                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
                    ticket --;
                }else{
                    break;
                }
            }finally {
                lock.unlock();
            }

        }
    }
}

public class WindowTest1 {
    public static void main(String[] args) {
        Window1 w1 = new Window1();
        Window1 w2 = new Window1();
        Window1 w3 = new Window1();

        w1.setName("窗口1");
        w2.setName("窗口2");
        w3.setName("窗口3");

        w1.start();
        w2.start();
        w3.start();
    }
}
2、实现Runnable接口的方式
package com.test;

import java.util.concurrent.locks.ReentrantLock;

class Window2 implements Runnable {
    private int ticket = 100;

    private ReentrantLock lock = new ReentrantLock();

    @Override
    public void run(){

        while(true){
            try {

                lock.lock();

                if(ticket > 0){
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + ticket);
                    ticket --;
                }else{
                    break;
                }
            }finally {
                lock.unlock();
            }

        }
    }

}

public class Window2Test {
    public static void main(String[] args) {
        Window2 w = new Window2();
        Thread t1 = new Thread(w);
        Thread t2 = new Thread(w);
        Thread t3 = new Thread(w);

        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");

        t1.start();
        t2.start();
        t3.start();
    }
}
线程通信的例子:使用两个线程打印1到100,线程1和线程2交替打印

涉及到3个方法

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

注意:
wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中。
wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器。否则,会出现IllegalMonitorStateException异常
wait(),notify(),notifyAll()三个方法是定义在java.lang.Object类中。

package com.test;

class Number implements Runnable {
    private int number = 1;

    @Override
    public void run(){
        while(true){
            synchronized (this){
                notify();//等价于this.notify()
                if(number <= 100){
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为: " + number);
                    number ++;

                    try {
                        //使用wait()方法的线程进入阻塞状态
                        wait();//等价于this.wait()
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

public class Communication {
    public static void main(String[] args) {
        Number number = new Number();
        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程1");
        t2.setName("线程2");

        t1.start();
        t2.start();
    }
}

sleep() 和 wait() 的异同

1、相同点:一旦执行方法,都可以使得当前线程进入阻塞状态
2、不同点:
(1)两个方法声明的位置不同:Thread类中声明sleep(),Object类中声明wait()
(2)调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
(3)关于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。

你可能感兴趣的:(多线程的基础知识)