Java多线程

一、程序、进程、线程

程序

    为了解决某个问题、实现某个功能用某种编程语言编写的代码文件,是静态的;(例如在本地未运行的各种应用程序QQ、微信等);

进程

    当程序被运行,程序由静态变为动态,就会产生相应的进程,是动态的,有生命周期;(例如打开QQ程序,就会有QQ的进程);

线程

    是程序的一次执行过程,每一个线程便是做一件任务的一条通道。一个Java程序至少有三个线程,即主线程、垃圾回收线程、异常处理线程;在未使用多线程时,我们写的所有代码都在主线程中执行。

二、单线程与多线程区别

    单线程就是同一时间只能做一件事,而多线程则是同时可以做多件事;例如你在吃饭时来了电话,如果饭局很重要,你只能先吃了饭在打电话,那就是单线程,只能做一件事;如果饭局很随意,你可以一边吃饭一边打电话,同时完成了两件事,就是多线程。

三、何时使用多线程

    1、程序需要同时执行多个任务时;
    2、程序中有需要等待的任务,例如用户输入、文件读写、网络操作等;
    3、需要后台运行;

四、线程的分类

    Java线程分为两类:守护线程和用户线程;其实两类线程的唯一区别就是判断JVM何时退出,其他没有区别,其中守护线程是用来服务用户线程的,可以通过在start()方法之前调用thread.setDaemon(true)将用户线程设置为守护线程垃圾回收线程就是守护线程,若JVM中全是守护线程,则当前JVM退出。

五、线程的创建和启用

Thread类的概述

Thraed特性

    1、每个线程都是通过某个Thread()对象的run()方法完成操作,经常将run()方法的方法体称为线程体。
    2、通过调用start()方法启用线程,而不是调用run()方法。

Thraed构造器

    Thread():创建新的Thread对象
    Thread(String threadname): 创建线程并指定线程实例名
    Thread(Runnable target): 指定创建线程的目标对象,它实现了Runnable接口中的run方法
    Thread(Runnable target, String name): 创建新的Thread对象

Thraed常用方法

    void start(): 启动线程,并执行对象的run()方法
    run(): 线程在被调度时执行的操作
    String getName(): 返回线程的名称
    void setName(String name):设置该线程名称
    static Thread currentThread(): 返回当前线程。在Thread子类中就是this,通常用于主线程和Runnable实现类
    static void yield(): 线程让步
    暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程
    若队列中没有同优先级的线程,忽略此方法
    join() : 当某个程序执行流中调用其他线程的 join() 方法时, 调用线程将被阻塞,直到 join() 方法加入的 join 线程执行完为止低优先级的线程也可以获得执行
    static void sleep(long millis): (指定时间:毫秒)令当前活动线程在指定时间段内放弃对CPU控制,使其他线程有机会被执行,时间到后重排队。
    stop(): 强制线程生命期结束,不推荐使用
    boolean isAlive(): 返回boolean,判断线程是否还活着

方式一:继承Thread

步骤

    1、定义子类继承Thread;
    2、重写run()方法;
    3、创建子类对象;
    4、调用start()方法启用线程;

代码

    Thread1类
    @Override
    public void run() {
        System.out.println("新线程");
    }
}
    Test类
    public static void main(String[] args) {
//        1、定义子类继承Thread;
//        2、子类中重写run() 方法;
//        3、创建子类对象;
        Thread1 t1 = new Thread1();
//        4、调用start() 方法启用线程;
        t1.start();
    }
}

方式二:实现Runnable

步骤

    1、定义子类实现Runnable;
    2、子类中重写run()方法;
    3、创建子类对象;
    4、通过Thread含参构造方法创建Thread对象;
    5、调用start()方法启用线程;

代码

    Thread1类
    @Override
    public void run() {
        System.out.println("新线程");
    }
}
    Test类
    public static void main(String[] args) {
//        1、定义子类实现Runnable;
//        2、子类中重写run() 方法;
//        3、创建子类对象;
        Thread1 thread1 = new Thread1();
//        4、通过Thread含参构造方法创建Thread对象;
        Thread thread = new Thread(thread1);
//        5、调用start() 方法启用线程;
        thread.start();
    }
}

六、实现和继承对比

不同之处

    1、继承只能单继承,而实现没有这一重限制;
    2、实现时,多个线程可共享同一个接口实现类的对象;

相同之处

    1、手动调用run()方法只是普通调用,并未启用多线程;
    2、run()方法调用的一切控制由cpu决定;
    3、要想启用多线程,必须调用start()方法;
    4、一个线程只能调用一次start()方法,否则会报异常;

七、线程优先级

Java线程调度方法

    同优先级线程组成先进先出队列(先到先服务),使用时间片策略;
    对高优先级,使用优先调度的抢占式策略,抢占式即高优先级线程抢占CPU资源;

线程的优先级等级

    MAX_PRIORITY: 10
    MIN _PRIORITY: 1
    NORM_PRIORITY: 5

涉及的方法

    getPriority() : 返回线程优先值
    setPriority(int newPriority) : 改变线程的优先级

说明

    线程创建时继承父线程的优先级
    低优先级只是获得调度的概率低,并非一定是在高优先级线程之后才被调用

八、线程生命周期

五种状态

    新建:当一个线程被创建时,处于新建状态;
    就绪:当线程调用start()方法后,表示该线程被启用,可以被调度,但是未获得CPU资源,所以未被调用;
    运行:当就绪的线程获得时间片后,进入运行状态,开始执行run方法,若时间片用完后run()方法未执行完,则回到就绪状态,继续争夺时间片,若再次争夺到时间片,则接着上次执行的地方开始执行,直到执行完run()方法;
    阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中 止自己的执行,进入阻塞状态;
    死亡:线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束;

九、线程安全问题

实例

    我们用一个实例来描述:模拟车站卖票,三个窗口卖票,票号为1-100;

代码

    Ticket类:
    private int ticket = 100;
    Object object = new Object();
    @Override
    public void run() {
        while (true) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出票,票号为" + ticket);
                    ticket--;
                } else
                    break;
            }
    }
}
    Test类:
    public static void main(String[] args) {
        Ticket t = new Ticket();
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        t1.setName("窗口1");
        t2.setName("窗口2");
        t3.setName("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }

存在问题

    出票存在错票(票号为0和-1)重票(多个相同号码的票)

问题分析

        while(true) {
          位置一:  if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
           位置二:     System.out.println(Thread.currentThread().getName() + "售出票,票号为" + ticket);
                ticket--;
            }
            else
                break;
        }
    }
    重票:例如有三个线程A、B、C,当线程A执行到位置二时被阻塞,在这过程中线程B开始操作共享数据,执行到位置二,然后线程A阻塞结束,线程A、B输出相同票号。当然也可能有多个线程同时输出相同票号,造成重票
    错票:出现票号为0或者1:当只剩下一张票时,即ticket等于1,线程A执行到位置一并判断为true进入下面的代码块,但此时线程A进入休眠被阻塞,因此ticket仍然等于1,线程B、C也执行到位置一,均判断为true进入下面的代码块,然后A、B、C相继退出阻塞,A先输出1,然后ticket减1,B输出0,再减1;C输出-1;
    综上所述:造成这样的问题根本原因是当某个线程操作共享数据未完成时,其他一个或者多个线程也操作共享数据;例如:你和你的女朋友拥有一个共同的银行账户,上面有3000的金额,在你取钱金额为2000时,系统先判定你所取金额和余额大小,显然是满足取钱的,然后银行系统给你出账、将余额减小2000,但是,如果在出钱但是余额还未更新时,你的女朋友也取2000元,那么你们都能成功拿到钱,余额最后为-1000,这显然是不合理的,这也就是线程安全问题。
    PS:共享数据是指多个线程共同操作的数据,例如ticket

解决方案

    要解决以上的问题的关键是解决某个线程操作共享数据未完成时,其他一个或者多个线程也操作共享数据的问题,联想我们平时上厕所,为了保证上厕所时别人不能进来,都会给门上锁。Java解决线程安全问题也是通过上锁。

方法一:同步代码块:(synchronized)

    synchronized (同步监视器) {
    //同步的代码块
    }
    同步监视器(俗称锁):任意类的对象
    隐藏要求:所有的线程用同一个监视器,如果用该方法不能实现线程安全,多半是该条件不能满足;可以考虑用当前对象(this)来充当锁,但要判断是否唯一(调用run()方法的对象是否只是初始化了一次),或者用当前类充当。
    同步代码块:操作共享数据的代码
例如:
    private int ticket = 100;
    Object object = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized(object){
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "售出票,票号为" + ticket);
                    ticket--;
                } else
                    break;
            }
        }
    }
}

方法二:同步方法:(synchronized)

    将操作共享的数据抽出来写入一个函数,将该方法用synchronized修饰,然后再run方法中调用。该种方法的也有同步监视器,若该方法是非静态方法,则同步监视器为this;若为静态方法则为当前类本身;何时静态何时不静态关键要满足锁的唯一性。
    例如:Test:
   Thread t1 = new Thread(t);
   Thread t2 = new Thread(t);
   Thread t3 = new Thread(t);```

<font face = "楷体" size = 5>
&ensp;&ensp;&ensp;&ensp;Ticket:
</font>

```public class Ticket implements Runnable {
    private int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    public synchronized void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出票,票号为" + ticket);
            ticket--;
        }
    }
}
    这个例子就是非静态方法,监视器就是this,即当前对象,也就是调用run()方法的类的对象,即Ticket类的对象,从Test类可以看出,Ticket只有一个对象ticket;
例如:Test:
   Ticket t2 = new Ticket();
   Ticket t3 = new Ticket();
   t1.start();
   t2.start();
   t3.start();
    Ticket:
    private static int ticket = 100;
    @Override
    public void run() {
        while (true) {
            show();
        }
    }
    public static synchronized  void show() {
        if (ticket > 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出票,票号为" + ticket);
            ticket--;
        }
    }
}
     这种情况就是静态方法,因为若用非静态方法。那么监视器就变成了t1,t2,t3,监视器不唯一;而用了静态方法,监视器就为Ticket.class;

注意事项

    同步代码块选择不能太大,否则会导致多线程变为单线程;也不可选择太小,否则不能解决安全问题。

何时释放锁

    1、当前线程同步方法、同步代码块执行完毕;
    2、当前线程在同步方法、同步代码块遇到break、return终止代码;
    3、当前线程同步方法、同步代码块中出现错误、未处理异常;
    4、执行当前线程wait()方法;

何时不释放锁

    1、当前线程执行时,其他线程执行该线程的suspend()方法使其挂起;
    2、执行当前线程sleep()、yield()方法;
    后续在本文章继续更新。。。。。。。。。。
posted @ 2020-07-26 18:37  万一啊  阅读(239)  评论(0编辑  收藏  举报