黑马程序员--Java多线程

来源:互联网 发布:u深度制作ubuntu启动盘 编辑:程序博客网 时间:2024/05/18 01:12

——- android培训、java培训、期待与您交流! ———-

进程与线程

要想了解多线程,必须先了解线程,而要想了解线程,必须先了解进程,因为线程是依赖于进程而存在。

什么是进程

通过任务管理器我们就看到了进程的存在。
而通过观察,我们发现只有运行的程序才会出现进程。
进程:就是正在运行的程序。
进程是系统进行资源分配和调用的独立单位。每一个进程都有它自己的内存空间和系统资源。

多进程的意义

单进程的计算机只能做一件事情,而我们现在的计算机都可以做多件事情。
举例:一边玩游戏(游戏进程),一边听音乐(音乐进程)。
也就是说现在的计算机都是支持多进程的,可以在一个时间段内执行多个任务。
并且呢,可以提高CPU的使用率。

一边打游戏,一边听音乐,不是同时进行的,单CPU在某一时间点上只能做一件事。
CPU在做程序间的高效切换让我们感觉打游戏和听音乐是同时进行的。

什么是线程

在同一个进程内又可以执行多个任务,而这每一个任务我就可以看出是一个线程。

  • 线程:是程序的执行单元,执行路径。是程序使用CPU的最基本单位。
  • 单线程:如果程序只有一条执行路径。
  • 多线程:如果程序有多条执行路径。

多线程的意义

多线程的存在,不是提高程序的执行速度。其实是为了提高应用程序的使用率。
程序的执行其实都是在抢CPU的资源,CPU的执行权。
多个进程是在抢这个资源,而其中的某一个进程如果执行路径比较多,就会有更高的几率抢到CPU的执行权。
我们是不敢保证哪一个线程能够在哪个时刻抢到,所以线程的执行有随机性。

并行和并发

  • 前者是逻辑上同时发生,指在某一个时间内同时运行多个程序。
  • 后者是物理上同时发生,指在某一个时间点同时运行多个程序。

Java程序的运行原理

由java命令启动JVM,JVM启动就相当于启动了一个进程。
接着由该进程创建了一个主线程去调用main方法。

JAVA虚拟机的启动是多线程的,除了主线程,至少还需要垃圾回收线程,否则内存很快就会溢出。

如何实现多线程

由于线程是依赖进程而存在的,所以我们应该先创建一个进程出来。
而进程是由系统创建的,所以我们应该去调用系统功能创建一个进程。
Java是不能直接调用系统功能的,所以,我们没有办法直接实现多线程程序。
但是Java可以去调用C/C++写好的程序来实现多线程程序。
由C/C++去调用系统功能创建进程,然后由Java去调用这样的东西,
然后提供一些类供我们使用。我们就可以实现多线程程序了。

方式一:继承Thread类

首先自定义一个继承Thread的方法

public class MyThread extends Thread {    @Override    public void run() {        for (int x = 0; x < 200; x++) {            System.out.println(x);        }    }}

重写run()方法的作用
不是类中的所有代码都需要被线程执行的。
为了区分哪些代码能够被线程执行,java提供了Thread类中的run()用来包含那些被线程执行的代码。

创建线程对象测试

public class MyThreadDemo {    public static void main(String[] args) {        // 创建两个线程对象        MyThread my1 = new MyThread();        MyThread my2 = new MyThread();        //my1.run();//这是单线程        //启动线程        my1.start();        my2.start();    }}

调用run()方法为什么是单线程?
因为run()方法直接调用其实就相当于普通的方法调用,所以你看到的是单线程的效果
run()和start()的区别?

  • run():仅仅是封装被线程执行的代码,直接调用是普通方法
  • start():首先启动了线程,然后再由jvm去调用该线程的run()方法。

获取和设置线程对象的名称
public final String getName():获取线程的名称
public final void setName(String name):设置线程的名称

public class MyThreadDemo {    public static void main(String[] args) {        // 创建线程对象        //无参构造+setXxx()        // MyThread my1 = new MyThread();        // MyThread my2 = new MyThread();        // //调用方法设置名称        // my1.setName("李延旭");        // my2.setName("康小广");        // my1.start();        // my2.start();        //带参构造方法给线程起名字        // MyThread my1 = new MyThread("赵磊");        // MyThread my2 = new MyThread("王澳");        // my1.start();        // my2.start();        //我要获取main方法所在的线程对象的名称,该怎么办呢?        //遇到这种情况,Thread类提供了一个方法:        //public static Thread currentThread():返回当前正在执行的线程对象        System.out.println(Thread.currentThread().getName());    }}

线程调度
我们知道,一般我们的计算机只有一个CPU,而CPU在某一个时刻只能运行一条指令,线程只有得到CPU的使用权,才能执行线程,那么Java是如何对线程进行调度的呢?
线程有两种调度模型:

  • 1.分时调度模型:所有线程轮流使用CPU的使用权,平均分配每个线程占用CPU的时间片。
  • 2.抢占式调度模型,优先让优先级高的线程使用CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取CPU时间片的几率稍大。

Java使用的是抢占式调度模型

设置和获取线程的优先级
public final int getPriority():返回线程对象的优先级
public final void setPriority(int newPriority):更改线程的优先级。
注意:

  • 程默认优先级是5。
  • 优先级的范围是:1-10。
  • 先级高仅仅表示线程获取的 CPU时间片的几率高,但是要在次数比较多,或者多次运行的时候才能看到比较好的效果。
public class ThreadPriorityDemo {    public static void main(String[] args) {        //创建线程对象        ThreadPriority tp1 = new ThreadPriority();        ThreadPriority tp2 = new ThreadPriority();        ThreadPriority tp3 = new ThreadPriority();        //设置对象名称        tp1.setName("徐凤年");        tp2.setName("李淳罡");        tp3.setName("黄阵图");        // 获取默认优先级        // System.out.println(tp1.getPriority());        // System.out.println(tp2.getPriority());        // System.out.println(tp3.getPriority());        // 设置线程优先级        // tp1.setPriority(100000);//报错        //设置正确的线程优先级        tp1.setPriority(10);        tp2.setPriority(1);        tp1.start();        tp2.start();        tp3.start();    }}

线程休眠
重写run()方法,加入休眠时间

import java.util.Date;/* * 线程休眠 *      public static void sleep(long millis) */public class ThreadSleep extends Thread {    @Override    public void run() {        for (int x = 0; x < 100; x++) {            System.out.println(getName() + ":" + x + ",日期:" + new Date());            // 设置休眠时间            try {                Thread.sleep(1000);            } catch (InterruptedException e) {                e.printStackTrace();            }        }    }}

加入线程

/* * public final void join():等待该线程终止。  * 别的线程需要等待这个加入的线程执行结束才能执行。 */public class ThreadJoinDemo {    public static void main(String[] args) {        //创建线程对象        ThreadJoin tj1 = new ThreadJoin();        ThreadJoin tj2 = new ThreadJoin();        ThreadJoin tj3 = new ThreadJoin();        //设置线程名称        tj1.setName("黄三甲");        tj2.setName("邓太阿");        tj3.setName("曹长卿");        //加入线程        tj1.start();        try {            tj1.join();        } catch (InterruptedException e) {            e.printStackTrace();        }        //启动线程        tj2.start();        tj3.start();    }}

礼让线程

/* * public static void yield():暂停当前正在执行的线程对象,并执行其他线程。  * 让多个线程的执行更和谐,但是不能靠它保证一人一次。 */public class ThreadYieldDemo {    public static void main(String[] args) {        ThreadYield ty1 = new ThreadYield();        ThreadYield ty2 = new ThreadYield();        ty1.setName("李延旭");        ty2.setName("黑马");        ty1.start();        ty2.start();    }}

守护线程

/* * public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。 * 当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。  *  */public class ThreadDaemonDemo {    public static void main(String[] args) {        ThreadDaemon td1 = new ThreadDaemon();        ThreadDaemon td2 = new ThreadDaemon();        td1.setName("关羽");        td2.setName("张飞");        // 设置守护线程        td1.setDaemon(true);        td2.setDaemon(true);        td1.start();        td2.start();        Thread.currentThread().setName("刘备");        for (int x = 0; x < 5; x++) {            System.out.println(Thread.currentThread().getName() + ":" + x);        }    }}

中断线程

/* * 中断线程: *                 public void interrupt() */public class TreadDemo {        public static void main(String[] args) {                // 创建线程                MyThread m1 = new MyThread();                MyThread m2 = new MyThread();                // 设置线程名称                m1.setName("黑马");                m2.setName("白马");                // 中断线程,后面的线程还可以继续运行                m1.interrupt();                // 启动线程                m2.start();        }}

线程的生命周期

这里写图片描述

方式二:实现Runnable接口

步骤:
* A:自定义类MyRunnable实现Runnable接口
* B:重写run()方法
* C:创建MyRunnable类的对象
* D:创建Thread类的对象,并把C步骤的对象作为构造参数传递

public class MyRunnable implements Runnable {    @Override    public void run() {        for (int x = 0; x < 100; x++) {            // 由于实现接口的方式就不能直接使用Thread类的方法了,但是可以间接的使用            System.out.println(Thread.currentThread().getName() + ":" + x);        }    }}

测试类

public class MyRunnableDemo {    public static void main(String[] args) {        // 创建MyRunnable类的对象        MyRunnable my = new MyRunnable();        // 构造实现:Thread(Runnable target, String name)        Thread t1 = new Thread(my, "林青霞");        Thread t2 = new Thread(my, "刘意");        t1.start();        t2.start();    }}

已有方式一,为何会有方式二出现?
1.可以避免由于java单继承带来的局限性
2.适合多个相同程序的代码去处理同一个资源的情况,把线程和程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

方式三:实现Callable接口

需要和线程池结合使用
实现类

import java.util.concurrent.Callable;/* * Callable<V>:带泛型的接口 *                 接口中只有一个方法:V call() *                 接口中的泛型是call()方法的返回值类型  *  */public class MyCallable implements Callable {        @Override        public Object call() throws Exception {                for (int i = 0; i < 100; i++) {                        System.out.println(Thread.currentThread().getName() + ":" + i);                }                return null;        }}

测试类

import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class MyCallableDemo {        public static void main(String[] args) {                // 创建线程池对象                ExecutorService pool = Executors.newFixedThreadPool(2);                // 添加Callable实现类                pool.submit(new MyCallable());                pool.submit(new MyCallable());                // 结束线程池                pool.shutdown();        }}

线程安全

解决线程安全的基本思想(判断线程是否有问题的标准)

  • 是否是多线程环境
  • 是否有共享数据
  • 是否有多条语句操作共享数据

电影票案例

public class SellTicket implements Runnable {    // 定义100张票    private int tickets = 100;    @Override    public void run() {        while (true) {            if (tickets > 0) {                try {                    Thread.sleep(100);                 } catch (InterruptedException e) {                    e.printStackTrace();                }                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (tickets--) + "张票");            }        }    }}

测试类

public class SellTicketDemo {    public static void main(String[] args) {        // 创建资源对象        SellTicket st = new SellTicket();        // 创建三个线程对象        Thread t1 = new Thread(st, "窗口1");        Thread t2 = new Thread(st, "窗口2");        Thread t3 = new Thread(st, "窗口3");        // 启动线程        t1.start();        t2.start();        t3.start();    }}

这样会出现以下问题
1.相同的票卖了多次
CPU的一次操作必须是原子性的

2.出现负数票
线程的随机性和延迟导致的

线程安全的解决方式

解决方式一:同步代码块

/* * synchronized(对象){ *                 代码; * } *  * 注意:同步代码块可以解决安全问题的根本原因在对象上,该对象如果锁一样的功能,别的线程不能进入。 *                  这个对象可以是任意对象,最好是用本身this作为这个对象。 *  */public class SellTicekt implements Runnable {        private int ticket = 100;        @Override        public void run() {                while (true) {                        synchronized (this) {                                if (ticket > 0) {                                        try {                                             Thread.sleep(100);} catch (InterruptedException e) {                                   e.printStackTrace();                                        }                                        System.out.println(Thread.currentThread().getName()+ "正在出售第" + (ticket--) + "张票");                                }                        }                }        }}

解决方式二:同步方法

/* * synchronized关键字修饰方法 * 锁对象是this */public class SellTicekt implements Runnable {        private int ticket = 100;        @Override        public synchronized void run() {                while (true) {                        if (ticket > 0) {                                try {                                        Thread.sleep(100);                                } catch(InterruptedException e) {                                        e.printStackTrace();                                }                                System.out.println(Thread.currentThread().getName() + "正在出售第"+ (ticket--) + "张票");                        }                }        }}

同步的特点:
前提:多个线程
解决问题的时候要注意:多个线程使用的是同一个锁对象

同步的好处
同步的出现解决了多线程的安全问题。

同步的弊端
当线程相当多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率。

Lock锁

为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock。

import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReentrantLock;public class SellTicket implements Runnable {    // 定义票    private int tickets = 100;    // 定义锁对象    private Lock lock = new ReentrantLock();    @Override    public void run() {        while (true) {            try {                // 加锁                lock.lock();                if (tickets > 0) {                    try {                        Thread.sleep(100);                    } catch (InterruptedException e) {                        e.printStackTrace();                    }                    System.out.println(Thread.currentThread().getName()+ "正在出售第" + (tickets--) + "张票");                }            } finally {                // 释放锁                lock.unlock();            }        }    }}

死锁
两个或两个以上的线程在争夺资源的过程中,发生的一种相互等待的现象。
举例:
中国人,美国人吃饭案例。

正常情况:
中国人:筷子两支
美国人:刀和叉

现在:
中国人:筷子1支,刀一把
美国人:筷子1支,叉一把

public class MyLock {    // 创建两把锁对象    public static final Object objA = new Object();    public static final Object objB = new Object();}
public class DieLock extends Thread {    private boolean flag;    public DieLock(boolean flag) {        this.flag = flag;    }    @Override    public void run() {        if (flag) {            synchronized (MyLock.objA) {                System.out.println("if objA");                synchronized (MyLock.objB) {                    System.out.println("if objB");                }            }        } else {            synchronized (MyLock.objB) {                System.out.println("else objB");                synchronized (MyLock.objA) {                    System.out.println("else objA");                }            }        }    }}

测试类

public class DieLockDemo {    public static void main(String[] args) {        DieLock dl1 = new DieLock(true);        DieLock dl2 = new DieLock(false);        dl1.start();        dl2.start();    }}

线程的状态转换图及常见执行情况
这里写图片描述

线程间通信

指不同种类的线程针对同一资源的操作
资源类

/* * 定义学生类 */public class Student {        String name;        int age;        boolean flag;// 用来判断是否存在资源,默认是flash,没有资源        public synchronized void set(String name, int age) {                // 生产者,如果有数据就等待                if (!this.flag) {                        try {                                this.wait();                        } catch (InterruptedException e) {                                e.printStackTrace();                        }                }                // 设置数据                this.name = name;                this.age = age;                // 修改标记                this.flag = false;                // 唤醒线程                this.notify();        }        public synchronized void get() {                if (this.flag) {                        try {                                this.wait();                        } catch (InterruptedException e) {                                e.printStackTrace();                        }                }                System.out.println(this.name + ":" + this.age);                // 修改标记                this.flag = true;                // 唤醒线程                this.notify();        }}

设置类

/* * 设置学生信息的线程 */public class SetThread implements Runnable {        private Student s;        private int i;        public SetThread(Student s) {                this.s = s;        }        @Override        public void run() {                while (true) {                        if (i % 2 == 0) {                                s.set("小明", 5);                        } else {                                s.set("汪汪", 2);                        }                        i++;                }        }}

获取类

/* * 设置获取学生信息的线程 */public class GetThread implements Runnable {        private Student s;        public GetThread(Student s) {                this.s = s;        }        @Override        public void run() {                while (true) {                        s.get();                }        }}

测试类

public class StudentDemo {        public static void main(String[] args) {                // 创建资源                Student s = new Student();                // 创建SetThread和GetThread对象                SetThread st = new SetThread(s);                GetThread gt = new GetThread(s);                // 创建线程                Thread t1 = new Thread(st);                Thread t2 = new Thread(gt);                // 开启线程                t1.start();                t2.start();        }}

这样线程安全是解决了,但是还存在着以下问题。
1.如果消费者先抢到CPU执行权,消费数据,这时数据如果是空,就没有意义。
应该等着数据生产出来,再去消费,这样才具有意义。
2.如果生产者先抢到CPU执行权,生产数据,但是生产完一定数量的数据以后,还继续持有执行权,
它还会继续生产数据,这还现实情况不符,需要等着消费者把数据消费以后,再生产。

正常思路:
1.生产者
先看是否有数据,有就等待,没有就生产,生产完通知消费者消费
2.消费者
先看是否有数据,有就消费,没有就等待,消费完通知生产者生产

java提供了一个等待唤醒机制来解决这个问题。
这里写图片描述

Object类中提供了三个方法:
wait():等待
notify():唤醒单个线程

为什么等待唤醒方法定义在Object类中:
这些方法都是通过锁对象进行调用的,锁对象可以是任意的
所以,这些方法必须定义在Object类中。

测试类

public class StudentDemo {    public static void main(String[] args) {        //创建资源        Student s = new Student();        //设置和获取的类        SetThread st = new SetThread(s);        GetThread gt = new GetThread(s);        //线程类        Thread t1 = new Thread(st);        Thread t2 = new Thread(gt);        //启动线程        t1.start();        t2.start();    }}

资源类

public class Student {    String name;    int age;    boolean flag; }

生产者类

public class GetThread implements Runnable {    private Student s;    public GetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                if(!s.flag){                    try {                        s.wait();                     } catch (InterruptedException e) {                        e.printStackTrace();                    }                }                System.out.println(s.name + "---" + s.age);                //修改标记                s.flag = false;                //唤醒线程                s.notify();             }        }    }}

消费者类

public class SetThread implements Runnable {    private Student s;    private int x = 0;    public SetThread(Student s) {        this.s = s;    }    @Override    public void run() {        while (true) {            synchronized (s) {                //判断有没有                if(s.flag){                    try {                        s.wait();                     } catch (InterruptedException e) {                        e.printStackTrace();                    }                }                if (x % 2 == 0) {                    s.name = "李延旭";                    s.age = 21;                } else {                    s.name = "黑马";                    s.age = 22;                }                x++; //x=1                //修改标记                s.flag = true;                //唤醒线程                s.notify(); //唤醒t2,唤醒并不表示你立马可以执行,必须还得抢CPU的执行权。            }        }    }}

总结

多线程的实现有三个方法,我们常用的是第二种,所以第二种是要必须掌握的,其他两种了解即可。对于线程的状态转换和执行图,也是必须要理解的,这样有利于学习多线程。等待唤醒机制,是很符合现实的生活一个机制,需要熟练掌握。

——- android培训、java培训、期待与您交流! ———-

0 0
原创粉丝点击