day_9/07(浅谈多线程)

目录

前言

线程与多线程的的概念

进程与线程

多线程

多线程的实现

Thread类

Runnnable接口

线程安全

同步代码块

同步方法

Lock锁

死锁

什么是死锁

等待唤醒机制

线程池

线程池的使用

Callable接口


前言

学习java编程的时候,我们都知道,Java程序都是以主函数为入口,执行主函数里面的语句和方法,直到执行完最后一条语句,整个Java语句才执行完成,但是不知道大家有没有发现一个问题,这样执行下去后,程序在某一时刻做的都是一件事情,执行的是一条语句,那有没有一种办法可以让计算机同时做多件事情呢,有的,使用多线程即可达到如上所属功能,下面就请跟随我的脚步一起来了解什么是多线程吧。

线程与多线程的的概念

进程与线程

学习多线程之前我们先来了解一下什么是进程和什么是线程,前面我们说到,计算机运行一个程序的时候是不是一个执行语句的过程,那么这个过程叫什么呢?它叫做进程;那一个进程是不是只做一件事情呢?也不是的,举一个简单的例子,我们在使用音乐播放器的时候是不是可以一边播放音乐一边去搜索音乐,这就是一个程序同时做的两件不同的事,单独拿出它们其中一件事,那么这一件事就可以叫做线程,一个线程只能存在一个进程当中,但是一个进程可以包含多个进程。

多线程

多线程就是一个程序开启多个线程,但是计算机不就一个CPU吗?一个CPU怎么同时做多件事呢?确实不能,从微观角度来看,在计算机只有一个CPU的情况下,我们所说的多线程并不是真实的多线程,CPU一次只能执行一条指令,JVM得到CPU的使用权后先让CPU执行某一线程的一条指令之后再去执行另一线程的一条指令,因为CPU的执行速度非常快,所以在宏观上我们是看不出区别的,好像就是计算机同时执行了多条线程,这里只介绍这么多,关于jvm是怎么得到CPU的使用权的,也就是CPU的调度问题,这里就不解释了。

多线程的实现

Thread类

Thread类翻译过来就是线程,Java里面就是创建Thread类的一个子类的对象或Thread类对象来表示一个线程的,随后调用start方法就可以开启线程了。

看一个简单的例子:

public class Test {
    public static void main(String[] args) {
        class MyThread extends Thread{
            public MyThread(String str) {
                super(str);
            }

            @Override
            public void run() {
                System.out.println("我是一个线程,我的线程名称是:"+Thread.currentThread().getName());
                while(true){
                }
            }
        }
        //创建Thread子类的对象
        MyThread thread1 = new MyThread("线程1");
        //调用start方法,启动线程
        thread1.start();
    }
}

这样便简单实现了一个线程的创建和启动了。

Runnnable接口

Runnable是一个接口,它只有一个run方法,是用来创建任务的,以一个Runnable的实现类对象作为参数创建Thread类对象也是一个线程,这里有一个问题,我既然可以直接继承Thread类来创建线程为什么还要搞个Runnable接口,然后又用Runnable接口的实现类对象来作为参数创建Thread类线程呢?对于这个问题,我想说的是,如果某个子类已经有父类了,那么它不就不能继承Thread类了,Java是单继承的啊,通过继承Thread类来创建线程的方法是有局限性的,所以才出现了Runnable接口。

看代码演示:

public class Test {
    public static void main(String[] args) {
        class Task implements Runnable {
            @Override
            public void run() {
                System.out.println("我是一个线程任务类,我的线程名称是:"+Thread.currentThread().getName());
                while(true){}
            }
        }
        Thread thread = new Thread(new Task(), "线程一");
        thread.start();
    }
}

线程安全

当我们开启的多个线程共享资源的时候,就会发生资源不统一的情况,看一个例子,我们现在用两个线程来输出1~100,看代码:

public class Test {
    public static void main(String[] args) {
        class MyThread extends Thread{
            int x = 1;
            @Override
            public void run() {
                while(true){
                    if (x<11){
                        System.out.println(Thread.currentThread().getName()+"输出了:"+x);
                        x++;
                    }
                }
            }
        }
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "线程1");
        Thread thread2 = new Thread(myThread, "线程2");
        thread1.start();
        thread2.start();
    }
}

运行结果:

观察运行结果,我们发现,1被线程1输出了一次,被线程2又输出了一次,这是什么原因呢?这又牵涉到CPU调度的问题了,因为CPU的调度是无法认为干预的,这是不可控的,即我也不知道在一个线程处理共享的数据时,另一个线程有没有过来使用这个共享的数据,那有没有解决办法呢,有的,既然CPU的调度不可控, 那就从线程入手啰,控制每次只能有一个线程去处理共享的数据,这样不就可以避免这种情况了吗?那如何控制呢?同步就出现了。

同步代码块

同步代码块不需要另外新建方法,直接在run方法里面书写就可以了,还是上面那个例子,看代码演示:

public class Test {
    public static void main(String[] args) {
        class MyThread extends Thread{
            int x = 1;
            @Override
            public void run() {
                while(true){
                    synchronized ("lock"){
                        if (x<101){
                            System.out.println(Thread.currentThread().getName()+"输出了:"+x);
                            x++;
                        }
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "线程1");
        Thread thread2 = new Thread(myThread, "线程2");
        thread1.start();
        thread2.start();
    }
}

运行结果: 

此时就不会发生共享数据不统一的问题了。 这里需要注意的问题是,同步的关键字synchronized,我们发现,它带了一个字符串类型的参数,这个参数是一个锁对象,可以是任意的,但是必须是唯一的,后面讲到死锁和等待唤醒机制的时候再讲。

同步方法

与同步类似,不过同步方法没有锁对象,看代码演示:

public class Test {
    public static void main(String[] args) {
        class MyThread extends Thread{
            int x = 1;
            @Override
            public void run() {
                while(true){
                    print();
                }
            }
            public synchronized void print(){
                if (x<101){
                    System.out.println(Thread.currentThread().getName()+"输出了:"+x);
                    x++;
                }
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "线程1");
        Thread thread2 = new Thread(myThread, "线程2");
        thread1.start();
        thread2.start();
    }
}

运行结果和直接使用同步代码块是类似的,不会出现数据不统一的情况。

Lock锁

除了使用同步代码块之外,还有一种方法可以控制线程的出入,它就是Lock接口,它其实就是同步锁的升级,在JDK5之后出现的,同步锁的上锁和解锁都是隐式的,我们并不知道哪里上了锁,哪里解了锁,而Lock接口提供了lock方法和unlock方法,用来给一段代码上锁和解锁,这样我们边可以直观的看出哪里上锁哪里解锁了,Lock接口有一实现类ReentrantLock,所以我们使用它来创建对象,从而调用lock方法和unlock方法,还是和前面一样的例子吧,使用多个线程来输出1到100,看代码:

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

public class Test {
    public static void main(String[] args) {
        class MyThread extends Thread{
            int x = 1;
            @Override
            public void run() {
                Lock lock = new ReentrantLock();
                while(true){
                    lock.lock();
                        if (x<101){
                            System.out.println(Thread.currentThread().getName()+"输出了:"+x);
                            x++;
                        }
                    lock.unlock();
                }
            }
        }
        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "线程1");
        Thread thread2 = new Thread(myThread, "线程2");
        thread1.start();
        thread2.start();
    }
}

死锁

什么是死锁

下面来介绍一下死锁,前面说了同步代码块需要一个锁对象才可以限制线程的出入,每次只能一个线程进入代码块并执行,那么如果同步代码块嵌套的情况,运行一段代码需要两个锁对象呢?讲的简单一点,一个线程进入了一个同步代码块,获取了一个锁对象lock_a,同步代码块里面还有一个同步代码块需要一个锁对象lock_b,两个锁对象同时获取后才可以执行同步代码块里面的代码,另一个线程刚好相反,需要先获取锁对象lock_b,再获取锁对象lock_a才可执行同步代码块里面的代码,那么会不会出现这样一种情况,在某一时刻,线程一获取了锁对象lock_a,但是线程二又获取了锁对象lock_b,此时两个线程互相占用了另一线程所需要的对象,但是自己没有完成任务,所占用的锁对象又不会交出去,这个时候,就会发生线程互相等待的情况,这个情况就叫做死锁。

下面用一个例子来模拟一下死锁,看代码:

public class Test {
    public static void main(String[] args) {
        class MyThread implements Runnable {
            boolean flag = true;
            @Override
            public void run() {
                if (flag) {
                    while (true) {
                        synchronized ("lock_a") {
                            System.out.println(Thread.currentThread().getName()+"获取到了锁对象lock_a,还需锁对象lock_b才可输出001");
                            synchronized ("lock_b") {
                                System.out.println("001");
                            }
                        }
                    }
                } else {
                    while (true) {
                        synchronized ("lock_b") {
                            System.out.println(Thread.currentThread().getName()+"获取到了锁对象lock_b,还需锁对象lock_a才可输出002");
                            synchronized ("lock_a") {
                                System.out.println("002");
                            }
                        }
                    }

                }
            }
        }

        MyThread myThread = new MyThread();
        Thread thread1 = new Thread(myThread, "线程1");
        Thread thread2 = new Thread(myThread, "线程2");
        thread1.start();
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        myThread.flag = false;
        thread2.start();
    }
}

运行结果: 

观察结果,发现,两个线程互相占用了另一线程所需要的的所对象,两个线程就互相等待,这就是死锁。 

死锁不能直接解决,只能说可以避免,使用嵌套同步代码块就容易产生死锁,所以在开发中尽量不要使用嵌套同步代码块,如果一定要使用也是可以的,只要保证使用的锁对象唯一就可以了。

等待唤醒机制

前介绍的同步即死锁都是多个线程围绕着一个任务进行的,但在实际开发中往往都是多个线程处理多个任务的,这是没有问题的,现在有个问题来了,有没有一种办法可以让处理不同任务的不同线程产生联系,互相通信呢?有的,等待唤醒机制就是来让线程之间又通信的。看一个例子,蓝牙耳机,它是不是有两种状态,一种是有电的时候可以播放音乐,没电的时候充电,我们把播放音乐放电看作一个线程,充电看作另一个线程,两个线程不能同时进行,那播放音乐把电量耗尽之后怎么去充电,电充满后怎么去播放音乐呢?先看代码实现:

public class Test {
    public static void main(String[] args) {
        //创建一个类代表蓝牙耳机
        class BluetoothHeadset {
            //用一个Boolean类型的变量来代表电量状态,有电为true,没电为false
            //boolean类型成员变量没赋值默认为false
            boolean battery;
        }
        //创建一个充电线程任务类
        class Charge implements Runnable {
            BluetoothHeadset bh;
            public Charge(BluetoothHeadset bh) {
                this.bh = bh;
            }
            @Override
            public void run() {
                while (true) {
                    synchronized ("lock") {
                        //如果电池有电,则充电线程等待
                        if (bh.battery == true) {
                            try {
                                //使当前锁对象进入监视状态,等待被唤醒
                                "lock".wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        //代码执行到这里说明电池没电
                        System.out.println("电池没电了,充电中...");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("电池充满了,可以播放音乐了");
                        bh.battery = true;
                        //唤醒在监视器上等待的线程
                        "lock".notify();
                    }
                }
            }
        }
        //创建一个播放音乐耗电的线程任务类
        class DisCharge implements Runnable {
            BluetoothHeadset bh;

            public DisCharge(BluetoothHeadset bh) {
                this.bh = bh;
            }

            @Override
            public void run() {
                while (true) {
                    synchronized ("lock") {
                        if (bh.battery == false) {
                            try {
                                "lock".wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        System.out.println("电池有电,播放音乐中...");
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println("电池没电了需要充电了");

                        bh.battery = false;
                        "lock".notify();
                    }

                }
            }
        }
        BluetoothHeadset bh = new BluetoothHeadset();
        Charge charge = new Charge(bh);
        DisCharge disCharge = new DisCharge(bh);
        Thread thread = new Thread(charge);
        Thread thread1 = new Thread(disCharge);
        thread.start();
        thread1.start();
    }
}

运行结果: 

观察该结果,我们发现,输出的内容是由两个线程交替输出的,这是怎么实现的呢?代码里面出现了两个新方法,一个是wait方法,一个是notify方法,wait方法是用来让当前线程进入监视器等待被唤醒的,notify方法是用来唤醒一个线程的,不过这个唤醒是随机的,上面代码中只使用了两个线程,所以它们之间每次唤醒的线程都是对方那个线程,这里还有一个细节,两个类里面的同步代码块使用的锁对象是相同的,并且wait方法和notify方法都是定义在Object类里面的,这就实现了锁对象可以是任意类,为了方便,我这里使用的一个String对象, 因为多个String对象的内容相同的时候,它们在内存里面的地址是相同的,这就保证了锁对象的唯一性,看一段代码:

public class Test {
    public static void main(String[] args) {
        String a = "王建国";
        String b = "王建国";
        //两个对象之间,==比较的是地址,equals方法比较的才是内容
        System.out.println(a.equals(b));          //true,结果显而易见,定义的时候是一样的内容
        System.out.println(a.equals("王建国"));    //true,这个结果是毫无疑问的
        System.out.println(a == b);               //true,a与b地址相同,说明指向了同一个对象
        System.out.println(a == "王建国");         //true,a与另一个内容是”王建国“的String对象地址相同,说明指向的也是同一个对象
        System.out.println(("王"+"建国") == a);    //true,两个String对象通过字符串连接符变成了一个新对象,内容与a一样,并且地址也相等
        //由此证明,几个String对象内容相同的时候它们就是同一个对象
    }
}

这段代码就证明了String对象的唯一性;

除此之外还有一个细节,这里用的是同步代码块,因为只有同步代码块才有锁对象。 

线程池

线程池,见其名便知其意,这是用来存放线程的,它存在的意义是什么,我需要几个线程我就创建几个线程,线程结束后,我重新创建不就可以了,为什么还要多次一举,把线程放到线程池里面呢,在线程数量不多的时候确实对系统的影响不大,但是线程数量一多,并且线程很短的时间仍无就结束了,那影向就体现出来了,频繁的创建和销毁线程是需要时间的,所以多线程概念就出来了,先创建一些线程放到放到线程池里面,需要执行任务了就从线程池里面拿,用完就放回去,不用销毁线程,这样不仅实现了线程的复用,还节省了线程创建和销毁的时间,这系统效率不就高起来了。

线程池的使用

自己配置一个线程池是比较复杂的,配置的好那没问题,配置的不好可能还会负优化,所以我们一般使用Executors线程工厂类里面的静态方法来创建线程池,还是那个例子,输出1~100,看代码实现:

public class Test {
    public static void main(String[] args) {
        //创建一个线程任务类
        class Task implements Runnable{
            int x = 1;
            @Override
            public void run() {
                while(true){
                    synchronized ("lock"){
                        if (x<101){
                            System.out.println(Thread.currentThread().getName()+"输出了"+x++);
                        }
                    }
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        //创建线程任务类对象
        Task task = new Task();
        //创建线程池对象
        //使用Executors类里的newFixedThreadPool静态方法创建线程,
        // 需要几个创建几个,现在我们先创建3个线程来测试一下
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        //从线程池拿线程来完成任务
        //开启线程的方法是submit,
        executorService.submit(task);
        executorService.submit(task);
        executorService.submit(task);
    }
}

运行结果: 

Callable接口

Callable接口和Runnable接口类似,不过Runnable接口里的run方法没有返回值,而Callable接口的call方法有返回值,现在用一个线程来求1~100的和,并把结果返回,看一段代码:

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个线程任务类,实现Callable接口
        class Task implements Callable{
            @Override
            public Object call() throws Exception {
                int sum = 0;
                for (int i = 1; i < 101; i++) {
                    sum += i;
                }
                return sum;
            }
        }
        //创建线程任务类对象
        Task task = new Task();
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        //获取返回值并输出
        System.out.println(executorService.submit(task).get());
    }
}

运行结果:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值