Java学习笔记 多线程

Java学习笔记 多线程

0,放在前面
  • 不懂的类多点点ctrl+鼠标左键看看源码可能就懂了
1,基本概念
  1. 进程与线程
    • 进程:指的是内存中运行的应用程序,每个进程都有一个独立的内存空间
    • 线程:是进程中的一个执行路径,同一个进程中的线程共享一个内存空间,线程之间可以自由切换,并发执行,一个进程至少有一个线程。一个进程启动之后,里面的若干执行路径又可以划分为若干个线程。
  2. 线程调度
    • 分时调度:所有线程轮流使用CPU,平均分配每个线程占用CPU的时间(时间片)
    • 抢占式调度:让优先级高的线程先使用CPU,如果优先级相同,则随机选择一个。Java默认抢占式调度。
  3. 同步与异步
    • 同步:排队执行,效率低但安全
    • 异步:同时执行,效率高但不安全
  4. 并发与并行
    • 指两个或多个事件在同一个时间段内发生
    • 指两个或多个事件在同一时刻发生(同时发生)
2,Java中常见的创建线程的两种方式
  1. 继承Thread

    public class Demo1 {
        public static void main(String[] args) {
            //创建线程
            MyThread t = new MyThread();
            //启动
            t.start();
            for(int i=0; i<10; i++)
                System.out.println("world"+i);
        }
        //继承Thread的类要重写run方法
        static class MyThread extends Thread{
            @Override
            public void run() {
                for(int i=0; i<10; i++)
                    System.out.println("hello"+i);
            }
        }
    }
    
  2. 实现Runnable

    public class Demo2 {
        public static void main(String[] args) {
            //创建Runnable实例
            MyRunnable r = new MyRunnable();
            //Thread的众多构造方法中,可以穿一个Runnable对象
            Thread t = new Thread(r);
            t.start();
            for(int i=0; i<10; i++)
                System.out.println("你很捞"+i);
        }
        //实现Runnable接口的类也要重写run方法
        static class MyRunnable implements Runnable{
            @Override
            public void run() {
                for(int i=0; i<10; i++)
                    System.out.println("我认为"+i);
            }
        }
    }
    
    • 一般来说,第二种方法更常见,因为java中只能继承一个父类,继承了Thread类就不再能继承其他类了。

    • 查看API可以发现,Thread类其实也实现了Runnable接口。但没有重写Runnable接口中的run方法,这个方法留给程序员自己弄。

3,Thread类
  • 用一段代码学习一下Thread类中的一些常用的方法。

    public class Demo3 {
        public static void main(String[] args) {
            //使用lambda表达式创建Thread对象
            Thread t = new Thread(()-> {
                for(int i=0; i<10; i++){
                    System.out.println(Thread.currentThread().getName()+":"+i);
                    //休眠,休眠1s后自动唤醒,会抛中断异常,trycatch处理一下
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        System.out.println("产生中断,线程死亡");
                        return;
                    }
                }
            });
            //设置为守护进程,将在所有用户线程执行完毕后自杀,这里用户线程只有main,所以main线程执行完后t将停止打印
            t.setDaemon(true);
            //启动
            t.start();
            //main线程中执行的代码块儿
            for(int i=0; i<5; i++){
                System.out.println(Thread.currentThread().getName()+":"+i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //线程中断,这里将在main线程执行完后执行中断,执行中断后将抛出InterruptedException异常,转而执行catch代码块中的内容
            //t.interrupt();
        }
    }
    
    • 用匿名内部类和lambda表达式创建Thread对象并重写run方法,与main线程做比较。
4,线程安全问题(互斥)
  • 考虑这样一个场景,多个机构同时卖票。

    public class Demo4 {
        public static void main(String[] args) {
            Runnable r = new Ticket();
            new Thread(r).start();
            new Thread(r).start();
            new Thread(r).start();
        }
        static class Ticket implements Runnable{
            private static int count = 10;
            @Override
            public void run() {
                while(count>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName()+"->余票:"+count);
    
                }
            }
        }
    }
    
    • 此时会发生什么?3个机构同时卖票,很容易出现安全问题,会发生逻辑错误,余票会出现-1,-2的情况
    • 当count=1时,Thread0刚刚进入while循环,并休眠了1s;此时Thread1抢占了CPU,它也刚刚进入while循环,并休眠了1s;但此时Thread2又抢占了CPU,它不仅进入了while循环,休眠1s后还执行了count–,并打印了语句,此时count=0,Thread2结束运行。
    • 但是此时Thread0和Thread1都不知道count=0,他们继续卖票,假设Thread0又获得了时间片,它执行完count–并打印,此时count=-1,结束循环,Thread0结束运行
    • Thread1接着获得了时间片,也执行完count–并打印,此时count=-2,结束循环,Thread1结束运行。

    如何解决线程安全问题呢?可以将count设置为互斥变量,就行了,可以理解为排队,在一个机构卖票时,其他机构等着,卖完了再抢下一个卖票的机会。换成代码,就是给访问count变量的那段代码上锁。

    1. 同步代码块(synchronized代码块)
    public class Demo5 {
        public static void main(String[] args) {
            Runnable r = new Ticket();
            new Thread(r).start();
            new Thread(r).start();
            new Thread(r).start();
        }
        static class Ticket implements Runnable{
            private static int count = 10;
            //声明一个锁对象,只要是对象就可以,作为参数传到synchronized代码块中
            private Object o = new Object();
            @Override
            public void run() {
                while(true) {
                    //声明synchronized代码块,给这段代码加锁,相当于互斥,在操作count时只能有一个线程在运作
                    synchronized (o) {
                        if(count<=0) break;
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        count--;
                        System.out.println(Thread.currentThread().getName() + "->余票:" + count);
                    }
                }
            }
        }
    }
    
    1. 同步方法(synchronized关键字)
    public class Demo6 {
        public static void main(String[] args) {
            Runnable r = new Ticket();
            new Thread(r).start();
            new Thread(r).start();
            new Thread(r).start();
        }
        static class Ticket implements Runnable{
            private static int count = 10;
            @Override
            public void run() {
                while(true){
                    if(!sale())
                        break;
                }
            }
            //将整个sale方法上隐式锁
            public synchronized boolean sale(){
                if(count > 0) {
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    count--;
                    System.out.println(Thread.currentThread().getName() + "->余票:" + count);
                    return true;
                }
                return false;
            }
        }
    }
    
    
    1. 显示锁Lock
    public class Demo7 {
        public static void main(String[] args) {
            Runnable r = new Ticket();
            new Thread(r).start();
            new Thread(r).start();
            new Thread(r).start();
        }
        static class Ticket implements Runnable{
            private int count = 10;
            //Lock是个接口,用到了多态的概念
            private Lock l = new ReentrantLock();
            @Override
            public void run() {
                while(count>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //手动上锁
                    l.lock();
                    count--;
                    //访问完count后,手动解锁
                    l.unlock();
                    System.out.println(Thread.currentThread().getName()+"->余票:"+count);
    
                }
            }
        }
    }
    
    
5,线程死锁

​ 考虑这样一个场景:绑架,罪犯抓了人质,警察要救人质,要抓罪犯。罪犯的要求是,先放我走,我再放人质,警察说你先放人质,我再放你走。

public class Demo10 {
    public static void main(String[] args) {
        Culprit c = new Culprit();
        Police p = new Police();
        Runnable r = new MyThread(p, c);
        Thread t = new Thread(r);
        t.start();
        c.say(p);
    }

    static class MyThread implements Runnable{
        private Culprit c;
        private Police p;
        MyThread(Police p, Culprit c){
            this.p = p;
            this.c = c;
        }
        @Override
        public void run() {
            p.say(c);
        }
    }
    static class Culprit {
        public synchronized void say(Police p){
            System.out.println("罪犯:放了我,我就放人质");
            p.release();
        }
        public synchronized void runAway(){
            System.out.println("罪犯放了人质,然后逃跑了");
        }
    }
    static class Police {
        public synchronized void say(Culprit c){
            System.out.println("警察:放了人质,我就放你走");
            c.runAway();
        }
        public synchronized void release(){
            System.out.println("警察救了人质,但是罪犯逃跑了");
        }
    }
}

​ 可以这样理解这段代码:myThread线程调用p.say©时给c上了锁,在这个方法刚刚打印语句,准备执行p.release()的时候,此时main线程抢占CPU,它要调用c.say§,也给p上了锁。打印了语句,准备执行c.runAway()的时候,发现c被上锁了,此时CPU给了myThread,myThread想继续推进自己的程序,但是发现p也被上锁了,于是两边都互相等待对方的临界资源的释放。但是他们永远不会释放,这就造成了死锁。

6,多线程通信问题(生产者与消费者)

​ 考虑这样一个场景:一个厨师,一个服务生,一个盘子,厨师做菜,做完放盘子里,服务生上菜,洗盘子,然后将盘子放到厨师那儿。

public class Demo11{
    public static void main(String[] args) {
        Food f = new Food();
        new Cooker(f).start();
        new Waiter(f).start();
    }
    static class Cooker extends Thread{
        private Food f;
        public Cooker(Food f){
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0; i<100; i++){
                if(i%2==0)
                   f.setNameAndTaste("老干妈", "香辣味");
                else
                    f.setNameAndTaste("煎饼果子", "甜辣味");
            }
        }
    }
    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++){
                f.get();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class Food{
        private String name;
        private String taste;
        public void setNameAndTaste(String name, String taste){
            this.name = name;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.taste = taste;
        }
        public void get(){
            System.out.println("服务生端走的菜的名称是:" + name + ",味道:"+ taste);
        }
    }
}

​ 按照最好的情况,厨师没隔0.1s做一道菜,服务生每隔0.1s端一道菜,交替执行,就不会有错乱了,但是实际上着基本上是不可能的。基本上是这种情况:在厨师上老干妈的时候,刚刚设置名字,还没来得及设置味道,服务生就抢占CPU把菜端走了。所以就出现了甜辣味的老干妈。同理也会出现香辣味的煎饼果子。

​ 如何解决?在厨师做饭的时候,服务生睡觉,厨师做好饭,叫醒服务生,自己睡觉。服务生上菜的时候,让厨师睡觉,洗完盘子,叫醒厨师,自己睡觉。同时,在厨师做饭的时候,上锁,服务生不能操作盘子(此处是flag)。同样,服务生在上菜的时候,初始也不能操作盘子。

public class Demo12{
    public static void main(String[] args) {
        Food f = new Food();
        new Cooker(f).start();
        new Waiter(f).start();
    }
    static class Cooker extends Thread{
        private Food f;
        public Cooker(Food f){
            this.f = f;
        }
        @Override
        public void run() {
            for(int i=0; i<100; i++){
                if(i%2==0)
                    f.setNameAndTaste("老干妈", "香辣味");
                else
                    f.setNameAndTaste("煎饼果子", "甜辣味");
            }
        }
    }
    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++){
                f.get();
            }
        }
    }
    static class Food{
        private String name;
        private String taste;
        //flag=true表示可以做菜
        private boolean flag=true;
        public synchronized void setNameAndTaste(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);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //上完菜,叫醒厨师,自己睡觉
                flag = true;
                this.notifyAll();
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
7,线程的6种状态
  • NEW:线程刚被创建,但没启动
  • RUNNABLE:执行中的线程
  • BLOCKED:被阻塞的线程
  • WAITING:无限期等待里另一个线程执行操作
  • TIMED_WAITING:指定时间等待另一个线程执行操作
  • TERMINATED:已退出的线程
8,Callable(创建线程的另一种方法)
  1. Runnable Callable

    接口定义 
    //Callable接口 
    public interface Callable<V> { 
        V call() throws Exception;
    }
    //Runnable接口 
    public interface Runnable { 
        public abstract void run(); 
    }
    
  2. Callable****使用步骤

    1. 编写类实现Callable接口 , 实现call方法
        class XXX implements Callable<T> {
            @Override 
            public <T> call() throws Exception {
                return T; 
            } 
        }
    2. 创建FutureTask对象 , 并传入第一步编写的Callable类对象 
        FutureTask<Integer> future = new FutureTask<>(callable);
    3. 通过Thread,启动线程
        new Thread(future).start();
    
  3. 与Runnable的相同点

  • 都是接口
  • 都可以编写多线程程序
  • 都采用Thread.start()启动线程
  1. 与Runnable的不同点
  • Runnable没有返回值,Callable可以返回执行结果

  • Callable接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主线程继续进行,转而等待子线程执行完毕。

  1. 举个栗子

    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    public class Demo13 {
        public static void main(String[] args) {
            Callable<Integer> c = new MyCallable();
            FutureTask<Integer> f = new FutureTask<>(c);
            new Thread(f).start();
            for(int i=0; i<10; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        static class MyCallable implements Callable<Integer>{
    
            @Override
            public Integer call() throws Exception {
                for(int i=0; i<10; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(1000);
                }
                return 100;
            }
        }
    }
    

    ​ 和Runnable运行效果没什么区别,只不过多了个返回值

    ​ 但是可以调用get()方法获得返回值,这样主线程main就会等待MyCallable线程执行完毕后,获得其返回值,再执行。

    import java.util.concurrent.Callable;
    import java.util.concurrent.ExecutionException;
    import java.util.concurrent.FutureTask;
    
    public class Demo14 {
        public static void main(String[] args) {
            Callable<Integer> c = new MyCallable();
            FutureTask<Integer> f = new FutureTask<>(c);
            new Thread(f).start();
            try {
                Integer integer = f.get();
                System.out.println(integer);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
            for(int i=0; i<10; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        static class MyCallable implements Callable<Integer>{
    
            @Override
            public Integer call() throws Exception {
                for(int i=0; i<10; i++) {
                    System.out.println(Thread.currentThread().getName() + ":" + i);
                    Thread.sleep(1000);
                }
                return 100;
            }
        }
    }
    
    
9,线程池

​ 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程 就会大大降低 系统的效率,因为频繁创建线程和销毁线程需要时间. 线程池就是一个容纳多个线程的容 器,池中的线程可以反复使用,省去了频繁创建线程对象的操作,节省了大量的时间和资源。

​ 线程池的好处:

1. 降低资源消耗
2. 提高响应速度
3. 提高线程的可管理性
  • Java中的4中线程池
  1. 缓存线程池

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Demo15 {
        public static void main(String[] args) {
            /**
             * 缓存线程池长度无限制
             * 执行流程:
             *      1. 判断线程池是否存在空闲线程
             *      2. 存在则使用
             *      3. 不存在则创建线程,放入线程池,并使用
             */
            ExecutorService service = Executors.newCachedThreadPool();
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
    
    
  2. 定长线程池

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Demo16 {
        public static void main(String[] args) {
            /**
             * 定长线程池(指定长度)
             * 执行流程:
             *      1. 判断线程池是否有空闲线程
             *      2. 存在则使用
             *      3. 不存在空闲线程,且线程池为满,则创建线程并放入线程池,然后使用
             *      4. 不存在空闲线程,且线程池已满,则等待,直到线程池存在空闲线程
             */
            ExecutorService service = Executors.newFixedThreadPool(2);
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
    
  3. 单线程线程池

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Demo17 {
        public static void main(String[] args) {
            /**
             * 单线程线程池(定长为1)
             * 其他与定长线程池相同
             */
            ExecutorService service = Executors.newSingleThreadExecutor();
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
            service.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
    }
    
  4. 周期性任务定长线程池

    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    
    public class Demo18 {
        public static void main(String[] args) {
            /**
             * 周期任务定长线程池
             * 定长执行流程:
             *      1. 判断线程池是否存在空闲线程
             *      2. 存在则使用
             *      3. 不存在空闲线程,且线程池未满的情况下,创建线程,并放入线程池,再使用
             *      4. 不存在空闲线程,且线程池未满的情况下,等待,直到线程池存在空闲线程
             * 周期性执行流程:
             *      定时执行,当某个时机被触发时,自动执行某任务
             */
            ScheduledExecutorService service = Executors.newScheduledThreadPool(2);
            /**
             * 定时执行:
             *      1. 参数1:     Runnable类型的任务
             *      2. 参数2:     延迟执行的时长数字
             *      3. 参数3:     时长数字的单位
             */
            service.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            }, 5, TimeUnit.SECONDS);
            /**
             * 周期执行:
             *      1. 参数1:     Runnable类型任务
             *      2. 参数2:     延迟执行的时长数字
             *      3. 参数3:     周期时长(每次执行的间隔时间)
             *      3. 参数3:     时长数字的单位
             */
            service.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            },5, 2, TimeUnit.SECONDS);
        }
    }
    
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java是一种广泛使用的编程语言,有简单、面向对象、跨平台等特点。下面是Java学习的一些重要知识点和学习笔记: 1. Java基础: - 数据类型:Java提供了多种数据类型,包括基本数据类型和引用数据类型。 - 控制流程:学习如何使用条件语句(if-else、switch)、循环语句(for、while)等控制程序的流程。 - 数组:了解如何声明、初始化和操作数组。 - 方法:学习如何定义和调用方法,以及方法的参数和返回值。 - 类和对象:理解类和对象的概念,学习如何定义类、创建对象和使用类的成员变量和方法。 - 继承和多态:掌握继承和多态的概念,了解如何使用继承创建子类,并实现方法的重写和多态的应用。 2. 面向对象编程: - 封装:学习如何使用访问修饰符(public、private、protected)来控制类的成员的访问权限。 - 继承:了解继承的概念和作用,学习如何使用extends关键字创建子类。 - 多态:理解多态的概念和实现方式,学习如何使用父类引用指向子类对象,实现方法的动态绑定。 3. 异常处理: - 异常的分类:了解异常的分类(Checked Exception和Unchecked Exception)和常见的异常类型。 - 异常处理机制:学习如何使用try-catch语句捕获和处理异常,以及使用throws关键字声明方法可能抛出的异常。 4. 输入输出: - 文件操作:学习如何读写文件,包括使用File类、字节流和字符流等。 - 序列化:了解对象的序列化和反序列化,学习如何将对象保存到文件或网络中。 5. 集合框架: - 学习Java提供的集合框架,包括List、Set、Map等常用的集合类,以及它们的特点和用法。 6. 多线程编程: - 学习如何创建和管理线程,了解线程同步和线程间通信的方法。 7. 数据库连接: - 学习如何使用Java连接数据库,执行SQL语句,进行数据的增删改查操作。 以上是Java学习的一些重要知识点和学习笔记,希望对你有帮助!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值