美文网首页
Java高级-多线程

Java高级-多线程

作者: ttiga | 来源:发表于2021-04-25 07:48 被阅读0次
  • 同步和异步的区别
    同步: 多个线程在同步过程中,只有一个线程在工作,其他线程在等待,这个过程是单线程的(串行)
    异步: 多个线程同时在在进行,各干各的事(并行)

8.1.程序,进程,线程的概念

  • 程序 静态,相较于动态来说,静指没有加载到内存中,没有CPU没有参与运算,默默存储在存储空间中.
  • 进程: 把程序运行起来,这时需要加载到内存空间中,同时需要CPU分配计算资源开始做运算,可可理解为正在运行的一个程序.
  • 线程(thread):进程可以进一步细化为线程,是一个程序内部的一条执行路径.
    每个线程,拥有自己独立的: 栈,程序计数器
    多个线程,共享同一个进程的结构: 方法区,堆
image.png
  • 单核CPU和多核CPU的理解
    单核CPU,其实是一种假的多线程,因为CPU可以快速切换,看起来像并行执行
    多核CPU,肯定是多个线程同时执行
  • 并行和并发的理解
    并行: 多个CPU同时执行多个任务. 比如: 多个人同时做不同的事: 多个篮球场,每个场都在玩
    并发: 一个CPU(采用时间片)同时执行多个任务.比如: 秒杀,多个人做同一件事: 某一个场所有人都去抢一个篮球

8.2.线程的创建和使用

  • 创建多线程方式一: 继承Thread类
  • 创建线程过程中两个问题的说明

多线程的创建,方式一: 继承于Thread类
1.创建一个继承Thread类的子类
2.重写Thread类的run()方法 --> 将此线程要执行的操作声明在run()中
3.创建Thread类的子类的对象
4.通过此对象调用start()

public class ThreadTest {
    public static void main(String[] args) {
        // main方法里面是主线程做的事
        // 3.创建Thread类子类的对象
        MyThread t1 = new MyThread();
        // 4.通过上面对象调用thread类的start(): ①启动当前线程 ②启动到线程后自动地start方法会调用当前线程的run()
        // 调start之前包括调他本身,都是主(main)线程帮做的事
        // 此时有两个线程同时在执行,彼此具有交互性,
        t1.start(); // 要想启动线程,必须调start方法,不要去调run()
        // 问题一: 不能通过直接调用run()的方式启动线程
        // t1.run(); // 没有多分出来一个线程,就只是一个造了对象,调Thread类的run方法,只是体现对象调方法,根本没开启新的线程,这个方法执行完后才会走下面的逻辑,意味着,这个调的run方法里面的逻辑仍然是在主线程当中做的
        // 问题二: 再启动一个线程,遍历100以内的偶数,不能还让已经调start()的线程去执行,会报IllegalThreadStateException异常
        // t1.start(); // 报错,一个线程只能start一次
        // 要想创建多个线程就去造多个对象
        // 需要重新创建一个线程的对象
        MyThread t2 = new MyThread();
        t2.start();
        // 如下操作仍然是main线程中执行的
        for (int i = 0;i < 100;i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i + " main()");
            }
        }
    }
}
// 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(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
  • 继承方式的练习:

创建两个分线程,其中一个线程遍历100以内的偶数,另一个线程遍历线程100以内的奇数

public class ThreadDemo {
    public static void main(String[] args) {
        // 创建两个子类的对象
        MyThread1 m1 = new MyThread1();
        MyThread2 m2 = new MyThread2();
        // 两个对象分别调Thread类的start方法启动线程
        m1.start();
        m2.start();
        // 创建Thread类的匿名子类的的匿名对象方式(简便写法)
        // new Thread().start(); // 这样写不对,这里调start是调Thread自己类里的run了,要调的是新线程重写的run方法
        new Thread(){ // 重写了run方法后这里new的就是Thread类的匿名子类的对象,子类没名,就用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();
    }
}
// 两个线程做的事不一样,造两个Thread类的子类
// 遍历偶数的子类
class MyThread1 extends Thread{
    // 重写Thread类的run方法

    @Override
    public void run() {
        for (int i = 0; i < 100; i++){
            if (i % 2 == 0){
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
// 遍历奇数的子类
class MyThread2 extends Thread{
    // 重写Thread类的run方法

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 != 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
        }
    }
}
  • 线程的常用方法:

测试Thread类的常用方法:
1.start():启动当前线程; 调用当前线程的run()
2.run(): 通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread(): Thread类的静态方法,返回执行当前代码的线程的对象,方法定义就是返回一个当前实例
相当于线程的name属性的set,get方法
4.getName(): 获取当前线程的名字
5.setName(): 设置当前线程的名字
6.yield(): 释放当前CPU的使用权.当然有可能在下一刻有分配到当前线程
7.join(): 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,CPU想让他执行也执行不了,直到线程b完全执行完以后,线程a才结束阻塞状态,接下来就看CPU啥时候给你分配资源了,分配到资源就接着往后执行
8.stop(): 已过时,当执行此方法时,强制结束当前线程.该线程就进入生命周期末尾了,直接就消亡了
9.sleep(long millitime): 让当前线程"睡眠"指定的millitime毫秒,睡眠完,等着CPU给分配资源,分配到就可以执行.在指定的millitime毫秒时间内,当前线程是阻塞状态
10.isAlive(): 判断当前线程是否存活
线程通信: wait()/ notify()/notifyAlll(): 此三个方法定义在Object类中的

  • 补充 : 线程的分类
    一种是守护线程,例如gc()垃圾回收线程
    一种是用户线程,例如: main()主线程,用户线程结束,守护线程也结束
image.png
  • 线程的调度
image.png
  • 线程优先级设置

线程的优先级:

  1. 分为10档
    MAX_PRIORITY: 10
    MIN_PRIORITY: 1
    NORM_PRIORITY: 5 --> 默认线程优先级
  2. 如何获取和设置当前线程的优先级:
    getPriority(): 获取线程的优先级
    setPriority(int p): 设置线程的优先级
    说明: 高优先级的线程要抢占低优先级线程CPU的执行权.但只是从概率上讲,高优先级的线程会高概率的情况下被执行.
    并不意味着只有当高优先级的线程执行完后,才执行低优先级的线程
public class ThreadMethodTest {
    public static void main(String[] args) {
        // 默认提供的线程名为Thread-0,因为这里调的是当前子类的空参构造器,该构造器就会调父类的super(),根据Thread源码,Thread类的空参构造器初始线程名为Thread-0,依次递增
        // 给线程命名方式二: 通过构造器
        HelloThread h1 = new HelloThread("Thread:1");
        // 自定义线程名方式一:
        // h1.setName("线程一"); // 必须在start()前执行,如果在线程启动后就晚了
        // 设置分线程的优先级,同样在线程启动前设置
        h1.setPriority(Thread.MAX_PRIORITY);
        h1.start();
        // 给主线程命名: 在执行getName()前执行才有效
        Thread.currentThread().setName("主线程");
        // 设置主线程优先级
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                // 输出当前线程的线程名,线程优先级
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
            /*if (i == 20) {
                // 要线程去调join(),因为join是Thread类里的方法,而在main方法所在是public类,这个类没有join方法,所以直接调报错
                // 在线程a中调用线程b的join(),此时线程a就进入阻塞状态,直到线程b完全执行完以后,线程a才结束阻塞状态
                try {
                    h1.join(); // 到i==20当分线程执行完后才开始主线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }*/
        }
        // System.out.println(h1.isAlive()); // 判断h1线程是否存活,线程还没执行完就是还活着
    }
}
class HelloThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                // sleep();抛的异常只能用try-catch解决不能用throws,因为该类的run方法是重写Thread类的run方法,而Thread类的run方法没有抛异常
                // 父类的方法没有throws过,子类重写的方法就一定不能throws; 子类抛的异常≤父类的异常,所以sleep()只能用try-catch
                // sleep的意思是: 一旦线程执行到sleep方法的时候,他就也阻塞了一秒,一秒结束了也不是马上输出后面的信息,还要等CPU分配给出资源,得到资源了才能继续往后走
                // 一秒钟内,即使CPU想分配资源也不能往下走,因为此时线程处于阻塞状态
                /*try {
                    sleep(10); // 让当前线程强制阻塞,
                }catch(InterruptedException e){
                    e.printStackTrace();
                }*/
                // 输出当前线程的线程名,线程优先级
                System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":" + i);
            }
            /*if (i % 20 == 0) {
                // 当前线程就是当前类的对象
                // yield(); 释放CPU执行权,但也有可能还是原来的线程抢夺回CPU的使用权
                this.yield(); // this就是当前类的对象,相当于h1,也就是当前线程Thread.currentThread(),this省略掉也可以呀
            }*/
        }
    }
    // 在子类中通过构造器给线程命名
    public HelloThread(String name) {
       super(name); // 调用Thread类的带参构造器
    }
}
  • 例题: 继承Thread方式: 多窗口卖票

例子: 创建三个窗口卖票,总票数为100张
存在线程的安全问题,待解决

public class WindowTest {
    public static void main(String[] args) {
        // 创建三个线程对象
        Window t1 = new Window();
        Window t2 = new Window();
        Window t3 = new Window();
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
class Window extends Thread {
    private static int ticket = 100; // 静态属性,造几个线程总共就只有一百张票,每个对象共享同一个静态变量
    public Window(){

    }
    // 重写Thread类的run方法
    @Override
    public void run() {
        while (true){
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName() + " 卖票,票号为: " + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
  • 创建多线程方式二: 实现Runnable接口
  1. 创建一个实现了Runnable接口的类
  2. 实现类去实现Runnable中的抽象方法: run()
  3. 创建实现类的对象
  4. 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
  5. 通过Thread类的对象调用start()
// 1.创建一个实现了Runnable接口的类
class MThread implements Runnable{
    //2.实现类去实现Runnable中的抽象方法: run()
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(Thread.currentThread().getName() + ":" + i); // 这里不能直接用getName(),因为MThread类没有继承Thread而是继承了Object类,实现的接口也没有getName方法
            }
        }
    }
}
public class ThreadTest1 {
    public static void main(String[] args) {
        // 3.创建实现类的对象
        MThread mthread = new MThread();
        // 4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
        Thread t1 = new Thread(mthread);// 多态形式: 实参: Runnable target = new mThread();
        // 5.通过Thread类对象调用start(),①启动线程;②调用当前线程的run() --> 调用了Runnable类型的target的run(),target被mthread赋值,所以:
        // 根据Thread类的源码,因为Thread类的其中一个构造器有Runnable target形参,而此形参是Thread中定义的实现的Runnable接口为类型的变量
        // 根据Thread类重写的run()源码得知,如果形参target有被赋值,则调用该形参的run方法,没赋值则调用继承Thread子类重写的run(),所以就调mthread的run()
        t1.setName("线程一");
        t1.start();// 这时线程是t1,谁start线程就谁
        // 再启动一个线程,遍历100以内偶数,共用同一个接口实现类就行
        Thread t2 = new Thread(mthread); // 匿名类
        t2.setName("线程二");
        t2.start(); // 最后也会回归到所在接口实现类的run方法的调用
    }
}
  • 例题: 实现Runnable方式: 多窗口卖票
class Window1 implements Runnable{
    // 因为只创建了一个Window1类的对象,所以ticktet是同一个,所以不用static
     private int ticket = 100;
    // 重写run()
    @Override
    public void run() {
        while (true){
            if (ticket > 0){
                System.out.println(Thread.currentThread().getName() + ":" + ticket);
                ticket--;
            }else{
                break;
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        // 三个线程构造器共用同一个new Window1(),一个窗口三个线程
        Window1 w = new Window1();
        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();
    }
}
  • 两种创建方式的对比:

比较创建线程的两种方式:
开发中: 优先选择: 实现Runnable接口的方式
原因:
1.实现的方式没有类的单继承性的局限性
2.实现的方式更适合来处理多个线程有共享数据的情况.可以把多个线程共享的数据封装在实现Runnable接口的类中,然后这个类的对象就可以作为参数传递到线程(Thread类)的构造器中,天然就是共享数据
联系: public class Thread implements Runnable
相同点: 两种方式都需要重写run(),将线程要执行的逻辑声明在run()中.Thread也是实现Runnable中的run()

8.3.线程的生命周期

image.png

8.4.线程同步

  • 理解线程的安全问题
  • 安全问题的举例和解决措施

例子: 创建三个窗口卖票,总票为100张,用实现Runnable接口的方式
1.问题: 卖票过程中,出现了重票,错票 --> 出现了线程的安全问题
2.问题出现的原因: 当某个线程操作车票的过程中,尚未操作完成出去时,其他线程也参与进来,也操作车票(相当于共享数据).
例如: 去厕所时候,坑位有限,坑位相当于共享数据,每个人都是一个线程,正常来讲,进去了完事后出去,别人再进来,这就是安全的,安全问题: 进去了还没出来,另一个人也进来了,就出现线程安全问题了
3.如何解决: 当一个线程a在操作ticket的时候,其他线程不能参与进来,直到线程a操作完ticket时,其他线程才可以开始操作ticket.
这种情况即使线程a出现了阻塞,也不能被改变
4.在Java中,通过同步机制,来解决线程的安全问题
方式一: 同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明: 1.操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了,最低使用原则,否则无谓增加线程开销
包少了: 线程在执行剩余的同步代码可能会阻塞,这是没有锁,其他线程就会进来,会混乱
包多了: 假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
2.共享数据: 多个线程共同操作的变量. 比如: ticket就是共享数据
3.同步监视器,俗称: 锁.任何一个类的对象,都可以充当锁(本类对象会栈溢出) 进厕所的时候,这个时候就放一把锁,谁进去就拿着这把锁,没进去的人就拿不到这把锁,谁能拿到锁谁就操作这段代码
同步监视器要求: 多个线程必须要共用同一把锁
补充: 再实现Runnable接口创建多线程的方式中,我们可以考虑用this(还要看是否同一个对象)充当同步监视器
方式二: 同步方法
如果操作共享数据的代码完整的声明在一个方法中,可以将此方法声明为同步的.
5.同步的方式:
好处: 解决了线程的安全问题
局限性:操作同步代码,只能有一个线程参与,其他线程等待.相当于是一个单线程的过程(外面还是并行),效率低

class Window1 implements Runnable{
    // 因为只创建了一个Window1类的对象,所以ticktet是同一个,所以不用static
     private int ticket = 100; // 共享数据
    // 创建随便一个类的对象
    //Object obj = new Object();//保证锁的唯一性,要声明在这里,如果声明在run方法里,几个线程就会造几个锁,或者在同步监视器里new对象也不行
    Dog dog = new Dog();
    // 重写run()
    @Override
    public void run() { // 多个线程来操作的方法都在run里
        while (true){
            synchronized (dog) { // 必须共用同一个锁
                // 此时能保证包住的这段代码,一个进程进来,即使线程sleep阻塞了,别的线程也得在外等着,直到在执行的线程醒来操作完出去了,其他线程包括本身(抢完还能再抢),看谁又进去了
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        // 三个线程构造器共用同一个new Window1(),一个窗口三个线程
        Window1 w = new Window1();
        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();
    }
}
// 造一个锁类也可以
class Dog{

}
  • 同步代码块处理实现Runnable的线程安全问题

4.在Java中,通过同步机制,来解决线程的安全问题
方式一: 同步代码块
synchronized(同步监视器){
// 需要被同步的代码
}
说明: 1.操作共享数据的代码,即为需要被同步的代码 --> 不能包含代码多了,也不能包含代码少了,最低使用原则,否则无谓增加线程开销
包少了: 线程在执行剩余的同步代码可能会阻塞,这是没有锁,其他线程就会进来,会混乱
包多了: 假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
2.共享数据: 多个线程共同操作的变量. 比如: ticket就是共享数据
3.同步监视器,俗称: 锁.任何一个类的对象,都可以充当锁(本类对象会栈溢出) 进厕所的时候,这个时候就放一把锁,谁进去就拿着这把锁,没进去的人就拿不到这把锁,谁能拿到锁谁就操作这段代码
同步监视器要求: 多个线程必须要共用同一把锁
补充: 再实现Runnable接口创建多线程的方式中,我们可以考虑用this(还要看是否同一个对象)充当同步监视器

class Window1 implements Runnable{
    // 因为只创建了一个Window1类的对象,所以ticket是同一个,所以不用static
     private int ticket = 100; // 是天然的共享数据
    // 创建随便一个类的对象
    //Object obj = new Object();//保证锁的唯一性,要声明在这里,如果声明在run方法里,几个线程就会造几个锁,或者在同步监视器里new对象也不行
    // Dog dog = new Dog(); // 天然也是共享的锁,因为就只造了一个Window1对象
    // 重写run()
    @Override
    public void run() { // 多个线程来操作的方法都在run里
        while (true){ // 循环不能包在同步代码块中,假如把while包进去了,他会执行完整个循环才会释放锁,那时候就已经没票了,相当于一个线程就把票卖完了
            // 用this(当前对象)做同步锁,需要只new一个Window1对象,所以只能用于implements不能用于继承
            // this就是w变量,后面可以看到是造的w对象,这里相当于是动态获取的,就是调这个方法的对象就是this,这个方法是在Window1中定义的,Window1的对象就是this.自始至终就只造了一个Window1对象,所以这个this的w就是唯一的
            synchronized (this){ //此时的this: 唯一的Window1的对象,用当前对象充当 // 方式二: synchronized (dog) { // 必须共用同一个锁
                // 此时能保证包住的这段代码,一个进程进来,即使线程sleep阻塞了,别的线程也得在外等着,直到在执行的线程醒来操作完出去了,其他线程包括本身(抢完还能再抢),看谁又进去了
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":" + ticket);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket--;
                } else {
                    break;
                }
            }
        }
    }
}
public class WindowTest1 {
    public static void main(String[] args) {
        // 三个线程构造器共用同一个new Window1(),一个窗口三个线程
        Window1 w = new Window1();
        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();
    }
}
// 造一个锁类也可以
class Dog{

}
  • 同步代码块处理继承Thread类的线程安全问题

用同步代码块解决继承Thread类的方式的线程安全问题
例子: 创建三个窗口卖票,用继承Thread类的方式
说明: 在继承Thread类创建多线程的方式中,慎用this充当同步监视器,考虑用当前类(类名.class)充当同步监视器,一定保证对象的唯一性
反射就是当类加载进内存后,动态的获取类中的信息,以及用对象调用方法

class Window2 extends Thread{
    // 有共享数据
    private static int ticket = 100;
    // private Object obj = new Object(); // 错误,这时候的锁不唯一,下面new了三个Window2的对象,每个对象都有一个实例变量,每个线程有一个obj,锁就不是共享的
    // private static Object obj = new Object(); // 这样写三个Window2的对象才能共享一个obj
    // 重写Thread类中的run方法
    @Override
    public void run() {
        while (true) {
            // 这是后的this是当前Window2类的对象,当前new了三个对象,不唯一,所以这里不能用this
            // synchronized (this) { // 错误方式: this代表t1,t2,t3三个对象
            // synchronized (obj) { // 正确的
            // 拿当前类充当对象,类也是对象: Class c = Window2.class,类类型 变量 = 变量值(相当于类类型的对象)
            // Window2类只加载一次,所以Window2.class对象唯一
            synchronized (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) {
        Window4 w1 = new Window4();
        Window4 w2 = new Window4();
        Window4 w3 = new Window4();
        w1.setName("窗口一");
        w2.setName("窗口二");
        w3.setName("窗口三");
        w1.start();
        w2.start();
        w3.start();
    }
}
同步方法处理线程安全问题

1.同步方法仍然涉及到同步监视器,只是不需要显式的声明,相当于用默认的
2.非静态的同步(synchronized)方法,同步监视器是: this
静态的同步(synchronized)方法,同步监视器是: 当前类本身

  • 同步方法处理实现Runnable的线程安全问题
/**
 * 用同步方法解决实现Runnable接口的线程安全问题
 */
class Window3 implements Runnable{
    private int ticket = 100;
    boolean isFlag = true;
    // 重写Runnable接口的run方法
    @Override
    public void run() {
        while (isFlag){
            show();
        }
    }
    // 同步方法: 在声明方法时加synchronized关键字就行
    // 同步方法可以保证在方法内部这些代码和同步代码块包起来一样,方法外面有多个线程,方法里只有一个线程,所以是安全的
    // 有默认同步锁: this,因为this是唯一的w对象
    public synchronized void show(){ // 同步监视器: this(调用show方法的当前对象)
        if (ticket > 0){
            try {
                Thread.sleep(10);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖票,票号为: " + ticket);
            ticket--;
        }else{
            isFlag = false;
        }
    }
}
public class WindowTest3 {
    public static void main(String[] args) {
        Window3 w = new Window3();
        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();
    }
}
  • 同步方法处理继承Thread类的线程安全问题
class Window4 extends Thread {
    // 有共享数据
    private static int ticket = 100;
    // 重写Thread类中的run方法
    @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--;
        }
    }
}

public class WindowTest4 {
    public static void main(String[] args) {
        Window4 t1 = new Window4();
        Window4 t2 = new Window4();
        Window4 t3 = new Window4();
        t1.setName("窗口一");
        t2.setName("窗口二");
        t3.setName("窗口三");
        t1.start();
        t2.start();
        t3.start();
    }
}
  • 线程安全的单例模式之懒汉式

用同步机制将单例模式中的懒汉式改写为线程安全的
双重检查单例模式,属性还要记得加一个volatile关键字

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

    }
}
// 懒汉式
class Bank extends Thread{
    private Bank(){

    }
    private static Bank instance = null;
    // 有可能线程进入getInstance方法后进入阻塞状态,阻塞后又回到就绪状态,在判断变量后还没赋值的时候,其他线程也进来了
    // 简单处理,直接在方法层面上加synchronized,此时就已经是线程安全了,当多个线程调用该方法时候,同步锁是Bank.class(当前类本身),类本身也充当一个对象,锁一定是对象
    // public static synchronized Bank getInstance(){
    public static Bank getInstance(){
        // 方式一: 效率稍差
        //里面代码都算是对共享数据的操作
        /*synchronized (Bank.class){
            if (instance == null){ // 判断变量
                instance = new Bank();// 给变量赋值
            }
            return instance;
        }*/
        // 方式二: 效率更高 (面试建议写方式二)
        // 假设第一批可能有好几个线程一起过来,getInstance方法都能进来,第一个if判断也都能过,都同时排在锁前看谁能抢到,
        // 假设线程一抢到了,并且new好对象出去了,后面几个线程要稍微等一下才能进,因为进去发现instance对象不是null了,就直接拿着现成的instance出去了,后面几个似乎是多等了一下
        // 但是再后面来的线程,再进来方法的时候,判断第一个if,instance就不是null了,后面来的线程就没必要等着再进入同步代码块了,就直接拿着造好的对象出去,所以比方式一效率稍微高些
        if (instance == null){
            synchronized (Bank.class){
                // 里面两行代码是在操作共享数据
                if (instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance; // 不看做是操作共享数据
    }
}
  • 线程的死锁问题

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

public class ThreadTest2 {
    public static void main(String[] args) {
        // 造两个常用类对象: 特殊字符串
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();
        // 多线程是同时执行的,谁先执行看CPU,所以才会打架
        //匿名方式创建线程,只能用一次
        new Thread(){
            @Override
            public void run() {
                // 嵌套锁
                // 手握s1锁
                synchronized (s1){
                    s1.append("a");
                    s2.append(1);
                    // 死锁出现概率增加
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 再握s2锁
                    synchronized (s2){
                        s1.append("b");
                        s2.append(2);
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();
        // 提供实现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();
    }
}
  • Lock锁方式解决线程安全问题

解决线程安全问题的方式三: Lock锁 --> jdk5.0新增
相同: 二者都可以解决线程安全问题
不同: synchronized机制在执行完相应的同步代码后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的执行(unlock()),操作更灵活
2.优先使用顺序:
lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外),没有同步代码块灵活
如何解决线程安全问题? 有几种方式

  • lock解决实现Runnable接口的线程安全问题
class Window5 implements Runnable{
    // Lock是个接口,具体使用它的实现类ReentrantLock
    // 在Runnable实现类中造一个ReentrantLock对象
    // 公平锁: 当构造器参数为空时,则fair为false,当参数为true时,为公平的,排队的线程先来先服务
    // 1.实例化ReentrantLock
    private ReentrantLock lock = new ReentrantLock();
    private int ticket = 100;
    // 重写Runnable接口的run方法
    @Override
    public void run() {
        while (true){
            // try  finally是要保证lock后必须执行unlock
            try {
                // 2.调用锁定方法:lock(), 类似于线程获取了同步监视器,从lock()开始下面的代码被锁住了,保证这个过程中,他是单线程的,类似于同步代码块
                // 当这些代码执行完的时候或是出现异常,也一定会执行finally里的代码
                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) {
        Window5 w = new Window5();
        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解决继承Thread类的线程安全问题
class Window6 extends Thread{
    private static int ticket = 100; // 将属性静态化,多个窗口共卖一百张票
    static ReentrantLock lock = new ReentrantLock(); // 将锁静态化,多个线程共用同一把锁
    @Override
    public void run() {
        while (true){
            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();
            }
        }
    }
}
public class LockTest2 {
    public static void main(String[] args) {
        Window6 t1 = new Window6();
        Window6 t2 = new Window6();
        Window6 t3 = new Window6();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
  • lock处理实现Runnable接口的线程安全问题
class Window6 extends Thread{
    private static int ticket = 100; // 多个窗口共卖一百张票
    static ReentrantLock lock = new ReentrantLock(); // 将锁静态化,多个线程共用同一把锁
    @Override
    public void run() {
        while (true){
            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();
            }
        }
    }
}
public class LockTest2 {
    public static void main(String[] args) {
        Window6 t1 = new Window6();
        Window6 t2 = new Window6();
        Window6 t3 = new Window6();
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}
  • 同步机制的练习

银行有一个账户
有两个储户分别向同一个账户存3000元,每次存1000,存3次.每次存完打印张华余额.
分析:
1.是否是多线程问题? 是,有两个储户线程
2.是否线程安全取决于有无共享数据,有共享数据: 账户(余额)
3.有线程安全问题
4.如何解决线程安全问题? 同步机制: 有三种方式

// 造一个class,专门作为账户
class Account{
    private double balance;//余额
    // 构造器初始化余额
    public Account(double balance){
        this.balance = balance;
    }
    // 造存钱方法
    // 存在的安全问题: 当一个线程先执行存钱后,阻塞了没来得及输出,另一个线程也进来了也执行存钱,第一个线程醒了,就会输出两个线程存钱的余额
    // 同步监视器能用this的原因: 此时的this不是多个Customer对象,而是共用的唯一一个Account对象,两个Customer又共用同一个Account
    public synchronized void deposit(double amt){
        if (amt > 0){
            try {
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            balance += amt; // 余额增加,这里也相当于操纵共享数据
            System.out.println(Thread.currentThread().getName() + "存钱成功,余额为: " + balance);
        }
    }
}
// 造一个线程客户类继承于Thread类
class Customer extends Thread{
    // 将账户作为客户的属性,体现账户共享
    private Account acct;
    // 用构造器初始化属性
    public Customer(Account acct){
        this.acct = acct;//对象作为属性
    }
    // 重写Thread类的run方法
    @Override
    public void run() {
        // 存三次钱
        for (int i = 0; i < 3; i++) {
            // 让账户加钱
            acct.deposit(1000); // 对象调方法
        }
    }
}
public class AccountTest {
    public static void main(String[] args) {
        // 创建一个账户对象
        Account acct = new Account(0);// 假设初值为0
        // 此时两个客户就共用一个账户了
        Customer c1 = new Customer(acct);
        Customer c2 = new Customer(acct);
        // 改名
        c1.setName("甲");
        c2.setName("乙");
        // 启动线程,启动后就要调用重写Thread类的run方法
        c1.start();
        c2.start();
    }
}

8.5.线程的通信

线程通信的例子: 用两个线程打印1~100.线程一,线程二 交替打印
涉及到的三个方法:

  • wait(): 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器.意味着其他线程可以拿到同步监视器,进入同步代码块
  • notify(): 一旦执行此方法,就会唤醒被wait的一个线程.如果有多个线程被wait,就唤醒优先级高的那个.总之只能唤醒一个
  • notifyAll(): 一旦执行此方法,就会唤醒所有被wait的线程

说明:
1.wait(),notify(),notifyAll()三个方法必须使用在同步代码块或同步方法中
2.wait(),notify(),notifyAll()三个方法的调用者必须是同步代码块或同步方法中的同步监视器.所以他们才只能在上面两种情况内
否则会出现IllegalMonitorStateException异常
lock方法需要Condition来实现线程通信
3.wait(),notify(),notifyAll()三个方法是定义在java.lang.Ojbect类中.
同步监视器可以用任何对象充当,而这三种方法又必须要拿该对象调用,那得保证任何一个对象都得有这些方法,所以这些方法就在Ojbect类中定义

// 三个方法的调用必须在同步方法或同步代码块当中,包含在lock内也不行
class Num implements Runnable {
    private int num = 1;//共享数据
    // 重写run方法
    @Override
    public void run() {
        while (true) {
            // synchronized同步代码块
            synchronized (this) { // this(同步监视器)为当前Num类的对象,唯一
                // 调用该方法的线程可以唤醒另一个被阻塞的线程,如果有多个线程被wait,就唤醒优先级高(大概率被CPU分配到资源)的那个.
                this.notify();// 省略了this
                if (num <= 100) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + num);
                    num++;
                    // 一个线程打印完后应该阻塞一下,另一个线程才能进来
                    // 调用如下wait方法后释放资源,进入等待池,会释放同步锁
                    try {
                        this.wait(); // 省略了this
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    break;
                }
            }
        }
    }
}

public class ConnectionTest {
    public static void main(String[] args) {
        Num n = new Num();
        Thread t1 = new Thread(n);
        Thread t2 = new Thread(n);
        t1.setName("线程1");
        t2.setName("线程2");
        t1.start();
        t2.start();
    }
}
  • 面试题: sleep()和wait()的异同?

1.相同点: 一旦执行方法,都可以是的当前的线程进入阻塞状态,都需要处理异常
2.不同点:
①两个方法声明的位置不同:
sleep()声明在Thread类中
wait()声明在Ojbect类中
②调用的要求不同: sleep()可以在任何需要的场景下调用. wait()必须使用在同步代码块或同步方法中
③关于是否释放同步监视器: 如果两个方法都用在同步代码块或同步方法中,sleep()不会释放锁(同步监视器),wait()会释放锁
④sleep()会自动唤醒,wait()不会自动唤醒,需要notify()来唤醒

  • 线程通信的应用: 生产者/消费者问题

分析:
1.是否为多线程问题? 是,生产者线程,消费者线程
2.是否有共享数据: 店员(或产品)
3.如何解决线程的安全问题? 同步机制,有三种方法
4.涉及到线程通信

// 店员类: 把店员改为为产品更好理解
class Clerk{
    // 可以理解为店员记账
    // 产品属性
    private int productCount = 0;
    // 获取产品
    public synchronized void produceProduct(){
        if (productCount < 20){
            productCount++;
            System.out.println(Thread.currentThread().getName() + " 开始生产第: " + productCount + "个产品");
            // 生产者生产出一件商品就可以唤醒消费者
            notify();
        }else{
            try {
                wait(); // 可省略this
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
    // 销售产品
    public synchronized void comsumeProduct(){
        if (productCount > 0){
            System.out.println(Thread.currentThread().getName() + ": 消费第" + productCount + "个产品");
            productCount--;
            // 只要消费了一个商品,就可以把生产者唤醒
            notify();
        }else{
            try {
                wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}
// 生产者类
class Producer extends Thread{
    private Clerk clerk;//店员对象作为共享属性
    // 构造器,共享店员
    public Producer(Clerk clerk){
        this.clerk = clerk;
    }
    // 重写run方法

    @Override
    public void run() {
        System.out.println(getName() + " : 开始生产产品...");
        while (true){
            try {
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            // 通过店员调用生产方法
            clerk.produceProduct();
        }
    }
}
// 消费者类
class Comsumer extends Thread{
    private Clerk clerk;//店员对象作为共享属性
    // 构造器,共享店员
    public Comsumer(Clerk clerk){
        this.clerk = clerk;
    }
    // 重写run方法

    @Override
    public void run() {
        System.out.println(getName() + ": 开始消费产品...");
        while (true){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 店员调用消费方法
            clerk.comsumeProduct();
        }
    }
}
public class ProductTest {
    public static void main(String[] args) {
        Clerk clerk = new Clerk();
        Producer p1 = new Producer(clerk);
        Comsumer c1 = new Comsumer(clerk);
        p1.setName("生产者1");
        c1.setName("消费者1");
        p1.start();
        c1.start();
    }
}

8.6.创建多线程的方式三: 实现Callable接口

创建线程的方式三: 实现Callable接口. -- jdk5.0新增
如何理解实现Callable接口的方式创建多线程比实现Runnable接口创建多线程方式强大?
1.call()可以有返回值的
2.call()可以抛出异常,被外面的操作捕获,获取异常的信息
3.Callable是支持泛型的

// 创建一个实现Callable接口的实现类
class NumThread implements Callable{
    // 实现(重写)Callable接口的call(回调)方法,将此线程要执行的操作声明在call()中,同时该方法可以有返回值,如果不要返回可以return null
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if (i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;// 相当于把int转换为Integer:自动装箱,然后Integer作为子类赋给Object,体现多态性
    }
}
public class ThreadNew {
    public static void main(String[] args) {
        // 3.创建Callable接口实现类的对象
        NumThread numThread = new NumThread();
        // 实现了Callable接口但没有Thread类作为父类,所以不能直接调用start方法
        // 要用线程要借用FutureTask类
        // 4.将此Callable接口实现类的对象作为参数传递到FutureTask构造器中,创建FutureTask的对象
        FutureTask futureTask = new FutureTask(numThread);// 其中有一个构造器要传一个实现Callable接口的实现类的对象
        // 启动线程一定会newThread对象并且调用start方法
        // futureTask实现的接口继承了Runnable接口,相当于也实现了Runnable接口,所以这里体现多态性: Runnable target = new futureTask();
        // start后线程才会进入就绪状态
        // 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
         new Thread(futureTask).start(); // Thread构造器实参可以放futureTask不会报错,因为futureTask同时实现了Runnable和Future接口
        // 调这个对象的get方法,返回一个值
        try {
            // 6.获取Callable中call方法的返回值
            // get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值
            Object sum = futureTask.get();//调方法返回值用一个变量接收,该变量的值就是call方法return的值
            System.out.println("总和为: " + sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 新增方式二: 使用线程池
image.png image.png

创建线程的方式四: 使用线程池
好处:
1.提高响应速度(减少了创建新线程的时间)
2.降低资源消耗(重复利用线程池中线程,不需要每次都创建)
3.便于线程管理: 可以设置线程池的属性,限制线程池的创建,包括对线程池的维护
corePoolSize:核心池的大小
maximumPoolSize: 最大线程数
keepAliveTime: 线程没有任务时最多保持多长时间后会终止
面试题: 创建多线程有几种方式: 有四种

// 创建一个线程类
class NumberThread implements Runnable{
    // 重写run方法,遍历1~100的偶数
    @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.提供指定线程数的线程池
        // Executors:工具类,线程池的工厂类,用于创建并返回不同类型的线程池
        // 调用工具类中的静态方法,可重用固定线程数的线程池
        // 返回值是线程池ExecutorService接口类型的实现(子)类(ThreadPoolExecutor)的对象,体现多态性,这里不是new对象
         ExecutorService service = Executors.newFixedThreadPool(10); // 造了一个线程池线程数为10
        // 设置线程池的属性
        System.out.println(service.getClass());// 获取该对象是哪个类造的
        // service是父类,service1是子类,父类要用子类中特有的方法,必须向下强转
         ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
         service1.setCorePoolSize(15);// 类中的属性可以是变量,所以可以设置,接口中的属性只能是常量,不能设置
        // service1.setKeepAliveTime();
        // 调用执行方法,传入要执行的Runnable接口的实现类的对象,自然就知道线程的run方法要做的事
        // 提供方法参数的目的主要是知道该线程要做什么
        // 2.执行指定的线程的操作,需要提供实现Runnable接口或Callable接口实现类的对象
        service.execute(new NumberThread());// 没有返回值,适用与Runnable
        // 创建新的线程
        service.execute(new NumberThread1());
        // service.submit(Callable callable);// 有返回值,适用于Callable,submit方法返回一个Callable类型的值,可以用FutureTask类型的对象接收,再调get()查看返回值
        // 3.不用线程池可以关闭连接池
        service.shutdown();
    }
}

相关文章

  • 使用Netty,我们到底在开发些什么?

    您可能感兴趣的文章: 大数据成神之路系列:Java高级特性增强-集合Java高级特性增强-多线程Java高级特性增...

  • 10.3多线程详解

    Java高级-多线程 多线程创建 多线程通讯 线程池 1.多线程创建 thread/runnable图:继承Thr...

  • 零基础学Java10系列三:Java高级编程​​​​​​​

    更多关于Java高级编程内容链接:零基础学Java10系列三:Java高级编程——阿里云大学 多线程:生产者和消费...

  • 网络编程 - 收藏集 - 掘金

    15 个高级 Java 多线程面试题及回答 - 后端 - 掘金在任何Java面试当中多线程和并发方面的问题都是必不...

  • Tools - 收藏集 - 掘金

    15 个高级 Java 多线程面试题及回答 - 后端 - 掘金在任何Java面试当中多线程和并发方面的问题都是必不...

  • java 多线程

    大多数高级Java开发职位都要求开发者精通多线程技术并且有丰富的Java程序开发、调试、优化经验,多线程并发编程是...

  • 程序猿学习书籍推荐

    深入理解Java虚拟机:JVM高级特性与最佳实践 java多线程编程核心技术 Effective Java中文版 ...

  • 招聘贴--京东物流平台数据部

    招聘岗位:Java Web高级工程师: 3年以上JAVA开发经验、JAVA基础扎实,熟悉多线程、IO、JVM等。 ...

  • Java高级-多线程

    同步和异步的区别同步: 多个线程在同步过程中,只有一个线程在工作,其他线程在等待,这个过程是单线程的(串行)异步...

  • Java多线程如何实现

    Java多线程是Java高级特性之一,通过多线程,我们可以实现多任务同时协同工作,在一定情况下提升程序效率,但是J...

网友评论

      本文标题:Java高级-多线程

      本文链接:https://www.haomeiwen.com/subject/kjhnlltx.html