多线程java,这是一篇练习时长两年半的多线程总结,小黑子勿进

多线程java,这是一篇练习时长两年半的多线程总结,小黑子勿进,第1张

这是一个荔枝的目录:
  • 1.为什么要有线程
    • (1)什么是进程
    • (2)什么是线程
    • (3)线程和进程的区别
  • 2.线程的使用
    • (1)线程的五种创建方式
      • a.外部类继承Thread类
      • b.外部类实现Ruannable接口
      • c.匿名内部类继承Thread
      • d.匿名内部类实现Ruannble接口
      • e.Lambda表达式
    • (2)线程的常用方法
      • 下文涉及方法:currentThread()---静态方法
      • a.中断线程:interrupt()
      • b.测试当前线程是否被中断:interrupted() --静态方法
      • c.测试这个线程是否被中断: isInterrupted()
      • d.等待线程死亡:join()
      • e.join()的其他重载方法:join(long millis),join(long millis, int nanos)
      • f.线程暂停(ms级):sleep(long millis),sleep(long millis, int nanos)
      • i.有关当前线程的一些方法
  • 3.线程的七种状态(可通过getState方法查看)
      • (1)New
      • (2)Runnable(Running,Ready)
      • (3)Terminated
      • (4)Waiting
      • (5)Timed_waiting
      • (6)Blocked
      • 线程状态间的转换图
  • 4.线程安全与解决方案
    • (0)线程不安全的原因
      • a.原子性
      • b.内存可见性
      • c.代码顺序性
    • (1)synchronized
      • a.线程加锁
      • b.线程死锁
      • c.锁策略*(了解即可)
      • d.wait,notify方法介绍
    • (2)volatile关键字的作用
      • a.保证内存可见性
      • b.防止指令重排序
      • PS:不保证原子性
  • 结尾

1.为什么要有线程 (1)什么是进程

想要知道为什么要有线程,必须要先了解什么是进程。进程在我们的生活随处可见,比如我们点击桌面上QQ的快捷方式,其实就是打开了QQ的相应exe文件,就是创建了一个进程,我们打开任务管理器就可以发现:

(2)什么是线程

线程又被称为“轻量化进程”,可能线程是什么并不好说清楚,在这里打个比方大家就明白了:

如果说进程是工厂,那么线程就是工厂里面的各种流水线,他们共同占据着工厂里面的空间。

从这个例子就可以发现:进程>线程,更准确来说,一个进程包含多个线程,而这多个线程共同占据着这个进程的内存空间,并且每个进程至少有一个线程存在,即主线程(我们的java程序大多主线程是main线程)。

(3)线程和进程的区别

线程又被称作轻量化进程,他最大的特点也是区别就是创建和销毁都比进程要快速,调度也比进程快。有了这个优势,为什么要有线程这个问题也就显而易见了。

依然是工厂-流水线的例子
假设我斯某人买了一块地,开办了一家工厂,最近生意不错想扩大规模,我有两种方案:一是再买一块地,设备和原材料等等,二是继续压榨当前这块地,直接买入新的设备增加流水线的数量便可。如果我想快速,高效的扩展规模,由于买地,就相当于进程申请内存一样,这一步很消耗时间(要各种手续),无疑第二种是更好的选择。

这就是多线程所带来的好处

那么在只因算只因里,为了充分的压榨cpu的性能,我们不可能让cpu一次只执行一个程序,这样效率太低了,因此并发(同时进行)编程成了“刚需”,由于线程的优势,所以多线程比多进程更加高效。

当然线程和进程还有其他的区别:

1.同一个进程的不同线程,共用这个进程的空间;而不同的进程的内存空间不同
2.进程是系统资源分配的基本单位(分配内存),线程是系统调度(cpu执行的先后)的基本单位。

2.线程的使用 (1)线程的五种创建方式 a.外部类继承Thread类
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("继承 Thread, 重写 run");
    }
}

public class CreateThreadDemo {
    public static void main(String[] args) {
        MyThread mt=new MyThread();
        mt.start();
        }
   }
b.外部类实现Ruannable接口
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println(" 实现 Runnable, 重写 run");
    }
}

public class CreateThreadDemo {
    public static void main(String[] args) {
        MyRunnable mr=new MyRunnable();
        Thread t2=new Thread(mr);
        t2.start();
   }
c.匿名内部类继承Thread
Thread t3=new Thread(){
            @Override
            public void run() {
                System.out.println("继承 Thread, 重写 run, 使用匿名内部类");
            }
        };
d.匿名内部类实现Ruannble接口
Thread t4=new Thread(new Runnable(){
            @Override
            public void run() {
                System.out.println("实现 Runnable, 重写 run, 使用匿名内部类");
            }
        });
        t4.start();
e.Lambda表达式
Thread t5=new Thread(()->{
            System.out.println("Lambda");
        });
        t5.start();
(2)线程的常用方法 下文涉及方法:currentThread()—静态方法

currentThread()获取当前所在线程,和this很像

a.中断线程:interrupt()

这个中断并不是真的把当前的线程掐断了,而是一个标记位。我们用以下代码测试:

package bokecode;

public class InterruptDemo {
    public static void main(String[] args) {
        Thread thread=new Thread(new Runnable(){
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted()){
                    System.out.println("0000");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        thread.start();
        System.out.println("线程中断了!!!");
        thread.interrupt();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main线程结束了!!");
    }
}

我们发现程序只是抛出了一个异常,而并没有真的被中断,依旧在输出0000。为什么呢,原因在这行代码

Thread.sleep(1000);

这个代码会在后面的线程休眠会讲到,线程出于休眠状态时不会被中断,而是会抛出一个异常,之后这个线程被提前唤醒。如果去掉该行代码,线程就会正常中断,当然这并不是主要解决手段,线程的中断主要还是看我们的代码决定,例如我们在catch这个异常后我们直接break中断循环,这是的interrupt()方法就像是一个标志。

b.测试当前线程是否被中断:interrupted() --静态方法

从源码看出他会调用获取当前线程的方法再判断是否被中断

c.测试这个线程是否被中断: isInterrupted()

由线程实例对象调用,功能和(2)一致,不同点在于:静态方法的(2)判断完之后会清楚中断标志,而(3)不会

d.等待线程死亡:join()

例:编写代码,我们创建一个线程A想要A打印1~1000,main线程打印1001到1999;

public class JoinDemo {
    public static void main(String[] args) {
        System.out.println("main线程开始了!!");
        Thread thread=new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<1000;i++){
                    System.out.println(i);
                }
            }
        });
        thread.start();
        for(int i=1000;i<2000;i++){
            System.out.println(i);
        }
        System.out.println("main线程结束了!!");
    }
}

可结果不尽人意:打印的非常乱

因为两个线程是同时进行的,想让main线程等A线程跑完再跑,我们只需要调用join方法便可:

在thread.start();
后插入thread。join();

e.join()的其他重载方法:join(long millis),join(long millis, int nanos)

参数代表等多少时间,不再是无限等待下去。

f.线程暂停(ms级):sleep(long millis),sleep(long millis, int nanos)

顾名思义,让线程休息一会,比如一个线程打印1~10,可通过调用此方法来实现每隔一秒打印一次

Thread.sleep(1000);第二个方法精度更高,精确到了纳秒级

i.有关当前线程的一些方法

currentThread():在哪个线程里调用就返回这个线程的实例

getId()和getName():前者是线程创建时就被赋予的编号,唯一存在,不可修改。
而后者可以修改setName(name),可以相同,相当于线程的姓名一样。

getState()返回线程的状态,线程的状态分类会在下文进行介绍

setDaemon(boolean);设置当前线程是守护(后台)线程还是前台线程。
我们创建线程时默认是前台线程(isDeamon()可以检测当前是什么线程)

守护线程:前台线程如果是坤坤,那么守护线程就是我们ikun,坤在ikun就在(除非ikun提前消失),坤无ikun也立即无,就相当于人在塔在的这种感觉。
守护我们最好的坤坤

前台线程:程序是否结束取决于前台线程是否执行完毕,main线程就是典型的前台线程

public static void main(String[] args) {
        Thread thread=new Thread(()->{
            System.out.println("守护线程执行");
            for(int i=0;i<100;i++){
                System.out.println(i);
            }
            System.out.println("守护线程执行完毕");
        });
        //thread.setDaemon(true);
        thread.start();
    }

如果没有注释的那行代码,结果为thread线程正常结束:

反之程序在main线程执行完毕后,(因为该程序的所有前台线程都结束了)直接终止。

3.线程的七种状态(可通过getState方法查看) (1)New

Thread对象被创建,但还未启动,比较稚嫩
也就是还未调用start方法,一个线程被真的创建,是取决于是否调用了start方法。

(2)Runnable(Running,Ready)

线程运行状态
Thread对象被创建,并且调用了start方法
(就绪)Ready状态,假设cpu一次只可跑一个线程,一次执行区间,两个线程抢这个cpu,那么没抢到cpu的那个线程就是Ready状态,抢到的那个就是Running状态

(3)Terminated

线程结束状态,线程所要执行的任务结束了
start方法执行结束

(4)Waiting

线程等待状态
调用了wait,join的无参方法

(5)Timed_waiting

线程超时等待状态
调用了sleep,join(带时间参),wait(带时间参)的方法,调用有关时间的方法都会进入这个状态

(6)Blocked

线程阻塞状态
这里涉及到线程锁
假设几个线程抢一把锁,没抢到的线程便会进入阻塞状态,直到抢到为止。
如果不清楚锁是什么,就可以这么理解或者直接看下文:
假如cpu是马桶,那么锁就是厕所的门,把线程比作人,每个人想在马桶上执行,就得抢这个门(上厕所不得锁门不是嘛),那么抢到这个门之后就把他锁上,这是其他人就进入了阻塞状态,直到抢到这个门为止。

线程状态间的转换图

4.线程安全与解决方案

既然是线程,那么逃不掉的,就是线程安全问题。常见的线程安全问题,比如多个线程同时读写(写就是修改)同一个数据,就导致可能你这边数据修改完了,而我没有及时收到信息,我这边还是用的旧数据,这就不对了,这就是修改共享数据导致的问题。所以,线程安全问题本质上就是内存的安全问题。那么为了解决这些问题,java提供了一些手段来帮助我们程序猿简化代码,便于我们保证线程安全。

(0)线程不安全的原因 a.原子性

由于cpu的调度是不确定了,所以如果一个 *** 作不是原子性,很容易导致被另一个线程的突然插入而导致线程安全问题。
那么原子性是什么呢?举个例子
n++;
一个看似简单的自增 *** 作,其实暗藏玄鲲,它并不是原子性的 *** 作,大致分为下面三个步骤:
首先从内存中取n的值于寄存器中
其次对寄存器的值进行++ *** 作
最后,把寄存器写回内存
如果两个线程都执行n++,很可能出现,第一步取的值都是旧数据,使得最后n只++的一次的效果。
线程加锁的第一个例子就是这个原因

b.内存可见性

这个在下文的volatile有说。

c.代码顺序性

这个在下文的volatile也有说,即指令重排序。

(1)synchronized

synchronized是java提供的一个关键字,通过它对线程进行加锁,可以用来保证一个数据被修改的时候,同一时间有且至多只有一个数据可以修改。其他线程想要修改就必须等待获取锁的哪个线程执行完。就相当于人为的把我们的代码变成了原子性的 *** 作。

a.线程加锁

线程加锁,也就是synchronized的作用,通过修饰代码块或者方法,来形成加锁的效果。

举个通俗的例子,张三,李四,王五三个人合租,但现在只有一间浴室,张三,李四,王五都打算去洗澡,由于3个人都是直的,所以这时候就看谁跑的快了,如果张三抢到了浴室,就把门锁上了,这个锁门就是加锁,这时其他人就进入阻塞状态,直到张三出来。这时,不管张三在里面干什么,哪怕是睡觉(sleep()方法),另外两个人都得在外面等

具体使用:
现在我们举一个计数器的例子:

    int count=0;

    public void increase(){
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadStateDemo tsd=new ThreadStateDemo();
        Thread t1=new Thread(()->{
            for(int i=0;i<10000;i++){
                tsd.increase();
            }
        });
        Thread t2=new Thread(()->{
            for(int i=0;i<10000;i++){
                tsd.increase();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("最后count的结果为:"+tsd.count);
    }

按理说结果应该为20000,但实际上只有14684,而且每次执行的都不一样,这就是典型的线程安全问题

那么解决方法也很简单,就是加上synchronized便可

修饰方法:同步方法

和上图一样,只需在要加锁的方法前加上synchronized修饰即可

修饰代码块:同步代码块

修饰代码块,主要是缩小锁范围,因为一个方法里面不是所有的地方都需要锁

括号里可以不是this,只要保证不同线程调用这个方法的时候,括号里面的对象都是同一个就可以,这也是java加锁比较特殊的地方。

b.线程死锁

当然加锁也不能随便加,不能因为锁好用就到处加,因为加锁也是一种资源消耗,每次加锁释放锁都是要额外消耗时间的,如果 *** 作不当更会造成线程死锁这个大问题。
什么是线程死锁?

死锁就是一个拿着锁,缺迟迟不释放或者无法释放,导致程序无法继续执行下去

死锁实例

两个线程互相持有各自的锁资源,使得线程死锁
比如:

    static Object lockA=new Object();
    static Object lockB=new Object();
    //死锁测试:
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (lockA){
                System.out.println("t1第一步");
                synchronized (lockB){
                    System.out.println("t1第二步");
                }
            }
        });

        Thread t2=new Thread(()->{
            synchronized (lockB){
                System.out.println("t2第一步");
                synchronized (lockA){
                    System.out.println("t2第二步");
                }
            }
        });
        t1.start();
        t2.start();
    }

这样一行代码,最终执行结果:全卡在了第一步。
因为t1拿了lockA还没释放,t2拿了lockB也没释放,导致后续都无法进行

为什么为产生这种情况呢?

因为一个线程获取到这个锁时,直到该线程执行完,或者调用wait方法之前,其他线程都无法获取到这个锁,因为这个锁不会被提前释放

c.锁策略*(了解即可)

常见十种锁策略
1.乐观锁
即锁认为 *** 作A发生线程冲突的概率很低,也就是锁竞争不激烈,那么干脆就不加锁了,如果检测出有冲突,再抛出来让程序猿解决。
2.悲观锁
即锁认为 *** 作A发生线程冲突的概率很高,也就是锁竞争很激烈,每次执行都会加锁。
这两种策略在不同场景下各有优劣,乐观锁可以省资源,悲观锁更安全。
——————————————————————————————————————————
3.轻量级锁
加锁尽量在用户态处理
4.重量级锁
加锁主要在内核态处理,存在内核态->用户态的转换
——————————————————————————————————————————
5.读写锁
对于数据来说,有读和写两种 *** 作,以两个线程为例:
两个线程都是读数据,线程安全
一个线程读,一个写,线程不安全
两个都写,线程不安全
所以根据你的使用目的,可以判断你的 *** 作是否需要加锁,java给我们提供了相应的类来帮助我们处理,即ReentrantReadWriteLock类。
——————————————————————————————————————————
——————————————————————————————————————————
6.可重入锁
即同一个线程内部存在多次获取同一把锁的代码,不会重复去获取
7.不可重入锁
和可重入相反,每次都需要重新获取锁,一般获取不到第二次就死锁了
——————————————————————————————————————————
8.自旋锁
即线程抢锁失败了,不会阻塞,而是立即再次尝试获取(while循环的感觉),锁一旦放出来,就能第一时间去尝试获取。
——————————————————————————————————————————
9.公平锁
即获取锁的顺序遵顼先来后到的原则,不能抢
10.非公平锁
谁抢到算谁的

d.wait,notify方法介绍

我们在线程状态转换图中,发现了一些没有提到的方法,比如yield,wait等等,其中,wait和与其对于的notify/notifyAll方法都是很重要的方法
注意:下面的方法要搭配 synchronized 来使用. 脱离 synchronized会直接抛出异常,也就是要在同步方法/代码块内使用

wait()方法:使当前线程阻塞等待,并释放对象锁,直到该对象调用了notify/notifyAll方法后,停止阻塞并重新尝试获取到锁。
notify()方法:随机唤醒一个调用wait方法的线程,待当前线程结束后,使那个线程停止阻塞而去尝试获取锁。
notifyAll()方法:和notify唯一不同的是,它是全部唤醒,不是随机。
这两个方法处理不当也会造成线程死锁,就是可能出现notify比wait先执行,导致wait所处线程一直处于阻塞状态

使用实例:

public static void main(String[] args) {
        Object lock=new Object();
        Thread t1=new Thread(()->{
            synchronized (lock){
                System.out.println("线程开始等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程等待结束");
            }
        });
        t1.start();
        //代码补充
        Thread t2=new Thread(()->{
            synchronized (lock){
                System.out.println("唤醒t1线程");
                    lock.notify();//可能会出现先notify后wait的结果
            }
        });
        t2.start();
    }

结果为

进入阻塞,不在往下执行
增加补充代码后,t1被t2唤醒,继续执行

(2)volatile关键字的作用

场景介绍,观察以下代码:

    /*volatile*/ int count=0;
    static Scanner scanner=new Scanner(System.in);
    public static void main(String[] args) {
        Demo demo=new Demo();
        Thread t1=new Thread(()->{
            while(demo.count==0){
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("count等于0");
            }
            System.out.println("count不等于0了");
        });
        t1.start();
        Thread t2=new Thread(()->{
            System.out.println("修改count的值为:");
            demo.count=scanner.nextInt();
        });
        t2.start();
    }

可以发现,明明我们也就已经了修改了count的值,为什么程序还没有停止呢
当我们给count加上volitale就可以发现程序及时的停止了

那么原因是什么呢?

我们可以发现我们的t1线程,是循环判断count是否等于0,由于多次拿到的值都是0,在编译器优化下就认为这个值一直是0,如果每次我都去内存去取这个值不是很浪费吗?所以他就把这个值存到寄存器里,每次都从寄存器里取这个值,所以我们t2线程改了count内存上的值,t1也察觉不到,而volatile就帮助我们解决了这个问题。

获取数据时,强制读写内存. 速度是慢了, 但是数据变的更准确了
使用方法

volatile 类型 变量名//就是修饰变量用的
a.保证内存可见性

即一个线程修改某个数据的时候,另一个线程能及时看见

b.防止指令重排序

什么是指令重排序:

A a=new A();
我们在实例化对象时,大体可以分为三个步骤:
1.为A对象开辟内存空间,并记录内存首地址
2.调用A的构造方法来初始化对象
3.将内存首地址赋予a引用

可以发现步骤一必须率先执行,而2,3两条在顺序在单线程的情况下什么样的顺序都可以,我们在使用的时候,很可能编译器通过自身的优化,就将这个顺序反过来了。单线程下无所谓,但一旦到了多线程,就有大问题。如果指令变成了132,很可能cpu刚执行完3指令,被另一个线程插队了,导致虽然a引用不为null,但是里面并不是有效数据,这就造成了线程安全问题。

PS:不保证原子性

这个大家把加法器的synchronized去掉,给count加上volatile就可以测试出来了。

结尾

那么今天的分享就到这里了,四鲲磨一篇,感谢阅读😶‍🌫️
(可能会有一点点的错别字 )

欢迎分享,转载请注明来源:内存溢出

原文地址: https://www.outofmemory.cn/langs/3002686.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-09-27
下一篇 2022-09-27

发表评论

登录后才能评论

评论列表(0条)

保存