您的当前位置:首页正文

初探JUC,java大厂社招面试题

2024-11-21 来源:个人技术集锦

可能会发生线程冲突问题

}

复制代码

**注意:**在同步代码块中,多个线程必须使用的是同一把锁,即同一个对象。

一般情况下,在使用Runnable实现的线程类中,我们会使用_this_作为锁对象。

4.2使用同步方法

class Ticket {

private int number = 400;

// 1.同步方法

public synchronized void sale() {

// 2.同步代码块

synchronized (this) {

}

if (number > 0) {

System.out.println(Thread.currentThread().getName() + “卖第” + (number–) + “张票\t ,还剩” + number);

}

}

public void sale1() {

Lock lock = new ReentrantLock();

lock.lock();

try {

if (number > 0) {

System.out.println(Thread.currentThread().getName() + “卖第” + (number–) + “张票,还剩” + number);

}

} finally {

lock.unlock();

}

}

}

/**

  • @Description:三个卖票员卖30张票

*/

public class SaleTicket {

public static void main(String[] args) {

Ticket tk = new Ticket();

// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, “AA”).start();

// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, “BB”).start();

// new Thread(()->{for(int i = 0;i<30;i++) tk.sale();}, “CC”).start();

new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, “AA”).start();

new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, “BB”).start();

new Thread(()->{for(int i = 0;i<30;i++) tk.sale1();}, “CC”).start();

}

}

复制代码

注意:如果使用Thread继承的方式实现多线程,那么同步方法需要是一个静态的方法

4.3使用Lock解决线程安全

从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。Lock实现提供更广泛的锁定操作可以比使用 synchronized获得方法和声明更好。他们允许更灵活的结构,可以有完全不同的特性,可以支持多个相关的 Condition对象。Lock提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。

ReentrantLock 类实现了 Lock,它拥有与 synchronized 相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

三、多线程的创建方式


Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例。Java可以用四种方式来创建线程,如下所示:

1)继承Thread类创建线程

2)实现Runnable接口创建线程

3)使用Callable和Future创建线程

4)使用线程池例如用Executors工具类

1.继承Thread类

①定义子类继承Thread类。

②子类中重写Thread类中的run方法。

③创建Thread子类对象,即创建了线程对象。

④调用线程对象start方法启动线程,默认调用run方法。

注意:如果只是调用run方法,则此时会在调用该方法的线程中来执行,而不是另启动一个线程。

public class MyThread extends Thread{//继承Thread类

public void run(){

//重写run方法

//线程需要执行的任务

}

}

public class Main {

public static void main(String[] args){

new MyThread().start();//创建线程实例 并且调用start方法启动线程

}

}

复制代码

2.实现Runnable接口创建线程

①定义子类,实现Runnable接口。

②子类中重写Runnable接口中的run方法。

③通过Thread类含参构造器创建线程对象,将Runnable接口的子类对象作为实际参数传递给

Thread类的构造方法中。

④调用Thread类的start方法启动线程,其最终调用Runnable子类接口的run方法。

public class MyThread implements Runnable {//实现Runnable接口

public void run(){

//重写run方法

}

}

public class Main {

public static void main(String[] args){

//通过Thread类含参构造器创建线程对象

MyThread myThread=new MyThread();

//将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中 AA为该线程的名称

Thread thread=new Thread(myThread,“AA”);

//线程启动

thread().start();

}

}

复制代码

两种方式的区别:

  • 继承Thread:线程代码存放Thread子类run方法中。

  • 实现Runnable:线程代码存在接口的子类的run方法中。

实现Runnable接口避免了单继承的局限性,多个线程可以共享同一个接口子类的对象,非常适合多个相同线程来处理同一份资源。

3.使用Callable和Future创建线程

和Runnable接口不一样,Callable接口提供了一个call()方法作为线程执行体,call()方法比run()方法功能要强大。

  • call()方法可以有返回值

  • call()方法可以声明抛出异常

Java5提供了Future接口来代表Callable接口里call()方法的返回值,并且为Future接口提供了一个实现类FutureTask,这个实现类既实现了Future接口,还实现了Runnable接口,因此可以作为Thread类的target。在Future接口里定义了几个公共方法来控制它关联的Callable任务。

boolean cancel(boolean mayInterruptIfRunning):视图取消该Future里面关联的Callable任务 V get():返回Callable里call()方法的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值 V get(long timeout,TimeUnit unit):返回Callable里call()方法的返回值,最多阻塞timeout时间,经过指定时间没有返回抛出TimeoutException boolean isDone():若Callable任务完成,返回True boolean isCancelled():如果在Callable任务正常完成前被取消,返回True

①创建Callable接口的实现类,并实现call()方法,然后创建该实现类的实例(从java8开始可以直接使用Lambda表达式创建Callable对象)。

②使用FutureTask类来包装Callable对象,该FutureTask对象封装了Callable对象的call()方法的返回值

③使用FutureTask对象作为Thread对象的target创建并启动线程(因为FutureTask实现了Runnable接口)

④调用FutureTask对象的get()方法来获得子线程执行结束后的返回值

class myThread implements Callable {

@Override

public Integer call() throws Exception {

System.out.println(Thread.currentThread().getName()+" Come in call");

//睡5秒

TimeUnit.SECONDS.sleep(5);

//返回200的状态码

return 200;

}

}

public class CallableTest {

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

myThread myThread = new myThread();

FutureTask futureTask = new FutureTask<>(myThread);

new Thread(futureTask, “未来任务”).start();

System.out.println(“主线程结束!”);

Integer integer = futureTask.get();

System.out.println(integer);

}

}

复制代码

4.使用线程池

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。因此提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中。可以避免频繁创建销毁、实现重复利用。

优势:

  • 提高响应速度(减少了创建新线程的时间)

  • 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

  • 便于线程管理

JDK 5.0起提供了ExecutorService 和 Executors来实现线程池。

ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。

void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行Runnable

Future submit(Callable task):执行任务,有返回值,一般用来执行Callable

void shutdown() :关闭连接池

创建线程池的方式:

public static void main(String[] args) {

//创建一个包含10个线程的线程池

ExecutorService executorService = Executors.newFixedThreadPool(10);

//ExecutorService executorService = Executors.newSingleThreadExecutor();

for (int i = 0; i < 12; i++) {

executorService.execute(()->{

System.out.println(Thread.currentThread().getName());

});

}

executorService.shutdown();

}

复制代码

  • 1.5后引入的Executor框架的最大优点是把任务的提交和执行解耦。要执行任务的人只需把Task描述清楚,然后提交即可。这个Task是怎么被执行的,被谁执行的,什么时候执行的,提交的人就不用关心了。具体点讲,提交一个Callable对象给ExecutorService(如最常用的线程池ThreadPoolExecutor),将得到一个Future对象,调用Future对象的get方法等待执行结果就好了。Executor框架的内部使用了线程池机制,它在java.util.cocurrent 包下,通过该框架来控制线程的启动、执行和关闭,可以简化并发编程的操作。因此,在Java 5之后,通过Executor来启动线程比使用Thread的start方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免this逃逸问题——如果我们在构造器中启动一个线程,因为另一个任务可能会在构造器结束之前开始执行,此时可能会访问到初始化了一半的对象。

  • Executor框架包括:线程池,Executor,Executors,ExecutorService,CompletionService,Future,Callable等。

  • Executor接口中之定义了一个方法execute(Runnable command),该方法接收一个Runable实例,它用来执行一个任务,任务即一个实现了Runnable接口的类。ExecutorService接口继承自Executor接口,它提供了更丰富的实现多线程的方法,比如,ExecutorService提供了关闭自己的方法,以及可为跟踪一个或多个异步任务执行状况而生成 Future 的方法。 可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

ExecutorService的生命周期包括三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态,此时意味着ExecutorService不再接受新的任务,但它还在执行已经提交了的任务,当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。

public static ExecutorService newFixedThreadPool(int nThreads)

创建固定数目线程的线程池。

public static ExecutorService newCachedThreadPool()

创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。

public static ExecutorService newSingleThreadExecutor()

创建一个单线程化的Executor。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

创建一个支持定时及周期性的任务执行的线程池,多数情况下可用来替代Timer类。

这四种方法都是用的Executors中的ThreadFactory建立的线程,下面就以上四个方法做个比较

一般来说,CachedTheadPool在程序执行过程中通常会创建与所需数量相同的线程,然后在它回收旧线程时停止创建新线程,因此它是合理的Executor的首选,只有当这种方式会引发问题时(比如需要大量长时间面向连接的线程时),才需要考虑用FixedThreadPool。(该段话摘自《Thinking in Java》第四版)

public static void main(String[] args) {

//创建一个包含10个线程的线程池

//ExecutorService executorService = Executors.newFixedThreadPool(10);

ExecutorService executorService = Executors.newSingleThreadExecutor();

for (int i = 0; i < 12; i++) {

executorService.execute(()->{

System.out.println(Thread.currentThread().getName());

});

}

executorService.shutdown();

}

复制代码

在Java 5之后,任务分两类:一类是实现了Runnable接口的类,一类是实现了Callable接口的类。两者都可以被ExecutorService执行,但是Runnable任务没有返回值,而Callable任务有返回值。并且Callable的call()方法只能通过ExecutorService的submit(Callable task) 方法来执行,并且返回一个 Future,是表示任务等待完成的 Future。

Callable接口类似于Runnable,两者都是为那些其实例可能被另一个线程执行的类设计的。但是 Runnable 不会返回结果,并且无法抛出经过检查的异常而Callable又返回结果,而且当获取返回结果时可能会抛出异常。Callable中的call()方法类似Runnable的run()方法,区别同样是有返回值,后者没有。

当将一个Callable的对象传递给ExecutorService的submit方法,则该call方法自动在一个线程上执行,并且会返回执行结果Future对象。同样,将Runnable的对象传递给ExecutorService的submit方法,则该run方法自动在一个线程上执行,并且会返回执行结果Future对象,但是在该Future对象上调用get方法,将返回null。

下面给出一个Executor执行Callable任务的示例代码:

import java.util.ArrayList;

import java.util.List;

import java.util.concurrent.*;

public class CallableDemo{

public static void main(String[] args){

ExecutorService executorService = Executors.newCachedThreadPool();

List<Future> resultList = new ArrayList<Future>();

//创建5个任务并执行

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

//使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中

Future future = executorService.submit(new TaskWithResult(i));

//将任务执行结果存储到List中

resultList.add(future);

}

//遍历任务的结果

for (Future fs : resultList){

try{

//Future返回如果没有完成,则一直循环等待,直到Future返回完成

while(!fs.isDone);

//打印各个线程(任务)执行的结果

System.out.println(fs.get());

}catch(InterruptedException e){

e.printStackTrace();

}catch(ExecutionException e){

e.printStackTrace();

}finally{

//启动一次顺序关闭,执行以前提交的任务,但不接受新任务

executorService.shutdown();

}

}

}

}

class TaskWithResult implements Callable{

private int id;

public TaskWithResult(int id){

this.id = id;

}

/**

  • 任务的具体过程,一旦任务传给ExecutorService的submit方法,

  • 则该方法自动在一个线程上执行

*/

public String call() throws Exception {

System.out.println(“call()方法被自动调用!!!” + Thread.currentThread().getName());

//该返回结果将被Future的get方法得到

return "call(

《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》

【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享

)方法被自动调用,任务返回的结果是:" + id + “” + Thread.currentThread().getName();

}

}

复制代码

四、JUC工具类


1. ReentrantReadWriteLock

现实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源;但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写的操作了。

针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁。

当没有其他线程的写锁时,线程进入读锁。当没有其他线程的读锁和写锁时,才会进入当前线程的写锁!

class MyQueue {

private Object obj;

ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

public void readObj() {

//上读锁

readWriteLock.readLock().lock();

try {

System.out.println(Thread.currentThread().getName() + “读取的内容是:” + obj);

} finally {

//下读锁

readWriteLock.readLock().unlock();

}

}

public void writeObj(Object obj) {

//上写锁

readWriteLock.writeLock().lock();

try {

this.obj = obj;

System.out.println(Thread.currentThread().getName() + “写入的内容为:” + obj);

} finally {

//下写锁

readWriteLock.writeLock().unlock();

}

}

}

/**

*/

public class ReadWriteLockDemo {

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

// 创建资源对象

MyQueue queue = new MyQueue();

// 一个线程写

new Thread(() -> {

queue.writeObj(“放假了”);

}, “AA”).start();

//100个线程读

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

new Thread(() -> {

queue.readObj();

}, String.valueOf(i)).start();

}

}

}

复制代码

显示全文