多线程与单线程的关系(重要)
- 多线程是指在一个进程中并发执行了多个线程,每个线程都执行不同的任务。
- 并发是指单个cpu上执行多个任务,按照时间片轮询让每个线程都能得到CPU的时间进行执行,是一中微观上的同时执行
- 多线程存在上下文切换,因此它并不能提高程序的执行速度,但是可以减少用户的等待响应时间,提高资源利用率。
并发编程的优缺点
优点 | 缺点 |
---|---|
充分利用多核cpu的计算能力 | 内存泄漏和上下文切换和线程安全 |
方便进行业务拆分和提升系统的并发能力和性能 |
并发编程三要素是什么?在 Java 程序中怎么保证多线程的运行安全?
- 原子性:多个线程涉及到共享变量的操作,该操作不可再分
- 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到。
- 有序性:一个线程对内存操作在另外一个线程看来时有序的。
出现线程安全问题的原因:
- 多线程上下文切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法:
- 使用JUC中的原子类如atomicinteger等、synchronized、LOCK,解决原子性问题
- synchronized、lock、volatile可以解决可见性问题
- Happens-Before 规则、volatile可以解决有序性问题
并行和并发有什么区别?
- 并发:多个任务在一个 CPU 上,按细分的时间片轮流(交替)执行,从逻辑上来看那些任务是同时执行。
- 并行:多个CPU同时执行多个任务,是真正意义上的“同时进行”。
- 串行:一个线程按照顺序执行多个任务。由于任务、方法都在一个线程执行所以不存在线程不安全情况,也就不存在临界区的问题。
线程和进程区别
进程 | 线程 |
---|---|
是一个执行的程序,系统进行资源分配和调度的基本单位 | 是进程中的一个实体,一个进程一般拥有多个线程,多个线程共享地址空间和资源 |
一个进程中至少得拥有一个线程 | |
进程的上下文切换要慢一些 | 线程的上下文切换快一些 |
一个进程崩溃后不会对其他进程产生影响 | 一个线程崩溃后整个进程就死掉了,所以多进程比多线程健壮 |
进程之间常见的通信方式
- 通过socket套接字实现通信
- 写进程和读进程通过管道实现通信
- 映射一段共享的内存空间实现通信
什么是多线程的上下文切换?
在单个cpu上有多个任务,每个任务都是一个线程在负责执行,我们采取cpu时间片轮询机制,让每个线程都能得到时间片执行任务,线程1执行完线程2执行,就是上下文切换。
linux的上下文切换比windows的上下文切换耗费资源少
线程上下文切换比进程上下文切换快的原因,可以总结如下:
- 线程的上下文切换只需要保存少量寄存器的内容不需要进行存储管理操作;
- 进程上下文需要对当前进程的cpu环境进程保存另外需要对新被调度的进程cpu环境进行设置;
守护线程和用户线程有什么区别呢?
用户线程 | 守护线程 |
---|---|
运行在前台执行那些具体的任务,如:main函数启动、连接网络等 | 运行在后台,为用户线程提供服务,一旦所有的用户线程运行结束守护线程也会结束 |
线程的死锁、线程饥饿
- 死锁:多个线程之间相互等待对方的资源而没有释放而形成的静止的状态。
- 饥饿:线程一直没有获取到所需要的资源导致无法继续执行任务
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
输出结果
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
线程 1 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过Thread.sleep(1000);让线程 1 休眠 1s 为的是让线程 2 得到CPU执行权,然后获取到 resource2 的监视器锁。线程 1 和线程 2 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
Java 中导致饥饿的原因
- 高优先级线程吞噬所有的低优先级线程的 CPU 时间。
- 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
- 线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方法)
形成死锁的四个必要条件是什么
- 互斥条件:一个线程一个时刻只能获取到一个锁资源
- 请求与保持条件:一个线程因请求资源而发生阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程对已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:多个线程之间形成一种首位相接的循环等待资源关系
如何避免线程死锁?
破坏互斥条件
这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。
破坏请求与保持条件
一次性申请所有的资源。
破坏不剥夺条件
占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
破坏循环等待条件
指定资源的获取顺序
我们对线程 2 的代码修改成下面这样就不会产生死锁了。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
输出结果:
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
我们分析一下上面的代码为什么避免了死锁的发生?
线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁
防止死锁可以采用以下的方法:
- 指定获取锁/资源的顺序(破坏循环等待条件)
- 尽量使用 tryLock(long timeout, TimeUnit unit)方法设置超时时间,超时可以退出,防止死锁。
- 尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
创建线程的四种方式。
- 继承 Thread 类;
- 实现 Runnable 接口;
- 实现 Callable 接口;
- 使用 Executors 工具类创建线程池
继承 Thread 类
步骤
1.定义一个Thread类的子类,重写run方法,run()方法就是线程要执行的业务逻辑方法
2.创建自定义的线程子类对象
3.调用子类实例的star()方法来启动线程
public class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法正在执行...");
}
}
class TheadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行结束");
}
}
运行结果:
main main()方法执行结束
Thread-0 run()方法正在执行...
实现 Runnable 接口
步骤
1.定义Runnable接口实现类MyRunnable,并重写run()方法
2.创建MyRunnable实例myRunnable,以myRunnable作为target创建Thead对象,该Thread对象才是真正的线程对象
3.调用线程对象的start()方法
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
class RunnableTest {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
执行结果
main main()方法执行完成
Thread-0 run()方法执行中...
实现 Callable 接口
步骤
1.定义Callable接口的实现类MyCallable,并重写call()方法
2.创建MyCallable的实例myCallable,以它为target创建FutureTask对象
3.以futureTask为target创建Thread对象
4.调用线程对象的start()方法
public class MyCallable implements Callable<Integer> {
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + " call()方法执行中...");
return 1;
}
}
class CallableTest {
public static void main(String[] args) {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start();
try {
Thread.sleep(1000);
System.out.println("返回结果 " + futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " main()方法执行完成");
}
}
执行结果
Thread-0 call()方法执行中...
返回结果 1
main main()方法执行完成
使用 Executors 工具类创建线程池
Executors提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService接口。
主要有newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool,后续详细介绍这四种线程池
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " run()方法执行中...");
}
}
public class SingleThreadExecutorTest {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
MyRunnable runnableTest = new MyRunnable();
for (int i = 0; i < 5; i++) {
executorService.execute(runnableTest);
}
System.out.println("线程任务开始执行");
executorService.shutdown();
}
}
执行结果
线程任务开始执行
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running...
pool-1-thread-1 is running..
说一下 runnable 和 callable 有什么区别?
runnable | callable |
---|---|
都是接口 | 都是接口 |
都可以实现多线程 | 都可以实现多线程 |
都是调用start()启动线程 | 都是调用start()启动线程 |
run()没有返回值 | call()方法有返回值 |
run()异常在内部消化不能向上抛 | call()可以抛出异常 |
线程的 run()和 start()有什么区别?
run() | start() |
---|---|
执行线程运行时的代码,被称为线程体 | 启动线程,线程处于可运行状态 |
可以重复调用 | 只能调用一次 |
方法执行结束线程销毁 | – |
为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?(重点)
我们调用start()方法后启动线程了,线程处于可运行状态,当得到cpu时间片后,线程处于运行状态,这才是多线程的工作。
而直接执行 run() 方法,会把 run()当成普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
线程的状态和基本操作
说说线程的生命周期及五种基本状态?(重点)
阻塞的情况分三种:
- 等待阻塞:运行状态中的线程执行 wait()方法,JVM会把该线程放入等待队列中,使本线程进入到等待阻塞状态;
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),则JVM会把该线程放入锁池中,线程会进入同步阻塞状态;
- 其他阻塞: 通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
Java中线程调度算法有了解吗
- 分时调度:按照cpu时间片轮询的机制让线程轮询获取cpu的时间进行执行。
- 抢占式调度:按照线程优先级高低让线程执行,优先级相同随机选择一个线程执行
请说出与线程同步以及线程调度相关的方法。
- wait():object中的方法,使一个线程处于等待阻塞状态,线程进入等待队列中,并且释放所持有的对象的锁;
- sleep():thread类中的静态方法,使一个正在运行的线程处于睡眠状态,调用此方法要处理 InterruptedException 异常;
- notify():object中的方法,唤醒一个处于等待状态的线程,由 JVM 确定唤醒哪个线程与优先级无关,唤醒的线程会进入锁池中
- notityAll():object中的方法,唤醒所有处于等待状态的线程,唤醒的线程会进行锁池中;
- join():thread中的方法,当前线程调用该方法,其他线程会停止执行,直到该线程执行结束其他线程继续执行;
- yield():thread中的方法,当前线程调用该方法放弃cpu的执行权,从运行状态变成可运行状态;
- sleep() 和 wait() 有什么区别?
sleep | wait |
---|---|
thread中的静态方法 | object中的方法 |
调用该方法线程处于休眠状态,不会释放锁 | 调用该方法线程处于等待状态,进入等待队列中,会释放锁 |
线程的 sleep()方法和 yield()方法有什么区别?
sleep | yield |
---|---|
thread类中静态方法 | thread类中的方法 |
线程调用sleep()后,线程处于阻塞状态,时间到后线程处于可运行状态, | 线程调用该方法后会从运行状态转为可运行状态 |
sleep()需要抛出interruptedexception | 该方法不需要抛出任何异常 |
如何停止一个正在运行的线程?
- run()执行结束后线程正常退出;
- 使用interrupt()中断线程。
notify() 和 notifyAll() 有什么区别?
notify | notifyAll |
---|---|
都是object中的方法 | 都是object中的方法 |
唤醒某一个线程具体唤醒哪个线程由jvm决定 | 唤醒所有线程 |
– | – |
唤醒线程从等待队列中进入锁池中参与锁竞争,获取到锁后进入可运行状态 | 唤醒线程从等待队列中进入锁池中参与锁竞争,获取到锁进入可运行状态 |
线程间如何共享数据?
线程间可以通过共享变量实现数据共享
Java如何实现多线程之间的通讯和协作?
通过共享变量和中断的方式实现多线程之间的通讯和写作;比如说生产者-消费者模型:
当队列满的时候,生产者释放对队列的占有权,通知消费者去消费队列中的产品此时生产者处于等待状态;同样当队列为空时,消费者也必须释放对队列的占用权,通知生产者去生产产品,此时消费者处于等待状态,这就是线程之间的协作和通讯;
Java中线程通信协作的最常见的实现方式:
- 使用object类中的wait()和notify()/notifyall()方法;
- 使用volatile关键字实现;
同步方法和同步块的区别
同步方法 | 同步块 |
---|---|
会锁住整个对象 | 不会锁住整个对象(推荐使用) |
更加符合开闭原则,根据需要锁住代码块对应的对象 |
注意:同步的范围越小越好,避免死锁
servlet和springmvc是线程安全的吗?
- servlet是单例多线程的,不是线程安全的,当多线程的时候无法保证共享变量之间的安全性;
- springmvc中的controller也不是线程安全的;
- Servlet 和 SpringMVC 如果需要考虑线程安全问题,可以使用 ThreadLocal 来处理多线程的问题。
在 Java 程序中怎么保证多线程的运行安全?
- 方法一:使用原子类,比如 java.util.concurrent 下(juc)的类,使用原子类AtomicInteger
- 方法二:使用自动锁 synchronized。
- 方法三:使用手动锁 Lock。
手动锁 Java 示例代码如下:
Lock lock = new ReentrantLock();
lock. lock();
try {
System. out. println("获得锁");
} catch (Exception e) {
// TODO: handle exception
} finally {
System. out. println("释放锁");
lock. unlock();
}
Java 线程数过多会造成什么异常?
- 降低jvm的稳定性;
- 消耗过多的cpu资源,带来资源消耗过快;
- 线程的生命周期开销非常高
对synchronized关键字的理解(重点)
synchronized关键字内部锁可以用来修饰类、方法、变量、代码块,修饰方法和代码块的时候叫做同步方法和同步代码块;
synchronized关键字可以保证在多线程下只有一个线程竞争到锁进行同步操作,保证线程安全性,其他线程进入同步阻塞状态,其他线程进入到锁池中重新竞争锁,竞争到锁后进入到可运行状态;
在jdk1.6之后synchronized锁进行了大量优化如:自旋锁、锁消除;
说一下 synchronized 底层实现原理(重点)
synchronized 同步语句块的情况:
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 代码块");
}
}
}
通过JDK 反汇编指令 javap -c -v SynchronizedDemo
底层有一个monitorenter和两个monitorexist,当获取锁时执行monitorenter,计数器+1,当释放锁执行monitorexist计数器-1,当需要获取锁时去判断计数器是否为0,0表示可以获取锁,否则不能获取锁。
为什么会有两个monitorexit呢
主要为了防止线程因为异常退出没有对锁进行释放而造成死锁的情况,所以再增加一个monitorexist,释放锁的时候再执行一次monitorexist,计数器-1。
synchronized、volatile、CAS 比较
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- volatile轻量级锁,修饰变量, 提供多线程共享变量可见性和有序性(禁止指令重排)
- CAS 是基于冲突检测的乐观锁,非阻塞
synchronized 和 Lock 有什么区别?(重点)
synchronized | lock |
---|---|
关键字 | 接口 |
1.同步代码块执行结束释放锁 2.线程异常情况时自动释放锁 | 通常在finally中使用unlock()释放锁以防出现死锁 |
在等待锁释放的过程中不能中断等待 | 在等待锁释放的过程中可以使用interrupt()中断等待 |
— | 可以使用trylock()方法判断有没有获取到锁 |
synchronized 和 ReentrantLock 区别是什么?(重点)
synchronized | ReentrantLock |
---|---|
关键字 | 实现类 |
使用object对象内置监视器 | 通过reentranlock.newcondition()来获取条件监视器 |
使用object.wait()/notify()对线程进行等待和唤醒操作,notifyall()唤醒所有等待线程 | 使用await()/signal()对线程进行等待操作和唤醒操作,signalall()唤醒所有线程 |
new reentranlock()来创建锁对象 | |
– | 在try…cath…finally代码块中lock和unlock(),防止出现异常锁无法被释放 |
可以指定公平锁和非公平锁,默认使用非公平锁 |
Lock lock=new ReentrantLock();
try {
lock.lock();
} finally {
lock.unlock();
}
//公平锁和非公平锁:
public ReentranLock(){
syn=new NonfairSyn(); //默认非公平锁
}
public ReentranLock(boolean fair){ //传入true公平锁
sync=fair? new FairSync():new NonfairSync();
}
ReentrantLock lock = new ReentrantLock(true); // true:公平锁
ReentrantLock lock = new ReentrantLock(); // true:非公平锁
synchronized1.6以后做了哪些优化
锁自旋、锁消除、锁粗化、轻量级锁、偏向锁
volatile 关键字的作用(重要)
- volatile保证可见性,多线程环境下,线程1在自己的工作内存中修改共享变量后会同步到主内存中,线程2能将该修改后的变量从主内存中读取到工作内存中;
- volatile保证有序性,禁止指令重排;
- 不保证原子性
synchronized 和 volatile 的区别是什么?(重点)
synchronized | volatile |
---|---|
关键字 | 关键字 |
修饰类、方法、变量、代码块 | 修饰变量 |
可以保证原子性、可见性、有序性 | 保证可见性、有序性(禁止指令重排)、不保证原子性 |
会造成线程阻塞 | |
实际开发中使用较多 |
悲观锁与乐观锁:
悲观锁:当要对数据库中的数据进行操作时,为了避免同时被事务,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制实现,这点跟java中的synchronized很相似。
乐观锁:对数据库中的数据进行更新时
不进行加锁,而是更新后,会比较该数据和数据表中数据是否一致,以此来决定是否要更新数据。利用CAS和版本号实现。
乐观锁的实现方式
-
版本号: 表中的数据进行更新操作时,给数据表增加version字段,先查询出某条记录获取version值,如果要对该条记录进行更新,则判断此version与之前获取的version是否相等,如果相等,说明没有其他事务进行操作,可以更新,将version+1。(
ABA的一种解决办法
) -
CAS:CAS 操作中包含三个操作数 —— 内存位置V、预期原值A和新值B。如果内存位置的值与预期原值相匹配,那么会使用新值替换旧值;
面试官:“CAS导致的ABA问题有了解吗?如何解决?
两个线程A和B,它们都读取到主内存中的某个值10,A先操作将10进行+1在进行-1,线程B不知道继续进行进行操作,这就是ABA问题。
ABA可以使用版本号version解决,也可以使用时间戳机制解决。
Java中的线程池有了解吗?(重点)
java.util.concurrent.ThreadPoolExecutor类就是一个线程池,线程池里面存放的工作者线程的数量就是线程池的大小。有3种形态:
- 当前线程池大小:线程池中实际工作者线程的数量
- 核心线程大小:线程池的基本大小,即没有任务需要执行的时候线程池的大小
- 最大线程池大小:线程池中存放的工作者线程数量的上限
线程池的优势体现如下:
- 我们可以重复利用线程中创建的线程,一次创建多次利用,可以减少每次创建线程和销毁线程带来的资源消耗。
- 线程池可以更好的管理线程,做到统一分配、监控、调优线程,提高系统的稳定性。
ThreadPoolExecutor的参数解释:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
corePoolSize:核心线程数
maximumPoolSize:最大线程数
keepAliveTime :线程空闲但是保持不被回收的时间
unit:时间单位
workQueue:存储线程的队列
threadFactory:创建线程的工厂
handler:拒绝策略
当我们向线程池提交任务的时候,需要遵循一定的排队策略:
- 如果当前线程数量<核心线程数量(poolSize <corePoolSize),会新增一个线程处理新提交的任务。
- 如果当前线程数量>=核心线程数量(poolSize >= corePoolSize)且任务队列未满时,新提交的任务提交到阻塞队列中排队:(分为2种情况)
1.当前poolSize<maximumPoolSize,那么就新增线程来处理任务。
2.当前poolSize=maximumPoolSize,那么意味着线程池的处理能力已经达到了极限,此时需要拒绝新增加的任务。
常见的线程池类型:
- newCachedThreadPool:创建一个可缓存的线程池,线程池大小不受限制。适合用来执行大量耗时较短且提交频率较高的任务
- newFixedThreadPool:创建一个定长的线程池,可控制线程的最大并发数量,超出线程会在队列中等待,使用该线程池必须根据实际情况估算出线程的数量。
- newScheduledThreadPool:创建一个定长的线程池。此线程池支持定时和周期性执行任务。
- newSingleThreadExecutor:创建一个单线程的线程池,这个线程池只有一个线程在工作,单线程串行执行所有任务,如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它,保证所有任务执行顺序按照它的提交顺序执行。
Atomic有了解吗?
Atomic是juc包下面的,它里面提供了很多原子类如AtomicInteger、AtomicLong等这些原子类下面提供很多原子性的操作,它能保证线程安全;
先来看看i++是否是线程安全的?
i++不是线程安全的,它是一个复合操作不能保证原子性,包括3个步骤:
- 将i值给临时变量
- 临时变量++
- 将临时变量拷回给i
那么如何实现自增操作呢?
可以使用juc包下的AtomicInteger等原子类,使用getAndIncrement()和incrementAndGet()等原子性的自增操作。