线程同步、线程锁、线程通信

Java知识点总结:想看的可以从这里进入

9.5、线程同步

使用线程就免不了要考虑线程安全的问题,尤其是银行财务等等系统,如果多个线程之间的共享数据一旦出现问题,那么就会造成大乱子。(比如两个人同时向一张卡中存钱,如果在操作中这两个线程同时获取到了卡中的余额,那么在存钱后,就会出现两个不同的返回值)

image-20230206165240487

image-20230206165312708

image-20230206165419784

所以说在多线程的情况下,靠根据实际需求考虑线程的同步问题,线程同步就是当有一个线程在对某个内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,此时其他线程需要处于等待状态。

9.5.1、synchronized

Java中有一个关键字 synchronized ,它就是同步的意思,它就像是一把锁,它将两个任务访问相同的资源加上锁,第一个访问某项资源的任务必须锁定这项资源,其他任务在资源被解锁之前,是无法访问该资源的,只能进行等待,而在其被解锁之时,后续等待的任务才能继续访问。

在synchronized中,任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器),我们必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全:

  • 当时用synchronized 修饰方法时,该方法就是一个同步方法(它的锁不需要显示的声明:静态方法的锁(类名.class)、非静态方法的锁(this))

    上面的例子只需要在共享的银行卡中存款的方法上添加 synchronized 即可实现同步。

    image-20230206165454577

    image-20230206165514009

  • 修饰代码块时,就是一个同步代码块。(锁:可自己创建对象指定,也是指定为this或类名.class)

    image-20230206170149099 image-20230206170818971

需要注意的是对代码块或方法加上锁后,相当于这块代码从线程的并行变成了串行(相当于加锁的地方是单线程运行的),这样虽然解决了线程同步的问题,但同时也降低了运行的效率。所以需要注意加锁的范围(范围太小:没锁住所有有安全问题的代码。范围太大:会浪费多线程优势),所以我们在使用同步时需要:

  1. 明确哪些代码是多线程运行的代码
  2. 明确多个线程是否有共享数据
  3. 明确多线程运行代码中是否有多条语句操作共享数据

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中。

会释放锁的操作:

  1. 当前线程的同步方法、同步代码块执行结束。
  2. 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。
  3. 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束。
  4. 当前线程在同步代码块、同步方法中执行了线程对象的wait() 方法,当前线程暂停,并释放锁。

不会释放锁的操作:

  1. 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  2. 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。应尽量避免使用suspend()和resume()来控制线程

死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁()。出现死锁后,程序不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行。
比如线程1占用了资源A后,在运行时又需要资源B,但是此时资源B已经被线程2锁持有,所以线程1只能等待资源B被释放,但是占用资源B的线程2在运行中又需要资源A,所以两个线程就形成了一个环,但是两个线程又不放弃自身所占用的资源,这样就形成了一个死锁,两个线程一种处于等待资源的状态,但是又获取不到资源,所以程序就会卡在这里无法执行。
产生死锁的必要条件:

  1. 互斥条件:线程对所分配到的资源不允许其他线程进行访问,若其他线程访问该资源,只能等待,直至占有该资源的线程使用完成后释放该资源
  2. 请求和保持条件:线程获得一定的资源之后,又对其他资源发出请求,但是该资源可能被其他线程占有,此事请求阻塞,但又对自己获得的资源保持不放
  3. 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。
  4. 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链。
9.5.2、Lock

JDK 5.0后,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当,Java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock 接口,它是是可重入的互斥锁,虽然拥有与 synchronized 相同的并发性和内存语义,但是会比 synchronized更加灵活,可以手动开启、关闭锁。

ReentrantLock底层基于AbstractQueuedSynchronizer实现。AbstractQueuedSynchronizer抽象类定义了一套多线程访问共享资源的同步模板,解决了实现同步器时涉及的大量细节问题,能够极大地减少实现工作,AbstractQueuedSynchronizer为加锁和解锁过程提供了统一的模板函数,只有少量细节由子类自己决定。

Sync继承了AbstractQueuedSynchronizer,是ReentrantLock的核心。

image-20230206201339108
public class LockTest{
    //true是公平锁。默认为false
    ReentrantLock lock = new ReentrantLock(true);
    public void f() {
        //使用阻塞等待获取锁的方式中,必须在try代码块之外,并且在加锁方法与try代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁。
        //说明一:如果在lock方法与try代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
        //说明二:如果lock方法在try代码块之内,可能由于其它方法抛出异常,导致在finally代码块中,unlock对未加锁的对象解锁,它会调用AQS的tryRelease方法(取决于具体实现类),抛出IllegalMonitorStateException异常。
        //说明三:在Lock对象的lock方法实现中可能抛出unchecked异常,产生的后果与说明二相同。 
        lock.lock();	
        try{
            //保证线程安全的代码;
        }
        finally{
            lock.unlock(); 
        }
    }
}

9.6、线程锁

多个线程同时运行一个数据时,往往因为抢占资源而产生脏数据,所以就用到了加锁机制。

  • 公平锁/非公平锁
    • 公平锁是指多个线程按照申请锁的顺序来获取锁。
    • 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
  • 可重入锁:递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
  • 独享锁/共享锁
    • 独享锁是指该锁一次只能被一个线程所持有。
    • 共享锁是指该锁可被多个线程所持有。
  • 互斥锁/读写锁
    • 互斥锁在Java中的具体实现就是ReentrantLock
    • 读写锁在Java中的具体实现就是ReadWriteLock
  • 乐观锁/悲观锁
    • 乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
    • 悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
  • 分段锁:是一种锁的设计,并不是具体的一种锁(ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。)
  • 偏向锁/轻量级锁/重量级锁
    • 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。
    • 轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
    • 重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。
  • 自旋锁:自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

9.7、通信方式

  1. 同步:多个线程通过synchronized关键字这种方式来实现线程间的通信。本质上就是“共享内存”式的通信,多个线程需要访问同一个共享变量,谁拿到了锁(获得了访问权限),谁就可以执行。

  2. while轮询的方式:通过一个条件,线程A不断地改变条件,线程B不停地通过while语句检测这个条件是否成立 ,从而实现了线程间的通信。

  3. wait/notify 机制:在synchronized方法或synchronized代码块中才能使用,通过一个条件,当条件未满足时,线程A调用wait() 放弃CPU,并进入阻塞状态。当条件满足时,线程B调用 notify()唤醒线程A,并让它进入可运行状态。、

    1. wait():令当前线程挂起并放弃CPU、同步资源并等待,使别的线程可访问并修改共享资源,而当前线程排队等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

    2. notify():唤醒正在排队等待同步资源的线程中优先级最高者

    3. notifyAll ():唤醒正在排队等待资源的所有线程.

    @Override
    public void run() {
        while (true){
            synchronized (this){
                notify();	//将等待线程唤醒
                if(num < 20 ){
                    System.out.println(Thread.currentThread().getName()+"打印:"+num);
                    num++;
                }else {
                    break;
                }
                try {
                    //线程进入等待
                    wait();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
    

    image-20230207141527961

  4. 管道通信:使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

辰 羽

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

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

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

打赏作者

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

抵扣说明:

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

余额充值