Java多线程——线程同步

线程同步

基本概念

线程同步指的是在多线程环境下,通过某种机制来协调多个线程对共享资源的访问,确保在同一时刻只有一个线程能够访问共享资源,从而避免数据不一致和其他并发问题。可以将其类比为多个线程在使用同一台打印机,为了避免打印内容混乱,需要规定一次只能有一个线程使用打印机,这个规定的过程就是线程同步。

存在以下问题

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起。
  • 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题。

产生问题的原因

当多个线程同时访问和修改共享资源时,可能会出现以下问题:

  • 数据不一致:例如,一个线程正在读取共享变量的值,而另一个线程同时在修改这个变量的值,就可能导致读取到的数据是错误的。
  • 竞态条件:多个线程对共享资源的操作顺序不确定,可能会导致程序的执行结果出现不可预测的情况。

实现线程同步的方式

1.synchronized 关键字(本质利用队列和锁)
  • 修饰实例方法:当 synchronized 修饰实例方法时,同一时刻只有一个线程能够访问该方法所属对象的这个方法。锁对象是当前对象实例。(一般是修饰那些会修改共享资源的方法,其他只读的方法可以不用修饰)

“锁对象是当前对象实例”,这句话的意思是把当前这个对象实例上锁,操作这个对象都要有锁才行,当有线程对这个对象进行操作时,这个对象就被锁上了,只有等这个线程结束操作这个对象才会释放锁,别的线程才能够对这个对象进行操作。

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

在上述代码中,increment() 方法被 synchronized 修饰,当一个线程进入该方法时,会自动获取 Counter 对象的锁,其他线程必须等待该线程执行完方法并释放锁后才能进入。

  • 修饰静态方法:如果 synchronized 修饰的是静态方法,那么锁对象是该类的 Class 对象。所有线程在访问该静态方法时,都需要竞争同一个锁。

回想之前的线程休眠,是不是提到过:"每一个对象都有一个锁,sleep不会释放锁“,这句话的意思就是假设a线程占领了这个锁,就算此时a线程调用sleep方法休眠了,但是由于锁没有释放,其他需要该锁的线程仍然不能执行。

class StaticCounter {
    private static int count = 0;

    public static synchronized void increment() {
        count++;
    }

    public static int getCount() {
        return count;
    }
}

这里的 increment() 静态方法被 synchronized 修饰,所有线程调用该方法时都要竞争 StaticCounter 类的锁。

  • 修饰代码块:可以指定一个对象作为锁,只有获取到该对象锁的线程才能执行代码块中的内容。

当使用 synchronized 修饰实例方法时,锁的对象是当前对象实例(this);修饰静态方法时,锁的对象是类的 Class 对象。如果共享资源不属于当前对象实例或者类,而是其他对象的数据,使用 synchronized 修饰方法可能无法达到预期的同步效果,因为此时锁错了对象。而修饰代码块可以自己指定锁的对象。

例如,有两个类 AB,类 B 中有一个共享资源,而类 A 中的方法需要操作类 B 的这个共享资源。如果在类 A 的方法上加 synchronized,锁的是类 A 的实例对象,而不是类 B 的实例对象,其他线程仍然可以同时操作类 B 的共享资源,无法实现同步。

class B {
    int sharedResource = 0;
}

class A {
    B b;

    public A(B b) {
        this.b = b;
    }

    // 错误示例:锁的是A的实例对象,但是我们其实操作的共享数据是B类里面的,无法对B的共享资源同步,
    public synchronized void wrongModify() {
        b.sharedResource++;
        System.out.println(b.sharedResource);
    }

    // 正确示例:使用synchronized代码块,锁B的实例对象
    // 其实区别不大,就是把原来修饰方法的synchronized关键字,放到方法内部去修饰块,而块里的内容就是原来方法体里的代码,只是主要把要锁的对象给synchronized关键字
    public void correctModify() {
        synchronized (b) {
            b.sharedResource++;
            System.out.println(b.sharedResource);
        }
    }
}

public class Main {
    public static void main(String[] args) {
        B b = new B();
        A a = new A(b);

        // 创建两个线程调用wrongModify方法,无法同步
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.wrongModify();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.wrongModify();
            }
        });

        // 创建两个线程调用correctModify方法,可以同步
        Thread t3 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.correctModify();
            }
        });
        Thread t4 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                a.correctModify();
            }
        });

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

在上述代码中,wrongModify 方法使用 synchronized 修饰,锁的是 A 的实例对象,无法对 B 的共享资源进行同步;而 correctModify 方法使用 synchronized 代码块,锁的是 B 的实例对象,可以实现对 B 的共享资源的同步访问。

2.ReentrantLock(可重入锁)
  • 简介ReentrantLockjava.util.concurrent.locks 包下的一个类,它是一个可重入的互斥锁,功能与 synchronized 类似,但提供了更灵活的锁机制。是显式的加锁,看起来更好理解。
  • 使用方式:
    • 加锁与解锁:通过 lock() 方法获取锁,unlock() 方法释放锁。为确保锁最终能被释放,通常将 unlock() 方法放在 finally 块中。
    • 可中断锁:支持可中断的锁获取模式,通过 lockInterruptibly() 方法实现,当线程在等待锁的过程中可以被中断。
    • 尝试获取锁:使用 tryLock() 方法可以尝试获取锁,如果锁可用则获取并返回 true,否则返回 false,不会阻塞线程。
  • 示例代码
package com.demo01;

import java.util.concurrent.locks.ReentrantLock;

public class ThreadLock {
    public static void main(String[] args) {
        station station = new station();
        // 开启三个线程
        new Thread(station).start();
        new Thread(station).start();
        new Thread(station).start();

    }
}
class station implements Runnable {
    private int ticketNumes = 10;
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while(true) {
            try{
            lock.lock();
            if(ticketNumes > 0) {
                System.out.println(ticketNumes--);

                    Thread.sleep(1000);

            }else{
                break;
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }finally {
                lock.unlock();
            }
        }
    }
}
  1. 整体架构ThreadLock 类的 main 方法作为程序入口,创建 station 类实例并启动三个线程,让它们共享该实例执行售票任务。
  2. station:实现 Runnable 接口,包含车票数量 ticketNumesReentrantLock 类型的锁 lock
  3. run 方法逻辑
    • 进入无限循环,先调用 lock.lock() 获取锁,保证同一时刻只有一个线程能执行后续操作。
    • 检查车票数量,若大于 0 则打印车票号并减 1,模拟售票,线程休眠 1 秒模拟处理时间。
    • 若车票售罄,跳出循环结束线程。
    • 无论是否异常,在 finally 块调用 lock.unlock() 释放锁,确保锁资源正确释放。

注意事项:

在这段代码中,ReentrantLocklock 方法锁的不是传统意义上类似 synchronized 中隐式的 this 对象,它锁的是创建的 ReentrantLock 实例 lock 本身所代表的锁资源。所以当锁(即ReentrantLock lock)已经被其他线程持有时,当前线程会被阻塞,进入等待状态。它会一直等待,直到持有锁的线程释放锁,然后当前线程才有机会获取到锁并继续执行后续代码。

Lock 锁的加锁 lock() 方法和解锁 unlock() 方法一般和 try{} catch{} finally{} 语句块一起使用,这样不管是否有异常发生,都能保证在加锁后,即使在临界区代码执行过程中抛出异常,也可以在 finally 块中调用 unlock() 方法来释放锁,避免因异常导致锁无法释放而造成死锁,从而确保其他线程能够正常获取该锁并继续执行后续操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值