• Java深入学习(1):多线程


    多线程目的:在同一时刻有多条不同路径执行程序,提高程序运行效率

    多线程应用:数据库连接池,多线程文件下载等

    注意:在文件下载中使用多线程,无法提高速度

    在一个进程中,一定会有主线程

    从基础开始,多线程的使用方式:

    1.继承Thread类:(不推荐)

    public class ThreadDemo extends Thread {
        @Override
        public void run() {
            //写入线程执行的代码
        }
    
        public static void main(String[] args) {
            ThreadDemo threadDemo = new ThreadDemo();
            threadDemo.start();
        }
    }

    注意:threadDemo调用的是start方法;如果调用了run方法,本质上还是单线程

    2.实现Runnable接口:

    public class ThreadDemo implements Runnable {
        @Override
        public void run() {
            //写入线程执行的代码
            System.out.println("demo");
        }
    
        public static void main(String[] args) {
            ThreadDemo threadDemo = new ThreadDemo();
            new Thread(threadDemo).start();
        }
    }

    3.匿名内部类

    public class ThreadDemo {
        public static void main(String[] args) {
            new Thread() {
                @Override
                public void run() {
                    //写入线程执行的代码
                }
            }.start();
        }
    }

    Java8可以简写为这样

    public class ThreadDemo {
        public static void main(String[] args) {
            new Thread(() -> {
                //写入线程执行的代码
            }).start();
        }
    }

    多线程的状态:

    1.新建状态:调用start方法之前

    2.就绪状态:调用start方法,等待CPU分配执行权

    3.运行状态:执行run方法中的代码

    4.死亡状态:run方法执行完毕

    5.阻塞状态:调用wait或sleep方法,线程变为阻塞状态,阻塞状态可以直接变成就绪状态

    守护线程:

    在Java程序中,有主线程和GC线程(用于回收垃圾),主线程死亡后,GC线程也会死亡,同时销毁

    这种和主线程一起销毁的线程就是守护线程

    非守护线程:线程的状态和主线程无关

    用户线程:以上的三种方式创建的都是用户现场,由主线程创建,也是非守护线程

    示例:

    public class ThreadDemo {
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i = 0; i < 30; i++) {
                    try {
                        Thread.sleep(300);
                        System.out.println("子线程i:" + i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            for (int i = 0; i < 5; i++) {
                System.out.println("主线程i:" + i);
            }
            System.out.println("主线程执行完毕");
        }
    }

    观察输出发现:打印主线程执行完毕之后,还在继续打印子线程执行信息

    只需要对子线程进行设置,即可变成守护线程:

    public class ThreadDemo {
        public static void main(String[] args) {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 1000; i++) {
                    System.out.println("子线程i:" + i);
                }
            });
            thread.setDaemon(true);
            thread.start();
            for (int i = 0; i < 10; i++) {
                System.out.println("主线程i:" + i);
            }
            System.out.println("主线程执行完毕");
        }
    }

    观察输出发现:子线程还未打印到999,程序已经结束

    join方法:

    A线程调用了B线程的join方法,那么A等待B执行完毕之后再执行(A释放CPU执行权)

    示例:主线程让子线程执行完毕再执行

    public class ThreadDemo {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                for (int i = 0; i < 60; i++) {
                    System.out.println("子线程i:" + i);
                }
            });
            thread.start();
            thread.join();
            for (int i = 0; i < 10; i++) {
                System.out.println("主线程i:" + i);
            }
            System.out.println("主线程执行完毕");
        }
    }

    观察输出发现:子线程打印完59,才开始主线程的打印

    线程安全问题:

    当多个线程共享同一个全局变量,做写的操作时候,会发生线程安全问题

    模拟线程安全问题:车站卖票经典案例

    public class ThreadDemo implements Runnable {
        //一共有一百张票
        private int count = 100;
    
        @Override
        public void run() {
            while (count > 0) {
                try {
                    Thread.sleep(100);
                    sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private void sale() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
                count--;
            }
        }
    
        public static void main(String[] args) {
            ThreadDemo threadDemo = new ThreadDemo();
            Thread t1 = new Thread(threadDemo, "窗口1");
            Thread t2 = new Thread(threadDemo, "窗口2");
            t1.start();
            t2.start();
        }
    }

    观察输出发现:很多票重复出售

    线程安全问题解决:

    1.在sale方法上使用synchronized关键字

    原理:当线程进入该方法时候会自动获取锁,一旦某线程获取了锁,其他线程就会等待,等到执行完毕该线程代码,释放锁

    缺点:降低程序效率,每次执行该方法都需要进行判断

        private synchronized void sale() {
            if (count > 0) {
                System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
                count--;
            }
        }

    2.使用同步代码块

    public class ThreadDemo implements Runnable {
        //一共有一百张票
        private int count = 100;
    
        private final Object object = new Object();
    
        @Override
        public void run() {
            while (count > 0) {
                try {
                    Thread.sleep(100);
                    sale();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        private void sale() {
            synchronized (object) {
                if (count > 0) {
                    System.out.println(Thread.currentThread().getName() + "出售第" + (100 - count + 1) + "张票");
                    count--;
                }
            }
        }
    
        public static void main(String[] args) {
            ThreadDemo threadDemo = new ThreadDemo();
            Thread t1 = new Thread(threadDemo, "窗口1");
            Thread t2 = new Thread(threadDemo, "窗口2");
            t1.start();
            t2.start();
        }
    }

    观察输出:问题解决

    注意:如果写成这样还是存在问题

        public static void main(String[] args) {
            ThreadDemo threadDemo1 = new ThreadDemo();
            ThreadDemo threadDemo2 = new ThreadDemo();
            Thread t1 = new Thread(threadDemo1, "窗口1");
            Thread t2 = new Thread(threadDemo2, "窗口2");
            t1.start();
            t2.start();
        }

    这时候需要给全局变量加上static关键字:共享同一个锁

        private static int count = 100;
    
        private static final Object object = new Object();

    观察输出:问题解决

    多线程死锁问题:

    产生场景:初学者喜欢每个地方都加入synchronized,于是synchronized中嵌套synchronized,容易产生死锁

    产生原因:A线程拿到了锁2,现在需要拿锁1;B线程拿了锁1,现在需要拿锁2;A线程拿不到锁1就不会释放锁2;B线程拿不到锁2就不会释放锁1

    ThreadLocal类:

    什么是ThreadLocal:给每一个线程提供局部变量

    原理:底层是一个Map集合,获取当前线程,然后调用Map的put和get方法实现

    初始化:

        public static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

    获取:

            threadLocal.get();

    设置:

            threadLocal.set(count);

    多线程特性:

    1.原子性

    2.可见性

    3.有序性

    Java内存模型(JMM):

    JMM决定一个线程对共享变量的写入时,能够另一个线程是否可见

    主内存:共享存储的变量

    本地内存:共享变量的副本

    线程安全问题根本原理:共享变量存放于主内存中,每一个线程都有本地内存。比如我在主内存中存入count=100,那么两个本地内存都存放了count=100副本。这时候两个线程同时操作共享变量count-1,首先两个线程要现在本地内存进行count-1操作,然后刷新到主内存。于是,出现了线程安全问题!

    Volatile关键字:

    一个示例:

    class ThreadTest extends Thread {
        public boolean flag = true;
    
        @Override
        public void run() {
            System.out.println("线程开始");
            while (flag) {
    
            }
            System.out.println("线程结束");
        }
    
        public void setRunning(boolean flag) {
            this.flag = flag;
        }
    }
    
    public class ThreadDemo {
        public static void main(String[] args) throws InterruptedException {
            ThreadTest threadTest = new ThreadTest();
            threadTest.start();
            Thread.sleep(3000);
            threadTest.setRunning(false);
            System.out.println("flag改为false");
            Thread.sleep(3000);
            System.out.println("flag:" + threadTest.flag);
        }
    }

    打印如下:

    线程开始
    flag改为false
    flag:false

    然后程序卡死

    为什么已经把flag改为false,子线程还是走入了while循环

    因为:主线程把flag改了,还没有刷入主内存,子线程一直在读本地内存中的变量

    解决:只需要加入volatile关键字

    作用:将修改的值立即更新到主内存,保证其他线程对该变量的可见

        public volatile boolean flag = true;

    打印如下:

    线程开始
    flag改为false
    线程结束
    flag:false 

    注意:volatile只能保证可见性,不能保证线程安全

    使用场景:观察主流框架,可以发现只要是全局共享的变量,都加入了volatile关键字

    Synchronized与Volatile关键字区别:

    Volatile保证可见性,不能保证原子性,也就是不能保证线程安全,禁止重排序

    Synchronized既可以保证原子性,也可以保证线程安全,不禁止重排序

    重排序:

    概念:CPU会对代码实现优化,不会对有依赖关系性做重排序

    什么是依赖关系:

                int a = 1;
                int b = 2;
                int c = a + b;

    c依赖a,b。c和a,b都有关系。c一定在a,b之后执行,而a,b执行顺序不一定

    所以在代码执行时候,可能先执行的是int b = 2而不是int a = 1

    但是在这里执行的结果不会发生改变

    注意:一般只会在多线程中遇到重排序问题

    重排序问题的解决:加入volatile关键字 

    线程之间的通信:

    多个线程在处理同一个资源,但是线程的任务却不相同,通过一定的手段使各个线程能有效地利用资源,

    这种手段即:等待唤醒机制,又称作线程之间的通信

    涉及到的方法:wait(),notify()

    示例:

    两个线程一个输入,一个输出

    package demo;
    
    public class Resource {
        public String name;
        public String sex;
    }

    输入线程:

    package demo;
    
    public class Input implements Runnable {
        private Resource r = new Resource();
    
        public void run() {
            int i = 0;
            while (true) {
                if (i % 2 == 0) {
                    r.name = "张三";
                    r.sex = "男";
                } else {
                    r.name = "李四";
                    r.sex = "女";
                }
                i++;
            }
        }
    
    }

    输出线程:

    package demo;
    
    public class Output implements Runnable {
        private Resource r = new Resource();
        public void run(){
            while (true) {
                System.out.println(r.name+"..."+r.sex);
            }
        }
    }

    测试类:

    package demo;
    
    public class ThreadDemo {
        public static void main(String[] args) {
            Input in = new Input();
            Output out = new Output();
            Thread tin = new Thread(in);
            Thread tout = new Thread(out);
            
            tin.start();
            tout.start();
        }
    }

    运行后却发现输出的都是null...null

    因为输入线程和输出线程中创建的Resource对象使不同的

    解决null问题:

    package demo;
    
    public class Input implements Runnable {
        private Resource r;
        
        public Input(Resource r){
            this.r = r;
        }
    
        public void run() {
            int i = 0;
            while (true) {
                if (i % 2 == 0) {
                    r.name = "张三";
                    r.sex = "男";
                } else {
                    r.name = "李四";
                    r.sex = "女";
                }
                i++;
            }
        }
    
    }
    package demo;
    
    public class Output implements Runnable {
        private Resource r;
        
        public Output(Resource r){
            this.r = r;
        }
        
        public void run(){
            while (true) {
                System.out.println(r.name+"..."+r.sex);
            }
        }
    }
    package demo;
    
    public class ThreadDemo {
        public static void main(String[] args) {
            
            Resource r = new Resource();
            
            Input in = new Input(r);
            Output out = new Output(r);
            Thread tin = new Thread(in);
            Thread tout = new Thread(out);
            
            tin.start();
            tout.start();
        }
    }

    运行后又发现了另一个问题:

    输出中含有:张三...女或者李四...男,性别出错

    发生原因:

    赋值完张三和男后,继续赋值李四和女,这时候还未还得及赋值女,就进入了输出线程,这时候就会输出李四...男

    于是想到加上同步:

        public void run() {
            int i = 0;
            while (true) {
                synchronized (this) {
                    if (i % 2 == 0) {
                        r.name = "张三";
                        r.sex = "男";
                    } else {
                        r.name = "李四";
                        r.sex = "女";
                    }
                    i++;
                }
            }
        }
        public void run() {
            while (true) {
                synchronized (this) {
                    System.out.println(r.name + "..." + r.sex);
                }
            }
        }

    然而问题并没有解决:

    原因:

    这里的同步失去了作用,用到的不是一个锁

    解决办法:

    使用一个共同的锁即可

    public void run() {
            int i = 0;
            while (true) {
                synchronized (r) {
                    if (i % 2 == 0) {
                        r.name = "张三";
                        r.sex = "男";
                    } else {
                        r.name = "李四";
                        r.sex = "女";
                    }
                    i++;
                }
            }
        }
        public void run() {
            while (true) {
                synchronized (r) {
                    System.out.println(r.name + "..." + r.sex);
                }
            }
        }

    这时候就是正常的输出了

     但是还是存在一个问题,我们希望的是张三和李四交错出现,一个张三一个李四,现在依然是随机出现的,大片的张三或李四

    解决办法:

    先让input线程赋值,然后让output线程输出,并且让输入线程等待,不允许再赋值李四,等待输出张三结束后,再允许李四赋值,依次下去

    输入线程也需要同样的方式,输出完后要等待

    这时候就需要用到等待唤醒机制:

    输入:赋值后,执行方法wait()永远等待

    输出:打印后,再输出等待之前,唤醒输入notify(),自己再wait()永远等待

    输入:被唤醒后,重新赋值,必须notify()唤醒输出的线程,自己再wait()等待

    依次循环下去

    代码实现:

    package demo;
    
    public class Resource {
        public String name;
        public String sex;
        public boolean flag = false;
    }
    package demo;
    
    public class Input implements Runnable {
        private Resource r;
    
        public Input(Resource r) {
            this.r = r;
        }
    
        public void run() {
            int i = 0;
            while (true) {
                synchronized (r) {
                    if (r.flag) {
                        try {
                            r.wait();
                        } catch (Exception e) {
                        }
                    }
                    if (i % 2 == 0) {
                        r.name = "张三";
                        r.sex = "男";
                    } else {
                        r.name = "李四";
                        r.sex = "女";
                    }
                    r.flag = true;
                    r.notify();
                }
                i++;
            }
        }
    }
    package demo;
    
    public class Output implements Runnable {
        private Resource r;
    
        public Output(Resource r) {
            this.r = r;
        }
    
        public void run() {
            while (true) {
                synchronized (r) {
                    if (!r.flag) {
                        try {
                            r.wait();
                        } catch (Exception e) {
                        }
                    }
                    System.out.println(r.name + "..." + r.sex);
                    r.flag = false;
                    r.notify();
                }
            }
        }
    }
    package demo;
    
    public class ThreadDemo {
        public static void main(String[] args) {
    
            Resource r = new Resource();
    
            Input in = new Input(r);
            Output out = new Output(r);
            Thread tin = new Thread(in);
            Thread tout = new Thread(out);
    
            tin.start();
            tout.start();
        }
    }

    这时候就是张三李四交错输出了

    完成

  • 相关阅读:
    hdu1251 字典树trie 模板题
    SPOJ 1479 +SPOJ 666 无向树最小点覆盖 ,第二题要方案数,树形dp
    POJ 2125 最小点权覆盖集(输出方案)
    dfs序+主席树 或者 树链剖分+主席树(没写) 或者 线段树套线段树 或者 线段树套splay 或者 线段树套树状数组 bzoj 4448
    dfs序+主席树 BZOJ 2588 当然树链剖分+主席树也可以?
    最小生成树的边的概念问题!!! 最小生成树的计数 bzoj 1016
    BZOJ 2083 vector的巧用+二分
    vector的哈希值 Codecraft-17 and Codeforces Round #391 (Div. 1 + Div. 2, combined) C
    codeforces Good bye 2016 E 线段树维护dp区间合并
    莫对 和分块 模板
  • 原文地址:https://www.cnblogs.com/xuyiqing/p/11619561.html
Copyright © 2020-2023  润新知