Java多线程

多线程

一:程序、进程、线程的基本概念

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

例如:IDEA程序,一堆文件夹就可以看作是静态的指令
IDEA程序

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

例如:运行中的PotPlayer、IDEA……

进程

3.线程(thread):是一个程序内部的一条执行路径。

多线程——若一个进程同一时间并行执行多个线程,就是支持多线程的。
一个Java应用程序java.exe,至少有三个线程:main()主线程、gc()垃圾回收线程、异常处理线程。
例如:电脑管家、360安全卫士可以并行进行电脑检测,垃圾清理,病毒查杀。
进程与线程

4.进程与线程的细节:

①、进程作为资源分配单位,系统在运行时会为每个进程分配不同的内存区域。JVM会为每一个进程分配堆和本地方法区。
②、线程作为调度和执行单位,每个线程独立拥有一套虚拟机栈和程序计数器,线程切换的开销小。
③、一个进程中的多个线程共享相同的内存单元(内存地址空间),那么它么从同一堆中分配对象,可以访问相同的变量和对象。这就使得线程之间的通信更简便、高效。但是:多个线程操作共享的系统资源可能会带来安全问题。(线程安全为题)
JVM内存分配图


二:线程的创建和使用(重点)

1.继承Thread类的方式创建线程
package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-17 19:44
 * @Description 多线程的创建方式一:继承Thread类
 *          1.继承Thread类
 *          2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
 *          3.创建Thread类的子类对象
 *          4.通过对象调用父类start()方法
 *
 *    例如:求100以内的偶数
 */
public class ThreadTest {

    public static void main(String[] args) {
        // 3.创建Thread类的子类对象
        MyThread myThread = new MyThread();

        // 4.调用父类start()方法:①启动当前线程、②调用当前线程的run()方法
        myThread.start();

        // 以下代码仍然在main线程中执行的
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(i + "*************main()***************");
            }
        }
    }
}
// 1.继承Thread类
class MyThread extends Thread{
    // 2.重写run()方法

    @Override
    public void run() {
        // 求100以内的偶数
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

运行结果:
运行结果
start()方法的说明:①开始当前线程 ② JVM调用当前线程的run()方法
start方法的说明

  • 问题一:为何不直接调用run()方法呢?
    如果直接调用run()方法的话,就不存在多线程了。只存在主线程按照顺序依次执行代码。为了证明不存在多线程了,我们使用Thread类的API证明。
package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-17 19:44
 * @Description 多线程的创建方式一:继承Thread类
 *          1.继承Thread类
 *          2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
 *          3.创建Thread类的子类对象
 *          4.通过对象调用父类start()方法
 *
 *    例如:求100以内的偶数
 */
public class ThreadTest {

    public static void main(String[] args) {
        // 3.创建Thread类的子类对象
        MyThread myThread = new MyThread();

        // 4.调用父类start()方法 ①启动当前线程 ②调用当前线程的run()方法
//        myThread.start();

        myThread.run();
        // 以下代码仍然是在main线程中执行的
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "*************main()***************");
            }
        }
    }
}
// 1.继承Thread类
class MyThread extends Thread{
    // 2.重写run()方法

    @Override
    public void run() {
        // 求100以内的偶数
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

证明发现:执行完run()方法,才会执行下面的代码。说明并没有启动该线程,调用start()方法。

  • 问题二:如何再次启动一个线程?
    创建Thread类的另一个字类对象,调用start()方法。
package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-17 19:44
 * @Description 多线程的创建方式一:继承Thread类
 *          1.继承Thread类
 *          2.重写run()方法 ---> 将此线程执行的操作声明在run()方法当中
 *          3.创建Thread类的子类对象
 *          4.通过对象调用父类start()方法
 *
 *    例如:求100以内的偶数
 */
public class ThreadTest {

    public static void main(String[] args) {
        // 3.创建Thread类的子类对象
        MyThread myThread0 = new MyThread();

        // 4.调用父类start()方法 ①启动当前线程 ②调用当前线程的run()方法
        myThread0.start();

//        myThread.run();

        // 再次创建一个线程对象
        MyThread myThread1 = new MyThread();
        myThread1.start();
        // 以下代码仍然是在main线程中执行的
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i + "*************main()***************");
            }
        }
    }
}
// 1.继承Thread类
class MyThread extends Thread{
    // 2.重写run()方法

    @Override
    public void run() {
        // 求100以内的偶数
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

why—>为什么一个线程对象不能调用两次start()方法呢?
通关观察源码发现:Thread类中的start()方法会改变当前线程的状态,即调用start()方法之后,线程的状态为0;如果再次调用start()方法,状态不再是0,则会抛出线程状态异常(IllegalThreadStateException)。


2.实现Runnable接口的方式创建线程
package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-18 14:25
 * @Description 创建多线程的方式二: 实现Runnable接口
 *              1.创建一个Runnable接口的实现类
 *              2.实现接口中的抽象方法,并完成特定的操作
 *              3.创建实现类对象
 *              4.将实现类对象作为参数传递到线程对象中
 *              5.通过线程对象调用start()方法
 */
public class ThreadTest1 {

    public static void main(String[] args) {
        // 3.创建实现类对象
        RunnableImpl r1 = new RunnableImpl();
        // 4.将实现类对象作为参数传递到线程对象中
        Thread t1 = new Thread(r1,"线程1");
        // 5.通过线程对象调用start()方法:①启动当前线程 ②调用当前线程的run()方法
        // 问题一:为什么是调用了实现类中的run()方法--->看源码
        t1.start();

        // 再创建一个线程
        Thread t2 = new Thread(r1,"线程2");
        t2.start();
    }
}
// 1.创建一个Runnable接口的实现类
class RunnableImpl implements Runnable{
    // 2.实现接口中的抽象方法,并完成特定的操作
    @Override
    public void run() {
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}

  • 问题一:为什么是调用了实现类中的run()方法?—>看源码发现
    在这里插入图片描述

3.比较创建线程的两种方式

例题:实现卖票的功能。总票数100张,创建3个窗口卖票。(3个线程)
存在线程安全为题,待解决。仅仅是用来体现开发中常用实现Runnable接口的方式创建线程。
①:使用继承Thread类的方式实现例题

package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-18 14:00
 * @Description     创建三个窗口买票,总票数为100张.使用继承Thread类的方式实现
 *                  存在线程安全问题,待解决。
 *
 */
public class SellTickets {
    public static void main(String[] args) {

        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

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

        w1.start();
        w2.start();
        w3.start();
        // 运行之后发现 3 个线程对象独立拥有ticket属性,相当于300张票,这是错误的。
        // 解决方式:将ticket修改为静态成员变量,3 个线程对象共享

        // 如果不把成员变量变为静态的,该如何解决这个问题呢?
        
        // 改为静态成员变量之后发现第100张票还是卖了3次,这就涉及到线程的安全问题。待解决。

    }
}
class Window extends Thread{
    private static int ticket = 100;   // 总票数

    // 实现run()方法

    @Override
    public void run() {

        while (true) {
            if (ticket > 0) {
                // 进行卖票
                System.out.println(getName() + ": 卖票,票号为:" + ticket);
                ticket--;
            } else {
                break;
            }
        }
    }
}

②:使用实现Runnable接口的方式完成例题

package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-18 14:57
 * @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
 *              存在线程安全问题,待解决。
 */
public class SellTickets1 {

    public static void main(String[] args) {
        // 创建实现类对象
        Window1 w1 = new Window1();
        // 创建线程对象
        Thread t1 = new Thread(w1, "窗口1");
        Thread t2 = new Thread(w1, "窗口2");
        Thread t3 = new Thread(w1, "窗口3");

        // 启动窗口,卖票
        t1.start();
        t2.start();
        t3.start();

        // 此时,还会存在第100张票重复,出现线程安全问题。待解决
        // 但是,使用实现Runnable接口的方式创建多线程,可以共用同一个对象,也就可以共用同一个成员变量。
        // 使用继承Thread的方式,字类必须将实例变量声明为静态变量,此时才可共用这个变量。否则是每个对象所单独拥有的一份


    }

}
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 {
                break;
            }
        }
    }
}

结论: 在实际开发中,优先选择实现Runnable接口的方式创建线程。
①.打破了java中的单继承性;
②.实现Runnable接口的方式可以处理多个线程之间共享数据的情况;
③.Thread类也实现了Runnable接口。

4.callable接口的方式创建线程(JDK5.0新增)
package cn.xuguowen.java4;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author xuguowen
 * @create 2021-02-21 14:05
 * @Description     使用实现Callable接口的方式创建多线程
 *
 *                  如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建的多线程强大?
 *                      1.call()方法有返回值,可以作为其他线程的资源
 *                      2.可以抛出异常,外面可以捕获异常,获取异常信息
 *                      3.Callable接口是支持泛型的
 */
public class CallableTest {
    public static void main(String[] args) {
        // 3.创建实现类对象
        NewThread newThread = new NewThread();

        // 创建FutureTask对象,将实现类对象作为参数传入进去
        FutureTask<Integer> f = new FutureTask<>(newThread);

        // 创建线程类,将FutureTask对象作为Runnale接口的实现类对象传递进去
        //  FutureTask<V> implements RunnableFuture<V>
        //  RunnableFuture<V> extends Runnable, Future<V> {
        Thread t = new Thread(f);
        t.start();

        // 当我们对call方法的返回值感兴趣时,可以调用FutureTask类中的get方法获取call方法的返回值
        try {
            Integer sum = f.get();
            System.out.println("总和:" + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}
// 1.创建实现类对象
class NewThread implements Callable<Integer> {
    // 2.实现call方法
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        // 求100以内偶数之和
        for (int i = 1; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
                sum += i;
            }
        }
        // 自动装箱
        return sum;
    }
}

5.线程池
5.1背景:经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
5.1好处:
  • 提高响应速度(减少了创建新线程的时间)
  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)
  • 便于线程管理
package cn.xuguowen.java4;

import java.util.concurrent.*;

/**
 * @author xuguowen
 * @create 2021-02-21 14:46
 * @Description     使用线程池的方式创建多线程
 *
 *      线程池的好处?
 *         1.提高了响应速度,减少了创建新线程花费的时间
 *         2.降低了资源消耗,重复利用线程池中的线程,不需要每次都创建线程
 *         3.便于对线程的管理
 *                  corePoolSize:核心池的大小
 *                  MaximumPoolSize:最大线程数
 *                  keepAliveTime:线程没有任务时最多保持多长时间后会终止
 *                  …………
 */
public class ThreadPoolTest {
    public static void main(String[] args) {
        // 1.使用线程池的工具类创建线程池对象,线程池中有10个线程对象
        // ExecutorService是一个接口
        ExecutorService service = Executors.newFixedThreadPool(10);

        // 如何设置线程的属性呢?由于上面是一个接口,接口中全是全局常量,所以不会存在线程的属性的

        // 2.通过getClass方法获取接口的实现类对象
        System.out.println(service.getClass());     //class java.util.concurrent.ThreadPoolExecutor
        ThreadPoolExecutor s = (ThreadPoolExecutor) service;
        s.setMaximumPoolSize(2);

        // 3.执行指定的线程操作
        service.execute(new ThreadPool());      // 适用于Runnable接口3


        ThreadPool2 t = new ThreadPool2();
        FutureTask futureTask = new FutureTask(t);
        service.submit(futureTask);                     // 适用于Callable接口

        // 4.记住关闭线程池
        service.shutdown();

    }
}

class ThreadPool implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(i);
            }
        }
    }
}

class ThreadPool2 implements Callable{

    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i < 101; i++) {
            if (i % 2 != 0) {
                System.out.println(i);
            }
        }
        return sum;
    }
}

三:线程的常用方法和优先级

package cn.xuguowen.java1;

/**
 * @author xuguowen
 * @create 2021-02-18 10:40
 * @Description 测试Thread类中常用的方法
 *
 *      1.start():开启当前线程,并且JVM执行当前线程的run()方法
 *      2.run():通常重写Thread类中的run()方法,将创建的线程要执行的操作声明在次方法中
 *      3.currentThread():静态方法,获取执行当前代码的线程对象
 *      4.setName():设置当前线程的名字
 *          也可以通过构造器的方式为线程命名:Thread(String name)
 *      5.getName():获取当前线程的名字
 *      6.yield():释放当前cpu的执行权。
 *      7.join():在线程a中调用线程b的join()方法,此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束
 *                  阻塞状态。
 *      8.stop():已过时。当执行此方法时,强制结束当前线程
 *      9.sleep(long millitime):让当前线程‘睡眠’指定的毫秒数。在指定的毫秒数之内,当前线程是阻塞状态的。
 *      10.isAlive():判断当前线程是否存活。
 *
 *
 *      线程优先级的问题;
 *          1.首先Thread类提供了3个常量来设置线程的优先级
 *              public final static int MIN_PRIORITY = 1;
 *              public final static int NORM_PRIORITY = 5;  默认优先级
 *              public final static int MAX_PRIORITY = 10;
 *          2.通过方法设置和获取线程的优先级值
 *              getPriority():获取线程的优先级,默认情况下,线程的优先级都是 5
 *              setPriority():设置线程的优先级
 *
 *          说明:高优先级的线程要抢占低优先级线程的cpu执行权。这只是从概率上讲,高优先级的线程高概率情况下被执行。
 *                  并不意味着只有高优先级的线程执行完毕之后,低优先级的线程才会执行。
 */
public class ThreadMethodTest {
    public static void main(String[] args) {
        // 创建线程对象,通过构器的方式为线程命名
        SubThread st = new SubThread("Thread:1");

        // setName()
//        st.setName("线程一");
        // 设置线程的优先级
        st.setPriority(Thread.MAX_PRIORITY);
        st.start();

        Thread.currentThread().setName("main线程");
        // 设置main线程的优先级为 1
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":"  + Thread.currentThread().getPriority() + ":" + i);
            }

            // 当 i = 40,main线程调用st线程对象的join()方法
//            if (i == 40) {
//                try {
//                    st.join();
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
//            }
        }
        // 由于在main线程中,调用了st.join(),所以导致主线程进入阻塞状态,st线程执行完毕之后cpu才有可能执行main线程
        // main线程最后执行这段代码时,st线程早以销毁了。所以返回false
        System.out.println(st.isAlive());
    }

}

class SubThread extends Thread {

    public SubThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        // 线程体
        for (int i = 0; i < 101; i++) {
            if (i % 2 == 0) {
                // 默认情况下:线程的名字是Thread-0
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }

//            if (i == 40) {
//                // st线程对象释放cpu的执行权,但是cpu也有可能下一次再次执行它。
//                yield();
//
//            }
            if (i % 40 == 0) {
                try {
                    // 为什么只能用try catch包裹异常呢?
                    // 面向对象中方法重写的一个知识点:
                    // 当字类重写父类方法之后,字类重写方法抛出的异常不能大于父类被重写方法抛出的异常,父类中的run()方法本身没有抛出异常
                    sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

四:线程的同步(重点)

1.理解线程的安全问题

理解线程的安全问题

2.卖车票的案例引出线程安全问题
package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-18 14:57
 * @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
 *              存在线程安全问题,待解决。
 *
 *              1.问题:卖票过程中出现了重票和错票。--->线程安全问题
 *              2.问题出现的原因:当某个线程操作车票的过程中,还没有操作完成时,其他线程参与进来,也操作车票
 *              3.如何解决?     当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,
 *                              其他线程才可以开始操作ticket。这种情况即使线程a出现的阻塞,也不能被改变。
 */
public class SellTickets1 {

    public static void main(String[] args) {
        // 创建实现类对象
        Window1 w1 = new Window1();
        // 创建线程对象
        Thread t1 = new Thread(w1, "窗口1");
        Thread t2 = new Thread(w1, "窗口2");
        Thread t3 = new Thread(w1, "窗口3");

        // 启动窗口,卖票
        t1.start();
        t2.start();
        t3.start();



    }

}

class Window1 implements Runnable{

    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            if (ticket > 0) {
                // 没有sleep()方法时,也会出现重票和错票问题,只是可能性极小
                // 加上sleep()方法,大概率情况下会出现重票和错票的情况了,
                // 在这里加上sleep()方法:会出现错票

//                try {
//                    Thread.sleep(100);
//                } catch (InterruptedException e) {
//                    e.printStackTrace();
//                }
                System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);
                // 出现重票
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket--;
            } else {
                break;
            }
        }
    }
}

重票情况
错票情况

3.在Java中,使用同步机制解决线程安全问题—>同步代码块
3.1:使用同步代码块的方式解决上述 2 种创建线程方式出现的线程安全问题
	方式一:同步代码块
                 synchronized (同步监视器){
                       // 需要被同步的代码
                  }

               说明:1.操作共享数据的代码,就是需要被同步的代码。
                    2.共享数据:多个线程共同操作的变量。比如:ticket
                    3.同步监视器:俗称 锁。任何一个类的对象都可以充当锁。
                     		但是:多个线程必须共用同一把锁

①:解决实现Runnable接口出现的线程安全问题

package cn.xuguowen.java;

import com.sun.org.apache.bcel.internal.generic.NEW;

/**
 * @author xuguowen
 * @create 2021-02-18 14:57
 * @Description 创建三个窗口买票,总票数为100张.使用实现Runnable接口的方式
 *              存在线程安全问题,待解决。
 *
 *              1.问题:卖票过程中出现了重票和错票。--->线程安全问题
 *              2.问题出现的原因:当某个线程操作车票的过程中,还没有操作完成时,其他线程参与进来,也操作车票
 *              3.如何解决?     当一个线程a在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,
 *                              其他线程才可以开始操作ticket。这种情况即使线程a出现的阻塞,也不能被改变。
 *
 *      在Jaca中,我们使用同步机制来解决线程安全问题。
 *              方式一:同步代码块
 *                      synchronized (同步监视器){
 *                          // 需要被同步的代码
 *                      }
 *
 *                      说明:1.操作共享数据的代码,就是需要被同步的代码。
 *                           2.共享数据:多个线程共同操作的变量。比如:ticket
 *                           3.同步监视器:俗称 锁。任何一个类的对象都可以充当锁。
 *                                       但是:多个线程必须共用同一把锁
 */
public class SellTickets1 {

    public static void main(String[] args) {
        // 创建实现类对象
        Window1 w1 = new Window1();
        // 创建线程对象
        Thread t1 = new Thread(w1, "窗口1");
        Thread t2 = new Thread(w1, "窗口2");
        Thread t3 = new Thread(w1, "窗口3");

        // 启动窗口,卖票
        t1.start();
        t2.start();
        t3.start();
    }
}

class Window1 implements Runnable{

    private int ticket = 100;
    Object obj = new Object();  // 同步监视器,必须是多个线程共用的一把锁

    @Override
    public void run() {
        // 这是错误的:因为这样是每个线程都会拥有一把锁了
        // Object obj = new Object();
        while (true) {
            // synchronized (obj) {
            synchronized (this) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ": 卖票,票号为:" + ticket);

                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

②:解决继承Thread出现的线程安全问题

package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-18 14:00
 * @Description     创建三个窗口买票,总票数为100张.使用继承Thread类的方式实现
 *                  存在线程安全问题,待解决。
                使用synchronized同步代码块解决线程安全问题
 *
 */
public class SellTickets2 {
    public static void main(String[] args) {

        Window w1 = new Window();
        Window w2 = new Window();
        Window w3 = new Window();

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

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

class Window extends Thread{
    private static int ticket = 100;   // 总票数

    // 必须使用静态变量,因为锁要唯一
    private static Object object = new Object();

    // 实现run()方法

    @Override
    public void run() {

        while (true) {
            // synchronized (object){
            synchronized (Window.class){
                if (ticket > 0) {
                    // 进行卖票
                    System.out.println(getName() + ": 卖票,票号为:" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}

3.2:同步代码块的难点

①:同步监视器要保证它的唯一性。
补充: 在解决实现Runnable接口方式创建线程出现安全问题时,同步监视器可以考虑使用 this;在解决继承Thread方式创建线程出现安全问题时,同步监视器慎用 this,可以考虑使用 子类名.class获取字类对象,因为类只加载一次,所以也保证了同步监视器的唯一性。
②:同步代码块需要将操作共享数据的代码包裹起来。但是不能包裹少了,也不能包裹多了。因为包裹少了,还会存在多个线程由于某些原因操作共享数据出现的安全问题,包裹多了就达不到要求,与实际效果背道而驰。例如在本案例中,如果将整个 while 循环包裹进来的话,只能是一个线程参与卖票了,其他线程等待。

3.3:同步代码块利弊

①:解决了线程之间共享数据的安全问题。
②:局限性——操作需要被同步的代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。

4.在Java中,使用同步机制解决线程安全问题—>同步方法
同步方法:如果操作共享数据的代码完整的声明在一个方法当中,我们不妨将此方法声明为同步方法。

①:解决实现Runnable接口出现的线程安全问题

package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-19 18:14
 * @Description 使用同步方法解决实现Runnable接口的线程安全问题
 */

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

        Thread t1 = new Thread(w3, "窗口1");
        Thread t2 = new Thread(w3, "窗口2");
        Thread t3 = new Thread(w3, "窗口3");

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

class Window3 implements Runnable {

    private int ticket = 100;

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

    private synchronized void show() {      // 同步方法也用到了同步监视器:用的是this
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + ticket);
            ticket--;
        }

    }
}

②:解决继承Thread出现的线程安全问题

package cn.xuguowen.java;

/**
 * @author xuguowen
 * @create 2021-02-19 20:53
 * @Description     使用同步方法解决继承Thread类出现的线程安全问题
 */
public class SellTickets4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();

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

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

}
class Window4 extends Thread{
    private static int ticket = 100;

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

//    private synchronized void show() {  // 此时同步监视器是 t1 t2 t3,没有解决线程安全问题
    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--;
        }
    }
}

**总结:**同步方法仍然涉及到同步监视器,只是不需要我们显示声明。

  • 非静态同步方法:同步监视器是 this
  • 静态同步方法:同步监视器是 当前类本身
5.死锁
5.1:死锁就是:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,这样就形成了死锁。
5.2:死锁出现之后:程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
5.3:解决方法
  • 专门的算法、原则
  • 尽量减少同步资源的定义
  • 尽量避免嵌套同步
5.4:代码演示:
package cn.xuguowen.java2;

/**
 * @author xuguowen
 * @create 2021-02-20 10:06
 * @Description 死锁:不同的线程分别占用对方所需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源
 *                      就形成了线程的死锁
 *              出现死锁后:程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续。
 *              解决方法:
 *                      1.专门的算法、原则
 *                      2.尽量减少同步资源的定义
 *                      3.尽量避免嵌套同步
 */
public class DeadLock {
    public static void main(String[] args) {
        // 为了演示死锁
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();


        // 1.利用匿名内部类的方式创建线程:继承Thread类
        new Thread(){
            @Override
            public void run() {
                synchronized (s1) {
                    s1.append("a");
                    s2.append("1");

                    // 为了让代码出现死锁的概率大大增加,我让线程进入阻塞状态,出现死锁情况
                    // 为什么出现了死锁情况呢?
                    /* 因为:此线程在拿到s1锁之后,进入了睡眠状态(阻塞),那么下一个线程有可能获取到cpu的执行权
                            下一个线程也需要s1这把锁,此线程正在占用这把锁,所以出现死锁问题。

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

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

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

        // 2.利用匿名内部类的方式创建线程:实现Runnable接口
        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();
    }
}


6.在Java中,使用同步机制解决线程安全问题—>Lock锁

Lock:是JKD5.0新增的一个接口,通过显示定义同步锁对象来实现同步,解决线程安全问题。常用的实现类ReentrantLock。
细节都在代码中

package cn.xuguowen.java2;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author xuguowen
 * @create 2021-02-20 10:44
 * @Description     Lock(锁):JDK5.0新增的接口,通过显示定义同步锁对象来实现同步
 *                  常用的实现类:ReentrantLock
 *
 *              面试题:synchronized 与 Lock的异同?
 *                  相同:都可以解决线程安全问题
 *                  不同:synchronized机制在执行完相应的同步代码之后,自动的释放同步监视器(锁)。
 *                       Lock需要手动的启动锁(lock()),同时也需要手动的释放锁(unlock()),
 *                       并且使用Lock锁,JVM将花费较少的时间来调用线程,性能更好(体现在到boolean参数的构造器中),
 *                       并且具有更高的扩展性。
 *              开发中如何使用?
 *                  Lock-->同步代码块-->同步方法
 */
public class LcokTest {

    public static void main(String[] args) {
        Window w = new Window();

        Thread t1 = new Thread(w,"窗口1");
        Thread t2 = new Thread(w,"窗口2");
        Thread t3 = new Thread(w,"窗口3");

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

}

class Window implements Runnable{

    private int ticket = 100;
    // 实例化锁对象
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            // 主动在多个线程开始访问共享资源时,为其上锁,调用lock()方法
            try {
                // 主动上锁,别忘记释放锁
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

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

        }
    }
}


五:线程间的通信

1.举例学习线程间的通信问题:使用两个线程打印输出1-100,线程1、线程2交替执行。
package cn.xuguowen.java3;

/**
 * @author xuguowen
 * @create 2021-02-21 10:54
 * @Description     线程通信案例
 *                         使用两个线程打印输出1-100,线程1、线程2交替打印
 *
 *                  涉及到的3 个方法
 *                      1.wait():一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器
 *                      2.wait(long timeout) 导致当前线程等待,直到另一个线程调用 notify()方法或该对象的 notifyAll()方法,或者指定的时间已过。
 *                      3.wait(long timeout, int nanos)  导致当前线程等待,直到另一个线程调用该对象的 notify()方法或 notifyAll()方法,或者某些其他线程中断当前线程,或一定量的实时时间。
 *                      4.notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的
 *                      5.notifyAll():一旦执行此方法,就会唤醒所有被wait的线程
 *                  说明:1.wait()、notify()、notifyAll()三个方法必须使用在同步代码块或者同步方法中
 *                       2.wait()、notify()、notifyAll()三个方法的调用者必须是同步代码块或者同步方法中的同步监视器,
 *                          否则:会出现异常非法监视器状态异常 IllegalMonitorStateException
 *                       3.wait()、notify()、notifyAll()三个方法定义在java.lang.Object类中。
 *                          首先同步监视器可以是任意对象,那意味着三个方法的调用者也可以是任意对象,任意对象都要
 *                              有这三个方法,所以定义在Object类中
 *
 *                       面试题:sleep()和wait()的异同?
 *                              相同:执行此方法,都可以使得当前线程进入阻塞状态。
 *                              不同:1.声明的位置不同:sleep()方法声明在Thread类中。wait()方法声明在Object类中
 *                                   2.调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中
 *                                   3.是否释放同步监视器:如果二者都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
 */
public class ThreadCommunicationTest {
    public static void main(String[] args) {
        Number number = new Number();

        Thread t1 = new Thread(number, "线程1");
        Thread t2 = new Thread(number, "线程2");

        t1.start();
        t2.start();
    }
}
class Number implements Runnable{

    private int number = 1;

    private Object obj = new Object();

    @Override
    public void run() {
        while (true){

            synchronized (obj) {    // IllegalMonitorStateException出现异常,调用方法的是当前对象this,需要一致
                // 因为上一个线程进入阻塞状态后并释放了锁,所以下一个线程可以拿到锁再次进来
                // 进来第一时间将上一个线程唤醒,否则会出现死锁。
                obj.notify();
                // 这样就实现了线程间的通信
                if (number < 101) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + number);
                    number++;

                    // wait()方法:让当前线程进入阻塞状态,并主动释放锁(同步监视器)
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                } else {
                    break;
                }
            }
        }
    }
}


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

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

  • 声明的位置不同:sleep()方法声明在Thread类中。wait()方法声明在Object类中
  • 调用要求不同:sleep()可以在任何需要的场景下调用。wait()必须使用在同步代码块或者同步方法中
  • 是否释放同步监视器:如果二者都使用在同步代码块或者同步方法中,sleep()不会释放同步监视器,wait()会释放同步监视器
3.经典例题:生产者/消费者问题
package cn.xuguowen.java3;

/**
 * @author xuguowen
 * @create 2021-02-21 12:51
 * @Description     线程通信应用:生产者/消费者问题
 *
 *      生产者将产品交给店员,而消费者从店员处取走产品。店员一次只能持有固定数量的产品(比如:20),
 *      如果生产者试图生产更多的产品,店员会叫生产者停一下。如果店中有空位放置产品了,再通知生产者继续生产;
 *      如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
 *
 *      分析:
 *          1.是否是多线程问题? 是 生产者线程,消费者线程
 *          2.是否有共享数据? 是 店员(或产品)
 *          3.如何解决线程的安全问题?同步机制
 *          4.是否涉及到线程的通信?是
 */
public class ProductorTest {

    public static void main(String[] args) {
        Clerk clerk = new Clerk();

        Productor p1 = new Productor(clerk);
        p1.setName("生产者1");
        p1.start();

        Customer c1 = new Customer(clerk);
        c1.setName("消费者1");
        c1.start();
    }

}
// 店员
class Clerk {

    private int products = 0;
    // 生产产品
    public synchronized void product() {

        if (products < 20) {
            products++;
            System.out.println(Thread.currentThread().getName() + ":开始生产第" + products + "个产品");
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    // 消费产品
    public synchronized void consume() {
        if (products > 0){
            System.out.println(Thread.currentThread().getName() + ":开始消费第" + products + "个产品");
            products--;
            notify();
        } else {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
// 生产者
class Productor extends Thread{

    private Clerk clerk;

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

    @Override
    public void run() {
        while (true) {
            try {
                sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName() + ":开始生产产品");
            clerk.product();
        }
    }
}

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

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

    @Override
    public void run() {
       while (true) {
           try {
               sleep(20);
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
           System.out.println(getName() + ":开始消费产品");
           clerk.consume();
       }
    }
}

六:线程的生命周期

1.在Thread类中,提供了枚举类State,给出了线程的生命周期。JDK中是通过方法调用划分线程的生命周期。

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,	

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
    }

2.这样理解线程的生命周期:新建—>就绪–(阻塞)->运行—>死亡。
如下图所示:
线程的生命周期

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值