多线程学习过程

多线程:

进程和线程

进程:

正在进行中的程序。其实进程就是一个应用程序运行时的内存分配空间。

线程(thread):

其实就是进程中一个程序执行控制单元,一条执行路径。进程负责的是应用程序的空间的标示。线程负责的是应用程序的执行顺序。

一个进程至少有一个线程在运行,当一个进程中出现多个线程时,就称这个应用程序是多线程应用程序,每个线程在栈区中都有自己的执行空间,自己的方法区、自己的变量。
jvm在启动的时,首先有一个主线程,负责程序的执行,调用的是main函数。主线程执行的代码都在main方法中。

当产生垃圾时,收垃圾的动作,是不需要主线程来完成,因为这样,会出现主线程中的代码执行会停止,会去运行垃圾回收器代码,效率较低,所以由单独一个线程来负责垃圾回收。

随机性的原理:

因为cpu的快速切换造成,哪个线程获取到了cpu的执行权,哪个线程就执行。

返回当前线程的名称:Thread.currentThread().getName()
线程的名称是由:Thread-编号定义的。编号从0开始。
线程要运行的代码都统一存放在了run方法中。

线程要运行必须要通过类中指定的方法开启。
start方法。(启动后,就多了一条执行路径)
start方法:1)、启动了线程;2)、让jvm调用了run方法。

创建线程的方式

方式一:继承Thread类

Thread类是来代表线程的。
创建线程的第一种方式:继承Thread ,由子类复写run方法。
步骤:
1,定义类继承Thread类;
2,目的是复写run方法,将要让线程运行的代码都存储到run方法中;
3,通过创建Thread类的子类对象,创建线程对象;
4,调用线程的start方法,开启线程,并执行run方法。

线程状态:
被创建:start()
运行:具备执行资格,同时具备执行权;
冻结:sleep(time),wait()—notify()唤醒;线程释放了执行权,同时释放执行资格;
临时阻塞状态:线程具备cpu的执行资格,没有cpu的执行权;
消亡:stop()

优点 编码简单
缺点 存在单继承的局限性 继承Thread后不能继承其他类

方式二:实现一个接口Runnable。

创建线程的第二种方式:实现一个接口Runnable。
步骤:
1,定义类实现Runnable接口。
2,覆盖接口中的run方法(用于封装线程要运行的代码)。
3,通过Thread类创建线程对象;
4,将实现了Runnable接口的子类任务对象作为实际参数传递给Thread类中的构造函数。
为什么要传递呢?因为要让线程对象明确要运行的run方法所属的对象。
5,调用Thread对象的start方法。开启线程,并运行Runnable接口子类中的run方法。

优点 :线程任务类是实现接口 可以继续继承类或者是吸纳接口
缺点:编程多一层对象包装,如果线程有执行结果是不可以返回的(即只能跑功能)

为什么要有Runnable接口的出现?
1:通过继承Thread类的方式,可以完成多线程的建立。但是这种方式有一个局限性,如果一个类已经有了自己的父类,就不可以继承Thread类,因为java单继承的局限性。
可是该类中的还有部分代码需要被多个线程同时执行。这时怎么办呢?
只有对该类进行额外的功能扩展,java就提供了一个接口Runnable。这个接口中定义了run方法,其实run方法的定义就是为了存储多线程要运行的代码。
所以,通常创建线程都用第二种方式。
因为实现Runnable接口可以避免单继承的局限性。
2:其实是将不同类中需要被多线程执行的代码进行抽取。将多线程要运行的代码的位置单独定义到接口中。为其他类进行功能扩展提供了前提。
所以Thread类在描述线程时,内部定义的run方法,也来自于Runnable接口。

实现Runnable接口可以避免单继承的局限性。而且,继承Thread,是可以对Thread类中的方法,进行子类复写的。但是不需要做这个复写动作的话,只为定义线程代码存放位置,实现Runnable接口更方便一些。所以Runnable接口将线程要执行的任务封装成了对象。

3、线程的实现方法三:利用Callable、FutureTask接口

1)得到任务对象
(1)实现Callable接口 重写call方法 封装要做的事情
然后创建Callable对象
(2)利用FutureTask把Callable对象封装成线程任务对象
2)交给Thread对象处理
3)start()启动线程
4)线程执行完毕后 通过FutureTask的get方法获取执行此任务的结果

优点:线程任务类只是实现接口 仍然可以继承 或者实现接口 并且可以得到返回值
缺点:编程麻烦一点

Thread 的API

getname 取名字
setname 设置名字
currenThread() 返回当前正在执行的线程对象

sleep(mills);让线程休眠制定的时时间 单位毫秒 设置休眠时间
run();任务方法
start();启动方法

多线程安全问题的原因:

多个线程同事操作同一个共享资源的时候可能会出现业务安全问题 称为业务安全问题。

涉及到两个因素:
1,多个线程在操作共享数据。
2,有多条语句对共享数据进行运算。
原因:这多条语句,在某一个时刻被一个线程执行时,还没有执行完,就被其他线程执行了。

例子 账户余额1000元
小红和 1、判断是否够1000
2、吐出1000元
3、更新账户余额
小红1 小明1 小红2 小明2 小红3剩下0元 小明3 剩下0元

同步:

好处:解决了线程安全问题。
弊端:相对降低性能,因为判断锁需要消耗资源,产生了死锁。

思想

加锁:把共享资源上锁 每次只能一个线程进入访问完毕后进行解锁 然后其他线程才能进来

方法
方法一:同步代码块

作用:把出现线程安全的核心代码上锁
原理:每次只能有一个线程进入 使用完成后自动解锁

格式:
synchronized(同步锁对象) { // 任意对象都可以。这个对象就是锁。最好不要唯一
需要被同步的代码;(操作共享资源的代码)
}
规范上:建议使用共享资源作为锁对象 对于实例对象建议使用this加锁 对于静态方法建议是与类名.class加锁

方法二:同步方法

同步方法也是有隐式锁对象的 只不过锁对象是整个方法代码
实例方法默认使用 this作为同步锁对象
静态方法默认使用 类名.class作为锁对象

同步方法好还是同步代码块好?

1、同步代码块锁更小 性能更好
2、同步方法范围更大

方法三:Lock接口:多线程在JDK1.5版本升级时,推出一个接口Lock接口。

解决线程安全问题使用同步的形式,(同步代码块,要么同步函数)其实最终使用的都是锁机制。

到了后期版本,直接将锁封装成了对象。线程进入同步就是具备了锁,执行完,离开同步,就是释放了锁。
在后期对锁的分析过程中,发现,获取锁,或者释放锁的动作应该是锁这个事物更清楚。所以将这些动作定义在了锁当中,并把锁定义成对象。

所以同步是隐示的锁操作,而Lock对象是显示的锁操作,它的出现就替代了同步。

在之前的版本中使用Object类中wait、notify、notifyAll的方式来完成的。那是因为同步中的锁是任意对象,所以操作锁的等待唤醒的方法都定义在Object类中。

而现在锁是指定对象Lock。所以查找等待唤醒机制方式需要通过Lock接口来完成。而Lock接口中并没有直接操作等待唤醒的方法,而是将这些方式又单独封装到了一个对象中。这个对象就是Condition,将Object中的三个方法进行单独的封装。并提供了功能一致的方法 await()、signal()、signalAll()体现新版本对象的好处。

final private Lock lock = new ReentrantLock();//创建一个锁对象 唯一不可替换的
public void m(){
lock.lock(); //上锁
try{
method
}
finally{
lock.unlock //解锁
}
}
}

lock 中Condition 实现的语义为 Object.wait 与 Object.notify

定义同步是有前提的:

1,必须要有两个或者两个以上的线程,才需要同步。
2,多个线程必须保证使用的是同一个锁。

同步方法是用的哪个锁呢?

通过验证,方法都有自己所属的对象this,所以同步函数所使用的锁就是this锁。

同步代码块和同步方法的区别?

同步代码块使用的锁可以是任意对象。
同步函数使用的锁是this,静态同步函数的锁是该类的字节码文件对象。

在一个类中只有一个同步,可以使用同步方法。如果有多同步,必须使用同步代码块,来确定不同的锁。所以同步代码块相对灵活一些。

考点问题:请写一个延迟加载的单例模式?写懒汉式;当出现多线程访问时怎么解决?加同步,解决安全问题;效率高吗?不高;怎样解决?通过双重判断的形式解决。

//懒汉式:延迟加载方式。
当多线程访问懒汉式时,因为懒汉式的方法内对共性数据进行多条语句的操作。所以容易出现线程安全问题。为了解决,加入同步机制,解决安全问题。但是却带来了效率降低。
为了效率问题,通过双重判断的形式解决。
class Single{undefined
private static Single s = null;
private Single(){}
public static Single getInstance(){
if(s == null){
synchronized(Single.class){//也可以写this
if(s == null)
s = new Single();
}
}
return s;
}
}

线程间通信:

思路:多个线程在操作同一个资源,但是操作的动作却不一样。
1:将资源封装成对象。
2:将线程执行的任务(任务其实就是run方法。)也封装成对象。

等待唤醒机制:涉及的方法:
wait:将同步中的线程处于冻结状态。释放了执行权,释放了资格。同时将线程对象存储到线程池中。
notify:唤醒线程池中某一个等待线程。
notifyAll:唤醒的是线程池中的所有线程。

注意:
1:这些方法都需要定义在同步中
2:因为这些方法必须要标示所属的锁。
你要知道 A锁上的线程被wait了,那这个线程就相当于处于A锁的线程池中,只能A锁的notify唤醒。
3:这三个方法都定义在Object类中。为什么操作线程的方法定义在Object类中?
因为这三个方法都需要定义同步内,并标示所属的同步锁,既然被锁调用,而锁又可以是任意对象,那么能被任意对象调用的方法一定定义在Object类中。

wait和sleep区别:

分析这两个方法:从执行权和锁上来分析:
wait:可以指定时间也可以不指定时间。不指定时间,只能由对应的notify或者notifyAll来唤醒。
sleep:必须指定时间,时间到自动从冻结状态转成运行状态(临时阻塞状态)。
wait:线程会释放执行权,而且线程会释放锁。
Sleep:线程会释放执行权,但不是不释放锁。

线程的停止:通过stop方法就可以停止线程。

停止线程:原理就是:让线程运行的代码结束,也就是结束run方法。
怎么结束run方法?一般run方法里肯定定义循环。所以只要结束循环即可。
第一种方式:定义循环的结束标记。
第二种方式:如果线程处于了冻结状态,是不可能读到标记的,这时就需要通过Thread类中的interrupt方法,将其冻结状态强制清除。让线程恢复具备执行资格的状态,让线程可以读到标记,并结束。

---------< java.lang.Thread >----------
interrupt():中断线程。
setPriority(int newPriority):更改线程的优先级。
getPriority():返回线程的优先级。
toString():返回该线程的字符串表示形式,包括线程名称、优先级和线程组。
Thread.yield():暂停当前正在执行的线程对象,并执行其他线程。
setDaemon(true):将该线程标记为守护线程或用户线程。将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
join:临时加入一个线程的时候可以使用join方法。
当A线程执行到了B线程的join方式。A线程处于冻结状态,释放了执行权,B开始执行。A什么时候执行呢?只有当B线程运行结束后,A才从冻结状态恢复运行状态执行。

线程池

什么是线程池

线程池是一个复用的线程技术

不使用线程池的问题

当用户发起一个请求 后台就会创建一个新线程来处理 下次任务来了又要创建新的 创建线程的开销很大的 这样会严重影响系统性能

谁代表了线程池

jdk5中提供了代表线程池的接口ExecutorService

如何得到一个线程池对象

方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

public ThreadPoolExecutor(
int corePoolSize, 指定线程池的核心线程数 >=0
int maximumPoolSize, 可支持最大线程数 >=核心线程数
long keepAliveTime,指定临时线程最大存活时间 >=0
TimeUnit unit,指定存活时间单位(秒,分,时,天) 时间单位
BlockingQueue workQueue, 指定任务队列 不能null
ThreadFactory threadFactory, 指定用哪个线程工厂创建线程
不能null
RejectedExecutionHandler handler) 指定线程忙任务满的时候新任务来了怎么办 不能null

方式二:使用Excutors(线程池的工具类)调用方法返回不同特点的线程池对象

Executors是一个工具类,提供了大量的静态方法,方便我们在线程池中使用。

static Callable callable(Runnable task):将runnable接口实例转换成Callable接口实例.
static ExecutorService newCacheThreadPool():创建一个“按需创建”的线程池,此前创建的线程可用时将重用它们。对于执行那些短期异步任务的程序而言,这些线程池通常可以提高程序的性能;在调用execute方法时(提交任务),如果此时没有线程可用,将会创建一个新的线程来执行此任务;终止并从缓存中移除那些已经有60秒尚未被使用的线程。cachedThreadPool底层使用SynchronousQueue(同步队列,单工模式队列),这个队列的特性就是“单工”,插入到队列的元素必须等待直到有其他线程“获取”位置,有人用“交接模式”形容它。这种队列,是没有实际空间的;同步队列,还支持两种策略:公平策略和非公平策略;公平策略底层基于队列实现,非公平队列基于一个类似stack模式实现。
static ExecutorService newFixedThreadPool(int nThreads):创建一个可重用固定线程数的线程池,以共享的无界队列方式运行这些线程。底层通过一个无界的LinkedBlockingQueue来实现。因为LinkedBlockingQueue是无界的,所以,当任务不能被立即消费时,将会被加入队列;无界的队列不会造成任务被rejected。如果线程意外退出,将会创建新的线程补充。
static ExecutorService newSingleThreadPool():创建只有一个线程的线程池,队列为底层通过一个无界的LinkedBlockingQueue来实现;如果线程意外退出,将会创建新的线程.
static ScheduledExecutorService newSingleThreadScheduledExecutor():创建一个单线程的可延迟执行的线程池服务,底层直接通过ScheduledThreadPoolExecutor实现。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize):创建指定线程数量的线程池服务,底层直接通过ScheduledThreadPoolExecutor实现。底层使用DelayedWorkQueue.

线程池常见面试题

临时线程什么时候创建啊?

新任务提交时发现核心线程都在忙 任务队列也满了 并且还可以创建临时线程 此时才可以创建临时线程

什么时候开始会拒绝任务?

核心线程和临时线程都在忙 任务队列也满了 新的任务过来的时候才会开始拒绝任务

workQueue任务队列任务队列

它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列;

1、直接提交队列:设置为SynchronousQueue队列,

使用SynchronousQueue队列,提交的任务不会被保存,总是会马上提交执行。如果用于执行任务的线程数量小于maximumPoolSize,则尝试创建新的进程,如果达到maximumPoolSize设置的最大值,则根据你设置的handler执行拒绝策略。因此这种方式你提交的任务不会被缓存起来,而是会被马上执行,在这种情况下,你需要对你程序的并发量有个准确的评估,才能设置合适的maximumPoolSize数量,否则很容易就会执行拒绝策略;

2、有界的任务队列(阻塞队列):有界的任务队列可以使用ArrayBlockingQueue实现

阻塞队列通常用于生产消费模式,满队列时生产者阻塞,空队列时消费者阻塞。ArrayBlockingQueue是一个有界的阻塞队列,此队列按FIFO原则增加删除元素。底层实现是一个数组。
若有新的任务需要执行时,线程池会创建新的线程,直到创建的线程数量达到corePoolSize时,则会将新的任务加入到等待队列中。若等待队列已满,即超过ArrayBlockingQueue初始化的容量,则继续创建线程,直到线程数量达到maximumPoolSize设置的最大线程数量,若大于maximumPoolSize,则执行拒绝策略。在这种情况下,线程数量的上限与有界任务队列的状态有直接关系,如果有界队列初始容量较大或者没有达到超负荷的状态,线程数将一直维持在corePoolSize以下,反之当任务队列已满时,则会以maximumPoolSize为最大线程数上限。

3、无界的任务队列(阻塞队列):有界任务队列可以使用LinkedBlockingQueue实现,如下所示

使用无界任务队列,线程池的任务队列可以无限制的添加新的任务,而线程池创建的最大线程数量就是你corePoolSize设置的数量,也就是说在这种情况下maximumPoolSize这个参数是无效的,哪怕你的任务队列中缓存了很多未执行的任务,当线程池的线程数达到corePoolSize后,就不会再增加了;若后续有新的任务加入,则直接进入队列等待,当使用这种任务队列模式时,一定要注意你任务提交与处理之间的协调与控制,不然会出现队列中的任务由于无法及时处理导致一直增长,直到最后资源耗尽的问题。

4、优先任务队列(阻塞队列):优先任务队列通过PriorityBlockingQueue实现,下面我们通过一个例子演示下

除了第一个任务直接创建线程执行外,其他的任务都被放入了优先任务队列,按优先级进行了重新排列执行,且线程池的线程数一直为corePoolSize。
PriorityBlockingQueue其实是一个特殊的无界队列,它其中无论添加了多少个任务,线程池创建的线程数也不会超过corePoolSize的数量,只不过其他队列一般是按照先进先出的规则处理任务,而PriorityBlockingQueue队列可以自定义规则根据任务的优先级顺序先后执行。

PriorityBlockingQueue是一个支持优先级的无界阻塞队列,直到系统资源耗尽。默认情况下元素采用自然顺序升序排列。也可以自定义类实现compareTo()( Comparable)方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。但需要注意的是不能保证同优先级元素的顺序。PriorityBlockingQueue也是基于最小二叉堆实现,使用基于CAS实现的自旋锁来控制队列的动态扩容,保证了扩容操作不会阻塞take操作的执行。

拒绝策略

一般我们创建线程池时,为防止资源被耗尽,任务队列都会选择创建有界任务队列,但种模式下如果出现任务队列已满且线程池创建的线程数达到你设置的最大线程数时,这时就需要你指定ThreadPoolExecutor的RejectedExecutionHandler参数即合理的拒绝策略,来处理线程池"超载"的情况。ThreadPoolExecutor自带的拒绝策略如下:

1、AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;

2、CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者(主线程)线程当中运行;

3、DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;

4、DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理(不抛出异常)。当然使用此策略,业务场景中需允许任务的丢失;

并发与并行

正在运行的程序(软件)就是一个独立的进程 线程是属于进程的,多个线程其实是并发与并行同时进行

并发:

CPU同时处理线程的数量有限
CPU会轮询的为系统的没一个线程服务 由于CPU切换的速度很快 给我们的感觉是这些线程在同时执行,这就是并发。

并行

同一个时刻同时执行

生命周期

线程的状态

就是线程从生到死的过程,以及中间经历的各种状态及状态转换

JAVA线程的状态

Thead.State 是一个枚举类 共 6种
新建(new)还没运行 -> start() Runnable

可运行(Runnable)执行完毕terminated

阻塞(block)未获得锁Runnable进入阻塞 获得锁Runnable

无限期等待(waiting)Runnable时获得锁时调用了wait() waiting 其他线程notify()并获得锁对象 Runnable

限期等待(TIMED_WAITING)Runnable sleep(毫秒) wati(毫秒)TIMED_WAITING sleep时间到Runnable wait时间到冰淇获得锁对象Runnable wait时间没到但是被别人notifi() Runnable

结束(terminated)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值