欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-10-27 08:30:58
多线程目的:在同一时刻有多条不同路径执行程序,提高程序运行效率 多线程应用:数据库连接池,多线程文件下载等 注意:在文件下载中使用多线程,无法提高速度 在一个进程中,一定会有主线程 从基础开始,多线程的使用方式: 1.继承Thread类:(不推荐) 注意:threadDemo调用的是start方法; ......

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

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

 

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

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

 

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

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();
    }
}

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

完成