Java程序员面试之并发进阶,synchronized、volatile、线程池

本文详细介绍了Java并发编程中的synchronized关键字,包括使用方式、实现原理及与ReentrantLock的比较。接着讨论了volatile关键字的作用、与synchronized的区别,并分析了线程池的概念,如Runnable和Callable的区别以及execute和submit方法的差异。
摘要由CSDN通过智能技术生成

本文主要参考Java面试进阶指南(https://xiaozhuanlan.com/topic/2419358670),Synchronized关键字实现原理(https://blog.csdn.net/weixin_36759405/article/details/83034386

不足之处,欢迎大家批评指正!

本文主要内容:

  • synchronized关键字,使用方式,实现原理,与ReentrantLock的异同
  • volatile关键字,内存可见性问题,和synchronized的区别
  • 线程池,创建方式,Runnable和Callable的异同,submit和execute方法的区别

一、synchronized关键字

1.说说对synchronized关键字的了解

synchronized关键字可以解决多个线程之间访问资源的同步性问题,可以保证它修饰的方法或代码块在任意时刻只能有一个线程执行。

2.如何使用synchronized关键字

(1).修饰代码块:即同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象。

(2).修饰普通方法:即同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象。

(3).修饰静态方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象。

锁的类型:

区别:

使用示例:

/**
 * 对象锁
 */
    public class Test{ 
    // 对象锁:形式1(方法锁) 
    public synchronized void Method1(){ 
        System.out.println("我是对象锁也是方法锁"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 
 
    // 对象锁:形式2(代码块形式) 
    public void Method2(){ 
        synchronized (this){ 
            System.out.println("我是对象锁"); 
            try{ 
                Thread.sleep(500); 
            } catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
        } 
 
    } 
 }

/**
 * 方法锁(即对象锁中的形式1)
 */
    public synchronized void Method1(){ 
        System.out.println("我是对象锁也是方法锁"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 

/**
 * 类锁
 */
public class Test{ 
   // 类锁:形式1 :锁静态方法
    public static synchronized void Method1(){ 
        System.out.println("我是类锁一号"); 
        try{ 
            Thread.sleep(500); 
        } catch (InterruptedException e){ 
            e.printStackTrace(); 
        } 
 
    } 
 
    // 类锁:形式2 :锁静态代码块
    public void Method2(){ 
        synchronized (Test.class){ 
            System.out.println("我是类锁二号"); 
            try{ 
                Thread.sleep(500); 
            } catch (InterruptedException e){ 
                e.printStackTrace(); 
            } 
 
        } 
 
    } 
}

3.双重校验锁实现单例模式

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

4.介绍synchronized实现原理

JVM 是通过进入、退出对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

(1).synchronized同步语句块

public class Test {
	public static void main(String[] args) {
	    synchronized (Test.class){
	        System.out.println("Synchronize");
	    }
	}
}

生成字节码文件后,使用javap -c Test查看具体信息:

同步代码块的入口处和出口处分别有monitorenter和monitorexit指令。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。

(2).synchronized修饰方法

public class Test2 {
    public synchronized void method() {
        System.out.println("synchronized 方法");
    }
}

生成字节码文件后,执行javap -c -s -v -l Test.class命令

在synchronized修饰方法时是添加ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

(3).synchronized的特点

5.JDK1.6之后对synchronozed关键字的优化

JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

6.syschronized和ReentrantLock的异同

(1).二者都是可重入锁,可重入锁概念参考https://blog.csdn.net/u012545728/article/details/80843595

(2).synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API:synchronized是托管给JVM实现,并没有直接暴露给我们。ReentrantLockshi JDK层面的实现,可以查看源代码。

(3).ReentrantLock 比 synchronized 增加了一些高级功能

等待可中断:ReentrantLock提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。

可实现公平锁:ReentrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。 ReentrantLock默认情况是非公平的,可以通过 ReentrantLock类的ReentrantLock(boolean fair)构造方法来制定是否是公平的。

可绑定多个Conditon对象:可以分开处理wait-notify,ReentrantLock里的Conition应用,在线程调度上更加灵活。

(4).如何选择

如果synchronized关键字适合你的程序,优先使用它。

如果需要Lock/Condition结构提供的独有特性,才使用它。

 

二、volatile关键字

1.简述Java内存模型

在当前的 Java 内存模型下,线程可以把变量保存本地内存比如机器的寄存器)中,而不是直接在主存中进行读写。这就可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在寄存器中的变量值的拷贝,造成数据的不一致。

                                                 

将变量声明为volatile后,保证内存的可见性,每次直接从主存中读取数据:

                                      

2.synchronized和volatile的异同?

volatile关键字是线程同步的轻量级实现,性能优于synchronized,但volatile只能修饰变量。

volatile只能保证数据的可见性,不能保证数据的原子性。

volatile不会发生阻塞,synchronized可能发生阻塞。

volatile解决的是变量在多线程之间的可见性问题,synchronized解决的多线程间资源同步性问题。

 

三.线程池

1.为何使用线程池

降低资源消耗 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

提高响应速度: 当任务到达时,任务可以不需要的等到线程创建就能立即执行。

提高线程的可管理性 :线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2.实现Runnable接口和Callable接口的区别

线程池执行任务的话需要实现的 Runnable 接口或 Callable 接口。 Runnable 接口或 Callable 接口实现类都可以被 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行。两者的区别在于 Runnable 接口不会返回结果但是 Callable 接口可以返回结果。

3.执行 execute() 方法和 submit() 方法的区别?

execute() 方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;

submit() 方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功,并且可以通过 future 的 get() 方法来获取返回值,get() 方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完。

4.创建线程池的方法

(1).通过构造方法实现

参数解释:

  • int corePoolSize:该线程池中核心线程数最大值
  • int maximumPoolSize: 该线程池中线程总数最大值
  • long keepAliveTime:该线程池中非核心线程闲置超时时长
  • BlockingQueue workQueue:该线程池中的任务队列:维护着等待执行的Runnable对象
  • ThreadFactory threadFactory:创建线程的方式
  • RejectedExecutionHandler handler: 当提交任务数超过maxmumPoolSize+workQueue之和时,任务会交给RejectedExecutionHandler来处理

(2).通过 Executor 框架的工具类 Executors 来实现

  • FixedThreadPool : 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。
  • SingleThreadExecutor: 方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。
  • CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值