关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白

前言

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,即进程空间或(虚空间)。进程不依赖于线程而独立存在,一个进程中可以启动多个线程。
线程是指进程中的一个执行流程,一个进程中可以运行多个线程。线程总是属于某个进程,线程没有自己的虚拟地址空间,与进程内的其他线程一起共享分配给该进程的所有资源,统一进程内的线程共享一个堆内存,每个线程具有自己的栈内存。“同时”执行是人的感觉,在线程之间实际上轮换执行。

同步与异步

同步:排队执行,效率低但是安全。
异步:同步执行,效率高但是数据不安全。

并发与并行

并发:指两个或多个事件在同一时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。

线程创建的3种方式

1.继承方法

public  class MyThread extends Thread{
    @Override
    public void run(){
    }
}
        MyThread m = new MyThread();
        m.start();

2.接口方法

        //实现runnable
        //1 创建一个任务对象
        MyRunnable r = new MyRunnable();
        //创建一个线程并给他一个任务
        Thread t = new Thread(r);
        //启动线程
        t.start();

接口的优势:

实现Runnable与继承Thread相比有如下优势
1.通过创建任务,然后给线程分配任务的方式实现多线程,更适合多个线程同时执行任务的情况
2,可以避免单继承所带来的局限性
3,任务与线程是分离的,提高了程序的健壮性
4,后期学习的线程池技术,接受Runnable类型的任务,不接受Thread类型的线程

    public static void main(String[] args) {
        new Thread(){
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("12345"+i);
                }
            }
        }.start();

但是继承Thread有个很简单的实现方式,通过匿名内部类重写run()不用重新创建一个类而简单的实现了多线程,每个线程都有自己的栈空间,而共用一个堆内存。
由一个线程调用的方法,方法指挥执行在此线程中。

3.Callable实现线程的状态的返回(实现了Callalble接口)

Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执
行,如果不调用不会阻塞。

    Callable callable= new MyCallable();
    FutureTask future = new FutureTask<>(callable); 
    new Thread(future).start();
    Integer j=task.get();
    System.out.println("return"+j);
  1. 编写类实现Callable接口 , 实现call方法
class Mycallable implements Callable { 
    @Override 
    public  call() throws Exception { 
    return T; 
    } 
} 
  1. 创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(callable);
  2. 通过Thread,启动线程 new Thread(future).start();

如果调用get方法,则主线程等待其执行完成之后再执行,如果不调用则对主线程无影响可并行。

FutureTask()方法
关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白_第1张图片

其父类Future()方法
关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白_第2张图片

守护线程与用户线程

线程分为守护线程和用户线程
用户线程:当一个进程不包含任何的存活的用户线程时,进行结束
守护线程:守护用户线程的,当最后一个用户线程结束时,所有守护线程自动死亡。
java默认线程为用户线程,通过线程调用setDaemon(true)设置守护线程。

        Thread t1 = new Thread(new MyRunnable());
        //设置守护线程
        t1.setDaemon(true);
        t1.start();

线程同步的三种方法

1.同步代码块,指定锁比如同一个object

格式:synchronized(锁对象){}

public class Demo8 {
    public static void main(String[] args) {
        Object o = new Object();
        Runnable run = new Ticket();
        new Thread(run).start();
        new Thread(run).start();
        new Thread(run).start();
    }

    static class Ticket implements Runnable{
        //总票数
        private int count = 10;
        private Object o = new Object();
        @Override
        public void run() {
                while (true) {
                    synchronized (o) {
                        if (count > 0) {
                         //卖票
                            System.out.println("正在准备卖票");
                            try {
                            Thread.sleep(1000);
                            } catch (InterruptedException e) {
                            e.printStackTrace();
                            }
                            count--;
                            System.out.println(Thread.currentThread().getName()+"卖票结束,余票:" + count);
                        }else {
                            break;
                        }

                }
            }
        }
    }
}

2. 同步方法

run()里面调用同步方法,如果是普通方法使用方法内部的this(此对象)作为锁,如果是静态方法是类名.class(字节码文件对象)

        public synchronized boolean test(){
                System.out.println("测试");
            }

3.显示锁

同步代码块和同步方法都是隐式锁。
显式锁更加直观,体现面向对象,自己生成锁,自己加锁解锁。

    static class Test implements Runnable{
        //总票数
        //参数为true表示公平锁    默认是false 不是公平锁
        private Lock l = new ReentrantLock(true);
        @Override
        public void run() {
            l.lock();
            System.out.println("测试");
            l.unlock();
        }
    }

公平锁与不公平锁

公平锁:先来先到,一起排队
不公平锁:大家一起抢
java默认都是不公平锁,可以通过显式锁中的构造方法实现公平锁。

//设置公平锁
private Lock l= new ReentrantLock(true);

不传参数默认false不公平,传入参数则公平

死锁的避免

在任何有可能导致锁产生的方法里,不要调用另外一个有可能产生锁的方法让另外一个锁产生。
下面是死锁产生的例子:

public class Demo11 {
    public static void main(String[] args) {
        //线程死锁
        Culprit c = new Culprit();
        Police p = new Police();
        new MyThread(c,p).start();
        c.say(p);
    }

    static class MyThread extends Thread{
        private Culprit c;
        private Police p;
        MyThread(Culprit c,Police p){
            this.c = c;
            this.p = p;
        }

        @Override
        public void run() {
            p.say(c);
        }
    }
    static class Culprit{
        public synchronized void say(Police p){//两方法公用this(此对象)锁,第一个方法不执行完无法执行第二个方法
            System.out.println("罪犯:你放了我,我放了人质");
            p.fun();
        }
        public synchronized void fun(){
            System.out.println("罪犯被放了,罪犯也放了人质");
        }
    }
    static class Police{
        public synchronized void say(Culprit c){
            System.out.println("警察:你放了人质,我放了你");
            c.fun();
        }
        public synchronized void fun(){
            System.out.println("警察救了人质,但是罪犯跑了");
        }
    }
}

此时为死锁

若第一个线程执行(假设)p.fun()时,第二个线程p还没执行p.say()方法,此时没锁到,两方法都能执行,但出现错误。
关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白_第3张图片

生产者与消费者

1.一个类,可以修改可以读取,如果不使用线程同步,在修改时发生了另一线程的读取操作,会发生读取脏数据的情况。
2.通过加入synchronized线程同步方法,修改时无法读取可以解决读取脏数据的问题,但是无法实现两线程之间的同步,会发生一个线程的抢占时间片现象无法同步。
3.如果想要读取和修改可以互相等待同步,还需要加入wait和notify操作,一个执行完notifyall休息,置判别标识,另一个被唤醒后读取判别标识执行然后休息,在唤醒另一个。加入flag标志判断线程执行先后。

package com.java.demo;

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

public class Demo4  {

    /**
     * 多线程通信问题, 生产者与消费者问题
     * @param args
     */
    public static void main(String[] args) {
        Food f = new Food();
        new Cook(f).start();
        new Waiter(f).start();
    }

    //厨师
    static class Cook extends Thread{
        private Food f;
        public Cook(Food f) {
            this.f = f;
        }

        @Override
        public void run() {
            for(int i=0;i<100;i++){
                if(i%2==0){
                    f.setNameAndSaste("老干妈小米粥","香辣味");
                }else{
                    f.setNameAndSaste("煎饼果子","甜辣味");
                }
            }
        }
    }
    //服务生
    static class Waiter extends Thread{
        private Food f;
        public Waiter(Food f) {
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0;i<100;i++){
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                f.get();
            }
        }
    }
    //食物
    static class Food{
        private String name;
        private String taste;

        //true 表示可以生产
        private boolean flag = true;

        public synchronized void setNameAndSaste(String name,String taste){
            if(flag) {
                this.name = name;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.taste = taste;
                flag = false;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        public synchronized void get(){
            if(!flag) {
                System.out.println("服务员端走的菜的名称是:" + name + ",味道:" + taste);
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

线程的状态

关于Java多线程看这一篇就够了,从创建线程到线程池分析的明明白白_第4张图片

线程状态。 线程可以处于以下状态之一:
NEW
尚未启动的线程处于此状态。
RUNNABLE
在Java虚拟机中执行的线程处于此状态。
BLOCKED
被阻塞等待监视器锁定的线程处于此状态。
WAITING
无限期等待另一个线程执行特定操作的线程处于此状态。
TIMED_WAITING
正在等待另一个线程执行最多指定等待时间的操作的线程处于此状态。
TERMINATED
已退出的线程处于此状态。
线程在给定时间点只能处于一种状态。 这些状态是虚拟机状态,不反映任何操作系统线程状态。

线程池

1.缓存线程池

无限制长度
任务加入后的执行流程:
1.判断线程池是否存在空闲线程
2.存在则使用
3.不存在则创建线程并使用


        ExecutorService service = Executors.newCachedThreadPool();
        //指挥线程池执行新的任务
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"测试");
            }

        });

2.定长线程池

长度是指定的线程池
加入任务后的执行流程:
1 判断线程池是否存在空闲线程
2 存在则使用
3 不存在空闲线程 且线程池未满的情况下 则创建线程 并放入线程池中 然后使用
4 不存在空闲线程 且线程池已满的情况下 则等待线程池的空闲线程

        //设置定长线程池
        ExecutorService service = Executors.newFixedThreadPool(2);
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"测试");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        });

3.单线程线程池

效果与长度为1的定长线程池一样。
执行流程
1 判断线程池的那个线程是否空闲
2 空闲则使用
3 不空闲则等待它空闲后再使用

        ExecutorService service = Executors.newSingleThreadExecutor();
        service.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"测试");
            }
        });

4.周期定长线程池

执行流程
    1 判断线程池是否存在空闲线程
    2 存在则使用
    3 不存在空闲线程  且线程池未满的情况下  则创建线程  并放入线程池中  然后使用
    4 不存在空闲线程  且线程池已满的情况下  则等待线程池的空闲线程

周期性任务执行时:
定时执行 当某个任务触发时 自动执行某任务

执行一次:

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
        //定时执行一次
        //参数1:定时执行的任务
        //参数2:时长数字
        //参数3:2的时间单位    Timeunit的常量指定
           scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"测试");
            }
        },5, TimeUnit.SECONDS);      //5秒钟后执行*/

周期执行

        周期性执行任务
            参数1:任务
            参数2:延迟时长数字(第一次在执行上面时间以后)
            参数3:周期时长数字(没隔多久执行一次)
            参数4:时长数字的单位
        * **/
        scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+"测试");
            }
        },5,1,TimeUnit.SECONDS);

Lambda表达式

再使用匿名内部类作为参数时,可以使用lambda写法来极大的简化代码。

        //冗余的Runnable编写方式
       Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("测试");
            }
        });
        t.start();

保留传递的参数,保留要重写的方法体,中间用-> 连接

        Thread t = new Thread(() -> System.out.println("测试"));

最后

感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!

你可能感兴趣的:(java,多线程,程序员,面试)