Java笔记--多线程

“等待与希望,乃人生真谛。”这是基督山伯爵最后的箴言,也是我们在漫长人生旅途中的永恒指南。

                                                                                                          --大仲马《基督山伯爵》

 一、多线程

1、进程

正在运行的程序,是系统进行资源分配和调用的独立单位。
每一个进程都有它自己的内存空间和系统资源。
理解:一个正在运行的软件

2、线程

是进程中的单个顺序控制流,是一条执行路径
一个进程如果只有一条执行路径,则称为单线程程序。
一个进程如果有多条执行路径,则称为多线程程序。
举例:阿里云盘(进程)中多个同时进行的任务,每一个任务可以看作一个线程

3、如何创建一个线程

a. 自定义线程类继承Thread类,重写run方法
b. 自定义线程类实现Runnable接口,实现run方法

4、如何启动一个线程

调用start()方法启动

Thread无参构造方法
Thread() 分配一个新的 Thread对象。

5、注意

1、启动一个线程的时候,若直接调用run方法,仅仅是普通的对象调用方法,按照自上而下的顺序执行,底层不会额外的创建一个线程再执行
2、从执行的结果上来看,java线程之间是抢占式执行的,谁先抢到cpu执行权谁就先执行
3、每次运行的结果顺序不可预测的,完全随机的
4、每个线程都有优先权。 具有较高优先级的线程优先于优先级较低的线程执行。

二、Thread类(第一种多线程的实现方法)

Thread类中的成员方法

1、public final String getName()  获取线程对象的名字
2、设置线程对象名字的方式:
        a. 通过父类的有参构造方法,在创建线程对象的时候设置名字
        b. 线程对象调用setName(String name)方法,给线程对象设置名字
3、获取线程的等级(线程的调度)
        getPriority() 默认优先级都是5
4、设置线程优先级,setPriority(int i),在启动之前设置  [1,10]
        注意不是优先级高的一定先执行,只是可能性变高了。

package day19;

 */
class MyThread1 extends Thread {
    MyThread1() {
    }

    //public Thread(String name)
    MyThread1(String name) {
        super(name);
    }

//    MyThread1(){
//        //super()
//    }

    @Override
    public void run() {
        //将来线程启动后需要执行的逻辑
        for (int i = 1; i <= 200; i++) {
            System.out.println(this.getName() + " - Hello word " + i);
        }
    }
}

public class ThreadDemo1 {
    public static void main(String[] args) {
        //创建一个自己的线程对象
//        MyThread1 t1 = new MyThread1("张三"); // Thread-0
        MyThread1 t1 = new MyThread1();
        t1.setName("张三");
        t1.setPriority(1);
        System.out.println("t1: "+t1.getPriority());
        //创建一个自己的线程对象
//        MyThread1 t2 = new MyThread1("李四"); // Thread-1
        MyThread1 t2 = new MyThread1();
        t2.setName("李四");
        t2.setPriority(10);
        System.out.println("t2: "+t2.getPriority());
//        t1.run();
//        t2.run();//想要启动线程需要使用start()方法,不能使用run()方法,run()方法虽然可以运行,但是仅仅是以普通的程序状态去运行,并不是以线程的状态去运行。
        t1.start();
        t2.start();
    }
}

线程控制

(1)线程休眠

public static void sleep(long millis)

package day19;
/*
    线程控制,休眠线程
    当线程处于休眠状态的时候,该线程就没有cpu执行权了,若这时还有其他的线程,会被抢走cpu执行权。
 */

class SleepThread extends Thread{
    @Override
    public void run() {
        System.out.println(getName()+"睡着了。。。。");
        try {
            Thread.sleep(5000);//在哪个方法中调用,就是调用该方法的线程对象进行休眠
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName()+"睡醒了。。。。");
    }
}

public class ThreadSleepDemo1 {
    public static void main(String[] args) {
        SleepThread t1 = new SleepThread();
        t1.setName("程序");
        t1.start();
    }
}

(2)线程加入

public  final void join()

package day19;

class JoinThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println(getName()+"-"+i);
        }
    }
}

public class ThreadJoinDemo1 {
    public static void main(String[] args) {
        JoinThread t1 = new JoinThread();
        JoinThread t2 = new JoinThread();
        JoinThread t3 = new JoinThread();

        t1.setName("小张");
        t2.setName("小古");
        t3.setName("小季");

        t1.start();//t1线程优先运行,其他线程会等待他的结束
        try {
            t1.join();//其他线程等待该线程执行结束,其他线程之间会进行抢占式的执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();
        t3.start();
    }
}

(3)线程礼让

public static void yield()

package day19;

/*
    礼让线程
        yield()只是为了让运行结果看起来均匀一些
 */
class YieldThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            System.out.println(getName()+" - "+i);
            Thread.yield();
        }
    }
}

public class ThreadYieldDemo1 {
    public static void main(String[] args) {
        YieldThread t1 = new YieldThread();
        YieldThread t2 = new YieldThread();
        t1.setName("小张");
        t2.setName("小古");
        t1.start();
        t2.start();
    }
}

(4)后台线程

public final void setDaemon(boolean on)

package day19;

/*
    后台进程:
        用户线程
        守护线程
    在启动之前,设置一下,若一个进程中没有用户线程,守护线程也没有必要存在
    不同生,但是被守护的线程先死亡,守护线程会一起死亡
    假如有很多用户进程,只要有一个用户进程还存在,守护进程就不会死亡
 */
class DaemonThread extends Thread{
    @Override
    public void run() {
        for (int i = 1; i <= 200; i++) {
            System.out.println(getName() + " - " + i);
        }
    }
}
public class ThreadDaemonDemo1 {
    public static void main(String[] args) {
        DaemonThread t1 = new DaemonThread();
        DaemonThread t2 = new DaemonThread();
        DaemonThread t3 = new DaemonThread();

        t1.setName("刘备");
        t2.setName("关羽");
        t3.setName("张飞");
        t2.setDaemon(true);
        t3.setDaemon(true);

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

(5)中断线程

public final void stop()

public void interrupt() 

package day19;
import javafx.scene.paint.Stop;

/*
stop()方法已被弃用,现在使用的方法是interrupt()方法。
 */
class StopThread extends Thread{
    @Override
    public void run() {
        System.out.println(getName()+"睡着了。。。。");
        try {
            Thread.sleep(5000);//在哪个方法中调用,就是调用该方法的线程对象进行休眠
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName()+"睡醒了。。。。");
    }
}
public class ThreadStopDemo1 {
    public static void main(String[] args) {
        StopThread t1 = new StopThread();
        t1.setName("张三");
        t1.start();
        try {
            Thread.sleep(2000);
//            t1.stop();//已经弃用
            t1.interrupt();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

三、Runnable接口(多线程的实现方案2) 

实现接口的好处

  • 可以避免由于Java单继承带来的局限性。
  • 适合多个相同程序的代码去处理同一个资源的情况,把线程同程序的代码,数据有效分离,较好的体现了面向对象的设计思想。

关于电影院卖票程序题目

(1)第一次尝试

package day19.sellTickets;

/*
    使用Runnable的方式实现

    为了模拟更加真实的售票情况,我们加入延迟
    问题;
        我们加入了延迟之后,发现
        a.有重复售卖同一张票的情况
        b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
    原因:
        1.cpu小小的时间片,足以让程序执行很多次
        2.线程的执行具有随机性,且是抢占式执行的

    现象:线程不安全的现象
        如何判断一个程序是否存在线程不安全的现象呢?
        三要素(同时满足):
            1、是否存在多线程环境?
            2、是否存在共享数据?
            3、是否存在多条语句操作着共享数据
    如何解决线程不安全的现象?
        1、同步代码块
        2、lock锁
 */
class Window implements Runnable{
    private static int tickets=100;
    @Override
    public void run() {
        while(true){
            if(tickets>0){
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" 正在出售第 "+(tickets--)+" 张票 ");

            }
        }
    }
}

public class SellTicketDemo1 {
    public static void main(String[] args) {
        Window window = new Window();

        Thread w1 = new Thread(window,"窗口1");
        Thread w2 = new Thread(window,"窗口2");
        Thread w3 = new Thread(window,"窗口3");

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

(2)第二次改进(解决方案一)

package day19.SellTickets2;

/*
    使用Runnable的方式实现

    为了模拟更加真实的售票情况,我们加入延迟
    问题;
        我们加入了延迟之后,发现
        a.有重复售卖同一张票的情况
        b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
    原因:
        1.cpu小小的时间片,足以让程序执行很多次
        2.线程的执行具有随机性,且是抢占式执行的

    现象:线程不安全的现象
        如何判断一个程序是否存在线程不安全的现象呢?
        三要素(同时满足):
            1、是否存在多线程环境?
            2、是否存在共享数据?
            3、是否存在多条语句操作着共享数据
    如何解决线程不安全的现象?
        1、同步代码块
        2、lock锁

    解决方案1:加入同步代码块
        synchronized(对象){
            操作共享数据代码
        }
    这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。
 */
class Window implements Runnable{
    private static int tickets=100;
    private Object object = new Object();
    @Override
    public void run() {
        while(true) {
            synchronized (object) {
                if (tickets > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
                }
            }
        }
    }
}

public class SellTicketDemo1 {
    public static void main(String[] args) {
        Window window = new Window();
        Thread w1 = new Thread(window,"窗口1");
        Thread w2 = new Thread(window,"窗口2");
        Thread w3 = new Thread(window,"窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

(3)第三次改进(解决方案一)(将售票这个动作写成方法)

package day19.SellTickets3;

/*
    使用Runnable的方式实现

    为了模拟更加真实的售票情况,我们加入延迟
    问题;
        我们加入了延迟之后,发现
        a.有重复售卖同一张票的情况
        b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
    原因:
        1.cpu小小的时间片,足以让程序执行很多次
        2.线程的执行具有随机性,且是抢占式执行的

    现象:线程不安全的现象
        如何判断一个程序是否存在线程不安全的现象呢?
        三要素(同时满足):
            1、是否存在多线程环境?
            2、是否存在共享数据?
            3、是否存在多条语句操作着共享数据
    如何解决线程不安全的现象?
        1、同步代码块
        2、lock锁

    解决方案1:加入同步代码块
        synchronized(对象){
            操作共享数据代码
        }
    这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。

    synchronized的使用:
        1.同步代码块 - 锁对象 - 任意一个对象,前提是多个线程对象共享一个
        2.同步方法 - 锁对象 - this
        3.同步静态方法 - 锁对象 - 当前类的class文件对象

    解决方法2:lock锁

 */
class Window implements Runnable {
    private static int tickets = 100;
    private Object object = new Object();
    private int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                synchronized (Window.class) {
                    if (tickets > 0) {
                        try {
                            Thread.sleep(50);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
                    }
                }
            } else {
                sellTicket();
            }
            i++;
        }
    }

    //同步方法,将synchronized在方法定义上出现
//    public synchronized void sellTicket(){
//        if (tickets > 0) {
//            try {
//                Thread.sleep(50);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
//        }
//    }
//}

    public static synchronized void sellTicket() {
        if (tickets > 0) {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
        }
    }
}

public class SellTicketDemo1 {
    public static void main(String[] args) {
        Window window = new Window();

        Thread w1 = new Thread(window,"窗口1");
        Thread w2 = new Thread(window,"窗口2");
        Thread w3 = new Thread(window,"窗口3");

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

(4)第四次改进(解决方案二)(使用lock锁)

package day19.SellTickets4;
import java.util.concurrent.locks.ReentrantLock;


/*
    使用Runnable的方式实现
    为了模拟更加真实的售票情况,我们加入延迟
    问题;
        我们加入了延迟之后,发现
        a.有重复售卖同一张票的情况
        b.还出现了一个不该出现的票数据,比如第0张票,第-1张票(原因2)
    原因:
        1.cpu小小的时间片,足以让程序执行很多次
        2.线程的执行具有随机性,且是抢占式执行的

    现象:线程不安全的现象
        如何判断一个程序是否存在线程不安全的现象呢?
        三要素(同时满足):
            1、是否存在多线程环境?
            2、是否存在共享数据?
            3、是否存在多条语句操作着共享数据
    如何解决线程不安全的现象?
        1、同步代码块
        2、lock锁
    解决方案1:加入同步代码块
        synchronized(对象){
            操作共享数据代码
        }
    这里的对象,可以是任意一个new出来的对象,但是要保证多个线程之间的是同一个对象。

    synchronized的使用:
        1.同步代码块 - 锁对象 - 任意一个对象,前提是多个线程对象共享一个
        2.同步方法 - 锁对象 - this
        3.同步静态方法 - 锁对象 - 当前类的class文件对象

    解决方法2:lock锁,利用ReentrantLock类创建锁对象,要求多个线程对象共享一个
        不需要考虑锁对象是谁了?
 */
class Window implements Runnable {
    private static int tickets = 1000;
//    private Object object = new Object();
    private ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            lock.lock();    //加锁
                if (tickets > 0) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
                }
            lock.unlock();  //释放锁
        }
    }
}
    //同步方法,将synchronized在方法定义上出现
//    public synchronized void sellTicket(){
//        if (tickets > 0) {
//            try {
//                Thread.sleep(50);
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//            System.out.println(Thread.currentThread().getName() + " 正在出售第 " + (tickets--) + " 张票 ");
//        }
//    }
//}
public class SellTicketDemo1 {
    public static void main(String[] args) {
        Window window = new Window();
        Thread w1 = new Thread(window,"窗口1");
        Thread w2 = new Thread(window,"窗口2");
        Thread w3 = new Thread(window,"窗口3");
        w1.start();
        w2.start();
        w3.start();
    }
}

四、死锁

定义:

        死锁是指多个线程在执行过程中,由于竞争资源而造成的一种僵持状态,若无外力作用,这些线程都将无法向前推进。‌

package day19.dielock;

import java.util.concurrent.locks.ReentrantLock;

public class Locks {
    private Locks(){}

    public static final ReentrantLock lock1 = new ReentrantLock();
    public static final ReentrantLock lock2 = new ReentrantLock();

}

死锁的问题:线程之间存在相互等待的现象

经典的案例:中国人和外国人吃饭的问题

中国人:两个筷子

外国人:一把刀和一把叉

现在:

中国人:一支筷子,一把刀

外国人:一只筷子,一把叉

package day19.dielock;

/*
    死锁的问题:线程之间存在相互等待的现象
    经典的案例:中国人和外国人吃饭的问题
    中国人:两个筷子
    外国人:一把刀和一把叉
    现在:
        中国人:一支筷子,一把刀
        外国人:一只筷子,一把叉
 */
class Person extends Thread {
    private boolean flag;

    public Person(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        if (flag) {
            synchronized (Locks.lock1) {
                System.out.println("if lock1");
                //p1
                synchronized (Locks.lock2) {
                    System.out.println("if lock2");
                }
            }
        } else {
            synchronized (Locks.lock2) {
                System.out.println("else lock2");
                //p2
                synchronized (Locks.lock1) {
                    System.out.println("else lock1");
                }
            }
        }
    }
}
public class DieLockDome {
    public static void main(String[] args) {
        Person p1 = new Person(true);
        Person p2 = new Person(false);

        p1.start();
        p2.start();
    }
}

五、生产者与消费者的问题

通过前面的所学,可以引出一个十分常用的模型--消费者生产者模型

等待唤醒机制:
    共享数据:学生对象(name,age)
    生产者线程:对学生对象进行赋值操作
    消费者线程:对学生对象进行取值操作

为了方便更好的效果,我们可以让生产者赋值不同的信息
这时候出现了重复取值和姓名与年龄对应不上的情况
检测线程程序是否存在安全问题的三要素:
    1、是否存在多线程环境?是 生产者和消费者
    2、是否存在共享数据? 是 Student
    3、是否存在多条语句操作共享的数据 是

解决方案:
    1、加入同步代码块
    2、加入lock锁
虽然解决了线程安全的问题,但是结果并不是我们想要的结果。在生产者消费者模型中,消费一次应该通知生产者生产。等待唤醒机制
注意:等待唤醒机制是建立在线程安全基础上设计的。

首先

创建一个产品类

package day19.waitdemo;

public class Student {
    private String name;
    private int age;
    private boolean flag;//boolean类型的默认值在内存是false

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public boolean getFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

然后

创建消费者类和生产者类

ConsumerThread类

package day19.waitdemo;

public class ConsumerThread extends Thread{
    private Student s;

    public ConsumerThread(Student s) {
        this.s = s;
    }
    @Override
    public void run() {
//        Student s = new Student();
        while(true){
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (s){
                //消费者消费之前,先判断数据有没有生产,若生产了,就取值打印,若没有生产,就等待,通知生产者生产
                if(!s.getFlag()){
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println(s.getName()+ " - "+s.getAge());
                s.setFlag(false);
                //通知生产者赋值新的数据
                s.notify();
            }
        }
    }
}

ProductThread类

package day19.waitdemo;

public class ProductThread extends Thread{
    private Student s;
    private int i = 0;

    public ProductThread( Student s) {
        this.s = s;
    }

    @Override
    public void run() {
        while(true){
            try {
                Thread.sleep(30);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (s){
                //应该先看一看数据有没有被消费,若被消费了,才赋新的值,通知消费者消费。若没有消费,等待消费者消费
                if (s.getFlag()){
                    //若学生对象中的flag成员变量是true,生产者不会去生产
                    try {
                        s.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(i%2==0){
                    s.setName("张三");
                    s.setAge(18);
                }else{
                    s.setName("李四");
                    s.setAge(20);
                }
                i++;
                s.setFlag(true);
                //赋值之后应该通知消费者进行消费
                s.notify();
            }
        }
    }
}

最后

写StudentDemo测试类

package day19.waitdemo;

/*
    等待唤醒机制:
        共享数据:学生对象(name,age)
        生产者线程:对学生对象进行赋值操作
        消费者线程:对学生对象进行取值操作

    为了方便更好的效果,我们可以让生产者赋值不同的信息
    这时候出现了重复取值和姓名与年龄对应不上的情况
    检测线程程序是否存在安全问题的三要素:
        1、是否存在多线程环境?是 生产者和消费者
        2、是否存在共享数据? 是 Student
        3、是否存在多条语句操作共享的数据 是

    解决方案:
        1、加入同步代码块
        2、加入lock锁
    虽然解决了线程安全的问题,但是结果并不是我们想要的结果。在生产者消费者模型中,消费一次应该通知生产者生产。等待唤醒机制
    注意:等待唤醒机制是建立在线程安全基础上设计的。

 */
public class StudentDemo {
    public static void main(String[] args) {
        Student s = new Student();
        ProductThread p1 = new ProductThread(s);
        ConsumerThread c1 = new ConsumerThread(s);
        p1.start();
        c1.start();

    }
}

运行的结果(会不断的运行张三-18和李四-20,需要手动停止)

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值