多线程&并发
Java中实现多线程的几种方法?
- 继承Thread类:创建一个继承自Thread类的新类,然后重写run()方法,并在run()方法中编写线程执行体。
public class MyThread extends Thread {
public MyThread() {
}
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread()+":"+i);
}
}
public static void main(String[] args) {
MyThread mThread1=new MyThread();
MyThread mThread2=new MyThread();
MyThread myThread3=new MyThread();
mThread1.start();
mThread2.start();
myThread3.start();
}
}
- 实现Runnable接口:创建一个实现了Runnable接口的类,实现run()方法,并创建一个Thread对象并将该Runnable对象作为参数传递给Thread的构造函数。
public class MyThread implements Runnable{
public static int count=20;
public void run() {
while(count>0) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"-当前剩余票数:"+count--);
}
}
public static void main(String[] args) {
MyThread Thread1=new MyThread();
Thread mThread1=new Thread(Thread1,"线程1");
Thread mThread2=new Thread(Thread1,"线程2");
Thread mThread3=new Thread(Thread1,"线程3");
mThread1.start();
mThread2.start();
myThread3.start();
}
}
- 实现Callable接口:创建一个实现了Callable接口的类,实现call()方法,创建一个该类的对象放到FutureTask构造函数中创建一个对象,此对象再放到Thread构造函数中创建线程。
public class MyThread implements Callable<String> {
private int count = 20;
@Override
public String call() throws Exception {
for (int i = count; i > 0; i--) {
// Thread.yield();
System.out.println(Thread.currentThread().getName()+"当前票数:" + i);
}
return "sale out";
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Callable<String> callable =new MyThread();
FutureTask <String>futureTask=new FutureTask<>(callable);
Thread mThread=new Thread(futureTask);
Thread mThread2=new Thread(futureTask);
Thread mThread3=new Thread(futureTask);
// mThread.setName("hhh");
mThread.start();
mThread2.start();
mThread3.start();
System.out.println(futureTask.get());//获取返回值
}
}
- 使用Executor框架:使用Executor框架的工厂类Executors的静态方法创建一个Executor对象,然后调用该对象的execute()方法以执行任务
- FixThreadPool(int n); 固定大小的线程池,使用于为了满足资源管理需求而需要限制当前线程数量的场合。使用于负载比较重的服务器。
- SingleThreadPoolExecutor :单线程池,需要保证顺序执行各个任务的场景
- CashedThreadPool(); 缓存线程池,当提交任务速度高于线程池中任务处理速度时,缓存线程池会不断的创建线程, 适用于提交短期的异步小程序,以及负载较轻的服务器
如何停止一个正在运行的线程?
从jdk手册中可以知道可以使用interrupt方法进行停止线程的运行。但是,由于线程终止不是一种简单的操作,这种方法只是提醒线程该终止了,真正的决定权还是再线程本身,尽量的保证线程的终止是一种安全的行为。
notify()和notifyALll()又什么区别?sleep()和wait()有什么区别?
- notify:唤醒正在等待此对象监视器的单个线程。
notifyAll:唤醒所有在此对象监视器上等待的线程。
注意:一个对象可以作为多个线程的监视对象,正在等待的单个线程指的是占用处理机的当前线程。
wait()是object类的方法,作用是让当前线程等待,直到另一个线程通过notify或者notifyall方法唤醒。
sleep()是Thread类的方法,作用是使当前正在执行的线程休眠(暂时停止执行)指定的毫秒数,这取决于系统计时器和调度器的精度和准确性。
volatile是什么?有什么用?
这是Java语言中的一个修饰符,用于修饰变量。当变量被这个关键字修饰之后,编译器和jvm都知道这是一个可被多个线程共享的变量。
- 可见性:volatile变量修改后,所有线程都能立即实时地看到它的最新值。此值不会读进寄存器或者cpu的缓存里面,直接在主存中读写。
- 有序性:有序性是指系统在进行代码优化时,不能把在volatile变量操作后面的语句放到其前面执行(普通的变量可能由于指令重排导致顺序变化,指令重排是为了提高),也不能将volatile变量操作前面的语句放在其后执行。这也是为了保证多线程共享的特性。
Thread类中的start()和run()方法的区别?yield?
- start():"用来启动一个线程的。当调用 “start()” 方法时,Java 虚拟机会调用该线程的 “run” 方法。调用 “run” 方法并不会启动一个新的线程,而只是在当前线程中普通的调用了 “run” 方法。要启动一个新的线程,必须创建一个新的线程对象,并调用其 “start()” 方法。
- run():这个方法如果使用了单独的Runnable对象创建线程,那么这个Runnable对象的run方法会被调用,否则这个方法什么也不做,直接返回。
- yield():主要作用是提示调度程序,当前线程愿意放弃它对处理器的当前使用权。它通常被用于在多线程环境中让线程让出自己的执行权以让其他线程运行,以实现线程调度的目的。
为什么wait,notify和notifyAll这些方法不在thread类里面?为什么wait和notify方法要在同步块内调用?
- Java提供的锁是针对对象的,每个对象都有锁,通过线程获得。这样可以清楚线程wait是由于哪个对象锁,如果是线程锁的话,造成wait的对象就不明显了。
- 这些方法都要求线程拥有某个对象的独占锁才能调用,也避免了wait和notify产生竞态条件。
Java中synchronized和ReentrantLock有什么不同?
-
两种方式有很多相似之处,都是加锁方式,而且都是阻塞式的同步,也就是当一个线程获得线程锁,进入了同步块,其他的访问该同步块的线程必须阻塞再同步块外面等待。
-
区别:最大的区别就是synchronized是Java语言的关键字,是原生语法层面的互斥,需要jvm来实现。而ReentrantLock是api层面的互斥锁,需要配合lock和unlock方法和try/finally语句块完成。
重入锁提供了一些高级功能:
1、 等待可中断:持有锁的线程长期不释放时,正在等待的线程可以选择放弃等待,这可以避免死锁的情况。
2、 公平锁:多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,synchronized锁是非公平锁,ReectrantLock默认的构造函数是非公平的,可以通过修改参数改变,性能不是很好。
3、 一个重入锁可以绑定多个对象。示例
import java.util.concurrent.locks.ReentrantLock;
public class MultiObjectLockExample {
private static ReentrantLock lock1 = new ReentrantLock();
private static ReentrantLock lock2 = new ReentrantLock();
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
try {
lock1.lock();
lock2.lock();
System.out.println("Thread 1: Holding lock 1 & 2");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock2.unlock();
lock1.unlock();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
lock2.lock();
lock1.lock();
System.out.println("Thread 2: Holding lock 1 & 2");
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock1.unlock();
lock2.unlock();
}
}
}).start();
}
}
有三个线程如何保证执行顺序?
Thread类有个join()方法,作用是:等待线程结束。如果A线程需要等待B线程结束才执行,可以再A中加上B.join()方法,等待B线程的结束,同理第三个线程。
SynchronizedMap和ConcurrentHashMap有什么区别?
SynchronizedMap 和 ConcurrentHashMap 都是 Java 中的并发 Map 类型,它们都用于在多线程环境下保证 Map 的线程安全。不过,它们的实现方式是不同的:
- SynchronizedMap 是通过在 Map 内部的所有操作方法上加锁来保证线程安全的,因此性能较差。
- ConcurrentHashMap 是通过分段锁和 CAS 算法来实现的,它的实现方式比 SynchronizedMap 更加高效。
因此,如果需要在多线程环境下使用 Map,通常推荐使用 ConcurrentHashMap,因为它的性能更好。
什么是线程安全?
- 线程安全是指在多线程环境下,一个对象或程序的状态不会被其他线程影响,并且可以保证对象或程序的正确执行。如果一个类是线程安全的,那么多个线程可以同时访问它,并且不会发生不一致的情况。线程安全的类通常通过同步机制来实现,比如说使用 synchronized 关键字,或者使用 java.util.concurrent 包中的线程安全类。
Java线程池中的submit和execute方法有什么区别?
- submit方法会将任务提交到线程池中,线程池会在某个线程上执行任务,并返回一个Future对象,可以通过这个对象来获取任务的执行结果,或者取消任务的执行。
- execute方法会将任务提交到线程池中,线程池会在某个线程上执行任务。如果任务抛出了异常,异常会被抛到任务提交的线程中,如果线程没有处理异常,那么异常就会导致线程终止。
synchronized关键字
- synchronized是Java中的关键字,用于实现线程同步。它可以用于方法或代码块中,用来保证在同一时间只有一个线程可以访问被synchronized修饰的代码。
- 当一个线程进入一个被synchronized修饰的方法或代码块时,会自动获取该方法或代码块所在对象的锁,如果此时锁已经被其他线程持有,则该线程会进入等待状态,直到锁被释放。只有当该线程获得锁并执行完synchronized块或方法后,才会释放锁,其他线程才有机会获得锁并进入synchronized块或方法。
- synchronized的主要作用是解决并发访问共享资源时的同步问题。在多线程程序中,如果多个线程同时对共享资源进行读写操作,可能会出现竞态条件(race condition)和内存可见性问题,从而导致程序出现不可预期的错误。使用synchronized可以保证同一时刻只有一个线程可以访问共享资源,从而避免竞态条件和内存可见性问题。
- 在Java中,每个对象都有一个内部锁,也称为监视器锁或互斥锁。当一个线程进入一个synchronized块或方法时,它会自动获取该对象的内部锁,当线程执行完synchronized块或方法时,它会自动释放锁。在Java中,synchronized可以修饰方法、实例变量和静态变量,分别对应不同的锁。
需要注意的是,synchronized虽然可以保证同步,但是过多的使用会影响程序的性能,因为每个线程在访问synchronized块或方法时都需要获取锁,如果线程数量较多,就会出现大量的锁竞争,从而影响程序的执行效率。因此,在使用synchronized时,应该尽量减少同步块的大小和粒度,以提高程序的并发性能。另外,在Java 5之后,Java提供了更加高效的并发编程工具,如Lock、Condition、Semaphore等,可以更灵活地实现线程同步。
Java程序是怎么执行的?
执行流程如下:
- 将源代码编译成.class文件
- class文件放置到Java虚拟机
-jvm使用类加载器Class Loader装载文件 - 类加载完成之后,Jvm将字节码翻译成机器码交给操作系统执行。
锁的优化机制?
- 锁的状态:无锁-偏向锁-轻量级锁-重量级锁。
- 自旋锁:让线程执行一个忙循环,防止从用户态转入内核态,从而减少上下文切换导致的性能影响。
- 自适应锁:自适应的自旋锁,自旋的时间参考前一次同一个锁的自旋时间。
- 锁消除:jvm检测到一些同步的代码块,不存数据竞争,即不需要加锁,自行消除。
- 锁粗化:很多操作都是对于同一个对象进行加锁,就会将锁的同步过程扩展到整个操作序列之外。
- 偏向锁:偏向第一个获得锁的线程。
- 轻量级锁:当一个线程第一次进入synchronized块时,虚拟机会将对象头中的标志位设置为“轻量级锁(L)”状态,并将当前线程的ID保存在锁对象的记录中(也称为“锁记录”),然后使用CAS(Compare and Swap)操作将对象头中的“指向锁记录的指针”指向该线程的锁记录。如果此时其他线程也要进入同一个锁对象的synchronized块,虚拟机会检查该对象头的状态,如果是轻量级锁状态并且保存了当前线程的ID,说明该线程已经拥有该锁,它可以直接进入临界区,不需要进行互斥同步。如果其他线程尝试进入同一锁对象的synchronized块,虚拟机会自旋一段时间(一般是几百个CPU时钟周期),尝试使用CAS操作来获得锁,如果自旋失败,表示锁已经升级成重量级锁,此时线程就需要进行互斥同步,进入阻塞状态等待锁被释放。
线程池的核心线程数怎么设置?核心参数?
- corePoolSize
corePoolSize是线程池中保留的核心线程数,当有新任务提交时,线程池会优先创建corePoolSize个线程处理任务,如果当前线程数少于corePoolSize,则创建新线程来处理任务,即使有空闲的线程。如果线程池中的线程数超过corePoolSize,超出部分的任务会被放入阻塞队列中等待执行。
- maximumPoolSize
maximumPoolSize是线程池允许创建的最大线程数。当阻塞队列已满时,线程池会继续创建新的线程,直到达到maximumPoolSize的限制。超出maximumPoolSize的任务将被拒绝,并抛出RejectedExecutionException异常。
- keepAliveTime
keepAliveTime是线程池中空闲线程的存活时间。当线程池中的线程数超过corePoolSize时,如果某个线程在空闲状态超过了keepAliveTime的时间,它就会被回收,以保持线程池中的线程数量不超过corePoolSize。
- workQueue
workQueue是线程池中用于存放任务的阻塞队列,当线程池中的线程数超过corePoolSize时,超出部分的任务会被放入阻塞队列中等待执行。Java中提供了多种阻塞队列的实现,如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
- threadFactory
threadFactory是线程池中创建新线程的工厂,它用于创建线程,并设置线程名称、优先级、是否为守护线程等属性。
- handler
handler是线程池中执行被拒绝任务的处理程序,它定义了线程池无法处理任务时的策略。Java中提供了四种策略:AbortPolicy(直接抛出异常)、CallerRunsPolicy(使用调用者线程执行任务)、DiscardPolicy(直接丢弃任务)、DiscardOldestPolicy(丢弃最早的未处理任务)。
通过调整这些参数,可以优化线程池的性能和效率,提高应用程序的并发处理能力。需要注意的是,过度使用线程池可能会导致系统负载过重,甚至出现死锁、内存泄漏等问题,因此在使用线程池时需要根据实际情况进行调整。
线程之间的通信?
- 共享内存:不同的线程通过读写同一个变量可以达成通信的效果
- 消息传递:线程之间通过显式的发送消息进行通信。wait、notify、blockingQueue。
CAS的原理?缺点?
CAS(compare and swap):比较并交换,主要是通过处理器的指令来保证操作的原子性,包含三个操作数:
1、变量内存地址V
2、 旧的预期值A
3、准备设置的新的值B
当执行cas指令时,只有当V==A时,才会用B更新V的值,否则就不会执行更新
ThreadLocal的原理?
每个线程都有一个ThreadLocalMap,存储了一个或者多个ThreadLocal,通过ThreadLocal的set、get、remove方法可以操作本线程的变量。
- ThreadLocal是Java中的一个线程封闭类,用于实现线程私有变量的存储。它通过为每个线程提供独立的变量副本来解决多线程并发访问共享变量时的线程安全问题。
- ThreadLocal的原理是,在每个线程中都会创建一个独立的副本,每个线程对ThreadLocal变量的操作都只会对自己的副本产生影响,不会对其他线程的副本产生影响。因此,ThreadLocal变量在多线程环境中可以保证线程安全。
JMM内存模型的理解?为什么需要这个JMM?
- Java内存模型(Java Memory Model,简称JMM)是Java虚拟机规范定义的一种内存模型,它描述了Java程序中多线程并发访问共享变量时,内存的可见性、有序性和原子性的保障。JMM规定了线程和主内存之间的交互方式,以及线程之间的通信机制,保证了多线程程序的正确性和稳定性。
AQS的理解?Semaphore?
- AQS(AbstractQueuedSynchronizer)是Java中一个重要的同步框架,是实现同步器(如ReentrantLock、Semaphore、CountDownLatch等)的基础类,它提供了一套简单易用的同步工具,可以帮助开发人员轻松地实现自定义的同步器。
- AQS的核心思想是通过一个FIFO队列来实现线程之间的竞争和协作,即当多个线程访问同一个资源时,AQS会将这些线程排成一个队列,通过自旋和阻塞等机制来保证线程之间的协作和互斥。
- Semaphore是一个计数器信号量,用于控制同时访问某个资源的线程数量,它同样是使用AQS来实现的,通过重写AQS中的方法来实现计数器的增减和线程的调度。
什么是Callable和Future?
- Callable接口定义了一个有返回值的任务,它有一个泛型参数V,表示任务执行完成后的返回值类型。Callable接口中只有一个方法call(),用于执行任务并返回结果。与Runnable接口不同,call()方法可以抛出异常,并且支持泛型。
- Future接口定义了一个异步计算的结果,它有一个泛型参数V,表示异步计算的结果类型。Future接口提供了获取异步计算结果的方法,可以通过get()方法来获取异步计算的结果,或者通过isDone()方法来判断异步计算是否完成。Future接口还提供了cancel()方法,可以用于取消异步计算。
什么是Daemon线程?意义?
-
在Java多线程编程中,线程可以分为两种类型:用户线程和守护线程。守护线程又被称为Daemon线程,它是一种特殊的线程,用于在程序运行期间提供后台服务和支持。
-
守护线程和用户线程的区别在于,当所有的用户线程执行完毕后,守护线程会自动退出。因此,守护线程通常用于执行后台任务,提供服务和支持,而不需要关注它们是否执行完毕。例如,垃圾回收线程就是一种守护线程,它在程序运行期间自动回收内存中的垃圾对象,而不需要手动干预。
-
在Java中,可以通过Thread类的setDaemon()方法将一个线程设置为守护线程。当一个线程被设置为守护线程后,它的所有子线程都将自动成为守护线程。当所有的用户线程执行完毕后,守护线程会自动退出,不需要手动停止它们的执行。
乐观锁和悲观锁?
乐观锁和悲观锁是并发控制中两种不同的策略。
-
悲观锁假定在并发访问的情况下,数据很有可能发生冲突,所以在每次访问数据时,都需要获取锁来保证数据的正确性。悲观锁的特点是对数据的并发访问会加锁,这会导致其他线程需要等待锁的释放才能访问数据。悲观锁常见的实现方式包括synchronized关键字和ReentrantLock等锁机制。
-
乐观锁则假定在并发访问的情况下,数据很少发生冲突,所以不需要每次访问数据时都加锁,而是通过一些特殊的手段来保证数据的正确性。乐观锁的特点是对数据的并发访问不加锁,而是通过版本号、时间戳等机制来保证数据的正确性。乐观锁常见的实现方式包括CAS操作和版本号机制。
JUC
-
原子变量类:提供了一系列线程安全的原子变量类,例如AtomicInteger、AtomicLong、AtomicBoolean等,可以通过原子性操作实现线程安全的数值计算和状态控制。
-
线程池:提供了一系列线程池类和接口,例如ThreadPoolExecutor、ScheduledExecutorService、ExecutorCompletionService等,可以实现线程池的创建、销毁、执行和管理,提高多线程并发处理的效率和性能。
-
阻塞队列:提供了一系列线程安全的阻塞队列类和接口,例如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等,可以实现线程之间的数据传递和同步,避免了手动实现线程等待和唤醒的复杂性和风险。
-
锁和同步器:提供了一系列线程安全的锁和同步器类和接口,例如ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch等,可以实现线程之间的互斥访问和协作执行,保证了数据的一致性和线程的安全性。