Java多线程| 青训营笔记

96 阅读8分钟

这是我参与「第四届青训营 」笔记创作活动的的第2天

Java多线程编程| 青训营笔记

线程的创建

方法一:继承Thread类

package com.atguigu.java;
​
/**
 * Created with IntelliJ IDEA.
 *多线程的创建,方式一:继承Thread类
 * 1、创建一个继承于Thread类的子类
 * 2、重写Thread类中的run()方法 -->将此线程要执行的操作声明在run()方法中
 * 3、创建子类对象
 * 4、通过子类对象调用start()方法
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/11/14:04
 * @Description: 例子:遍历100以内的偶数
 *///1、创建一个继承于Thread类的子类
class MyThread extends Thread{
    //2、重写Thread类中的run()方法 -->将此线程要执行的操作声明在run()方法中
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println( Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
public class ThreadTest {
    public static void main(String[] args) {
        //3、创建子类对象
        MyThread myThread = new MyThread();
        //4、通过子类对象调用start()方法:①启动当前线程 ②调用run()方法
        myThread.start();
​
        //问题一:不能直接通过run方法创建线程,这样只是普通的调用方法,如果直接调用run,使用的则是主线程
        //myThread.run();
​
        //问题二:要启动多个线程应该是多个对象分别调用start,而不是一个对象多次调用start
        MyThread myThread1 = new MyThread();
        myThread1.start();
    }
}

练习:Thread的匿名子类

package com.atguigu.exer;
​
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/11/14:51
 * @Description:创建两个分线程,一个遍历100以内的偶数,一个遍历100以内的基数
 */class EvenThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
class OddThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
​
}
​
public class ThreadDemo {
    public static void main(String[] args) {
//        方式一:
//        EvenThread evenThread = new EvenThread();
//        OddThread oddThread = new OddThread();
//        evenThread.start();
//        oddThread.start();//        方式二:创建Thread类的匿名子类
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i%2==0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();
​
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i%2!=0){
                        System.out.println(Thread.currentThread().getName()+":"+i);
                    }
                }
            }
        }.start();
​
    }
}

线程的常用方法

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/11/21:12
 * @Description:测试Thread类的常用方法
 * 1、start():开启一个线程,并调用run方法
 * 2、run():开启新线程想要执行的操作
 * 3、Thread.currentThread():返回当前运行的线程对象
 * 4、getName():获取当前线程对象的线程名
 * 5、setName():设置当前线程对象的线程名
 * 6、yield():立即释放cpu,但是仍可能在分配cpu时再次获取到cpu继续执行
 * 7、join():插队,抢占cpu直至调用join()的线程完成所有操作再继续下一个线程 注意:只有开启了的【执行了start方法的线程才有join的作用】
 * 8、stop():过时方法,不推荐使用,立即停止调用线程的运行
 * 9、sleep(x):x的单位是毫秒,调用sleep()方法的线程进入阻塞态,直到经过x毫秒再重新进入就绪态,没有其他线程时则是一个类似等待sleep状态
 * 10、isAlive():判断调用线程是否仍然存活
 *
 */
class ExerThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                try {
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
            if (i%20==0){
                yield();
            }
        }
    }
}
​
class ExerThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
public class ThreadMethodTest {
    public static void main(String[] args) {
        ExerThread exerThread = new ExerThread();
        ExerThread1 exerThread1 = new ExerThread1();
        exerThread.setName("线程1");
        exerThread1.setName("线程2");
​
        exerThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
            if (i==20){
                try {
                    exerThread.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
​
        System.out.println(exerThread.isAlive());
​
        //exerThread1.start();
    }
}

线程的调度

线程的优先级

 /**
  * The minimum priority that a thread can have.
  */
 public final static int MIN_PRIORITY = 1;
​
/**
  * The default priority that is assigned to a thread.
  */
 public final static int NORM_PRIORITY = 5;
​
 /**
  * The maximum priority that a thread can have.
  */
 public final static int MAX_PRIORITY = 10;

线程调度的具体方式

package com.atguigu.java;
​
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/11/21:12
 * @Description:线程的调度:优先级高的线程只是获取cpu的概率更高。一定要等高优先级的线程执行完才执行低优先级的说法是错误的
 * 1、getPriority():获取当前线程的优先级
 * 2、setPriority():设置当前线程的优先级
 *
 */
class ExerThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
class ExerThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2!=0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
public class ThreadMethodTest {
    public static void main(String[] args) {
        ExerThread exerThread = new ExerThread();
        ExerThread1 exerThread1 = new ExerThread1();
        System.out.println(exerThread.getPriority());
        exerThread.setPriority(Thread.MAX_PRIORITY);
        System.out.println(exerThread.getPriority());
        exerThread.start();
        exerThread1.start();
    }
}

方法二:实现Runnable

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/12:03
 * @Description:创建线程的方式二:实现Runnable接口
 * 1、创建实现Runnable接口的实现类
 * 2、重写run方法
 * 3、创建该类对象
 * 4、把该类对象传入Thread类构造器,创建Thread类对象
 * 5、调用Thread类对象start方法
 */
//1、创建实现Runnable接口的实现类
class MyThread1 implements Runnable{
// * 2、重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i%2==0){
                System.out.println( Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
​
public class ThreadTest1 {
    public static void main(String[] args) {
//         * 3、创建该类对象
        MyThread1 myThread1 = new MyThread1();
//         * 4、把该类对象传入Thread类构造器,创建Thread类对象
//         * 5、调用Thread类对象start方法
        new Thread(myThread1).start();
        new Thread(myThread1).start();
    }
}

窗口卖票题目

对比下面两个方式,最大的差异就是:是否需要使用静态变量

  • 普通变量的数目与对象个数绑定
  • 静态变量个数与类个数绑定

总结:由于方式创建的拥有票数的对象不同,来定义是否需要使用静态变量

注意:以下编程方式都会存在线程同步安全问题

方式一:继承方法

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/11:41
 * @Description:创建三个线程卖100张票,使用继承方法实现
 * 可能出现的问题:
 * 1、静态变量的使用
 * 2、线程安全问题
 */
class Window extends Thread{
    private static int ticket=100;
​
    @Override
    public void run() {
        while(true){
            if (ticket>0){
                System.out.println(getName()+"卖票,票号为:"+ticket);
                ticket--;
            }else {
                System.out.println("票卖完了");
                break;
            }
        }
    }
}
​
public class WindowTest {
    public static void main(String[] args) {
        Window w0 = new Window();
        Window w1 = new Window();
        Window w2 = new Window();
        w0.start();
        w1.start();
        w2.start();
​
    }
}

方式二:实现方法

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/12:14
 * @Description:创建三个线程卖100张票,使用实现方法实现
 * 可能出现的问题:
 * 1、线程安全问题
 */
class Window1 implements Runnable{
    private int ticket=100;
    @Override
    public void run() {
        while(true){
            if (ticket>0){
                System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket);
                ticket--;
            }else {
                System.out.println("票卖完了");
                break;
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        Window1 window1 = new Window1();
        Thread thread = new Thread(window1);
        Thread thread1 = new Thread(window1);
        Thread thread2 = new Thread(window1);
        thread.setName("窗口0");
        thread1.setName("窗口1");
        thread2.setName("窗口2");
        thread.start();
        thread1.start();
        thread2.start();
    }
}

两种创建方式的比较

开发中:优先使用实现方式

原因:

  1. 实现方式没有单继承的局限性
  2. 实现方式更加适合线程间有共享数据的情况

联系:

public class Thread implements Runnable

相同点:都要重写run方法,将需要执行的操作生命在run方法中

线程的生命周期

image-20220715154258806

线程的安全问题

下面以窗口买票为例

  1. 重票、错票
  2. sleep()会使线程经过判断后进入阻塞态,使得错票率更高(判断和售出应该是捆绑操作(原子性))
  3. 解决方法:当一个线程在操作ticket时,其他线程不可参与
  4. 在Java中,我们通过同步机制,来解决Java线程安全问题

Java同步机制解决线程安全问题

方式一:同步代码块
  • 同步监视器:俗称:锁
  • 锁:任何类的对象都可以充当锁
  • 多线程保持同步使用的锁必须是同一把锁

同步:

  • 解决了线程安全问题
  • 同步代码块是单线程的
synchronized (同步监视器){
    //需要被同步的代码:操作共享数据的代码
    //共享数据:多个线程共同操作的变量
}

以下是经过同步代码块修改后的安全线程

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/12:14
 * @Description:同步代码块解决Runnable实现问题
 */
class Window1 implements Runnable{
    private int ticket=100;
    @Override
    public void run() {
        while(true){
            synchronized (Window1.class){
                if (ticket>0){
                    System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket);
                    ticket--;
                }else {
                    System.out.println("票卖完了");
                    break;
                }
            }

        }
    }
}
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/11:41
 * @Description:同步代码块实现Thread继承多线程安全问题
 *
 */
class Window extends Thread{
    private static int ticket=100;

    @Override
    public void run() {
        while(true){
            synchronized (Window.class){
                if (ticket>0){
                    System.out.println(getName()+"卖票,票号为:"+ticket);
                    ticket--;
                }else {
                    System.out.println("票卖完了");
                    break;
                }
            }
        }
    }
}

注意:不要把循环放进同步代码块之中,不然一个线程会一直持有锁,直至循环退出

方式二:同步方法

在方法头载入关键字synchronized

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/12:14
 * @Description:同步方法解决Runnable实现问题
 */
class Window1 implements Runnable{
    private int ticket=100;
    @Override
    public synchronized void run() {//同步监视器:this
        while(true){
                if (ticket>0){
                    System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket);
                    ticket--;
                }else {
                    System.out.println("票卖完了");
                    break;
                }
        }
    }
}
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/11:41
 * @Description:同步方法实现继承Thread多线程安全问题
 *
 */
class Window extends Thread{
    private static int ticket=100;
    @Override
    public void run() {
        while(true){
           show();
        }
    }
//    private synchronized void show(){//同步监视器:this
    private static synchronized void show(){//同步监视器:class
        if (ticket>0){
            System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket);
            ticket--;
        }
    }
}

同步机制解决单例模式之懒汉式的安全问题

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/18:35
 * @Description:利用同步机制将单例模式中的懒汉式变为线程安全的
 */
class Bank{
    //1、私有化构造器
    private Bank(){}
    private static Bank instance =null;//2、创建单例引用
    public Bank getInstance(){
        //注意:这里多设置一个判断条件可以大幅度提高整个多线程的运行速度
        //因为这里使用同步机制,是为了阻止new操作时引用多次改变,换言之,就是第一个线程需要new对象,后面的线程直接引用对象即可
        if (instance==null){
            synchronized (Bank.class){
                if (instance==null){
                    instance=new Bank();
                }
            }
        }
        return instance;
    }
}

死锁

定义:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃 自己需要的同步资源,就形成了线程的死锁

表现:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于 阻塞状态,无法继续

解决:

  1. 专门的算法、原则
  2. 尽量减少同步资源的定义
  3. 尽量避免嵌套同步

死锁的演示

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/13/16:18
 * @Description:死锁的演示
 */
public class ThreadTest {
    public static void main(String[] args) {
        final StringBuffer s1 = new StringBuffer();
        final 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();
    }
}
方式三:Lock锁
/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/12/12:14
 * @Description:方式三:Lock锁实现同步
 * 1、创建Lock的实例化对象
 * 2、try出同步内容,并在内容头上锁【lock()】
 * 3、finally中解锁【unlock()】
 */
class Window1 implements Runnable{
    private int ticket=100;
    private ReentrantLock lock=new ReentrantLock();
    @Override
    public  void run() {//同步监视器:this
        while(true){
            try{
                lock.lock();
                if (ticket>0){
                    System.out.println(Thread.currentThread().getName()+"卖票,票号为:"+ticket);
                    ticket--;
                }else {
                    System.out.println("票卖完了");
                    break;
                }
            }finally {
                lock.unlock();
            }

        }
    }
}
练习:存钱题

两人向同一个账户分别每次存1000,存三次(共存入六千元),保证线程安全

import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/14/10:53
 * @Description:模拟两个用户各向银行同一账户存储3000,每次存1000
 */
class Bank implements Runnable{
    private int money=0;

    private ReentrantLock lock=new ReentrantLock(true);//true,就是线程交替得到锁

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            try{
                lock.lock();
                money+=1000;
                System.out.println(Thread.currentThread().getName()+"现在余额为:"+money);
            }finally {
                lock.unlock();
            }
        }
    }
}

public class BankTest {
    public static void main(String[] args) {
        Bank b1 = new Bank();
        Thread t1 = new Thread(b1);
        Thread t2 = new Thread(b1);
        t1.start();
        t2.start();
    }
}

线程通信问题

线程通信方法

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/14/11:53
 * @Description:模拟线程通信,交替打印1-100
 * 1、wait():让当前线程进入阻塞态,并且释放同步锁
 * 2、notify():唤醒优先级最高的线程
 * 3、notifyALL():唤醒所有线程
 *
 * 说明:
 * 1、以上三个方法必须存在于同步代码块中
 * 2、调用者必须是同步监视器
 * 3、定义在Object中,所以所有锁可以作为锁并且调用
 *
 */
class Number implements Runnable{
    private int number=1;

    @Override
    public void run() {
        while (true){
            synchronized (this){
                this.notify();
                if (number<=100){
                    System.out.println(Thread.currentThread().getName()+":"+number);
                    number++;

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

sleep()与wait()的异同

同:都会让线程进入阻塞状态

异:

  1. 声明位置:sleep声明在Thread类中,wait声明在Object类中
  2. 调用:sleep是静态方法,随便调用;wait必须在同步代码块内由同步监视器调用

生产者消费者问题

package com.atguigu.java2;

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/14/23:23
 * @Description:生产者和消费者的问题
 * 1、产品Clerk,0-20
 * 2、生产者Producer
 * 3、消费者Consumer
 */
class Clerk{
    private static int ClerkCount=0;
    public synchronized void productClerk(){
            if (ClerkCount<20){
                ClerkCount++;
                System.out.println(Thread.currentThread().getName()+":完成生产,共"+ClerkCount);
                notify();
            }else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
    public synchronized void consumeClerk(){
            if (ClerkCount>0){
                ClerkCount--;
                System.out.println(Thread.currentThread().getName()+":完成消费,共"+ClerkCount);
                notify();
            }else {
                try {
                    wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    }
}

class Producer extends Thread{
    private Clerk clerk;
    public Producer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while (true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.productClerk();
        }
    }
}

class Consumer extends Thread{
    private Clerk clerk;
    public Consumer(Clerk clerk) {
        this.clerk = clerk;
    }
    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.consumeClerk();
        }
    }
}

public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer producer = new Producer(clerk);
        Consumer consumer = new Consumer(clerk);
        producer.setName("生产者");
        consumer.setName("消费者");
        producer.start();
        consumer.start();
    }
}

线程创建方式三:实现Callable接口

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/15/0:05
 * @Description:创建线程的方式三:实现Callable接口
 * 步骤:
 * 1、创建实现Callable接口实现类
 * 2、重写call()方法
 * 3、创建Callable接口实现类的类对象
 * 4、将3中的Callable接口实现类的类对象作为参数,创建FutureTask类对象
 * 5、将4中的FutureTask类对象作为参数创建Thread对象调用start()开启线程
 * 6、调用4中FutureTask类对象的get()方法可以获得call()方法的返回值
 *
 * 实现Callable接口的注意点
 * 1、通过FutureTask类的对象调用get()获得了call()的方法
 * 2、可以抛出异常
 * 3、支持泛型返回值
 * 4、借助FutureTask类,获取返回结果
 */

class NumberThread implements Callable {
    private int num=0;
    @Override
    public Object call() throws Exception {
        for (int i = 0; i <= 4; i++) {
            num+=i;
        }
        return num;
    }
}

public class ThreadNew {
    public static void main(String[] args) {
        NumberThread numberThread = new NumberThread();
        FutureTask future = new FutureTask(numberThread);
        Thread thread = new Thread(future);
        thread.start();
        try {
            Object num = future.get();
            System.out.println("总和为:"+num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

线程创建方式四:使用线程池

/**
 * Created with IntelliJ IDEA.
 *
 * @Author: 谭铭豪
 * @Date: 2022/07/15/1:12
 * @Description:创建线程方式四:使用线程池
 *  JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors
 *  ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor
 *  void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行
 * Runnable
 *  <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一般又来执行
 * Callable
 *  void shutdown() :关闭连接池
 *  Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池
 *  Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池
 *  Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池
 *  Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池
 *  Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
 * 行命令或者定期地执行。
 */
class NumThread implements Runnable{
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            if (i%2==0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}
class NumThread1 implements Runnable{
​
    @Override
    public void run() {
        for (int i = 0; i < 10; 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);
​
        //2、执行指定线程的操作,需要提供Runnable接口的对象或Callable接口的对象
        //service.submit(Callable callable);//submit传Callable接口对象
        service.execute(new NumThread());//execute传Runnable接口对象
        service.execute(new NumThread());
        service.execute(new NumThread1());
        
        //3、关闭线程池
        service.shutdown();
    }
}