个人理解线程用
例子: 仓库搬货送货
一个仓库有一个管理员(CPU)负责取货,仓库有100箱货(资源),管理员要管理这些货,现在要人(线程)把仓库的货全部搬到车上然后送到目的地,假设只有一个人1搬货,一次搬1箱,就需要搬100趟,线程搬出仓库门的时候,管路员就暂时空闲了,这是时候如果不利用起来不是就浪费了?这个时候就可以变成多线程,一个线程搬货要花100分钟,如果有10个线程,那就只需要花10分钟就搬完货了。
1、线程、进程、多线程
进程:运行中的程序,操作系统会给这个程序分配一定的资源(占用内存资源)。
理解:把整个仓库的货搬到车上并送完货这一整个任务就可以看作是进程。
线程:CPU调度的基本单位,进程中的一个独立控制单元,线程在控制着进程的执行,一个进程中至少有一个线程。
理解:假设搬货是一个线程人1(也可以说是一个任务),送货是一个线程人2,第二个任务必须等到第一个任务结束后才能进行,整个事情肯定至少有一个线程人要来完成搬货送货这整个事情。
多线程:单个进程中同时运行多个线程。如果一个进程只有一个线程,要干的任务很多,那肯定就很慢。
理解:线程人1去送货的时候,管理员闲着,这样效率很低,那就增加线程,多来几个线程人搬货和送货,多个线程人都是共享这个仓库的货物,他们就搬这个就行。可以是两个线程人,一个干一件事,比如线程3搬货,线程4送货,也可以一起都做,比如线程3搬完货就去送货,线程4也搬完货就去送货。
说明:对于CPU的一个核而言,某个时刻, 只能执行一个线程,而CPU的在多个线程间切换速度相对我们的感觉要快,看上去就是在同一时刻运行。其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU的使用率更高。真正的多线程就是当你的机器为双cpu或者是双核的。那么这个时候确实是真正的多线程在运行。
2、线程调度
(1)分时调度:每个线程平均分配时间用cpu
(2)抢占式调度:哪个线程优先级高就先给他用cpu
3、串行、并行、并发
串行:按任务顺序处理。
理解:可以是相对于单条线程,比如线程1搬完货就去开车送货,也可以是多个线程,比如线程1搬完货,线程2就去送货。
并行:同时处理。多核CPU同时调度多个线程,是真正的多个线程同时执行。
理解:比如有两个管理员同时从仓库取货,这个就可以理解为同时执行,相同的一段代码逻辑就是取货到线程人手上。
并发:同一个时间段内处理。通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时,单核CPU无法实现并行效果,单核CPU是并发。
理解:管理员给线程1取完货,线程2又来了,马上给线程2取货,管理员取货很快,导致你在外面看起来就以为线程1和线程2同时拿到货了。
4、同步异步、阻塞非阻塞
同步与异步:执行某个功能后,被调用者是否会主动反馈信息
阻塞和非阻塞:执行某个功能后,调用者是否需要一直等待结果的反馈。
两个概念看似相似,但是侧重点是完全不一样的。
同步阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,需要一直等待水烧开。
同步非阻塞:比如用锅烧水,水开后,不会主动通知你。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能,但是需要时不时的查看水开了没。
异步阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,需要一直等待水烧开。
异步非阻塞:比如用水壶烧水,水开后,会主动通知你水烧开了。烧水开始执行后,不需要一直等待水烧开,可以去执行其他功能。
异步非阻塞这个效果是最好的,平时开发时,提升效率最好的方式就是采用异步非阻塞的方式处理一些多线程的任务。
5、多线程实现方式
1、继承Thread类,重写run方法
2、实现Runnable接口,重写run方法
3、实现Callable 重写call方法,配合FutureTask
4、基于线程池构建线程
底层,其实只有一种,都是实现Runnable
6、run()和start()的区别
run():只是调用了一个普通方法,并没有启动一个新线程,只会是在原来的线程中调用,哪个线程调用的哪个线程就去执行。
理解:比如主线程A说搬货,你人都没有指定,那只有你自己干了,如果主线程A给线程1说了可以去搬货,线程1就开始准备了。
start():重新开启一个线程,不必等待其他线程运行完,只要得到cpu就可以运行该线程。
理解:主线程A给新来的线程人说可以去搬货了start,然后新来的人就去搬货了run,所有的具体怎么搬、搬几个这种业务逻辑在run方法里面的。
7、run()和call()的区别
Callable的 call()方法有返回值和抛出异常,而Runnable的run()方法没有这些功能。Callable可以返回装载有计算结果的Future对象。
8、线程的常见方法
currentThread():获取取当前线程对象,静态方法
sleep(): 线程休眠,静态方法,当前RUNNABLE状态的线程休眠一段时间,自动唤醒,从RUNNABLE状态变成TIME_WAITING状态。
join(): 线程加入,当前线程等待,哪个线程说加入的,等他执行完当前线程再执行。
yield(): 线程让步,静态方法,当前线程从运行状态转为就绪状态,RUNNABLE的就绪变成RUNNABLE的运行。
9、线程的终止
线程结束方式很多,最常用就是让线程的run方法结束,无论是return结束,还是抛出异常结束,都可以
1、stop( ) —— 已过时,基本不用,不管线程逻辑是否完整,直接中断;
2、volatile修饰的共享变量——很少用,可通过使用一个标志指示run方法退出,终止线程。变量是volatile(或必须同步访问变量),会等到当前运行的逻辑结束后再检查是否中断;
3、interrupt( ) —— 粗暴的终止方式,推荐,会等到当前运行的逻辑结束后再检查是否中断。通过打断WAITING或者TIMED_WAITING状态的线程,从而抛出异常自行处理,这种停止线程方式是最常用的一种,在框架和JUC中也是最常见的。
10、锁
锁:多线程下,访问同一资源需要加锁
理解:假设多个线程人都想搬苹果,苹果共有10箱,只搬5箱,如果10个线程都同时去搬,那就麻烦了,每个线程一看都是10箱,自己搬完一箱都还有9箱,结果实际上一次就全搬完了,这个时候可以把这10箱苹果锁起来,一次只进去一个人去搬,线程1去看的时候1线10箱,线程2去看只有9箱了,线程6去看就发现只有5箱了,不能搬了。
死锁:两个线程,彼此在等待对方占据的锁,造成一直等待。
理解:线程1获得锁A,想继续获得锁B,线程2获得锁B,想继续获得锁A,锁又没法拆,一次只能在一个人手里(互斥),两个人都不放锁(请求和保持),两个人又不能强抢对方的锁(不剥夺),然后继续做自己的事,然后又要去获取对方的锁就一直僵持(循环等待)。
解决死锁:破坏四个条件即可。
synchronized锁膨胀的过程:无锁(没有线程)->偏向锁(1个线程)->轻量级锁(有第二个及以上的线程竞争锁)->重量级锁(有三个及以上的线程竞争锁)
偏向锁
如何加锁:线程A第一次访问同步代码块中的代码时,先检查当前锁是否可偏向(偏向锁位为0,此时是无锁),是可偏向的,则通过CAS获取锁,获取锁之后会在synchronized关键字对应的锁对象的对象头中的Markword里记录本线程ID,线程A再次访问该同步代码块中的代码时,直接比较锁对象头的Markword的线程ID是否是本线程ID,若是,则线程A可重入取锁进而直接访问同步代码块,否则说明是另外一个线程B想访问同步代码块,从而B竞争同一个锁,此时锁升级。
如何锁升级为轻量级锁:当线程B想访问synchronized同步代码块时,会检查synchronized关键字对应的锁对象的对象头中,Markword中线程ID是否为线程B的ID,若不是则B再检查锁对象头中记录的线程A是否还存活,不存活则直接把锁对象先置为无锁状态,再获取锁使其变为偏向锁;若存活则先暂停线程A,撤销偏向锁,再把锁升级为轻量级锁,然后B线程自旋(不断循环调用cas获取锁,自旋会消耗cpu使得cpu空转,所以自旋有次数限制)。
轻量级锁
如何升级为重量级锁:B线程在访问同步代码块时发现A线程正在占用锁对象,故把锁升级为轻量级锁,然后B自旋了,这时又有C线程来,此时轻量级锁升级为重量级锁,同时除了得到锁的那个线程,其他线程均会被阻塞。
11、sleep和wait的区别
sleep:线程休眠,是Thread类的静态方法,不会释放锁,调用后进入TIMED_WAITING状态,方法执行完成后,线程会自动唤醒,可以在持有锁或者不持有锁时执行,谁调用谁休息。
理解:线程1搬货的中途休息了一会,如果门是他锁住的,他只是休息一会,门的锁还是他的,不会放锁,等他休息完,他自己又继续搬了,就算是他没有拿到锁,他也可以休息,反正是他自己的事,线程1就算调用线程2的休眠,那也是线程1休息,线程2不会听他的,谁喊休息谁自己休息。
wait:线程等待,是Object类的方法,会释放锁,调用后进入WAITING状态,被动唤醒,需要别的线程调用同一个对象上的notify()或者notifyAll()方法,通常被用于线程间通信,wait方法必须在只有锁时才可以执行,否则会抛IllegalMonitorStateException。
理解:门对象让线程1暂停了,他会交出锁,然后他会去WaitSet等待集合中(假设就是这个门对象边上的通道),他把锁交出去了,他对门也没有控制权了,门不叫他,他是没法去搬货的,wait方法会修改ObjectMonitor对象,也就是修改门对象控制权。线程2去拿锁搬完货,让门对象通知之前暂停的人来搬,线程2只是让门通知了,但是线程2可能还没搬完货,他还不会交出锁,等他搬完他才会交出锁。线程1没有拿到锁他是不可以暂停的,因为暂停这个指令是门发出来的(理解为智能门或有门卫),是他有控制权的时候,让门对象喊暂停他才暂停,如果他连锁都没有拿到,他对这个门根本控制权都没有,这个时候他让门喊暂停,门就会说,你无权。
12、Object类中线程的相关方法
wait():线程等待,立即释放锁,notify()和notifyAll()方法通知是延迟通知,必须等待当前线程体执行完所有的同步方法/代码块中的语句退出释放锁才通知wait线程,这点可以从生产者消费者模型看出来,生产者线程调用wait()之后,下一个肯定就是消费者,调用了notify()之后可能还会继续运行生产者的代码,直到执行完(run方法结束)或者调用了wait方法。
notify():随机唤醒持有相同锁的一个线程,不能唤醒某个具体的线程,通常被用于线程间通信,被唤醒的线程不能马上从wait方法返回并执行,需要获取到监视器锁才可以继续执行。
理解:门对象在WaitSet集合里的线程中,随便通知了一个线程,假设是线程9,线程9准备好了之后还是需要拿到对这个门的控制权(拿到锁)才能继续搬货,这个时候假设线程8正在搬货,要等他搬完把锁交给门之后,线程9才能真正继续搬货,所以也可以认为notify()是延迟通知。
notifyAll():唤醒所有等待的线程,唤醒所有线程并允许他们争夺锁确保了至少有一个线程能继续运行,通常被用于线程间通信,也需要当前线程获取到监视器锁。
13、为什么wait()、notify()和notifyAll()方法必须加锁?
(1)如果不加锁,那么等待,唤醒这些方法就没有意义了,这些本来就是线程间通信的方法,而线程间是因为操作共享资源才会通信,而操作共享资源也就是加锁。
在Java中任何一个时刻,同一个对象的控制权只能被一个线程拥有,调用对象的wait()、notify()和notifyAll()方法时,需要先取得对象的控制权,可以通过同步锁来获得对象控制权,如synchronized(object){},不加同步锁就会抛IllegalMonitorStateException异常(非法控制状态异常),这个时候对象调用wait()就可能会造成丢失唤醒问题,即没有其他线程会唤醒这个对象所在线程,因为这个对象不是同步共享的。
理解:例如,线程7和线程8都要去搬香蕉仓库的货,加上同步锁之后,线程7和线程8才是搬的同一个香蕉仓库A的货,同一个时刻只能进去一个线程去搬,为了防止香蕉仓库7的货不对应,
如果没有加同步锁,就表明他们不会去搬同一个仓库的货,线程7和线程8就是搬的不同的香蕉仓库的货,线程7搬香蕉仓库7的货,如果这个时候线程7这边的门对象调用了wait()方法,就可能一直在等待,别人并不知道他在等待,也无权去唤醒线程7,因为没有加锁,香蕉仓库7门的控制权始终只有线程7,不会有其他线程来这里的,香蕉仓库7门的控制权不会转移到别的线程,只有加上锁才表明有其他线程也要来搬香蕉仓库7的货,这个香蕉仓库7就是共享的,那么其他线程有控制权时就可以使用门对象的通知方法去唤醒线程7。
(2)另外一个原因就是避免wait和notify之间产生竞态条件。
理解:假设线程5取货,线程6去放货,这个时候就需要用到等待和唤醒,线程5取货的时候还没货就要先等待,线程6进去放货后就通知可以取货了,这个时候如果货有很多种,线程5咋个知道线程6说的是哪个品种放好货了?但是如果有锁就好办了,比如香蕉库的锁,线程5拿到香蕉库的锁,
发现没有货,就等待并释放锁,然后在香蕉库门口(节点上)等待,线程6拿到香蕉库的锁,然后进去放货,然后再拿着香蕉库的锁去唤醒5,唤醒和等待就是因为他们拥有和曾经拥有同一把锁。
14、wait()、notify()和notifyAll()方法的理解
理解:苹果仓库门这个对象,没有加锁的时候,肯定是属于主线程的,假设就是主管,整个仓库搬货送货这些事和物归他管,现在线程1跟他说进去搬货,苹果仓库B的货是要加同步锁的,也就是只要有一个人进去了,就要把门锁起来,别人进不去,主管给了他一把同步锁之后(一旦给了同步锁之后,也表明苹果仓库B是大家都要去搬货的,这个仓库就算是公共属性了,别的线程也会操作它,没有这个同步锁的时候,每个线程都是搬自己的那个苹果仓库),线程1暂时就可以对门操作了,算是对门有控制权了,线程1搬完一箱后,门(智能门或者有门卫)说,你先暂停,线程1就出仓库不占用管理员(cpu)并交出锁,即调用wait,然后站在这个苹果仓库门的通道(WaitSet)等待,这个时候其他线程拿到同步锁也就可以进去了(同一个苹果仓库门对象的锁),线程2进去取货然后出了仓库,然后,线程2就让门对象去通知了,门就会去叫在这个苹果仓库门通道等待的人,即调用notify(获得锁的线程2才能去通知)。
15、为什么 wait, notify 和 notifyAll 这些方法不在 Thread类里面?
因为它们是用于线程之间通信的方法,而不是用于控制线程的方法。
Java允许任何对象都可以成为一个锁,也叫做对象监视器(也就是每个对象都是一个monitor监视器),监视器本身是一种信号量,对于信号量应该是共享的用来互斥或者线程通信的。
wait()表示当前获取到锁的线程A进入休眠状态并释放锁,notify()表示当前获取到锁的线程B去唤醒曾经获取到过该锁的某一个正在等待队列的线程,这里有一个很关键的点,就是他们通信的基础是获得或者曾经获得的是同一个锁,所以可以说这两个线程的通信要依赖于这个对象才行,
如果放在Thread类里面,那就意味着不同的线程需要相互侵入才能完成通信,比如A线程调用了自己的wait()方法,然后它需要告诉B线程,你可以工作了,这就是典型的侵入依赖,
其实A线程可以不用知道其他任何的线程,它只需要告诉监视器自己暂停了,然后监视器自己去通知其他的一样使用该监视器的线程就可以了。
理解:假设提供的锁是线程的,一个线程1去仓库搬苹果,用他自己的锁(锁1)锁了苹果仓库的门,带走了锁(谁的东西就属于谁),其他线程来搬货的时候,看到门锁了,但是也不知道哪个线程锁的,就要跑去挨个问是谁锁的,或者线程1带上锁,然后暂停了,就要去通知其他线程可以进去搬货了,然后其他线程搬完货,又得跑去通知线程1,并且把锁还给线程1,等线程1释放锁之后,线程2就又可以抢占仓库用自己的锁(锁2),又继续相互通知,麻不麻烦?这就是线程直接和线程通信,相互侵入了。
事实上,线程1和其他线程的通信是不是本来就可以只看门就完事?假设提供的锁是对象的,锁是属于苹果仓库门的, 现在,线程1来搬苹果,用苹果门这个对象的锁(苹果锁)锁了门,锁就还在门上(谁的东西就属于谁),线程1现在暂停,释放锁,他不需要去通知其他线程,自己去苹果仓库通道等待就行,其他线程来了,就拿到门上的锁,就进去搬货,搬完之后(假设还没搬完线程2就让门去通知notify,这个是延迟通知,别的线程来了也得等线程2把锁释放后才能拿到锁搬货),释放锁,也不需要再去通知线程1,门去通知等待的线程来工作就可以了,这就是用对象来通信。也就是wait, notify 和 notifyAll这几个方法用于线程间通信的而不是线程间相互控制的,线程间通信借助另外的对象信号量就行,线程间只需要关心门这个对象锁没锁,不需要管其他线程的情况)。