多线程

程序、进程、线程

程序(program):是为完成特定任务、用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。

进程(process):是程序的一次执行过程,或是正在运行的一个程序。是一个动态的过程:有它自身的产生、存在和消亡的过程。

线程(thread):进程可进一步细化为线程,是一个程序内部的一条执行路径。

\

进程可以细化为多个线程。
每个线程,拥有自己独立的:栈、程序计数器
多个线程,共享同一个进程中的结构:方法区、堆。

并行与并发

并行:多个CPU同时执行多个任务。比如:多个人同时做不同的事。

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

线程的创建和使用

方式一:继承Thread类创建线程

具体实现:

  1. 创建继承于Thread类的子类
  2. 重写Thread中的run()方法,并将此线程要执行的操作声明在run()方法中
  3. 在main方法中创建Thread类子类的对象
  4. 通过此对象调用start(),其中start()包括启动当前线程、调用当前线程的run()
//创建线程遍历100以内的偶数
class MyThread extends Thread {
    
    @
    public void run(){
        for(int i = 1;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(i)
            } 
        }
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        myThread.start();
    }
}

注意:

  • 我们启动一个线程,必须调用start(),不能调用run()的方式启动线程。
  • 如果再启动一个线程,必须重新创建一个Thread子类的对象,调用此对象的start()。

方式二:实现Runnable接口

具体实现:

  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法run()
  3. 创建实现类的对象
  4. 将Runnable实现类的对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
//创建线程遍历100以内的偶数
class MyThread implements Runnable {
    
    @
    public void run(){
        for(int i = 1;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(i)
            } 
        }
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        Thread t1 = new Thread(myThread);
        t1.start();
    }
}

以上方式是jdk5.0之前的方式,jdk5.0之后有新增了Callable和线程池的方式创建新线程

以上两种方式的异同

开发中优先选择Runnable接口的方式,原因如下

  • 实现的方式不会受到单继承的局限性
  • 实现的方式更适合来处理多个线程拥有共享数据的情况

相同点:1.两种方式都需要重写run(),都将线程需要执行的逻辑声明在run()中。2.都是调用的Thread类中的start()。

方式三:实现Callable接口

具体实现:

  1. 创建一个实现Callable接口的实现类
  2. 实现call方法,将此线程需要执行的操作声明在call()中
  3. 创建Callabale接口实现类的对象
  4. 将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
  5. 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
  6. 获取Callable中call方法的返回值,get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
//创建线程遍历100以内的偶数
class MyThread implements Callable {
    
    @
    public void call() throws Exception {
        for(int i = 1;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(i)
            } 
        }
    }
}

public class ThreadTest {
    public static void main(String[] args){
        MyThread myThread = new MyThread();
        FutureTask futureTask = new FutureTask(myThread);
        new Thread(futureTask).start();
        
        try {
            Object sum = futureTask.get();
            System.out.println("总和为:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?

1.call()可以返回值的。2.call()可以抛出异常,被外面的操作捕获,获取异常的信息

3.Callable是支持泛型的

方式四:线程池

具体实现:

  1. 提供指定线程数量的线程池
  2. 执行值得的线程的操作,需要实现Runnable接口或Callable接口实现类的对象
  3. 关闭线程池

实际上,线程池还是基于实现前面连个接口而来的,主要原因如下:

  1. 提高想赢速度(减少创建新线程的时间)
  2. 降低资源消耗(重复利用线程池中)
class NumberThread implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

class NumberThread1 implements Runnable{

    @Override
    public void run() {
        for(int i = 0;i <= 100;i++){
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        }
    }
}

public class ThreadPool {

    public static void main(String[] args) {
        //1. 提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        //设置线程池的属性
//        System.out.println(service.getClass());
//        service1.setCorePoolSize(15);
//        service1.setKeepAliveTime();


        //2.执行指定的线程的操作。需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());//适合适用于Runnable
        service.execute(new NumberThread1());//适合适用于Runnable

//        service.submit(Callable callable);//适合使用于Callable
        //3.关闭连接池
        service.shutdown();
    }

}

Thread类中的常用的方法

序号方法描述
1public void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
2public void run() 如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法;否则,该方法不执行任何操作并返回。
3public final void setName(String name) 改变线程名称,使之与参数 name 相同。
4public final void setPriority(int priority) 更改线程的优先级。
5public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。
6public final void join(long millisec) 等待该线程终止的时间最长为 millis 毫秒。
7public void interrupt() 中断线程。
8public final boolean isAlive() 测试线程是否处于活动状态。

测试线程是否处于活动状态。 上述方法是被Thread对象调用的。下面的方法是Thread类的静态方法。

序号方法描述
1public static void yield() 暂停当前正在执行的线程对象,并执行其他线程。
2public static void sleep(long millisec) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。
3public static boolean holdsLock(Object x) 当且仅当当前线程在指定的对象上保持监视器锁时,才返回 true。
4public static Thread currentThread() 返回对当前正在执行的线程对象的引用。
5public static void dumpStack() 将当前线程的堆栈跟踪打印至标准错误流。

线程的优先级:

  1. MAX_PRIORITY:10
    MIN _PRIORITY:1
    NORM_PRIORITY:5 -->默认优先级
  2. 如何获取和设置当前线程的优先级:

    • getPriority():获取线程的优先级
    • setPriority(int p):设置线程的优先级

说明:高优先级的线程要抢占低优先级线程cpu的执行权。但是只是从概率上讲,高优先级的线程高概率的情况下被执行。并不意味着只当高优先级的线程执行完以后,低优先级的线程才执行。

线程的生命周期

线程的同步机制

例子:创建三个窗口卖票,总票数为100张.


class Window extends Thread{
    
    private int ticket = 100;
    
    @Override
    public void run(){
        while(true){
            if(ticket > 0){
                System.out.println(Thread.currentThread.getName() + ":" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}

public class WindowTest{
    public static void main(String[] args){
        Window t1 = new Window();
        window t2 = new window();
        window t3 = new window();

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

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

以上代码,会出现线程安全问题。

  1. 问题:卖票过程中,出现了重票、错票 -->出现了线程的安全问题
  2. 问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票。

我们通过同步机制,来解决线程的安全问题。

方式一:同步代码块

  1. 操作共享数据的代码,即为需要被同步的代码(不能包含代码多了,也不能包含代码少了)
  2. 共享数据:多个线程共同操作的变量。
  3. 同步监视器:俗称"锁"。任何一个类的对象,都可以充当锁
  4. 要求多个线程必须共用一把锁才能保证线程的安全
synchronized(同步监视器){
    //需要被同步的代码
}
//实现Runnable接口

class Window1 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                }else{
                    break;
                }
            }
        }
    }
}

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

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

        t1.start();
        t2.start();
        t3.start();
    }
}
//继承Thread类

class Window2 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            synchronized (Window2.class) {//Class clazz = Window2.class,Window2.class只会加载一次
                if (ticket > 0) {

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

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

        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

        w1.start();
        w2.start();
        w3.start();
    }
}

注意:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器;在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑使用当前类充当同步监视器。

方式二:同步方法

将操作共享数据的代码完整的声明在一个方法中,并将此方法声明同步。

private synchronized void show(){
    //需要被同步的代码
}
//继承Thread类

class Window4 extends Thread {

    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private static synchronized void show(){//同步监视器:Window4.class
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
        }
    }

}

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

        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");

        w1.start();
        w2.start();
        w3.start();
    }


}
//实现Runnable接口

class Window3 implements Runnable {

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            show();
        }
    }

    private synchronized void show(){
        if (ticket > 0) {

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
        }
    }

}

public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w3 = new Window3();

        Thread t1 = new Thread(w3);
        Thread t2 = new Thread(w3);
        Thread t3 = new Thread(w3);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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

注意:非静态的同步方法,同步监视器是this,静态的同步方法,同步监视器是当前类本身

方式三:Lock锁

  1. 实例化ReentrantLock
  2. 调用锁定方法lock()
  3. 调用解锁方法unlock()
class window implements Runnable{

    private int ticket = 100;
    //1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock(true);//true代表是否公平

    @Override
    public void run() {
        while (true){
            try {
                //2.调用锁定方法lock
                lock.lock();

                if(ticket > 0){

                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    ticket--;
                }else{
                    break;
                }
            } finally {
                //3.调用解锁方法:unlock()
                lock.unlock();
            }
        }
    }
}


public class LockTest {
    public static void main(String[] args) {
        window w1 = new window();

        Thread t1 = new Thread(w1);
        Thread t2 = new Thread(w1);
        Thread t3 = new Thread(w1);

        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");

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


}

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

解决单例模式中懒汉式中线程不安全

public class Bank{
    
    private static Bank bank;
    
    private Bank(){
    }
    
    public static Bank getIstance(){
        //方式一:效率较差
        synchronized(Bank.class){
            if(bank == null){
                bank = newBank();
            }
            return bank;
        }
    }
    
    //方式二;效率更高
        if(bank == null){
        
            synchronized(Bank.class){
                if(instance == null){
                    instance = new Bank();
                }
            }
            return bank;
        }
}

线程死锁问题

  1. 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了死锁
  2. 出现死锁后,不会出现异常,不会出现提示,只是所以的线程都处于阻塞状态,无法继续
  3. 出现同步时,应该避免死锁

死锁演示

public class ThreadTest {

    public static void main(String[] args) {

        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {

                synchronized (s1){

                    s1.append("a");
                    s2.append("1");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }


                    synchronized (s2){
                        s1.append("b");
                        s2.append("2");

                        System.out.println(s1);
                        System.out.println(s2);
                    }


                }

            }
        }.start();


        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){

                    s1.append("c");
                    s2.append("3");

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    synchronized (s1){
                        s1.append("d");
                        s2.append("4");

                        System.out.println(s1);
                        System.out.println(s2);
                    }


                }



            }
        }).start();
    }
}

线程的通信

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

说明:

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

面试题:sleep() 和 wait()的异同?

相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。

不同点:

  1. 两个方法声明的位置不同:Thread类中声明sleep() , Object类中声明wait()
  2. 调用的要求不同:sleep()可以在任何需要的场景下调用。 wait()必须使用在同步代码块或同步方法中
  3. 于是否释放同步监视器:如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放锁,wait()会释放锁。
  4. sleep()会抛出异常,而wait()不会
class Number implements Runnable {
    private int number = 1;
    private Object obj = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (obj) {
                if (number <= 100) {

                    obj.notify();

                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}


public class CommunicationTest {

    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number);
        Thread t2 = new Thread(number);

        t1.setName("线程一");
        t2.setName("线程二");

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

经典例题:生产者、消费者问题

生产者(Productor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,
店员一次只能持有固定数量的产品(比如:20),如果生产者试图生产更多的产品,店员
会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产;如果店中没有产品
了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
分析:

  1. 是否是多线程问题?是,生产者线程,消费者线程
  2. 是否有共享数据?是,店员(或产品)
  3. 如何解决线程的安全问题?同步机制,有三种方法
  4. 是否涉及线程的通信?是
class Clerk{
    private int productCount = 0;
    
     public synchronized void prducteProduct() { //生产
        if(productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + productCount + "个商品");

            notify();

        }else{
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void cusumeProduct() { //消费
        if(productCount > 5){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + productCount + "个商品");
            productCount--;

            notify();

        }else{
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class Producer extends Thread{ //生产者
    private Clerk clerk;
    
    public Producter(Clerk clerk){
        this.clerk = clerk;
    }
    
    @Override
    public void run() {
        System.out.println(getName() + ":开始生产产品....");
        while (true){

            try {
                Thread.sleep(150);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.prducteProduct();
        }
    }
}

class Consumer extends Thread{ //消费者
    private Clerk clerk;

    public Consumer(Clerk clerk){
        this.clerk = clerk;
    }

    @Override
    public void run() {
        System.out.println(getName() + ":开始消费产品....");
        while (true){

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            clerk.cusumeProduct();
        }
    }
}
最后修改:2021 年 09 月 07 日 01 : 56 PM
如果觉得我的文章对你有用,请随意赞赏