Java中的多线程

一、线程概述
进程:计算机中特定功能的程序在数据集上的一次运行。一款正在运行的app就可以认为是一个进程
线程:线程是进程中的一个单元,功能的执行离不开线程
多线程:一个进程中有多个线程同时执行,如迅雷下载,可以同时下载多个电影
                    每一个下载任务就是一个线程
单线程:任务的执行是依次的,必须等待前一个任务执行完毕,下一个任务才能开启
JVM是多线程的,在我们运行JVM的时候后台会运行垃圾回收线程,来清理没有被引用的对象
 
二、现成的实现(创建方法)
1、继承Thread类(在thread类的内部,也是实现了runnable接口) ,
子类必须重写父类中的run方法,run方法里封装的就是要实现的功能。
这种方法在执行的时候,不能直接调用run方法,是因为线程的运行需要本地操作系统的支持
2、实现Runnable接口(Thread.currentThread()可以得到当前线程对象)
继承实现线程的局限性:
因为继承是单继承,当一个类继承了其他类,就不能再继承Thread类,也就无法实现多继承了,
那么,实现Runnable接口则更加灵活了。
 
三、同一时间,其实cpu资源只能被一个线程访问
线程的并发执行,是通过不断切换cpu资源来实现的
因为这个速度非常快,我们根本感知不到,我们能感知到的就是三个线程在并发执行
不是真的并发
生命周期
1、创建:线程被new出来
2、准备就绪:此时线程具有执行的资格,即调用了start()方法,但没有执行的权力
3、运行:抢占到CPU资源,具备执行的权力和资格
4、阻塞(挂起):没有执行的资格和权力
线程休眠sleep(),休眠有时间
线程之间协作可以让线程等待wait(),唤醒线程可以使用notify()
5、销毁执行完run方法,或者执行了stop()方法:线程对象变成垃圾,被回收机制释放资源
 
四、线程的并发问题与解决方案
互联网项目中存在着大量的并发案例,如买火车票,电商抢购等
此时一定要注意并发的安全性问题:
解决方法就是加锁,
那么加锁的应用前提:
1、代码被多个线程访问
2、代码中有共享的数据(这个变量可以使用static关键字修饰)
3、共享的数据被操作
加锁的语法格式:
synchronizaed(锁对象){
      操作共享资源的代码
}
锁对象和共享的数据一样也必须是共享的,
synchronizaed同步代码块的锁对象可以是任意对象,
前提是:线程的实现方式是采用继承实现的!
synchronizaed是可以修饰方法的,如果是静态方法,默认锁对象是当前的类对象,前提是继承
如果修饰的是非静态方法,默认的锁对象就是当前类对象,前提是实现接口
示例:模拟卖票,火车站有100张票,4个窗口同时卖
      分析:多线程解决,继承、接口
      四个窗口就是四个线程对象,而100张票属于共享资源
     
五、线程的休眠
线程的休眠很重要,因为在做服务端的时候为了减少服务器的压力,需要休眠
如果休眠放在并发的代码块,休眠是不会让出锁对象的

六、线程的通信
生产者、消费者
卖水果:
产水果:
买水果:
生产者线程,生产水果,如果水果没有被买走,就不生产处于等待状态(挂起),如果水果被买走
消费者线程会通知生产者水果被买走,你该生产了,此时继续生产
消费者线程,如果水果已经生产出来,那么就买走水果,通知生产者生产,如果没有水果
就等待水果生产(挂起),生产者通知消费者,有水果了,你该买了,此时继续购买
注意:
         线程通信必须有共享数据,而且共享数据的操作要有同步代码块(synchronized)
         一定要有wait(),notify()方法,而且是成对存在的
七、线程的优先级
我们可以通过setPriority()来设置线程的优先级,
如,setPriority(10)
优先级最小值1,最大值10,默认值5
优先级,只是相对的,并不是绝对的,优先级高的线程只是意味着抢占到CPU资源的可能性较大
八、加入线程JoinThread、等待线程Thread、守护线程Prio
 
1、JoinThread

这种情况下线程的执行顺序就会发生改变

join线程会抢先拿到CPU资源来执行,然后执行完毕再来执行其余的线程

一个线程如果执行join语句,那么就有新的线程加入,执行该语句的线程必须要让步给新加入的线程先完成任务,然后才能继续执行。

使用场景:当一个进程中存在多条线程,其余线程的执行需要依赖某条线程的资源或者某条线程的结束,

则可以把此条线程设置为加入线程。

2、yieldThread等待线程

使用等待线程,暂停当前正在执行的线程对象(运行-->准备就绪),并执行其他线程,这样会让线程之间抢占CPU资源更加均匀。。

3、daemonThread守护线程

该方法必须在启动线程之前调用,参数为true,表示该线程被标记为守护线程。

守护线程与普通线程的唯一区别是:当JVM中所有的线程都是守护线程的时候,JVM就可以退出了;

如果还有一个或以上的非守护线程则不会退出。(以上是针对正常退出,调用System.exit则必定会退出)   

所以setDeamon(true)的唯一意义就是告诉JVM不需要等待它退出,让JVM喜欢什么退出就退出吧,不用管它。

九、深层次的理解,综合理解:

1、守护线程在没有用户线程可服务时自动离开,在Java中比较特殊的线程是被称为守护(Daemon)线程的低级别线程。

这个线程具有最低的优先级,用于为系统中的其它对象和线程提供服务。

将一个用户线程设置为守护线程的方式是在线程对象创建之前调用线程对象的setDaemon方法。

典型的守护线程例子是JVM中的系统资源自动回收线程,我们所熟悉的Java垃圾回收线程就是一个典型的守护线程,

当我们的程序中不再有任何运行中的Thread,程序就不会再产生垃圾,垃圾回收器也就无事可做,所以当垃圾回收线程是Java虚拟机上仅剩的线程时,

Java虚拟机会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

守护进程(Daemon)是运行在后台的一种特殊进程。

它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。也就是说守护线程不依赖于终端,但是依赖于系统,与系统“同生共死”。

2、什么是线程安全和线程不安全

<1>可以这样理解,加锁的就是安全的,不加锁的就是不安全的(感觉比较生动,不知道对不对,望指出)

线程安全,  就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程不安全,  就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据.

<2> 到底是什么意思呢?
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
好比你有两个一模一样的银行卡(账户一张,余额一张,当然现实中是没有的),假如卡上余额1000块,而你跟你女朋友同时在不同的ATM上面取1000块钱(是同时哦,理想中的同时),如果线程不安全,那么俩人都能同时取出1000块(赚死了)。而如果线程安全的话,只能一个人同时操作一个账户,当这个账户正在被操作时,是被锁起来的,不给别人动的,只能你自己动,你动完了别人才能动。
 
<3> 为什么会有线程安全问题?
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
安全性:
比如一个 ArrayList 类,在添加一个元素的时候,它可能会有两步来完成:1. 在 Items[Size] 的位置存放此元素;2. 增大 Size 的值。
在单线程运行的情况下,如果 Size = 0,添加一个元素后,此元素在位置 0,而且 Size=1;
而如果是在多线程情况下,比如有两个线程,线程 A 先将元素存放在位置 0。但是此时 CPU 调度线程A暂停,线程 B
 得到运行的机会。线程B也向此 ArrayList 添加元素,因为此时 Size 仍然等于 0 (注意哦,我们假设的是添加一个元素是要两个步骤哦,而线程A仅仅完成了步骤1),所以线程B也将元素存放在位置0。然后线程A和线程B都继续运行,都增加 Size 的值。
那好,我们来看看 ArrayList 的情况,元素实际上只有一个,存放在位置 0,而 Size 却等于 2。这就是“线程不安全”了

 

 
 
 
 
 
 
 
 
 
 
 
 
原文地址:https://www.cnblogs.com/beiyi888/p/9922958.html