多线程知识点总结

进程和线程

进程: 程序在执行过程中分配和管理资源的基本单位,是一个动态概念,竞争计算机系统资源的基本单位。
线程: 是进程的一个执行单元,是进程内部的调度实体;比进程更小的独立运行的基本单位;线程也被称为轻量级进程 。
一个进程可以包含一个或多个线程,一个线程就是一个程序内部的一条执行线索;同一进程内的线程共享本进程的地址空间。
进程和线程的区别

  • 每个进程都有独立的代码和数据空间,进程的切换会有很大的开销;
  • 同一类线程共享代码和数据空间,每个线程有独立运行的栈和程序计数器,线程切换的开销小;
  • 进程可以独立运行,线程不能独立执行,必须依存在某个应用程序中,由应用程序提供多个线程执行控制。

使用多线程的好处
使用多线程可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

线程的状态及生命周期

Java中的线程生命周期大体可以为五种状态:
1、新建(New):新创建一个线程对象。
2、可运行(Runnable):对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权。
3、运行(Running):可运行状态(runnable)的线程获得了cpu 时间片(timeslice) ,执行程序代码。
4、阻塞(Blocked):阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。
阻塞的情况分三种:

  • 等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。
  • 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。

5、死亡(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
image.png

多线程实现的两种方式

(1) 创建线程类
继承Thread类 或
实现Runnable接口
(2) 通过Thread类构造器来创建线程对象
Thread( )
Thread(Runnable target)
(3) 通过start()方法激活线程对象
两种实现方式比较
实现Runnable接口相对于继承Thread类来说,有如下显著的好处:
1)适合多个相同的程序代码的线程去处理同一个资源,比如卖票问题
2)可以避免java中的单继承的限制
3)增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

终止线程

1、使用退出标志终止线程
当run方法执行完后,线程就会退出。但有时run方法是永远不会结束的。如在服务端程序中使用线程进行监听客户端请求,或是其他的需要循环处理的任务。在这种情况下,一般是将这些任务放在一个循环中,如while循环。如果想让循环永远运行下去,可以使用while(true){……}来处理。但要想使while循环在某一特定条件下退出,最直接的方法就是设一个boolean类型的标志,并通过设置这个标志为true或false来控制while循环是否退出。下面给出了一个利用退出标志终止线程的例子。
2、使用stop方法终止线程
使用stop方法可以强行终止正在运行或挂起的线程;但是,使用stop方法是很危险的,就像突然关闭计算机电源,而不是按正常程序关机一样,可能会产生不可预料的结果,因此,并不推荐使用stop方法来终止线程。
3、使用interrupt方法终止线程
使用interrupt方法来终止线程可分为两种情况:
(1)线程处于阻塞状态,比如使用了sleep方法;
(2)使用while(!isInterrupted()){……}来判断线程是否被中断;
在第一种情况下使用interrupt方法,sleep方法将抛出一个InterruptedException异常,而在第二种情况下线程将直接退出。
注意: 在Thread类中有两个方法可以判断线程是否通过interrupt方法被终止。一个是静态的方法interrupted(),一个是非静态的方法isInterrupted(),这两个方法的区别是interrupted用来判断当前线是否被中断,而isInterrupted可以用来判断其他线程是否被中断。因此,while(!isInterrupted())也可以换成while(!Thread.interrupted())。

线程的优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照线程的优先级来决定应调度哪个线程来执行。
线程优先级的几个特性:

  • 继承性:也就是如果线程A启动线程B,那么线程A和B的优先级是一样的;
  • 规则性:即线程会根据优先级的大小顺序执行,但不一定是优先级较大的先执行;
  • 随机性:线程的优先级无法保障线程的执行次序。

优先级高的线程获取CPU资源的概率较大。无论是级别相同还是不同,线程调用都不会绝对按照优先级执行,每次执行结果都不一样,调度算法无规律可循,所以线程之间不能有先后依赖关系。
无时间片轮循机制时,高级别的线程优先执行,如果低级别的线程正在运行时,有高级别线程可运行状态,则会执行完低级别线程,再去执行高级别线程。如果低级别线程处于等待、睡眠、阻塞状态,或者调用yield()函数让当前运行线程回到可运行状态,以允许具有相同优先级或者高级别的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

线程类中的主要方法
image.png

线程的休眠
image.png

线程同步

有时两个或多个线程可能会试图同时访问一个资源
例如,一个线程可能尝试从一个文件中读取数据,而另一个线程,则尝试在同一文件中修改数据;
这种情况下,数据可能会变得不一致;
为了确保在任何时间点一个共享的资源只被一个线程使用,必须使用“同步”
当一个线程运行到需要同步的语句后,CPU不去执行其他线程中的、可能影响当前线程中的下一句代码的执行结果的代码块,必须等到下一句执行完后才能去执行其他线程中的相关代码块,这就是线程同步;

Java实现线程同步的几种方法
方法一、使用Synchronized关键字
synchronized 关键字是用于保护“共享数据”。
注: synchronized关键字修饰静态方法时,此时如果调用该静态方法,将会锁住整个类。
同步是高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
方法二、wait()和notify()
wait():使一个线程处于等待状态,并且释放所持有的对象的lock;
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException异常;
notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的
唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且不是按优先级;
notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,
而是让它们竞争。
方法三、使用特殊域变量(volatile)实现线程同步
Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制;
image.png

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。
• 使用特殊域变量(volatile)实现线程同步实例
image.png
注意:volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性。
方法四、使用可重入锁实现线程同步
  在JDK中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而言功能更加丰富,使用起来更为灵活,也更适合复杂的并发场景。
  ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
  ReentrantLock和synchronized都是可重入的。synchronized可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。
  ReentrantLock可以实现公平锁; 公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权。而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公平锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用。
  ReentrantLock可以响应中断;当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁,否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。而ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly(),该方法可以用来解决死锁问题。
  获取锁限时等待;提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,可以使用该方法配合失败重试机制来更好的解决死锁问题。
方法五、使用局部变量(ThreadLocal)实现线程同步
  ThreadLocal从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
  概括来说,对于多线程资源共享的问题,其他方法采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
  void set(T value):设置当前线程的线程局部变量的值。
  public T get():该方法返回当前线程所对应的线程局部变量。
  public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
  initialValue():返回该线程局部变量的初始值,这个方法是一个延迟调用方法,在线程第1次调用get()或set(T)时才执行,并且仅执行1次。
image.png

死锁

死锁举例:
  如果多个线程要调用多个对象,而A线程调用A锁占用了A对象,B线程调用了B锁占用了B对象,A线程不能调用B对象,B线程不能调用A对象,于是一直等待。这就造成了线程“死锁”。
  Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了,导致了死锁。这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1,从而导致了死锁。
如何避免死锁的发生
一旦我们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,需要注意:
1)如果其它对象的这个方法会消耗比较长的时间,那么就会导致锁被长时间持有;
2)如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;
尽量使用“不可变对象”来避免锁的使用,在某些情况下还可以避免对象的共享,比如 new 一个新的对象代替共享的对象,因为锁一般是对象上的,对象不相同了,也就可以避免死锁,另外尽量避免使用静态同步方法,因为静态同步相当于全局锁。
还有一些封闭技术可以使用:比如堆栈封闭,线程封闭,ThreadLocal,这些技术可以减少对象的共享,也就减少了死锁的可能性。
死锁的必要条件?怎么克服?
产生死锁的四个必要条件:互斥条件:一个资源每次只能被一个进程使用。请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。
死锁的解决方法:
a撤消陷于死锁的全部进程;
b逐个撤消陷于死锁的进程,直到死锁不存在;
c从陷于死锁的进程中逐个强迫放弃所占用的资源,直至死锁消失。
d从另外一些进程那里强行剥夺足够数量的资源分配给死锁进程,以解除死锁状态。

sleep()和wait()有什么区别

sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,给执行机会
给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

yield与join方法的区别

yield()方法:暂停当前正在执行的线程对象,并执行其他线程。
  yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。因此,使用yield()的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证yield()达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。
结论:yield()从未导致线程转到等待/睡眠/阻塞状态。在大多数情况下,yield()将导致线程从运行状态转到可运行状态,但有可能没有效果。

jion()方法:线程实例的join()方法可以使得一个线程在另一个线程结束后再执行,即也就是说使得当前线程可以阻塞其他线程执行
  Thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Apple_Web

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值