Java中的多线程(Thread,Runnable)

首先了解一下java内存模型(并不是java内存结构,java内存结构是堆栈方法区https://mp.weixin.qq.com/s/x0cuhZdUN3XOppwRv-edPA

其次是线程池

线程池的执行流程https://www.nowcoder.com/questionTerminal/da3b91dcfc654995a7ee66081a12f22d?orderByHotValue=1&page=1&onlyReference=false

1.多线程(原理)

线程和进程各自有什么区别和优劣呢?

  • 进程是资源分配的最小单位,线程是程序执行的最小单位。

  • 进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。

  • 线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。

  • 但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。

Thread实现多线程

只要启用多线程就必须使用thread中的start函数。

runnable实现多线程(避免单继承的局限)

runable是接口,可以实现多继承,但是没有start方法,所以无法直接开始线程,但是thread的构造方法中可以把runnable作为参数,所以可以通过构造方法来进行传递。

其中Mythread实现了runnable接口。以后多线程的实现优先考虑runnable。

thread和runnable之间的关系

查看源码 :

发现thread类实现了runnable接口,在之前继承thread类的时候,实际上覆写的还是runnable中的run方法。而执行start之后实际上是调用run方法,一张图表示runnable和thread之间的关系:

在进行thread启动多线程的时候调用的是start方法,而后找到的是run方法。

当通过thread类的构造方法传递了一个runnable的接口对象的时候,那么该接口对象将被thread类中的target属性所保存。在thread中调用start方法时会调用下面的run方法:

而这个覆写的run方法会调用runnable接口子类(上面图中的new Mythread对象,这个对象实现了runnable接口)被覆写过的run方法。

当有多个线程时的结构:

在实际情况下这里的线程对象就可能是各个用户。

模拟多个用户买票的程序:

内存如下图所示:

 Callable实现多线程

callable调用内存关系图:

相关程序演示:

runnable和callable的区别

但是不管用什么,用thread中的start启动线程是不变的定理。

 线程的运行

实际上所有的线程都是通过start开始,但是start仅仅只是代表就绪,真正的开始是run方法,下面给出一张图:

深度好文:https://www.cnblogs.com/jijijiefang/articles/7222955.html

面试问题:请问为什么stop方法和suspend方法为什么不推荐使用?

https://blog.csdn.net/caolaosanahnu/article/details/19491351

2.线程的操作

线程的命名和取得

对于程序的开发过程之中,需要通过获得线程来进行一些操作,所以线程的名字至关重要:

获取名字的代码操作如下:

输出:,如果没有名字,会给一个默认的不重复的名字。

对于名字的不重复,使用到了static关键字:

其实main函数也是一个线程,查看下面的代码:

输出main和线程对象,可以发现main也是一个线程,那么进程是什么呢?

每当使用java命令执行程序的时候就表示启动了一个jvm的进程,一台电脑上可以启动若干个jvm进程,jvm进程都会有各自的线程。

在任何开发之中,主线程可以创建若干子线程,一般开发情况下,主线程负责整体流程,子线程负责处理耗时操作。

线程休眠: 

millis是毫秒,nanos是纳秒。

此时产生五个线程对象,5个线程对象执行的方法体相同。

执行的时候并不是同时休眠,同时唤醒,中间会有适当的延迟操作。

 线程中断

所有正在执行的线程都是可以被中断的,中断线程必须进行异常处理。

线程强制执行和礼让

 在进行强制执行的时候必须先获得线程对象,然后调用join方法,实现强制执行,强制执行方法中可以写入参数。

 https://www.cnblogs.com/lcplcpjava/p/6896904.html

 线程的礼让方法:yield方法,每次礼让都只会礼让当前当前的资源

 

当执行的时候发现有礼让,则礼让当前资源,下次又执行的时候,如果还有礼让则继续礼让当前资源。

 线程优先级

这些优先级都有对应的常量,分别是10,5,1。但是高优先级的只是有可能先执行,并不是绝对的先执行。其中主线程是中等优先级(5),而默认创建的线程也是中等优先级。

线程的同步和死锁: 

线程同步

问题引入,线程的不同步问题,模拟一个卖票程序,

前一个线程进入run()后开始执行售卖操作,票数减一,操作还没结束,另一个线程也进来了,此时的票数其实已经减过了(负数),后来的线程之后执行到输出语句的时候才知道是负数.
所以,就需要在一个线程进入之后锁住这一方法,等操作结束另一线程才能进入.这一线程操作的时候,其他线程只能等待:

Synchronized关键字https://blog.csdn.net/zjy15203167987/article/details/82531772

面试题:为什么分布式环境下synchronized失效?如何解决这种情况?(看了上面那篇文章都会了解到)

面试题:同步方法和同步代码块的区别?https://www.cnblogs.com/xujingyang/p/6565606.html

面试题:解释一下synchronize的可重入性?https://www.jianshu.com/p/7fc3eb310ca1

Synchronized和CAS(compare and swap)的区别https://www.cnblogs.com/myopensource/p/8177074.html

Synchronized和lock的区别https://www.cnblogs.com/iyyy/p/7993788.html

要实现同步可以由同步代码块和同步方法,其中常用的是同步方法。但是同步会实现性能的下降。

https://blog.csdn.net/weixin_39641494/article/details/78437823

 线程死锁和解决方法

https://blog.csdn.net/ls5718/article/details/51896159

生产者消费者模式

生产者消费者模式是最基本的,最原始的多线程实现方式。

https://www.cnblogs.com/fankongkong/p/7339848.html

volatile关键字

 首先了解java内存模型,java内存模型https://blog.csdn.net/hollis_chuang/article/details/80880118

volatile关键字详解https://www.cnblogs.com/zhengbin/p/5654805.html

volatile是如何保证可见性的
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

2)禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。

下面解释一下这段代码为何有可能导致无法中断线程。在前面已经解释过,每个线程在运行过程中都有自己的工作内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在自己的工作内存当中。

那么当线程2更改了stop变量的值之后,但是还没来得及写入主存当中,线程2转去做其他事情了,那么线程1由于不知道线程2对stop变量的更改,因此还会一直循环下去。

但是用volatile修饰之后就变得不一样了:

使用volatile关键字会强制将修改的值立即写入主存;

使用volatile关键字的话,当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(当然这里包括2个操作,修改线程2工作内存中的值,然后将修改后的值写入内存),会使得线程1的工作内存中缓存变量stop的缓存行无效,然后线程1读取时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

原文地址:https://www.cnblogs.com/ljq2622/p/11104593.html