多线程(二):线程怎么控?锁该怎么加?拒绝策略、volatile、单例全解析

1.自定义线程池(重点掌握)

(1)自定义线程池的核心元素:

好比银行窗口,有几个窗口是正常窗口(核心线程的数量),排队的客户(阻塞队列),某天可能业务爆满,新开几个临时窗口,注意加上新开的临时窗口后窗口数量不能超过银行的总窗口数(线程池中最大线程的数量),从哪里开临时窗口(创建线程的方式)。若某个临时窗口空闲时间(空闲时间值和单位)太长则关闭,若排队的人特别多并且超出正常窗口+临时窗口+等位区则拒绝服务,让他们下次再来(执行任务过多时候的解决方案)。并且有人在等位区排队时间过长则认定他不办业务把他请出去,然后让等位区的第一个来办理业务,等位区再加一人。

(2)自定义线程池的四种拒绝策略:

(3)自定义线程池的代码实现:

创建一个自定义线程池对象所需要的七个参数:

代码示例:

public class ThreadPoolExecutorDemo01{

public static void main(String[] args) throws ExecutionException, InterruptedException {

//创建一个自定义线程池对象

ThreadPoolExecutor pool=new ThreadPoolExecutor(

2,

3,

10l,

TimeUnit.MILLISECONDS,

new ArrayBlockingQueue<>(3),

Executors.defaultThreadFactory(),

new ThreadPoolExecutor.AbortPolicy()

);

for(int i=1;i<=1000;i++){

final int k=i;

final Future submit = pool.submit(() -> {

System.out.println(Thread.currentThread().getName() + "---" + k);

return Thread.currentThread().getName() + "-" + k;

});

}

pool.shutdown();

}

public static void main(String args){

}

}

在 Java 里,当匿名内部类(比如这里的 Lambda 表达式体,可看作简化的匿名内部类形式 )要访问定义在外部方法中的局部变量时,该局部变量必须是 final 或者事实上的 final(即值不会被后续修改):所以要在这定义一个k来等效i。

2.线程的控制:

(1)join方法(确保线程之间有严格的顺序关系)

预备知识:

Join线程:Thread提供了让一个线程等待另外一个线程完成的方法——join()方法。当在某个程序执行流中调用其他线程的join()方法是,调用线程将被阻塞,直到被join()方法加入的join线程执行完为止。

Join()方法有如下三种重载形式:

join():等待被join的线程执行完成。join(long millis):等待被join的线程的时间最长为millis毫秒,如果在millis毫秒内被join的线程还没有执行结束,则不在等待。join(long millis,int nanos):等待被join的线程的实际最长为millis毫秒加nanos微毫秒,很少使用.

问题:现在有T1,T2,T3三个线程,要保证T2在T1执行完后执行,T3在T2执行完后执行,那就得采用join来实现。

示例代码:

大学生从学校到就业正常的顺序是:大学1-3学年在学校进行专业基础课程学习!----->大学三年级到企业实习!----->大四毕业直接进入企业工作!代码实现时候,需要多线程,但是考虑到并发问题,不能保证顺序,此时我们就需要借助join()方法来保证线程之间有严格的先后顺序。

public class JoinTheadDemo01 {

static Thread t1,t2,t3;

public static void main(String[] args) {

// final Thread t1=null,t2=null,t3=null;

t1=new Thread(()->{

System.out.println("大学1-3学年在学校进行专业基础课程学习!");

});

t2=new Thread(new FutureTask<>(()->{

t1.join();

System.out.println("大学三年级到企业实习!");

return "hello";

}));

t3=new Thread(()->{

try {

t2.join();

} catch (InterruptedException e) {

throw new RuntimeException(e);

}

System.out.println("大四毕业直接进入企业工作!");

});

t1.start();

t2.start();

t3.start();

}

}

(2)线程让步(礼让):yield()方法(涉及优先权的问题!)

线程礼让:yield()方法介绍:

yield()方法是一个和sleep()方法有点相似的方法,它也是Thread类提供的一个静态方法,它也可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。Yield()只是让当前线程暂停一下,让系统的线程调用器重新调度一次,完全可能的情况是:当某个线程调用了yield()方法暂停之后,线程调度器又将其调度出来重新执行。实际上当某个线程调用了yield()方法暂停之后,只是让优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。

sleep()方法和yield()方法的联系与区别:

共同点:

1.都是Thread类中的类方法

2.都会导致正在执行的线程释放CPU

区别:

1.线程进入的状态不同

sleep方法导致线程进入到阻塞状态,yield方法导致线程进入就绪状态

2.是否考虑线程优先级

sleep方法不会考虑线程优先级,当一个线程调用sleep方法释放CPU后,所有优先级级别的线程都有机会获得CPU。

yield方法会考虑线程优先级,当一个线程调用sleep方法释放CPU后,与该线程具有同等优先级,或优先级比该线程高的线程有机会获得CPU。

3.可移植性

sleep方法比yield方法具有更好的可移植性

4.是否抛出异常

sleep方法声明抛出InterruptedException,而yield方法没有声明任何异常

5.是否有参数

sleep方法在Thread类中有两种重载形式,sleep(long ms),sleep(long ms,int nanos)yield方法没有参数。

线程的调度原理和线程的优先级说明:

代码示例(奇偶线程):

理论上使用yield()方法后:

每打印一个数字后,线程都会调用Thread.yield(),主动请求让出CPU,让对方线程有机会执行。这样,理论上两个线程会“交替”打印数字(但实际顺序不一定严格交替,因为线程调度不可控)。

public class YieldThreadDemo {

public static void main(String[] args) {

Thread t1=new Thread(()->{

for(int i=1;i<=100;i+=2){

System.out.println(Thread.currentThread().getName()+"-"+i);

Thread.yield();

}

},"奇数线程");

System.out.println(t1.getPriority());

t1.start();

new Thread(()->{

for(int i=2;i<=100;i+=2){

System.out.println(Thread.currentThread().getName()+"-"+i);

Thread.yield();

}

},"偶数线程").start();

}

}

运行结果:

奇数线程-95

偶数线程-94

奇数线程-97

偶数线程-96

奇数线程-99

偶数线程-98

偶数线程-100

和我们的设想一样,实际顺序确实不一定严格交替,因为线程调度不可控。

(3)后台线程:前台线程都死亡了,后台自动死亡

后台线程:setDaemon(boolean )

有一种线程,它是在后台运行的,它的任务是为其他的线程提供服务,这种线程被称为“后台线程(Daemon Thread)”,又称为“守护线程”或“精灵线程”。JVM的垃圾回收线程就是典型的后台线程。 后台线程有个特征:如果所有的前台线程都死亡,后台线程会自动死亡。调用Thread对象的setDaemon(true)方法可将指定线程设置为后台线程。 Thread类还提供了一个isDaemon()方法,用于判断指定线程是否为后台线程。前台线程死亡后,JVM会通知后台线程死亡,但从它接收指令到做出响应,需要一定时间,而且要将某个线程设置为后台线程,必须在该线程启动之前设置,也就是说,setDaemon(true)必须在start()方法之前调用,否则会引发IllegalThreadStateException异常。

我们日常生活中的:QQ上传文件,关闭了窗口,传输任务就会立刻结束,这就是个后台线程的实例。

代码示例:

public class DaemonDemo01 {

public static void main(String[] args) {

Thread t1=new Thread(()->{

while(true){

System.out.println(Thread.currentThread().getName()+"在运行-----");

}

});

t1.setDaemon(true); // 把t1线程设置为后台线程

t1.start();

for(int i=1;i<=1000;i++){

System.out.println(Thread.currentThread().getName()+"在运行");

}

}

}

(4)Suspend、resume、stop

这三种方法已经废弃掉了,已经过时,知道即可。

(5)Java线程中三种关于interrupt的方法:

1)三种方法介绍:

1. interrupt() 方法

interrupt() 是Thread类的一个实例方法,用于中断线程。

功能:

设置线程的中断标志位为true如果线程正在等待、睡眠或阻塞,会抛出InterruptedException异常不会立即停止线程的执行,只是发送中断信号

特点:

实例方法,需要调用特定线程对象的interrupt()可以中断任何线程(包括自己)中断标志位一旦设置,不会自动清除

2. isInterrupted() 方法

isInterrupted() 是Thread类的一个实例(非静态)方法,用于检查线程的中断状态。

功能:

返回线程的中断标志位的当前值不会清除中断标志位用于判断线程是否被中断

特点:

实例方法,检查特定线程的中断状态返回boolean值:true表示已中断,false表示未中断不会修改中断标志位

3. interrupted() 方法

interrupted() 是Thread类的一个静态方法,用于检查当前线程的中断状态。

功能:

检查当前执行线程的中断标志位会清除中断标志位(这是关键区别)返回清除前的状态

特点:

静态方法,只能检查当前线程返回boolean值:true表示之前被中断,false表示之前未中断会清除中断标志位

2)三种方法的对比:

特性

interrupt()

isInterrupted()

interrupted()

类型

实例方法

实例方法

静态方法

功能

设置中断标志位

检查中断状态

检查并清除中断状态

目标线程

指定线程

指定线程

当前线程

返回值

void

boolean

boolean

清除标志位

用途

发送中断信号

检查中断状态

检查并处理中断

3)总结:

interrupt是Thread类的实例方法,它的主要作用是给目标线程发送一个通知,有人希望你退出啦,同时会将目标线程的中断标志设置为true。至于目标线程如何处理,完全取决于目标线程自身。interrupt方法打断正在休眠的程序会抛出InterruptedException异常。详细解释:当一个线程处于休眠状态(例如调用了 Thread.sleep()、Object.wait()、Thread.join() 等方法)时,如果其他线程调用该线程的 interrupt() 方法,会导致休眠状态被打断,并抛出 InterruptedException。isinterrupted是Thread类的一个实例方法,主要判断目标线程的中断状态,该方法并不会改变目标线程的中断状态。interrupted是Thread类的一个静态方法,主要判断当前线程的中断状态。如果当前线程是中断标记是true,那么执行该方法后,会将当前线程的中断状态设置为false。

代码示例:

运行结果:

一直是:在路上运行,畅通无阻false,因为t1发送了中断信号,中不中断线程完全由线程自己决定,在这里的运行结果就是线程不想中断。

改良版:改成这样才能输出这个中断提示:遇到行人我要礼让行人。

3.线程的生命周期:

4.线程的同步:

还记得我们前面的问题吗,一个线程还没执行完可能就被另一个线程抢去了执行权,现在我们就来解决多线程的安全问题。

问题的原因:

当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。

解决办法:

对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

要想达到我们上述的理想状态,我们就要学会怎么去加锁!

(1)方法一:synchronized同步代码块

重量级锁、悲观锁:都是这个玩意。

解决线程安全问题:

1.采用同步代码块的方法,用法如下:

synchronized(对象锁){

把可能出现线程安全的代码放到该代码块中

}

注意:

任意的对象都可以充当锁,只要确保只有一把锁即可。

所以this也可以当锁,对于实例方法(非静态方法),默认采用this锁。

2.判断程序时候具有安全问题的判断条件:

是否在多线程状态下运行(2个或两个以上的线程)多个线程操作的是同一份共享的资源

代码示例:(火车售票系统)

class TicketThread implements Runnable{

Lock lock=new Lock();

Object object=new Object();

private int num=100;

@Override

public void run() {

while(true){

synchronized(TicketThread.class){ // 任意的对象都可以充当锁,只要确保只有一般锁即可

if(num>0){

num--;

System.out.println(Thread.currentThread().getName()+"卖出了一张票,目前还剩["+num+"]张票");

}else{

System.out.println(Thread.currentThread().getName()+"票已售完!");

break;

}

}

}

}

}

public class SynchronizedDemo01 {

public static void main(String[] args) {

TicketThread thread=new TicketThread();

for(int i=1;i<=5;i++){

new Thread(thread,"售票窗口-"+i).start();

}

}

}

运行结果:

售票窗口-3卖出了一张票,目前还剩[6]张票

售票窗口-3卖出了一张票,目前还剩[5]张票

售票窗口-3卖出了一张票,目前还剩[4]张票

售票窗口-3卖出了一张票,目前还剩[3]张票

售票窗口-3卖出了一张票,目前还剩[2]张票

售票窗口-3卖出了一张票,目前还剩[1]张票

售票窗口-3卖出了一张票,目前还剩[0]张票

售票窗口-3票已售完!

售票窗口-5票已售完!

售票窗口-4票已售完!

售票窗口-2票已售完!

售票窗口-1票已售完!

可以看到非常完美,达到了我们的预期结果!

(2)方法二:同步方法

同步的第二种方式,采用同步方法,语法如下:

[public ] synchronized 返回值类型 方法名(参数列表){

可能出现线程安全问题的代码

}

对于同步方法,采用隐式的锁,对于实例方法(非静态方法)采用的是this锁,对于类方法(也称为静态方法,用static修改的方法)采用的是当前类的二进制字节码文件对象作为锁,例如下面的CarTicket.class

代码示例:

@Data

class CarTicket{

private int ticket=100;

private boolean isSoldOut=false;

synchronized public void sell(){//对于实例方法默认采用this锁

if(ticket>0){

System.out.println(Thread.currentThread().getName()+"卖出了一张票,目前还剩["+(--ticket)+"]张票");

}else{

// 只有当还没打印过且票已售完时才打印

if(!isSoldOut){

System.out.println("票已售完");

isSoldOut = true; // 打印后设置为true,确保只打印一次

}

}

}

}

public class SynchronizedDemo02 {

public static void main(String[] args) {

CarTicket carTicket=new CarTicket();

for(int i=1;i<=5;i++){

new Thread(()->{

for(int j=1;j<=100;j++){

carTicket.sell();

}

},"售票窗口-"+i).start();

}

}

}

运行结果:

售票窗口-1卖出了一张票,目前还剩[5]张票

售票窗口-1卖出了一张票,目前还剩[4]张票

售票窗口-1卖出了一张票,目前还剩[3]张票

售票窗口-1卖出了一张票,目前还剩[2]张票

售票窗口-1卖出了一张票,目前还剩[1]张票

售票窗口-1卖出了一张票,目前还剩[0]张票

票已售完

可以发现,同一时刻只能一个窗口卖票,并发性受到了影响,直观表现在:窗口1卖完了所有票才能打开锁,其他窗口都没有票。

(3)注意事项:

加锁的部分不要太长(最好不要超过30行),超过了就写成个同步方法。尽量不要用二进制字节码文件对象作为锁,因为其生命周期太长。

(4)综合程序(简单模拟银行系统)

@Data

@NoArgsConstructor

@AllArgsConstructor

class BankAccount{

private String account="卢本伟";

private int balance;

public synchronized void withDraw(int money){

if(balance-money>=0){

System.out.println(Thread.currentThread().getName()+"对账号"+account+"进行了取款操作");

this.balance=this.balance-money;

System.out.println(Thread.currentThread().getName()+"取款["+money+"]操作成功,取款后的余额为:"+this.balance);

}else{

System.out.println("余额不足,"+Thread.currentThread().getName()+"取款操作失败!");

}

}

}

class BankAccountThread implements Callable{

private BankAccount bankAccount;

private int money;

public BankAccountThread(BankAccount bankAccount,int money){

this.bankAccount=bankAccount;

this.money=money;

}

@Override

public Integer call() throws Exception {

synchronized(bankAccount){

if(bankAccount.getBalance()-this.money>0){

System.out.println(Thread.currentThread().getName()+"对账户["+bankAccount.getAccount()+"]进行了取款操作");

this.bankAccount.setBalance(this.bankAccount.getBalance()-this.money);

System.out.println(Thread.currentThread().getName()+"取款操作成功,取款"+this.money+"后的余额为:"+this.bankAccount.getBalance());

}else{

System.out.println(this.bankAccount.getAccount()+"余额不足,取款操作失败!");

}

}

return null;

}

}

public class SynchronizedDemo03 {

public static void main(String[] args) {

BankAccount bankAccount=new BankAccount();

bankAccount.setBalance(1000);

ExecutorService pool=Executors.newCachedThreadPool();

BankAccountThread bankAccountThread=new BankAccountThread(bankAccount,100);

try {

for(int i=1;i<=100;i++){

if(i%2==0){

pool.execute(()->{

bankAccount.withDraw(50);

});

}else{

pool.submit(new FutureTask<>(bankAccountThread));

}

}

} finally {

pool.shutdown();

}

}

}

运行结果:

pool-1-thread-1对账户[卢本伟]进行了取款操作

pool-1-thread-1取款操作成功,取款100后的余额为:900

pool-1-thread-100对账号卢本伟进行了取款操作

pool-1-thread-100取款[50]操作成功,取款后的余额为:850

pool-1-thread-99对账户[卢本伟]进行了取款操作

pool-1-thread-99取款操作成功,取款100后的余额为:750

pool-1-thread-98对账号卢本伟进行了取款操作

pool-1-thread-98取款[50]操作成功,取款后的余额为:700

pool-1-thread-97对账户[卢本伟]进行了取款操作

pool-1-thread-97取款操作成功,取款100后的余额为:600

pool-1-thread-95对账户[卢本伟]进行了取款操作

pool-1-thread-95取款操作成功,取款100后的余额为:500

pool-1-thread-96对账号卢本伟进行了取款操作

pool-1-thread-96取款[50]操作成功,取款后的余额为:450

pool-1-thread-94对账号卢本伟进行了取款操作

pool-1-thread-94取款[50]操作成功,取款后的余额为:400

pool-1-thread-93对账户[卢本伟]进行了取款操作

pool-1-thread-93取款操作成功,取款100后的余额为:300

pool-1-thread-92对账号卢本伟进行了取款操作

pool-1-thread-92取款[50]操作成功,取款后的余额为:250

pool-1-thread-91对账户[卢本伟]进行了取款操作

pool-1-thread-91取款操作成功,取款100后的余额为:150

pool-1-thread-90对账号卢本伟进行了取款操作

pool-1-thread-90取款[50]操作成功,取款后的余额为:100

卢本伟余额不足,取款操作失败!

pool-1-thread-88对账号卢本伟进行了取款操作

pool-1-thread-88取款[50]操作成功,取款后的余额为:50

卢本伟余额不足,取款操作失败!

pool-1-thread-86对账号卢本伟进行了取款操作

pool-1-thread-86取款[50]操作成功,取款后的余额为:0

卢本伟余额不足,取款操作失败!

余额不足,pool-1-thread-84取款操作失败!

卢本伟余额不足,取款操作失败!

--------------------------------------------------------------------------

5.volatile:修饰符

具有可见性、有序性,但不具备原子性。可以防止重新排序从而确保了有序性,用于可能被多次修改的对象。

代码示例:

程序中创建了一个线程 t1,t1 会进入一个 while(true) 的循环,循环内根据 volatile 修饰的 flag 变量的值来决定是继续循环(continue)还是跳出循环(break)。主线程先启动 t1 线程,然后休眠 1000 毫秒(1 秒),之后将 flag 变量的值设置为 false。由于 flag 被 volatile 修饰,t1 线程能够及时 “看到” 主线程对 flag 的修改,从而跳出循环,使线程 t1 结束执行。

public class VolatileDemo01 {

// volatile修饰的变量:具有可见性,和有序性,但不具备原子性

private static volatile boolean flag=true;

public static void main(String[] args) throws InterruptedException {

Thread t1= new Thread(()->{

while(true){

if(flag){

continue;

}else{

break;

}

}

});

t1.start();

Thread.sleep(1000);

flag=false;

}

}

三个特性的详细介绍:

1.可见性

(1)定义:当一个线程修改了用volatile修饰的变量,其他线程能够立即看到这个变量的最新值。

(2)在本例中的体现:

主线程将flag设为false后,t1线程能够立刻感知到flag的变化,跳出循环。如果没有用volatile来修饰,t1线程可能一直在自己的工作内存中读取到flag为true,导致死循环。

2.有序性

(1)定义:volatile禁止指令重排序优化,保证代码的执行顺序符合预期(在单线程内,JVM和CPU可能会对指令进行重排序)。

(2)在本例中的体现:

对flag的写操作不会和前面的代码重排序,保证在flag=false之前的操作都已经完成。t1线程读取flag时,也不会把读取操作和后续操作重排序。

3.不具备原子性

(1)定义:原子性指的是一个操作不可分割,要么全部完成,要么全部不做。

(2)在本例中的体现:

flag = false; 这是一个简单的赋值操作,本身是原子的。但如果是volatile int count = 0; count++;,count++不是原子的(它包含读取、加一、写回三个步骤),即使加了volatile也不能保证原子性。volatile不能保证复合操作的原子性,只能保证单次读写的可见性。

4.总结对比

(1)对比如下:

// 不加volatile,t1可能永远无法感知flag=false

boolean flag = true;

// 加volatile,t1能及时感知flag-false

volatile boolean flag = true;

特性

volatile能保证

说明

可见性

线程间修改立即可见

有序性

禁止指令重排序,保证写前的操作先于写后操作

原子性

不能保证复合操作的原子性(如i++、i = i + 1等)

(2)结论:

用volatil修饰的变量,适合做状态标志(如flag),保证线程间的可见性和有序性。

如果需要保证复合操作的原子性,应该使用synchronized或AtomicInteger等原子类。

6.单例设计模式:懒汉式和饿汉式

在 Java 中,懒汉式和饿汉式是单例设计模式的两种常见实现方式,单例模式确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。

(1)饿汉式(Eager Singleton)

原理:饿汉式在类加载的时候就立即初始化,创建单例实例。不管后续是否会用到这个实例,在类加载阶段就会完成实例化。这种方式是线程安全的,因为在类加载过程中,JVM 会保证类的初始化是线程安全的。优点:实现简单,没有多线程同步的问题,因为实例在类加载时就已经创建好了。缺点:如果单例实例占用资源较多,且一直没有被使用,就会造成资源的浪费,因为在类加载的时候就创建了实例,不管需不需要。

代码示例:

public class EagerSingleton {

// 类加载时就立即创建实例

private static final EagerSingleton instance = new EagerSingleton();

// 私有化构造函数,防止外部实例化

private EagerSingleton() {}

// 提供全局访问点

public static EagerSingleton getInstance() {

return instance;

}

}

public class Main {

public static void main(String[] args) {

EagerSingleton singleton1 = EagerSingleton.getInstance();

EagerSingleton singleton2 = EagerSingleton.getInstance();

System.out.println(singleton1 == singleton2); // 输出 true,说明是同一个实例

}

}

(2)懒汉式(Lazy Initialization)

原理:懒汉式是在第一次使用该单例的时候才进行实例化。它的优点是延迟了对象的创建,避免了不必要的资源浪费。但如果在多线程环境下直接使用简单的懒汉式实现,会存在线程安全问题,因为多个线程可能同时判断实例为 null,从而创建多个实例。所以在多线程环境下需要进行线程安全的处理,常见的处理方式有双重检查锁定(Double-Checked Locking)和静态内部类等。优点:延迟实例化,只有在真正需要使用单例实例的时候才创建,节约资源。缺点:实现相对复杂,需要考虑线程安全问题。如果线程安全处理不当,可能会创建多个实例,破坏单例模式。

代码示例:

public class LazySingleton {

private static LazySingleton instance;

// 私有化构造函数,防止外部实例化

private LazySingleton() {}

// 提供全局访问点

public static LazySingleton getInstance() {

if (instance == null) {

instance = new LazySingleton();

}

return instance;

}

}

public class ThreadSafeLazySingleton {

private static volatile ThreadSafeLazySingleton instance;

// 私有化构造函数,防止外部实例化

private ThreadSafeLazySingleton() {}

// 提供全局访问点

public static ThreadSafeLazySingleton getInstance() {

if (instance == null) {

synchronized (ThreadSafeLazySingleton.class) {

if (instance == null) {

instance = new ThreadSafeLazySingleton();

}

}

}

return instance;

}

}

这里使用 volatile 关键字是为了防止指令重排,保证在多线程环境下 instance 的实例化过程是正确的。

代码示例2:

使用静态内部类实现的线程安全懒汉式(推荐),这种方式利用了类加载的机制,保证了单例的线程安全性,同时也实现了延迟加载 。

public class StaticInnerClassSingleton {

// 静态内部类,只有在调用 getInstance 方法时才会加载

private static class SingletonHolder {

private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();

}

// 私有化构造函数,防止外部实例化

private StaticInnerClassSingleton() {}

// 提供全局访问点

public static StaticInnerClassSingleton getInstance() {

return SingletonHolder.INSTANCE;

}

}

7.lock(锁)

在 Java 并发编程中,java.util.concurrent.locks.Lock接口是除了synchronized关键字之外的另一种实现线程同步的机制。与synchronized的隐式加锁 / 释放锁不同,Lock提供了手动加锁、手动释放锁的能力,对锁的控制更加灵活,在特定场景下能显著提升同步性能。

(1)Lock 接口的核心特点:

1.手动控制锁的生命周期

必须显式调用lock()方法获取锁,显式调用unlock()方法释放锁(通常在try-finally块中确保释放,避免死锁)。相比synchronized(由 JVM 自动管理锁的获取和释放),Lock让开发者更灵活地控制锁的获取时机和释放时机。

2.支持非阻塞获取锁

提供tryLock()方法,尝试获取锁时如果锁被占用,不会阻塞当前线程,而是立即返回false,可用于避免线程长时间等待。

3.支持中断获取锁

提供lockInterruptibly()方法,允许线程在等待锁的过程中响应中断(如其他线程调用interrupt()),避免线程无限期阻塞。

4.支持公平锁

可通过构造参数指定锁为 “公平锁”(Fair Lock),即等待时间最长的线程优先获取锁,避免线程饥饿;而synchronized只能是非公平锁。

5.可绑定多个条件

通过newCondition()方法创建多个Condition对象,实现更精细的线程间通信(类似synchronized中的wait()/notify(),但更灵活)。

(2)常用实现类:ReentrantLock

Lock是接口,最常用的实现类是ReentrantLock(可重入锁),它支持与synchronized类似的可重入特性(同一线程可多次获取同一把锁),同时具备上述所有灵活特性。

(3)Lock 的使用流程

使用Lock的标准流程如下(以ReentrantLock为例):

创建Lock实例(可指定是否为公平锁)。在try块中调用lock()获取锁。在finally块中调用unlock()释放锁(确保锁一定会被释放,避免死锁)。

(4)代码示例:(火车卖票系统)

class Ticket{

private int num=100;

private Lock lock=new ReentrantLock();

public void sell(){

lock.lock();

try{

if(num>0){

System.out.println(Thread.currentThread().getName()+"卖出了一张票");

num--;

System.out.println(Thread.currentThread().getName()+"网点售票成功目前还剩["+num+"]张票");

}else{

System.out.println(Thread.currentThread().getName()+"票已售完!");

}

}finally {

lock.unlock();

}

}

}

class TrainTicketThread implements Runnable{

private int num=100;

private ReentrantLock lock=new ReentrantLock(true); // 设置为公平锁

@Override

public void run() {

while(true){

lock.lock(); // 手动的枷锁

try{

if(num>0){

System.out.println(Thread.currentThread().getName()+"卖出了一张票,目前还剩["+(--num)+"]张票");

}else{

System.out.println("票已售完!");

break;

}

}finally {

lock.unlock(); // 手动的释放锁

}

}

}

}

public class LockDemo01 {

public static void main(String[] args) {

TrainTicketThread thread=new TrainTicketThread();

System.out.println("-------------------------------");

Ticket ticket=new Ticket();

for(int i=1;i<=5;i++){

new Thread(()->{

for(int j=1;j<=100;j++){

ticket.sell();

}

},"售票窗口-"+i).start();

}

}

}

(5)lock和synchronized的区别:

主要区别:

特性/对比点

synchronized

Lock(如 ReentrantLock)

实现层面

JVM 层面,属于内置关键字

Java API 层面,接口及实现类

加锁/解锁方式

自动加锁、自动释放(异常也会释放)

需手动加锁(lock())和释放(unlock())

可重入性

支持

支持

公平锁

不支持(默认非公平)

支持(可选公平/非公平)

中断响应

不支持(等待锁时不可中断)

支持(lockInterruptibly() 可响应中断)

超时获取锁

不支持

支持(tryLock(long timeout, TimeUnit))

条件变量

只能有一个(即对象的 wait/notify)

支持多个 Condition 对象

性能

JDK 1.6 以后两者性能差距不大

适合高并发、复杂同步场景

锁的范围

只能作用于代码块/方法

可灵活控制锁的范围

可见性

自动保证

需配合 volatile 或保证内存可见性

死锁检测

不支持

不支持(但可通过 tryLock 避免死锁)

适用场景建议:

简单同步:用 synchronized,代码简洁,自动释放锁。复杂同步(如可中断、超时、公平锁、多条件变量):用 Lock。

总结:

简单用 synchronized,复杂用 Lock。synchronized 自动加解锁,Lock 需手动加解锁。Lock 更灵活,功能更丰富。