线程简介,线程实现,线程状态,线程同步,线程协作(生产者与消费者模式),高级主题JUC

线程简介:

  • Thread 俗名毛线,是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
  • 平时开发都遵循三高原则:
    1.高可用:数据不能出错。
    2.高性能:用户体验要好,不能等太久。
    3.高并发:大家同时操作。

多线程:

在这里插入图片描述在这里插入图片描述   多任务是开启多线程的初衷。很多多线程都是模拟的。因为在一个不可分割的时间点内只能做一件事情,只是各个线程执行时间很短造成一边做某事一边又做其他事情的错觉。除非两个人做不同的事情或者两台电脑做不同的事情或者多个cpu多核才能做不同的事情。
实际生活中,多线程有很广泛的应用。例如在游戏中同一个界面,多个角色操控。上网时候多个用户同时上网,互不影响。双十一,同时抢东西,做到真正的秒杀等等。
程序,进程,多线程的关系:
在这里插入图片描述
  假如有一个视频软件:那么程序就是编写这个视频软件的代码,它是静态的,是一个代码的指令。一个进程就是一个程序,它是动态的(是指cpu调度到了),而一个进程开辟多个线程(多条路径)就是多线程。

特点:

1.线程是独立的执行路径。
2.在程序运行时,即使没有自己创建线程,后台也会存在多个线程。如gc线程,主线程。
3.main()称为主线程,是系统的入口,用于执行整个程序。
4.一个进程开辟多个线程。线程的运行由调度器安排,不是人为控制的。而调度器是与操作系统紧密相关,先后顺序是不能人为干预的。
5.对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制。
6.线程会带来额外开销,如cpu调度时间,并发控制干预。当然了忽略不计。
7.每个线程在自己的工作内存交互,加载和存储,主内存控制不当会造成数据不一致。

线程实现:

创建线程的3种方式:

1.继承Thread类+重写run()方法+创建子类对象+真实对象调用start()启动。等cpu调度,人为不能干预。
2.实现Runnable接口+重写run()方法+创建子类对象+代理对象调用start()启动。静态代理。
3.实现Callable接口+重写call()方法+启动:创建执行服务,提交服务,获取结果,关闭服务。好处是:可以返回值,抛出异常。

线程状态:

在这里插入图片描述

阻塞状态:

1.sleep():抱着资源睡觉
• 可以模拟网络延时,放大了发生问题的可能性。倒计时
• 存在异常interruptedException
• sleep时间达到后线程进入就绪状态。
• 每一个对象都有一把锁,sleep不会释放锁,抱着资源睡觉。
2.wait():就像红绿灯不占资源
3.join():合并线程,插队
• 合并线程由原来的多条线程合并成一条线程,应理解为插队线程,待这个线程执行完后,再执行其他线程,其他线程阻塞。
• 是个成员方法。
• 写在哪个线程哪个线程被阻塞。
4.read,write

就绪状态:

1.strat()
2.阻塞解除
3.yield()
• 高风亮节让出cpu调度,避免当前线程占用cpu过久,中断一下重新进入就绪状态。大家重新竞争,让cpu调度器重新调度
• Thread.yield()写在哪个线程中,哪个线程礼让
4.jvm本身将cpu从本地线程根据自己的算法切换到其他线程,此线程进入就绪状态

死亡状态:

停止线程:
1.线程正常执行完毕–>线程里面本身就有次数。比如一个循环10次100次或几行代码或者一个方法,方法实现它就完了。
2.外部干涉–>加入标识。提供一个布尔类型的终止变量,当这个变量置为false,则终止线程。
注意:别使用stop(),destory()。已过时(方法有bug或方法有更优的解决方案),不安全。

线程常用方法:

• sleep ()
使线程停止运行一段时间,将处于阻塞状态
如果调用了sleep方法之后,没有其他等待执行的线程,这个时候当前线程不会马上恢复执行!
• join ()
阻塞指定线程等到另一个线程完成以后再继续执行。
• yield ()
让当前正在执行线程暂停,不是阻塞线程,而是将线程转入就绪状态;
调用了yield方法之后,如果没有其他等待执行的线程,此时当前线程就会马上恢复执行!
• setDaemon()
可以将指定的线程设置成后台线程,守护线程;
创建用户线程的线程结束时,后台线程也随之消亡;
只能在线程启动之前把它设为后台线程
• setPriority(int newPriority) getPriority()
线程的优先级代表的是概率
范围从1到10,默认为5
• stop()停止线程
不推荐使用

Priority

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行。线程的优先级用数字表示,范围从1到10
• Thread.MIN_PRIORITY = 1• Thread.MAX_PRIORITY = 10
• Thread.NORM_PRIORITY = 5使用下述方法获得或设置线程对象的优先级。
• int getPriority();//获取
• void setPriority(int newPriority);//设置
优先级的设定建议在start()调用前
注意:优先级低只是意味着获得调度的概率低。并不是绝对先调用优先级高后调用优先级低的线程。

Daemon

用户线程:默认线程都是用户线程。
守护线程:后台记录操作日志,监控内存使用等。它是为用户线程服务的,jvm停止不用等待守护线程执行完毕。
setDaemon(true);// 设置为守护线程,默认false

其他方法:

isAlive() 判断线程是否还活着
setName() getName()代理名称,真实名称用面向对象设置。
currentThread() 当前线程对象。

线程同步:

在这里插入图片描述
现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。
比如:派发礼品,多个人都想获得。天然的解决办法就是,在礼品前,大家排队。前一人领取完后,后一人再领取。处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象。 这时候,我们就需要用到“线程同步”。 线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面的线程使用完毕后,下一个线程再使用。

并发:

同一个对象被多个线程同时操作。一旦存在并发,就会出现数据不准确,线程不安全。
12306抢票:
不安全:
1.数据有负数:最后一张票,B先进来,A,C也相继进来,Thread.sleep(200);然后都睡200毫秒,B先醒来拿最后一张票,A醒来就只能拿0,C醒来就只能拿-1了。
2.数据相同:开辟多线程会存在自己的工作内存。工作内存和主内存在拷贝时不一致,并没有等更新完就拷贝了。A拿10要把10拷贝过去减1变成9后覆盖主存。而B在10变成9的过程中也将10拷贝走,所以两张10就出现了。
取钱:
不安全:
1.同时去取钱,假如账户有100万,可能你和你对象都取了100万,这时银行不干了,这叫非法获取个人收入,犯罪的
2.存钱原来有100万,你再存100万,理论有200万,但是你对象在另一边取你不知道,假如取了100万,这时存完后你发现只有100万了。这时候你不干了,但是也没办法。(数据不一致)
容器:
一个集合放10000个数据,就会存在数据覆盖,数据不对。
数据修改需要同步,只是简单的读没必要。
在这里插入图片描述
  由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。为了保证数据在方法中被访问时的正确性,在访问时加入锁机制(synchronized),当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可。
存在以下问题:
• 一个线程持有锁会导致其它所有需要此锁的线程挂起;
• 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题;
• 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。
  加了锁之后,会感到慢,毕竟在数据结构中,鱼和熊掌不可兼得,时间复杂度和空间复杂度不可能同时解决,只有互相审视对方才可达到最优。
具体实现:
  由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized 方法和 synchronized 块。
• 同步方法:锁对象的资源this
public synchronized void method(int args) {}
synchronized 方法控制对“成员变量|类变量”对象的访问:每个对象对应一把锁,每个 synchronized 法都必须获得调用该方法的对象的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。
缺陷:
1.加了同步后,系统性能会有下降。
2.若将一个大的方法声明为synchronized 将会大大影响效率。所以要锁对,太大效率底下,太小锁不住。锁不对就会出现数据不准确,线程不安全。
• 同步块
• 同步块: synchronized (obj){ }锁一个地址不能变的对象,让锁住的对象的属性变。对象变的话就相当于房间一直变你锁一个锁不住。尽可能锁定合理的范围。多用同步块。
• obj可以是任何对象,但是推荐使用共享资源作为同步监视
器。
• 同步方法中无需指定同步监视器,因为同步方法的同步监视器是this即该对象本身,或class即类的模子,对象本身。
• 双重检测(DoubleChecked),考虑临界值的问题,减少并发系统中竞争和同步的开销。优化代码,提高性能。
块: Java有四种
1.方法里面:局部块解决变量作用域,比如声明一个变量a,a出了块就不能用了,快速释放内存。
2.在类中方法外:构造块,和构造方法一样是用来初始化构造信息的,初始化对象的。
3.如果在构造块上加一个static就叫静态块。只加载一次,用来初始化类的,加载类的。
4.同步块:在方法里面,解决线程安全的问题,synchronize(obj)同步块加一个synchronize来监视这个对象。
并发容器:
JUC:Java.util.concurrent并发包。
JUC并发编程中List有相应的容器CopyOnWriteArrayList直接供给我们使用,它内部直接实现锁定了,不用我们锁。
Java.util.concurrent.CopyOnWriteArrayList(写的基础上进行拷贝),就是在工作内存和主存相互写的过程中能够正确的控制,不用synchronized

死锁:

某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。
过多的同步可能会造成相互不释放资源,从而互相等待,一般发生于同步持有多个对象的锁。
避免死锁:不要在一个代码块中同时持有多个对象的锁。

线程协作:

在多线程环境下,只要发生并发,我们就有责任保证数据的准确性和安全,也就是通过synchronized达到数据的准确和安全。但是,线程和线程之间也需要通信,怎么通信?怎么协作?
生产者与消费者模式:就是处理并发线程通信合作的一个模型。不是23种设计模式之一。
解决方式1:
并发协作模型“生产者/消费者模式” -->管程法
在这里插入图片描述• 生产者:负责生产数据的模块(这里模块可能是:方法、对象、线程、进程);
• 消费者:负责处理数据的模块(这里模块可能是:方法、对象、线程、进程);
• 缓冲区:消费者不能直接使用生产者的数据,它们之间有个“缓冲区”;生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。
好处:
1.利用管道利用缓冲区操作,生产者和消费者之间不用打交道,不需要交流。解耦
就相当于商业活动中的中间商或商店,消费者并不知道商品从哪里生产或者进货渠道,同样生产者不知道货销给了谁。生产者仅向商店提供商品,消费者仅从商店拿商品。
2.解决了忙是忒忙,闲时忒闲,忙闲不均的问题。提高效率
解决方式2:
并发协作模型“生产者/消费者模式” -->信号灯法
在这里插入图片描述加一个标识。红灯:你停车走。绿灯:你走车停。
Java提供了3个方法解决线程之间的通信问题:
在这里插入图片描述均是java.lang.Object类的方法都只能在同步方法或者同步代码块中使用,否则会抛出异常
阻塞wait():
每个对象都有wait()等待,调用后,表示当前线程一直等待,与sleep不同的是会释放锁。当调用notify()或notifyAll()时,wait时间到,被唤醒后重新进入Runnable可运行状态。
在这里插入图片描述

高级主题:

1.任务定时调度:

某一个有规律的时间点干某件事:
通过Timer和Timetask,我们可以实现定时启动某个线程。
• java.util.Timer:类似闹钟的功能,你可以设置时间,本身实现的就是一个线程
API:线程调度任务以供将来在后台线程中执行的功能,任务可安排一次执行,或者定期重复执行。
• java.util.TimerTask:一个抽象类,该类实现了Runnable接口,所以该类具备多线程的能力。这个实现类重写run()方法,写代码写任务。
API:可以有Timer执行一次或重复执行的任务。
timer.schedule();

2.QUARTZ:

使用Timer和Timertask可以很方便的实现简单的任务调度,但是如果遇到复杂的就推荐使用QUARTZ 石英石。
任务调度框架:
1.Scheduler  调度器 控制所有调度
2.Trigger  触发条件,采用DSL模式。
3.JobDetail  需要处理的Job具体任务
4.Job  执行逻辑
DSL: Domain-specific language领域特定语言,针对一个特定的领域,具有受限表达性的一种计算机程序语言,即领域专用语言,声明式编程。用简单的代码表达特殊的含义来快速解决特殊的问题
• 1.Method Chaining 方法链 Fluent Style流畅风格 ,builder模式构建器构建器
• 2.Nested Functions 嵌套函数
• 3.Lambda Expressions/Closures
• 4.Functional Sequence 流

3.HappenBefore:

译过来就是发生之前。当我们写的代码没有按你编写的位置,期望顺序的执行,显然是发生了指令位置调换了。为什么调换,因为编译期和cpu会尝试重排指令,使代码更快运行,提高性能。
简单理解:发生在代码与代码之间没有相互的直接联系,没有相互的依赖,把后面的代码提前不影响结果就叫指令重排
执行一条指令的4个步骤:
1.抓取指令 fetch
2.解码翻译 从寄存器主存(拿值)拿到工作内存。存值
3.执行代码操作算值
4.将值写回 writeback 慢
  执行代码的顺序可能与编写代码不一致,即虚拟机优化代码顺序,则为指令重排happen-before即:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。
一般发生在:
1.在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。拿上面的例子来说:假如不是a=1的操作,而是a=new byte[1024*1024](分 配1M空间),那么它会运行地很慢,此时CPU是等待其执行结束呢,还是先执行下面那句flag=true呢?显然,先执行flag=true可以提前使用CPU,加快整体效率,当然这样的前提是不会产生错误(什么样的错误后面再说)。虽然这里有两种情况:后面的代码先于前面的代码开始执行;前面的代码先开始执行,但当效率较慢的时候,后面的代码开始执行并先于前面的代码执行结束。不管谁先开始,总之后面的代码在一些情况下存在先结束的可能。
2.在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。

4.Volatile:易变可变

volatile保证线程间变量的可见性(保证数据同步),只要你改了,我马上就可以知道,能看到变量的变动。
缺点:不能保证原子性。CAS可以保证。
例如:a++这条指令:
1.抓a
2.拿a到主存
3.a+1
4.存到a里面
a++是一个变量,volatile不能保证他们4个是一个整体发生的,不能保证同时操作,只能保证我改了这个主存,你马上能看到。
  有人说它是轻量级的synchronized ,synchronized 保证并发保证同步。volatile只保证了同步的数据可见,所以实现了synchronized 的一部分功能

5.设计模式之单例模式:

生活案例:
1.任务管理器,一台电脑就一个任务管理器,做web开发的计数器
2.写配置文件,比如log4j内部
3.数据库的连接池
4.序列号的生成
保证在DCL模式double-checking和volatile下,懒汉式套路上加入并发控制多线程环境下,对外存在一个对象:
1.构造器私有化–>避免外部new构造器
2.提供私有的静态属性–>存储对象的地址。饿汉式是在这马上给它new一个对象,没有new对象空着就是懒汉式。
3.提供公共的静态方法–>获取属性

6.ThreadLocal:线程本地环境

每个线程自身的存储本地、局部区域。存储自己的数据更改不会影响其他线程。
相等于一个银行这么大的区域A。每一个用户相当于一个线程,从银行A拿一块区域。各自使用各自的,大家互不影响。

  • 在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程。
  • ThreadLocal能够放一个线程级别的变量,其本身能够被多个线程共享使用,并且又能够达到线程安全的目的。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全,常用的方法,就是get/set/initialValue(初始化) 方法。
  • JDK建议ThreadLocal定义为private static
  • ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的方法都可以非常方便地访问这些资源。
  • Hibernate的Session 工具类HibernateUtil• 通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

7.可重入锁:ReentrantLock 锁可以延续使用+计数器

锁作为并发共享数据保证一致性的工具,大多数内置锁都是可重入的,也就是说,如果某个线程试图获取一个已经由它自己持有的锁时,那么这个请求会立刻成功,并且会将这个锁的计数值加1,而当线程退出同步代码块时,计数器将会递减,当计数值等于0时,锁释放。如果没有可重入锁的支持,在第二次企图获得锁时将会进入死锁状态。
  判断一下你进来的这个线程是不是锁定的线程如果是,不用等,直接用,计数器加1;如果不是就等到锁定线程用完才可以用。
锁分为两类:
• 悲观锁:synchronized是独占锁即悲观锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
• 乐观锁:每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。 
CAS(Compare and Swap 比较并交换):
CAS思想:如果原来值没动过就交换成功。如果动过就交换失败。
乐观锁的实现:
• 有三个值:一个当前内存值V、旧的预期值A、将更新的值B。先获取到内存当中当前的内存值V,再将内存值V和原值A作比较,要是相等就修改为要修改的值B并返回true,否则什么都不做,并返回false;
• CAS是一组原子(Atomicl)操作,cpu内部来做,不会被外部打断;
• 属于硬件级别的操作(利用CPU的CAS指令,同时借助JNI来完成的非阻塞算 法),效率比加锁操作高。
• ABA问题:如果变量V初次读取的时候是A,并且在准备赋值的时候检查到它仍然是A,那能说明它的值没有被其他线程修改过了吗?如果在这段期间曾经被改成B,然后又改回A,那CAS操作就会误认为它从来没有被修改过。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值