Java多线程1:线程的创建、synchronized的优化、死锁

27 篇文章 0 订阅

线程与进程的区别

  1. 从根本来看:进程是操作系统分配资源的基本单位;线程是任务调度和执行的基本单位
  2. 开销方面:每个进程都有自己独立的代码和数据空间,进程之间的切换有交大的开销;而一个线程类共享代码和数据空间,每个线程有独立的运行栈和程序计数器,线程间的切换开销较小。
  3. 所处环境:操作系统中可以同时开启多个进程,(可以理解为开启一个app),而一个进程中可以同时开启多个线程(可以理解为在一个app中一边下载一边听音乐)线程通过CPU进行调度,在每个时间片中只有一个线程在执行。
  4. 内存分配:系统在运行时为每个进程分配内存空间,而对于线程,除了CPU之外,系统不会为线程分配空间,线程使用的资源来源于所属进程的资源,线程组之间可以共享资源。
  5. 包含关系:线程属于进程的一部分。没有线程的进程可以看做单线程。

线程的状态及转换
线程的状态:

  1. 线程创建:
    线程由四种创建方式:
    1)继承Thread类并覆写其中的run()方法;
    2)实现Runnable接口并覆写其中的run()方法;可结合lambda表达式来创建;
    3)实现Callable接口并覆写run()方法,使用场景:需要有返回值的时候;返回值为泛型
    4)使用线程池,Java内置四大线程池
    启动线程使用start()方法。

  2. 线程运行:
    就绪态—运行态:获取CPU
    运行态—就绪态:yield()
    就绪态—阻塞态:sleep() join() wait()
    阻塞态—就绪态:sleep()结束 join()结束

  3. 线程终止:
    运行态–终止态:run()方法结束或者异常退出。

  4. 线程停止:
    三种线程停止的方法:
    1)设置标志位,可以使线程正常退出。
    2)使用stop()强制退出,由于不安全已经被废弃
    3)使用interrupt()中断线程

    interrupt()的三个方法:
    public void interrupt()中断线程,只是改变中断状态,不会立即终止一个正在运行线程。
    public static boolean interrupted()是否已经被中断,没有进行中断操作
    public boolean isInterrupted()测试先线程是否中断,如果连续两次进行判断就将状态置为false

wait与sleep、join的区别
1)相同点:线程调用这三个方法后都进入阻塞态。
2)wait方法是Object类的方法;join和sleep是Thread类的方法。
3)wait和join释放锁,sleep不会释放锁,调用sleep线程立即交出CPU,让CPU先去执行其他任务
4)join是wait方法做了一层包装;join不是本地方法,wait和sleep都是本地方法。
5)wait需要结合notify或notifyAll来使用,而join和sleep运行结束就进入就绪态等待下次CPU的调度。
yield()方法不会直接让线程进入阻塞态而是让线程进入就绪态,他不能控制具体交出CPU的时间。

守护线程
守护线程(后台线程):是一种特殊的线程,属于陪伴线程(垃圾回收线程)
Java中两种线程:用户线程(false)和守护线程(true),可以用isDaemon()来判断是不是守护线程
只要当前JVM进程中存在任何一个用户线程没有结束,守护线程就在一直工作,只有当最后一个用户线程停止后,守护线程会随着JVM进程一同停止。
setDaemon()将当前线程设置为守护线程
Java中启动的线程默认为用户线程,包括main线程。
线程的同步:
同步问题:每一个线程对象轮番抢占共享资源带来的问题(单线程不存在同步问题)
1.同步处理:
1)使用synchronized关键字来处理同步问题
synchronized处理同步有两种模式:同步代码块、同步方法
要使用同步代码块必须要设置一个锁定的对象,一般可以锁当前对象this
同步方法:在方法上添加synchronized关键字,表示此方法只有一个线程能进入。
隐式锁对象,this 如果一个类中有两个同步方法,线程1进入了第一个同步方法,第一个同步方法内部是死循环,线程1还没有释放锁,那么线程2能否进入第二个同步方法???
!!!不能,一定不能,一定要看对象;锁的是对象,此时线程1可以调用第二个同步方法,因为此时线程1拿到了锁
2)synchronized底层实现
同步代码块:
锁类的实例对象synchronized(this){}
锁类对象(class对象)synchronized(类名称.class){}-全局锁
锁任意实例对象,如:String lock="";synchronized(lock){}
同步方法:
1.普通方法+synchronized:锁的是当前对象
2.静态方法+synchronized:锁的是类-全局锁,效果等同于同步代码块的锁类对象
*****对象锁(monitor)机制–JDK6之前的synchronized(重量级锁)效率非常低
同步代码块:执行同步代码块后首先要执行monitorenter指令,退出时执行monitorexit指令。
使用内键锁(synchronized)进行同步,关键在于获取指定锁对象monitor对象,当线程获取monitor后才能继续向下执行,否则就只能等待。
这个获取过程是互斥的,即同一时刻只有一个线程能获取到对象monitor
通常一个monitorenter指令会包含多个monitorexit指令。原因在于JVM需要确保锁在正常执行路径以及异常执行路径上都能够正确的解锁。
同步方法:当使用synchronized标记方法时,编译后字节码中方法的访问标记多了一个ACC_SYNCHRONIZED。
该标记表示,进入该方法时,JVM需要进行monitorenter操作,退出该方法时 ,无论是否正常返回,JVM均需要进行monitorexit操作。 当执行monitorenter时,如果目标锁对象的monitor计数器为0,表示此对象没有被任何其他对象所持有。
此时JVM会将该锁对象的持有线程设置为当前线程,并且计数器+1;
如果目标锁对象的计数器不为0,判断锁对象的持有线程是否是当前线程,如果是再次将计数器+1(锁的可重入性)
如果锁对象的持有线程不是当前线程,当前线程需要等待,直至持有线程释放锁。当执行monitorexit时,JVM会将锁对象的计数器-1,将计数器的值减为0时,代表该锁对象已经被释放。

synchronized的优化:实际上优化的是等待的时间

  1. CAS操作:乐观锁策略,假设所有线程在访问资源的时候都不会产生冲突,那么就不会阻塞其他线程的操作,出现阻塞了就是用比较交换来鉴别线程是否出现冲突。出现冲突就比较交换到没有冲突为止。
    操作过程:三个值:V内存中存放的值;O旧值;N新值;当V和O相同的时候说明没有线程更改过,就可以自然而然的把新值N赋给V;当V和O不相同的时候,说明V已经是修改过的值了,不能将N赋给V,返回V即可。当多个线程同时CAS,只有一个线程会成功,其他的线程可以重试,也可以选择挂起。
    CAS的弊端及避免方案:
    1)ABA问题:当一个线程修改A值为B后又将值修改为A,另一个线程访问数据的时候发现V和O相同,但实际上修改了。解决方案:添加版本号。
    2)自旋问题:一个线程拿到锁后不释放,另一个线程一直尝试获取锁,造成CPU的大量浪费;解决方式:自适应自旋;让线程通过之前等待获取锁的时间来动态调整自旋次数。
    3)公平性:处于阻塞状态的线程无法立刻竞争被释放的锁。而处于自旋状态的线程则很有可能先获取这把锁。内键锁无法实现公平机制,lock体系可以实现公平锁。

  2. Java对象头:在同步的时候是获取对象的monitor,也就是获取对象的锁,对象的锁类似对对象的标志,这个标志就是存放在Java对象的对象头。Java对象头里默认存放的是对象的Hashcode、分代年龄和锁标记位。JDK1.6中,锁四种状态:无锁状态 0 01、偏向锁状态1 01、轻量级锁状态01、重量级锁状态11;锁只能升级不能降级的策略,提高获得锁和释放锁的效率。

  3. 偏向锁状态:最乐观的锁,假设同一时刻只有一个线程访问资源;
    只会在第一次请求时采用CAS操作,在锁对象的标记字段中记录下当前线程的地址,在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。
    偏向锁的获取:一个线程访问同步代码块并获取锁的时候,会在对象头的栈帧中的锁记录中记录下获取偏向锁线程的ID;以后当线程访问同步代码块的时候只需要简单测试一下对象头中的mark word中偏向锁ID是否是当前线程ID;如果成功,就表示该线程直接获取到锁;如果失败,检测偏向锁状态是否为0,若为0就将当前线程的ID记录到mark word字段中并将偏向锁字段置为1;如果没有设置就使用CAS竞争锁,如果设置了就尝试使用CAS将对象头的偏向锁指向当前线程。
    偏向锁的撤销:(等待竞争出现才释放锁)使用的是多个线程访问资源冲突时才释放锁的机制,当有其他线程竞争偏向锁时,锁持有者才释放。小提示:偏向锁的撤销开销比较大,需要等待线程进入全局安全点safepoint(当前线程在CPU上没有执行任何有用字节码)
    关闭偏向锁:偏向锁在JDK1.6以后默认启用,但是他在应用程序启动几秒后才激活,可以使用JVM参数Laura关闭延迟:-XX:BiasedLockingStartupDelay=0。如果确定应用程序下所有的锁通常情况下处于竞争状态,可以通过JVM参数来关闭偏向锁:=:-XX:-UseBiasedLocking=false,那么程序会默认进入轻量级锁状态。
    轻量级锁状态:假设多个线程在不同时间段获取同一把锁。
    采用CAS操作将锁对象的标记字段替换成一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。
    重量级锁状态:假设多个线程在同一时间获取同一把锁。重量级锁会阻塞、唤醒其他等待获取锁的线程,JVM采用自适应自旋;避免在线程面对非常小的Synchronized代码块时,仍然会被阻塞,唤醒的情况。

  4. 锁粗化:将多次连接在一起的加锁解锁操作合并为一次,将多个连续的锁扩展为一个范围更大的锁,如下
    1)使用线程安全的StringBuffer,它的append方法被synchronized修饰,每次追加都要进行加锁解锁。可以使用线程不安全的StringBuilder,它的append不需要进行加锁解锁,为保证线程安全,可以在第一次append操作的时候加锁,最后一次append结束的时候解锁。

  5. 锁消除:删除不必要的加锁操作,如果判断一段代码中,堆上的数据不会逃逸出当前线程,就可以认为这段代码是安全的,不需要加锁(代码逃逸技术)
    比如:在这里StringBuffer属于一个局部变量,并且不会从这段代码里逃逸出去,可以将锁消除

public class Main{
    public static void main(String[] args) {
        StringBuffer sb=new StringBuffer();
        sb.append("a").append("b").append("c");
    }
}

死锁:一个线程等待另一个线程执行完毕之后才可以继续执行,但是如果相关的几个线程彼此之间都在等待着,就会造成死锁
死锁一旦出现之后,整个程序就会中断执行,过多的同步会造成死锁,浪费CPU资源;所以对资源的上锁不能成“环”。
死锁的实现:Java的死锁实现
产生死锁的条件:

  1. 互斥条件:一个资源只能被一个线程使用
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得资源保持不放
  3. 不剥夺条件:进程已经获得的资源,在未使用完之前不能强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

避免死锁:

  1. 指定获取锁的顺序,但是这必须要知道所有可能会用到的所有的锁,但是大多数情况下我们不能确定。
  2. 指定获取锁等待的时间:在尝试获取锁的过程中设置一个超时等待时间,超过了这个时间没有获取到锁就自动放弃获取锁。弊端:有的时候不是死锁但是超时等待就放弃了。而且Java中不能对synchronized同步块设置超时时间,需要自定义一个锁。
  3. 死锁检测:当一个线程获取了锁会在线程和锁的相关数据结构中记录下来,如map、图等数据结构。除此之外,每当有线程请求锁,也需要记录在这个数据结构中,当一个线程请求锁失败的时候可以遍历锁的关系图来看看是否有死锁发生,在死锁发生的时候设置随机的优先级。
  4. 银行家算法

解除死锁:1.抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程来解除死锁。
2.终止或撤销进程。
预防死锁和避免死锁的区别
1)预防死锁是破坏产生死锁的四个必要条件之一或多个,但是施加的限制条件往往太严格,可能导致系统资源利用率和吞吐量降低。
2)避免死锁是在资源动态分配过程中,用某种方法防止系统进入不安全的状态,从而避免死锁,不需要事先破坏产生死锁的四个必要条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值