Java基础学习总结:多线程之(三)多线程同步

本文深入探讨了Java中线程同步的多种方法,包括synchronized关键字的不同应用场景和Lock接口的高级功能,如ReentrantLock的使用,以及解决多线程访问临界资源时的数据安全问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、线程同步

1、问题

线程同步,解决多线程访问临界资源时的数据安全问题

我们使用多线程实现一个需求:四个窗口共同卖100张票。

线程类(票类Ticket_1):

package basis.StuThread.Ticket_1;

import java.util.concurrent.TimeUnit;

public class Ticket_1 implements Runnable{
    private static int tickets = 100;

    @Override
    public void run() {
        while (true){
            if(tickets>0){
                try {
                    TimeUnit.MICROSECONDS.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"卖出第"+tickets+"张票");
                tickets--;
            }else {
                break;
            }
        }

    }
}

每要卖一张票,线程休眠100毫秒。

主类(测试类):

package basis.StuThread.Ticket_1;

public class TestTicket_1 {
    public static void main(String[] args) {
        Ticket_1 ticket = new Ticket_1();

        new Thread(ticket,"窗口1").start();
        new Thread(ticket,"窗口2").start();
        new Thread(ticket,"窗口3").start();
        new Thread(ticket,"窗口4").start();
    }
}

测试结果(部分):

窗口1卖出第100张票
窗口2卖出第99张票
窗口4卖出第99张票
窗口3卖出第99张票
窗口1卖出第96张票
窗口4卖出第95张票
窗口3卖出第94张票
窗口2卖出第95张票

问题:

我们发现很多张票被不同的窗口重复卖了多次,这就是多线程访问共享资源(又叫临界资源,这里指100张票)可能带来的安全问题。那么如何解决?为访问共享资源的代码块添加互斥锁。为访问共享资源的代码添加互斥锁有两种方式:Synchronized 关键字 和 Lock 接口。

2、Synchronized 关键字的使用

synchronized 关键字最主要有以下三种应用方式,下面分别介绍

  • 修饰实例方法:作用于当前实例加锁,进入同步代码前需要获取当前实例的锁;
  • 修饰静态方法:作用于当前类对象加锁,进入同步代码前需要获得当前类对象的锁;
  • 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码块前需要获得给定对象的锁;

(1)synchronized作用于实例方法:

所谓的实例对象锁就是用 synchronized 修饰实例对象中的实例方法,注意是实例方法不包括静态方法。改造卖票程序的Ticket_1类如下:

package basis.StuThread.Ticket_1;

import java.util.concurrent.TimeUnit;

public class Ticket_1 implements Runnable{
    private static int tickets = 100;

    @Override
    public void run() {
        while (sellTicket()){
        }
    }
    //买票方法
    public synchronized boolean sellTicket(){
        if (tickets > 0) {
            try {
                TimeUnit.MICROSECONDS.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
            tickets--;
            return true;
        } else {
            return false;
        }
    }
}

 主类不变:

package basis.StuThread.Ticket_1;

public class TestTicket_1 {
    public static void main(String[] args) {
        Ticket_1 ticket = new Ticket_1();

        new Thread(ticket,"窗口1").start();
        new Thread(ticket,"窗口2").start();
        new Thread(ticket,"窗口3").start();
        new Thread(ticket,"窗口4").start();

    }
}

运行结果:

窗口1卖了第100张票。
窗口1卖了第99张票。
窗口4卖了第98张票。
窗口4卖了第97张票。
...
窗口4卖了第58张票。
窗口4卖了第57张票。
窗口3卖了第56张票。
窗口3卖了第55张票。
窗口3卖了第54张票。

  上述代码中:

我们把买票的具体过程抽出来,写进一个实例方法,该方法使用synchronized 关键字进行修饰,即为当前的实例方法添加一把互斥锁,保证同时只能一个线程进入该方法进行卖票。这样就解决了同一张票被多个线程卖了多次的情况,即解决了线程安全问题。

注意:

synchronized 修饰的是实例方法 sellTicket,这样情况下当前的线程锁便是实例对象 ticket,而程序中Ticket_1的实例对象只有一个 即 ticket,当一个线程获取到该锁时,其他线程就不能再获取到该锁,也就是不能访问被synchronized 关键字修饰的方法(但是可以访问其他的 非synchronized 修饰的方法),所以保证了线程安全。

Java中线程同步锁可以是任意对象。

当一个线程类拥有两个或以上个实例对象时,两个或多个线程持有不同的 实例对象(即锁),同时访问 synchronized 修饰的实例方法这是允许的,因为多个线程持有的是不同的锁。演示如下:

Ticket_1代码不变。

主类代码修改如下:

package basis.StuThread.Ticket_1;

public class TestTicket_1 {
    public static void main(String[] args) {
        //Ticket_1 ticket = new Ticket_1();

        new Thread(new Ticket_1(),"窗口1").start();
        new Thread(new Ticket_1(),"窗口2").start();
        new Thread(new Ticket_1(),"窗口3").start();
        new Thread(new Ticket_1(),"窗口4").start();

    }
}

测试结果:

窗口4卖了第100张票。
窗口2卖了第100张票。
窗口3卖了第99张票。
窗口2卖了第99张票。
窗口4卖了第99张票。
窗口1卖了第99张票。
窗口3卖了第98张票。
窗口1卖了第98张票。

 我们发现此时又出现了线程安全问题。原因就是多个线程持有的是不同的锁,线程之间不构成互斥关系。

解决办法,将 synchronized 作用于静态的 increase 方法。这样对象锁就是当前类对象(Ticker_1.class),这样无论创建多少个实例对象,但由于类对象只有一个,所以对象锁也就只有唯一的一个,下面就看看如何使用 synchronized 修饰静态方法。

(2)synchronized作用于静态方法

当 synchronized 作用于静态方法时,其锁就是当前类的 class 对象锁。由于静态成员是类成员,不属于任何一个实例对象,因此通过 class 对象锁可以控制静态成员的并发操作。演示如下:

我们修改Ticket_1类,为卖票方法 sellTicket() 添加 static 关键字

主类不变:

package basis.StuThread.Ticket_1;

public class TestTicket_1 {
    public static void main(String[] args) {
        //Ticket_1 ticket = new Ticket_1();

        new Thread(new Ticket_1(),"窗口1").start();
        new Thread(new Ticket_1(),"窗口2").start();
        new Thread(new Ticket_1(),"窗口3").start();
        new Thread(new Ticket_1(),"窗口4").start();

    }
}

结果:

窗口1卖了第100张票。
窗口4卖了第99张票。
窗口4卖了第98张票。
窗口4卖了第97张票。
窗口4卖了第96张票。
...
窗口4卖了第42张票。
窗口4卖了第41张票。
窗口3卖了第40张票。
窗口3卖了第39张票。
窗口3卖了第38张票

 由于静态方法是类持有的,不属于任何对象,所以无论我们new 出来多少个对象,所有线程都用的是同一把锁(即 Ticket_1.class),所以不会出现线程安全问题。

需要注意的是:

一个线程 A 访问一个对象的非static 的 synchronized 方法,而线程 B 需要访问这个实例对象的静态 synchronized 方法,这样式允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的是当前类的 class 对象锁,而访问非静态 synchronized 方法占用的是实例对象锁。此时,当两个方法都操作共享资源时,会出现线程安全问题。

(3)synchronized作用于同步代码块

除了使用关键字 synchronized 修饰实例方法和静态方法外,还可以修饰同步代码块。在某些情况下,方法体较大,操作比较耗时,而需要同步的代码又只有一小部分,此时,可以使用同步代码块的方式对需要同步的代码进行包裹。示例如下:

修改票类(Ticket_1):

package basis.StuThread.Ticket_1;

import java.util.concurrent.TimeUnit;

public class Ticket_1 implements Runnable{
    private static int tickets = 100;
    //创建一个对象(可以是任意类型),作为同步代码块的锁。
    Object lock  = new Object();
    @Override
    public void run() {
        while (true){
            //同步代码块
            synchronized(lock) {
                if (tickets > 0) {
                    try {
                        TimeUnit.MICROSECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
                    tickets--;
                } else {
                    break;
                }
            }
        }
    }
}

测试类:

package basis.StuThread.Ticket_1;

public class TestTicket_1 {
    public static void main(String[] args) {
        Ticket_1 ticket = new Ticket_1();

        new Thread(ticket,"窗口1").start();
        new Thread(ticket,"窗口2").start();
        new Thread(ticket,"窗口3").start();
        new Thread(ticket,"窗口4").start();

    }
}

从代码上看,synchronized 作用于一个给定的实例对象 lock ,即当前实例对象就是锁对象,每次当线程进入 synchronized 包裹的代码块时就会请求当前线程持有的 lock 实例对象锁,如果当前有其他线程正在持有该对象锁,那么新到的线程就必须等待,从而保证线程安全。

当然,除了 lock 作为对象锁外,我们还可以使用 this 对象(代表当前实例)或者当前类的 class 对象作为锁,代码如下:

//this,当前实例对象锁
synchronized(this){

}
//class对象锁
synchronized(AccountingSync.class){

}

3、Lock接口

在Java多线程编程中,我们经常使用synchronized关键字来实现同步,控制多线程对变量的访问,来避免并发问题。但是有的时候,synchronized关键字会显得过于沉重,不够灵活。synchronized 方法或代码块的使用,提供了对与每个对象相关的隐式监视器锁(monitor)的访问,但却强制所有锁获取和释放均要出现在一个块结构中:当获取了多个锁时,它们必须以相反的顺序释放,且必须在与所有锁被获取时相同的词法范围内释放所有锁。

这个时候Lock出现。

Lock不是Java中的关键字而是 java.util.concurrent.locks 包中的一个接口。下面我们简单介绍一下Lock接口

(1)Lock接口简介

Lock 实现 提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的 Condition 对象。Lock相对于synchronized关键字而言更加灵活,你可以自由得选择我你想要加锁的地方。

(2)接口声明:

package java.util.concurrent.locks;

public interface Lock {}

 Lock接口位于JUC包的中的locks包。

(3)Lock接口中定义的方法

方法描述
void lock():获取锁。如果锁不可用,出于线程调度目的,将禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态。
void lockInterruptibly():如果当前线程未被中断,则获取锁。
Condition newCondition():返回绑定到此 Lock 实例的新 Condition 实例。
boolean tryLock():仅在调用时锁为空闲状态才获取该锁。如果锁可用,则获取锁,并立即返回值 true。如果锁不可用,则此方法将立即返回值 false
boolean tryLock(long time, TimeUnit unit):如果锁在给定的等待时间内空闲,并且当前线程未被中断,则获取锁。
void unlock():释放锁。在等待条件前,锁必须由当前线程保持。调用 Condition.await() 将在等待前以原子方式释放锁,并在等待返回前重新获取锁。

注意:我们通常在 try…catch 模块中使用锁,在 finally 模块中释放锁。

(4)Lock接口的实现类

Lock接口有三个实现类,分别是 ReentrantLock 、ReentrantReadWriteLock.ReadLock 、ReentrantReadWriteLock.WriteLock。后面两个是内部类。

接下来我们重点介绍使用最多的 ReentrantLock。

4、ReentrantLock

ReentrantLock是一种可重入且独占式的锁,它具有与使用 synchronized 监视器锁相同的基本行为和语义,但与 synchronized 关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。

ReentrantLock,顾名思义,它是支持可重入的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

(1)继承关系和类声明

java.lang.Object
|____java.util.concurrent.locks.ReentrantLock


public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
}

(2)构造方法:

    public ReentrantLock() {
        sync = new NonfairSync();
    }

    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

ReentrantLock 类有两个构造方法,一个是无参的构造,默认使用非公平锁;一个是接受一个boolean 类型的参数,如果为 true 就实例化一个公平锁,如果为 false 就实例化非公平锁。

(3)使用方法

还使用上面讲到的多个窗口共同卖一百张票的程序:

Ticket_1类:

package basis.StuThread.Ticket_2;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class Ticket_1 implements Runnable{
    private static int tickets = 100;
    //实例化一个可重入锁对象
    ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            //同步代码块
            try {
                //线程休眠100毫秒
                TimeUnit.MICROSECONDS.sleep(100);
                //加锁
                lock.lock();
                if (tickets <1) {
                    break;
                }
                System.out.println(Thread.currentThread().getName() + "卖了第" + tickets + "张票。");
                tickets--;
            }catch (Exception e){
                e.printStackTrace();
            }
            finally {
                //解锁
                lock.unlock();
            }
        }
        System.out.println("票以卖完,线程"+Thread.currentThread().getName()+"退出。");
    }
}

 注意:

此处解锁语句 lock.unlock(); 一定要放在同步代码块中执行。否者会造成一个线程获取锁后,通过break语句退出循环,不经过释放锁就直接结束线程,导致其他线程不能获取锁,从而结束自己的运行。

或者不使用try-catch语句,在break;之前再释放一次锁:

if (tickets <1) {
    lock.unlock();
    break;
}

 测试类:

package basis.StuThread.Ticket_2;

public class TestTicket_1 {
    public static void main(String[] args) {
        Ticket_1 ticket = new Ticket_1();
        new Thread(ticket,"窗口1").start();
        new Thread(ticket,"窗口2").start();
        new Thread(ticket,"窗口3").start();
        new Thread(ticket,"窗口4").start();
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值