Java 多线程看这一篇就够了

创建多线程

继承Thread类

创建多线程程序的第一种方式:创建 Thread类的子类
java.lang.Thread 类:是描述线程的类,我们想要实现多线程程序,就必须继承 Thread

实现步骤:

  1. 创建一个 Thread类的子类
  2. Thread(类的子类中重写 Thread类中的run方法,设置线程任务
  3. 创建 Thread类的子类对象
  4. 调用 Thread类中的start方法,开启新的线程,执行 run方法
    void start() : 使该线程开始执行;Java虚拟机调用该线程的run方法。
    结果是两个线程并发地运行:当前线程(main线程)和另一个线程(创建的新线程,执行其run方法)。
    多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

Java程序属于抢占式调度,哪个线程优先级高,哪个线程就先执行;同一个优先级,则随机选择一个执行。

实现Runnable接口

创建多线程程序的第二种方式:实现 Runnable接口

java.lang.Runnable:
Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run的无参数方法。


java.lang.Thread类的构造方法:

Thread(Runnable target) 分配新的 Thread对象。
Thread(Runnable target, string name) 分配新的 Thread对象。


实现步骤

  1. 创建一个 Runnable接口的实现类
  2. 在实现类中重写 Runnable接口的run方法,设置线程任务
  3. 创建一个 Runnable接口的实现类对象
  4. 创建 Thread类对象,构造方法中传递 Runnable接口的实现类对象
  5. 调用Thread类中的start方法开启新的线程,执行run方法
public class Demo {
    public static void main(String[] args){
        // 3. 创建一个 Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        // 4. 创建 Thread类对象,构造方法中传递 Runnable接口的实现类对象
        Thread t = new Thread(run);
        // 5. 调用Thread类中的start方法开启新的线程,执行run方法
        t.start();
    }

    // 1. 创建一个 Runnable接口的实现类
    static class RunnableImpl implements Runnable{

        // 2. 在实现类中重写 Runnable接口的run方法,设置线程任务
        @Override
        public void run() {
           // 方法内容
        }
    }

}


实现 Runnable接口创建多线程程序的好处

  1. 避免了单继承的局限性
    一个类只能继承一个类,类继承了 Thread类就不能继承其他的类
    实现了 Runnable接口,还可以继承其他的类或者实现其他的接口
  2. 增强了程序的扩展性,降低了程序的耦合性(解耦
    实现 Runnable接口的方式把设置线程任务和开启新线程进行了分离(解耦)
    实现类中,重写了run方法:用来设置线程任务
    创建 Thread类对象,调用 start方法:用来开启线程

匿名内部类

匿名:没有名字
内部类:写在其他类内部的类

匿名内部类作用:简化代码
把子类继承父类,重写父类的方法,创建子类对象合成一步完成
把实现类实现类接口,重写接口中的方法,创建实现类对象合成一步完成

匿名内部类的最终产物:子类/实现类对象,而这个类没有名字

格式:

new 父类/接口() {
    重写父类/接口中的方法
};

实例:

public class Demo {
    
    public static void main(String[] args) {
        
        // 1. 继承Thread类
        new Thread() {
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                // 方法内容
            }
        }.start();
        
        // 2. 实现Runnable接口
        Runnable r = new Runnable() {
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                // 方法内容
            }
        };
        new Thread(r).start();

        // 3. 实现Runnable接口简化版
        new Thread(new Runnable() {
            // 重写run方法,设置线程任务
            @Override
            public void run() {
                // 方法内容
            }
        }).start();
        
    }
}

常用方法

获取线程的名称

  1. 使用 Thread类中的方法 getName()
    String getName() : 返回该线程的名称。
  2. 可以先获取到当前正在执行的线程(Thread类中的静态方法),使用线程中的方法 getName()获取线程的名称
    static Thread currentThread() : 返回对当前正在执行的线程对象的引用
// Thread类的子类
public class MyThread extends Thread{
    @Override
    public void run() {
        // 1. 直接调用getName方法
        String name = getName();
        System.out.println(name);
        
        // 2. 通过获取当前线程对象再调用其getName方法
        Thread t = Thread.currentThread();
        String name = t.getName();
        System.out.println(name)
        
        // 将第二种方法缩短
        System.out.println(Thread.currentThread().getName());
    }
}

设置线程的名称

  1. 使用 Thread类中的方法 setName(名字)
    void setName (String name) : 改变线程名称,使之与参数name相同
  2. 创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法 super(String name),把线程名称传递给父类,让父类( Thread)给子线程起一个名字
    Thread (String name) : 分配新的 Thread对象
public class Demo {

    public static void main(String[] args){
        MyThread mt = new MyThread();
        // 1. 使用 Thread类中的 setName方法
        mt.setName("MyThread 0");

        // 2. 使用带参数的构造方法
        MyThread mt1 = new MyThread("MyThread 1");
    }


    static class MyThread extends Thread {
        public MyThread(){}

        // 2. 创建一个带参数的构造方法
        public MyThread(String name) {
            super(name); // 把线程名称传给父类,让父类(Thread)给子线程设置名字
        }

        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
    }
}

sleep方法

public static void sleep(Long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
毫秒数结束之后,线程继续执行。

如模拟一个秒表

public class Demo {
    public static void main(String[] args){
        for (int i = 1; i<=60; i++){
            System.out.println(i);
            
            try {
                Thread.sleep(1000);
            } catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

多线程安全问题

多个线程同时访问了共享的数据就会出现线程安全问题
例如

public class Demo {
    public static void main(String[] args) {
        // 创建接口实现类对象
        RunnableImpl run = new RunnableImpl();
        // 创建Thread类对象
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        // 调用start方法开启多线程
        t0.start();
        t1.start();
        t2.start();
    }

    static class RunnableImpl implements Runnable {

        private int tickets = 100;

        @Override
        public void run() {
            // 使用死循环,让卖票操作重复执行
            while(true) {
                // 判断票是否存在
                if(tickets>0){
                    // 提高安全问题出现的概率
                    try {
                        Thread.sleep(10);
                    } catch(Exception e) {

                    }
                    // 票存在,则卖票
                    System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                    tickets--;
                }
            }
        }
    }

}

同步代码块

格式:

synchronized(锁对象) {
	可能会出现线程安全问题的代码(访问了共享数据的代码)
}

注意:

  1. 同步代码块中的锁对象可以使用任意的对象
  2. 必须保证多个线程使用的锁对象是同一个
  3. 锁对象作用把同步代码块锁住,只让一个线程在同步代码块中执行

将上方出现线程安全问题的代码块添加同步代码块后如下代码所示,此时将不会再出现线程安全问题。

public class Demo {
    static class RunnableImpl implements Runnable {

        // 创建一个锁的对象
        Object obj = new Object();
        private int tickets = 100;

        @Override
        public void run() {

            // 在可能会出现线程安全问题的代码上加上同步代码块
            synchronized(obj) {
                while (true) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(10);
                        } catch (Exception e) {

                        }                        System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                        tickets--;
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

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

}

同步代码块原理

使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器

多个线程一起抢夺cpu的执行权,谁抢刭了谁执行run方法。

假设t0抢刭了cpu的执行权,执行run方法,遇到 synchronized代码块。

这时t0会检查 synchronized代码块是否有锁对象。
发现,就会获取到锁对象,进入到同步中执行

然后线程t1抢刭了cpu的执行权,执行run方法遇到 synchronized代码块。

这时t1会检查 synchronized代码块是否有锁对象。

发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象。

直到t0线程执行完同步中的代码会把锁对象归还给同步代码块,t1才能获取到锁对象进入到同步中执行。

总结: 同步中的线程没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

同步方法

步骤:

  1. 把访问了共享数据的代码抽取出来,放到一个方法中。
  2. 在方法上添加 synchronized修饰符。

格式:定义方法的格式

修饰符 synchronized 返回值类型 方法名(参数列表) {
    可能会出现线程安全问题的代码(访问了共享数据的代码)
}

哪个对象调用此方法,哪个对象就是这个方法的锁对象。

可以将上面的代码更改如下

public class Demo {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);
        
        t0.start();
        t1.start();
        t2.start();
    }

    static class RunnableImpl implements Runnable {

        private int tickets = 100;

        // 定义一个同步方法
        public synchronized void Playtickets(){
            if(tickets>0){
                try {
                    Thread.sleep(10);
                } catch(Exception e) {

                }                System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                tickets--;
            }
        }

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

}

Lock 锁

java.util.concurrent.locks.Lock接口
Lock实现提供了比使用 synchronized方法和语句可获得的更广泛的锁定操作

Lock接口中的方法:

  • void lock() : 获取锁。
  • void unlock() : 释放锁。

java.util.concurrent.locks.ReentrantLock implements Lock接口

使用步骤:

  1. 在成员位置创建一个 Reentrantlock对象
  2. 在可能会岀现安全问题的代码前调用Lock接口中的方法lock()获取锁
  3. 在可能会出现安全问题的代码后调用Lock接口中的方法 unlock()释放锁

更改后的没有线程安全的代码如下:

import java.util.concurrent.locks.ReentrantLock;

public class Demo {
    public static void main(String[] args) {
        RunnableImpl run = new RunnableImpl();
        Thread t0 = new Thread(run);
        Thread t1 = new Thread(run);
        Thread t2 = new Thread(run);

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

    static class RunnableImpl implements Runnable {

        private int tickets = 100;

        // 1. 在成员位置创建一个 Reentrantlock对象
        ReentrantLock l =new ReentrantLock();

        @Override
        public void run() {
            while(true) {
                // 2. 在可能会岀现安全问题的代码前调用Lock接口中的方法`lock()`获取锁
                l.lock();
                if(tickets>0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {                        System.out.println(Thread.currentThread().getName() + "正在卖第" + tickets + "张票");
                        tickets--;
                        // 3. 在可能会出现安全问题的代码后调用Lock接口中的方法 `unlock()`释放锁
                        l.unlock();
                    }
                }
            }
        }
    }
}

线程的状态

java.lang.Thread.State 中定义了六种线程状态:

  • BLOCKED:一个线程的线程状态阻塞等待监视器锁定
  • NEW:线程尚未启动的线程状态。
  • RUNNABLE:可运行线程的线程状态。
  • TERMINATED:终止线程的线程状态。
  • TIMED_WAITING:具有指定等待时间的等待线程的线程状态。
  • WAITING:等待线程的线程状态

等待唤醒机制

概念: 多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。

为什么要处理线程间通信:
多个线程并发执行时,在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,井且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。
就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。而这种手段即——等待唤酲机制

等待唤醒案例

创建一个顾客线程(消费者):告知老板要的包子的种类和数量,调用 wait()方法,放弃cpu的执行,进入到 WAITING状态(无限等待)

创建一个老板线程(生产者):花了5秒做包子,做好包子之后,调用 notify()方法,唤醒顾客吃包子。

注意:

  • 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行。
  • 同步使用的锁对象必须保证唯一。
  • 只有锁对象才能调用 wait()notify()方法

Object类中的方法:

  • void wait() : 在其他线程调用此对象的 notify()方法或 notifyAll()方法前,导致当前线程等待。
    wait方法可传入long类型毫秒值参数,表示在毫秒值结束之后,还没有被notify或者notifyAll唤醒,就会自动醒来,线程睡醒后进入Runnable/Blocked状态。

  • void notify() : 唤醒在此对象监视器上等待的单个线程,继续执行wait方法之后的代码。

  • void notifyAll() : 唤醒在此对象监视器上等待的所有线程

public class Demo {
    public static void main(String[] args) {
        // 创建锁对象
        Object lock = new Object();

        new Thread("顾客"){
            @Override
            public void run() {
                // 保证顾客和老板只有一个线程执行,所以要加同步锁
                synchronized (lock) {
                    try {
                        // 通过调用锁对象的wait方法来进入无限等待状态
                        System.out.println(this.getName() + "等待老板做好包子");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    // 被老板线程notify之后
                    System.out.println("被老板告知包子已经做好了,开吃");
                }
            }
        }.start();

        new Thread("老板"){
            @Override
            public void run() {
                // 保证顾客和老板只有一个线程执行,所以要加同步锁
                synchronized (lock) {
                    // 花5秒钟做包子
                    try {
                        Thread.sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(this.getName() + "把包子做好了");
                    // 唤醒顾客线程
                    lock.notify();
                    // lock.notifyAll();
                }
            }
        }.start();
    }

}

线程池

线程池的概念

概念: 其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。

优点:

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线程的数目,防止因为消耗过多的内存(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后可能会死机)

线程池的使用

Java里面线程池的顶级接口是 java.util.concurrentExecutor,但是严格意义上讲 Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent. Executorservice

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在 java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用 Executors工程类来创建线程池对象。

Executors类中有个创建线程池的方法如下

  • public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。创建的是有界线程池,nThreads就是池中的线程个数可以指定最大数量。
    获取到了一个线程池 ExecutorService对象,在这里定义了一个使用线程池对象的方法:
    public Future<?> submit (Runnable task) :获取线程池中的某一个线程对象,并执行。
    Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。

线程池的使用步骤:

  1. 使用线程池的工厂类Executors里边提供的静态方法 newFixedThreadPool生产一个指定线程数量的线程池
  2. 创建一个类,实现 Runnable接口,重写run方法,设置线程任务
  3. 调用 ExecutorService中的方法 submit传递线程任务(实现类)开启线程,执行run方法
  4. 调用 ExecutorService中的方法 shutdown销毁线程池(不建议执行

代码示例

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo01 {
    public static void main(String[] args) {

        ExecutorService es = Executors.newFixedThreadPool(2);
        es.submit(new Runn());
        es.submit(new Runn());
        es.submit(new Runn());

        es.shutdown(); // 销毁整个线程池,不建议执行
//        es.submit(new Runn()); // 报错

    }

    static class Runn implements Runnable{
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_44547562/article/details/105000207