Java并发基础
基本概念
进程与线程
进程
- 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。
- 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
- 进程就可以视为程序的一个实例。大部分程序可以同时运行多个实例进程(例如记事本、画图、浏览器 等),也有的程序只能启动一个实例进程(例如网易云音乐、360 安全卫士等)
进程调度算法(CPU 调度算法)
当CPU空闲的时候,操作系统会选择某个空闲处于就绪状态的进程,为其分配CPU资源。
- 先来先服务调度算法。每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
- 最短作业优先调度算法。优先选择运行时间最短的进程来运行。
- 高响应比优先调度算法。每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行。响应比 =(等待时间+要求服务时间)/要求服务时间。
- 时间片轮转调度算法。每个进程被分配一个时间段,允许该进程在该时间段中运行。
- 最高优先级调度算法。从就绪队列中选择最高优先级的进程运行。
- 多级反馈队列调度算法。「时间片轮转算法」和「最高优先级算法」的综合。
线程
- 一个进程之内可以分为一到多个线程
- 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
- Java 中,线程作为小调度单位,进程作为资源分配的最小单位。 在 windows 中进程是不活动的,只是作为线程的容器
两者对比
- 进程基本上相互独立的,而线程存在于进程内,是进程的一个子集。进程拥有共享的资源,如内存空间等,供其内部的线程共享。
- 进程间通信较为复杂,同一台计算机的进程通信称为IPC,不同计算机之间的进程通信需要通过网络。
- 线程通信相对简单,因为它们共享进程内的内存,一个例子是多个线程可以访问同一个共享变量。线程更轻量,线程上下文切换成本一般要比进程上下文切换低
并发与并行
- 并发(Concurrent)是一个CPU在不同的时间去不同线程中执行指令
- 并行(Parallel)是多个CPU同时处理不同线程
同步与异步
- 同步指需要等结果返回才能继续运行
- 异步指不需要等结果返回
举例:读取文件的,由于IO操作需要等文件全部读取结束才能继续运行,所以效率低下,才有了后来的NIO(非阻塞IO)
死锁
t1线程获得锁A,同时还想获得锁B。t2线程获得锁B,同时还想获得锁A。两者都进行不下去的情形称为死锁。
定位死锁工具,jconsole或者jps,jstack
public static void main(String[] args) {
final Object A = new Object();
final Object B = new Object();
new Thread(()->{
synchronized (A) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
}
}
}).start();
new Thread(()->{
synchronized (B) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A) {
}
}
}).start();
}
死锁四个条件
- 互斥条件。该资源任意一个时刻只由一个线程占有
- 请求并持有条件。一个线程因请求资源而阻塞时,对已获得的资源保护不放
- 不可剥夺条件。线程获取到的资源在未用完前不能被其他线程抢占,只由自己用完后才释放
- 环路等待条件。若干线程之间形成的头尾循环的等待资源关系。
破坏死锁
- 破坏请求并持有条件
- 破坏环路等待条件
避免死锁
有序申请资源(银行家算法等)
活锁
两个线程互相改变对方的结束条件,谁也没法结束。
解决方法,在线程执行时,给予不同的间隔时间
饥饿
因为优先级太低,一直得不到资源。或者在顺序加锁时,也会出现。
主线程与守护线程
守护(daemon)线程:当其他全部非守护线程结束时,守护线程就结束。只要有一个用户线程还没结束,正常情况下JVM就不会退出。举例:垃圾收集器线程就是一种守护线程。
并发编程三个重要特性
- 原子性。一个或多次操作,要么所有的操作全部都执行,要不都不执行,且不会受到任何因素的干扰而中断。synchronized可以确保原子性
- 可见性。当一个线程对共享变量进行了修改,那么其他线程都应立即能看到修改后的最新值。volatile可以确保可见性。
- 有序性。代码在执行的过程中的先后顺序,Java编译器会进行优化,即指令重排。volatile可以禁止指令重排,确保有序性。
Java线程
线程创建与运行
方法一,使用Thread
缺点:线程和任务合并在了一起,没有返回值
Thread thread = new Thread(() -> System.out.println(111));
thread.start();
方法二,使用Runnable配合Thread(推荐)
线程和任务分开,缺点:没有返回值
Runnable runnable = () -> System.out.println(111);
Thread thread = new Thread(runnable);
thread.start();
方法三,FutureTask配合Thread
优点:可以拿到返回值
FutureTask<String> futureTask = new FutureTask<>(() -> "hello");
new Thread(futureTask).start();
System.out.println(futureTask.get());//hello
线程运行原理
结合之前学习的JVM,JVM为每个线程分配虚拟机栈、程序计数器、本地方法栈。每个方法被执行的时候都会创建一个栈帧,存储在虚拟机栈中,每个线程都会维护自己的栈帧。
线程上下文切换(Thread Context Switch):任务从保存到再加载的过程就是一次上下文切换。
因为一些原因进行线程切换,程序计数器会记住下一条JVM指令的执行地址。
被动原因:
- 线程CPU时间片用完
- GC
- 有更高优先级的线程需要运行
主动原因:
线程自己调用了sleep、yield、wait、join、park、synchronized、lock等方法
Thread常见方法
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
start() |
启动一个新线程 | 只是让线程进入就绪状态,代码不一定立刻运行。start只能调用一次,调用多次会出现IllegalThreadStateException |
|
wait() |
线程被阻塞挂起 | 如果调用线程没有获取锁,则调用时会抛出IllegalMonitorStateException |
|
run() |
新线程启动时会调用的方法 | 如果在构造Thread对象时传递了Runnable参数,则线程启动后会调用Runnable中的run方法,否则默认不执行任何操作 | |
join() |
等待线程运行结束 | ||
join(long n) |
等待线程运行结束,最多等待n毫秒 | ||
getId() |
获取线程唯一长整型id | ||
getName() |
获取线程名 | ||
setName(String) |
修改线程名 | ||
getPriority() |
获取线程优先级 | ||
setPriority(int) |
修改线程优先级 | Java中规定优先级为1-10的整数,较大优先级能提高被CPU调度几率 | |
getState() |
获取线程状态 | Java中用枚举表示,NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED | |
isInterrupted() |
判断是否被打断 | 不会清除打断标记 | |
isAlive() |
线程是否存活(还没有运行完毕) | ||
interrupt() |
打断线程 | 如果被打断的线程正在sleep,wait,join则会导致interruptedException,并清除打断标记;如果打断的是正在运行的线程,则会设置打断标记,park的线程被打断,也会设置打断标记 | |
interrupted() |
static | 判断当前线程是否被打断 | 会清除打断标记(设置为false) |
currentThread() |
static | 获取当前正在执行的线程 | |
sleep(long n) |
static | 让当前执行的线程休眠n毫秒 | |
yield() |
static | 提示线程调度器让出当前线程对CPU的使用 |
start与run
start是让创建线程进入就绪状态,run是直接调用方法,线程还是处于新建状态。
调用start()
方法才可启动线程并使线程进入就绪状态,直接执行run()
方法的话不会以多线程的方式执行
可以看到直接用run,还是main线程在执行
Thread thread = new Thread(() -> {
System.out.println(Thread.currentThread()+"hello");//Thread[main,5,main]hello
});
thread.run();
sleep与yield
- sleep让线程阻塞。状态由Running变为Time Waiting。可用interrupt打断正在睡眠的线程,这时会抛interruptedException。推荐用TimeUnit替代,更好的可读性。
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
应用:防止CPU占用100%
某些时候可能要写死循环,如果不加sleep,CPU占用率会过高。
while(true){
try{
Thread.sleep(50);
}catch(InterruptedException e){
e.printStackTrace();
}
//do something
}
- yield让出当前线程。状态由Running变为Runnable
join
public static void main(String[] args) {
Runnable runnable = () -> {
try {
Thread.sleep(1000);
System.out.println("hello");
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
//main线程等thread线程执行结束
thread.join();
}
interrupt
- 如果一个线程在运行中被打断,打断标记会被置为true
- 如果打断因sleep、wait、join方法而被阻塞的线程, 打断标记会被置为false
Thread thread=new Thread(()->{
System.out.println("开始");
//暂停
LockSupport.park();
System.out.println("继续");
System.out.println(Thread.currentThread().isInterrupted());//true
});
thread.start();
try {
Thread.sleep(1000);
thread.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
线程状态
Java线程状态有6种,NEW、RUNNABLE、TERMINATED、TIMED_WAITING、WAITING、BLOCKED
public class Test {
public static void main(String[] args) {
// NEW
Thread t1 = new Thread(() -> {
System.out.println("NEW 状态");
}, "t1");
// RUNNABLE
Thread t2 = new Thread(() -> {
while (true) {
}
}, "t2");
t2.start();
// TERMINATED
Thread t3 = new Thread(() -> {
System.out.println("running");
}, "t3");
t3.start();
// TIMED_WAITING
Thread t4 = new Thread(() -> {
synchronized (Test.class) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t4");
t4.start();
// WAITING
Thread t5 = new Thread(() -> {
try {
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t5");
t5.start();
Thread t6 = new Thread(() -> {
synchronized (Test.class) {
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t6");
t6.start();
// 主线程休眠 1 秒, 目的是为了等待 t3 线程执行完
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1 线程状态: {}" + t1.getState());//NEW
System.out.println("t2 线程状态: {}" + t2.getState());//RUNNABLE
System.out.println("t3 线程状态: {}" + t3.getState());//TERMINATED
System.out.println("t4 线程状态: {}" + t4.getState());//TIMED_WAITING
System.out.println("t5 线程状态: {}" + t5.getState());//WAITING
System.out.println("t6 线程状态: {}" + t6.getState());//BLOCKED
}
}
共享模型之管程
一段代码块如果存在对共享资源的多线程读写操作,称这段代码块为临界区
多个线程在临界区内执行,由于代码的执行顺序不同而导致结果无法预测,称之为发生了竞态条件
为避免临界区的竞态条件发生,有多种解决方案:
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized
俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程持有对象锁,其他线程想获得时会被阻塞,这样就能保证持有锁的线程可以安全的执行临界区内的代码。用对象锁保证了临界区内代码的原子性。不能用synchronized修饰构造方法(构造方法本身就是线程安全的)。可重入锁,非公平锁。
class Test{
public synchronized void test(){
}
}
//等价于
class Test{
public void test(){
synchronized(this){
}
}
}
class Test{
public synchronized static void test(){
}
}
//等价于
class Test{
public static void test(){
synchronized(Test.class){
}
}
}
synchronized底层原理
每个Java对象都可以关联一个monitor,如果用synchronized给对象上锁,则该对象的对象头中的Mark word中被设置为指向monitor对象的指针。monitor中的owner指向该对象,后来的对象则是monitor中的entryList指向。
synchronized优化原理(了解)
锁膨胀
如果在尝试加轻量级锁的过程中,CAS操作无法成功,这时一种情况就是其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
偏向锁
轻量级锁在没有竞争时,每次重入仍需要执行CAS操作。
Java6中引入偏向锁来进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
锁消除
变量线程安全分析
成员变量和静态变量是否线程安全?
- 如果没有被共享,则线程安全
- 如果被共享了,如果只有读,那么安全。如果有读写,则不安全。
局部变量是否线程安全?
- 局部变量是线程安全的
- 但局部变量引用的对象则未必
- 如果该对象没有逃离方法的作用范围,则是线程安全的
- 如果该对象逃离方法的作用范围,则线程不安全。(逃逸分析)
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector(List的线程安全实现类)
- Hashtable(Hash的线程安全实现类)
- java.util.concurrent包下的类
但是方法组合不是原子的,线程不安全。
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
示例一:
MyAspect切面类只有一个实例,成员变量start 会被多个线程同时进行读写操作
@Aspect
@Component
public class MyAspect {
// 是否安全?不安全
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
示例二:
public class MyServlet extends HttpServlet {
// 是否安全?安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全?安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全?安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
示例三:
可以被子类继承重写,导致线程不安全
public abstract class Test {
public void bar() {
// 是否安全?不安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
foo(sdf);
}
public abstract foo(SimpleDateFormat sdf);
public static void main(String[] args) {
new Test().bar();
}
}
public void foo(SimpleDateFormat sdf) {
String dateStr = "1999-10-11 00:00:00";
for (int i = 0; i < 20; i++) {
new Thread(() -> {
try {
sdf.parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
}
wait/notify
必须获得对象锁才能调用这两个方法。
ReentrantLock reentrantLock = new ReentrantLock();
Thread thread = new Thread(()->{
reentrantLock.lock();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
});
thread.start();
thread.wait();//java.lang.IllegalMonitorStateException
notify随机唤醒一个wait线程,notifyAll唤醒全部。
sleep(long n)和wait(long n)区别
- sleep是Thread方法,wait是Object的方法
- sleep不需要强制配合synchronized,wait需要
sleep()
睡眠的同时不释放锁,wait()
释放锁
防止虚假唤醒(一个线程可以从挂起状态变为可运行状态,即使该线程没有被其他线程调用notify、notifyAll,或者被中断,或者等待超时。),wait的正确使用方式:
synchronized (lock) {
while(//不满足条件,一直等待,避免虚假唤醒) {
lock.wait();
}
//满足条件后再运行
}
synchronized (lock) {
//唤醒所有等待线程
lock.notifyAll();
}
park/unpark
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark;
与wait,notify比较:
- unpark不需要配合Object使用
- unpark可以唤醒指定线程,notify随机唤醒一个等待线程
- unpark可以先unpark,notify不能先notify(需要在wait之后)
线程状态转换
- 情况一:NEW –> RUNNABLE 当调用了 t.start() 方法时,由 NEW –> RUNNABLE
- 情况二: RUNNABLE <–> WAITING 当调用了t 线程用 synchronized(obj) 获取了对象锁后,调用 obj.wait() 方法时,t 线程从 RUNNABLE –> WAITING 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时,会在 WaitSet 等待队列中出现锁竞争,非公平竞争 竞争锁成功,t 线程从 WAITING –> RUNNABLE 竞争锁失败,t 线程从 WAITING –> BLOCKED
- 情况三:RUNNABLE <–> WAITING 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE –> WAITING 注意是当前线程在 t 线程对象的监视器上等待 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING –> RUNNABLE
- 情况四: RUNNABLE <–> WAITING 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE –> WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING –> RUNNABLE
- 情况五: RUNNABLE <–> TIMED_WAITING t 线程用 synchronized(obj) 获取了对象锁后 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE –> TIMED_WAITING t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时 竞争锁成功,t 线程从 TIMED_WAITING –> RUNNABLE 竞争锁失败,t 线程从 TIMED_WAITING –> BLOCKED
- 情况六:RUNNABLE <–> TIMED_WAITING 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE –> TIMED_WAITING 注意是当前线程在 t 线程对象的监视器上等待 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况七:RUNNABLE <–> TIMED_WAITING 当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE –> TIMED_WAITING 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING –> RUNNABLE
- 情况八:RUNNABLE <–> TIMED_WAITING 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线 程从 RUNNABLE –> TIMED_WAITING 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从 TIMED_WAITING–> RUNNABLE
- 情况九:RUNNABLE <–> BLOCKED t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE –> BLOCKED 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争 成功,从 BLOCKED –> RUNNABLE ,其它失败的线程仍然 BLOCKED
- 情况十: RUNNABLE <–> TERMINATED 当前线程所有代码运行完毕,进入 TERMINATED
ReentrantLock
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与synchronized一样,都支持可重入
基本语法
private static ReentrantLock reentrantLock = new ReentrantLock();
public static void main(String[] args) {
reentrantLock.lock();
try {
}finally {
reentrantLock.unlock();
}
}
可重入
如果同一个线程首次获得了这把锁,因为是锁的拥有者,因此有权利再次获得这把锁。
可中断
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//使用可中断方式获取锁,可以避免死锁
reentrantLock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("被打断,未能获取到锁");
return;
}
try {
System.out.println("获取到锁");
} finally {
reentrantLock.unlock();
}
}
});
reentrantLock.lock();
thread.start();
Thread.sleep(1000);
thread.interrupt();
锁超时
tryLock
进行有限时间等待,如果未能获取到锁,就返回false
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
//等待有限时间
if (!reentrantLock.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("未能获取到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("未能获取到锁");
return;
}
try {
System.out.println("获取到锁");
} finally {
reentrantLock.unlock();
}
}
});
reentrantLock.lock();
thread.start();
非公平锁
ReentrantLock默认是不公平的。可以通过构造函数开启公平锁(先等待的线程先获得锁),但一般没有必要开启,会降低并发度。
条件变量
- await 前需要获得锁
- await 执行后,会释放锁,进入condition等待
- await的线程被唤醒后去重新竞争lock锁
- 竞争lock锁成功后,从await后继续执行
//创建新条件变量
Condition condition1 = reentrantLock.newCondition();
Condition condition2 = reentrantLock.newCondition();
reentrantLock.lock();
//进入休息室等待
condition1.await();
//唤醒休息室中的随机一个线程
condition1.signal();
//唤醒全部
condition1.signalAll();
ThreadLocal
ThreadLocal是线程专属的本地变量,存储每个线程私有数据。
示例:
class Test implements Runnable {
//SimpleDateFormat线程不安全,每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
Test test = new Test();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(test, "" + i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter =" + formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name=" + Thread.currentThread().getName() + " formatter =" + formatter.get().toPattern());
}
}
当改变线程自己的结果时,其他线程的结果不受影响,可见ThreadLocal的值是线程私有的。
synchronized与threadlocal区别
synchronized | threadlocal | |
---|---|---|
原理 | 以时间换空间,只提供一份变量,让不同线程排队访问 | 以空间换时间,为每个线程提供了一份变量副本,从而实现同时访问而互不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
原理:
通过阅读源码,可以发现最终变量时存在ThreadLocalMap中,ThreadLocalMap存储以ThreadLocal为key,Object为value的键值对。
内存泄漏问题:
threadLocal使用的key为弱引用,垃圾回收时,key被清理掉,value不会被清理掉,使用完之后最好手动调用remove()
方法。
可见性问题
因为run值是存储在主内存中的,每次读取都比较耗费资源,所以JIT对此做了优化,把值存储到了自己工作内存的高速缓存中,当main线程改变run值时,线程中的run值并没改变。
public class Demo {
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (run) {
}
}).start();
Thread.sleep(1000);
System.out.println("开始停止");
run = false;//未能停止
}
}
解决方法:volatile关键词。修饰成员变量和静态成员变量,避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。
public class Demo {
volatile static boolean stop = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (stop) {
}
}).start();
Thread.sleep(1000);
System.out.println("开始停止");
stop = false;//停止
}
}
volatile原理
底层实现原理是内存屏障
- 对volatile变量的写指令后会加入写屏障。写屏障保证在该屏障之前对共享变量的改动,都同步到主存当中。
- 对volatile变量的读指令前会加入读屏障。读屏障保证在该屏障之后对共享变量的读取,加载的是主存中最新数据。
happens-before
一套可见性与有序性的规则总结
- 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
static int x;
static Object m = new Object();
new Thread(()->{
synchronized(m){
x=10;
}
}).start();
new Thread(()->{
synchronized(m){
System.out.println(x);
}
}).start();
- 线程对volatile变量的写,对接下来其他线程对该变量的读可见
volatile static int x;
new Thread(()->{
x=10;
}).start();
new Thread(()->{
System.out.println(x);
}).start();
- 线程start前对变量的写,对该线程开始后对该变量的读可见
static int x;
x=10;
new Thread(()->{
System.out.println(x);
}).start();
- 线程结束前对变量的写,对其他线程得知它结束后的读可见
static int x;
Thread t1=new Thread(()->{
x=10;
}).start();
t1.join();
System.out.println(x);
- 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后对变量的读可见
static int x;
public static void main(String[] args){
Thread t2=new Thread(()->{
while(true){
if(Thread.currentThread().isInterrupted()){
System.out.println(x);
break;
}
}
});
t2.start();
new Thread(()->{
sleep(1);
x=10;
t2.interrupt();
}).start();
while(!t2.isInterrupted()){
Thread.yield();
}
System.out.println(x);
}
- 对变量默认值的写,其他线程对该变量读可见
共享模型之无锁
compareAndSet
简称CAS,底层是CPU级别的原子操作,所以不需要加锁
CAS特点
CAS适合线程数少、多核CPU的场景。
- CAS是基于乐观锁的思想,不怕别的线程来修改共享变量,就算改了再重试
- synchronized基于悲观锁,我上锁了你们都别想改
- CAS体现的是无锁开发、无阻塞并发
- 因为没有使用synchronized,所以线程不会陷入阻塞
原子整数
AtomicBoolean
AtomicInteger
AtomicLong
AtomicInteger i = new AtomicInteger(10);
System.out.println(i.getAndIncrement());//10
System.out.println(i.incrementAndGet());//12
System.out.println(i.getAndAdd(10));//12
System.out.println(i.get());//22
System.out.println(i.updateAndGet(x -> x * 10));//220
原子引用
AtomicReference
AtomicMarkableReference
AtomicStampedReference
//无法察觉ABA问题,即从A变为B再变为A的情况。
AtomicReference<String> atomicReference=new AtomicReference<>("abc");
atomicReference.compareAndSet("abc","sss");
System.out.println(atomicReference.get());//sss
解决ABA问题,每次使用获取时间戳,如果时间戳不对则不修改
AtomicStampedReference<String> atomicReference=new AtomicStampedReference<>("abc",0);
int stamp = atomicReference.getStamp();
atomicReference.compareAndSet("abc","sss",stamp,stamp+1);
System.out.println(atomicReference.getReference());//sss
还可以使用AtomicMarkableReference
判断是否修改过
AtomicMarkableReference<String> atomicReference=new AtomicMarkableReference<>("abc",true);
atomicReference.compareAndSet("abc","sss",true,false);
System.out.println(atomicReference.getReference());//sss
原子数组
AtomicIntegerArray
AtomicLongArray
AtomicReferenceArray
字段更新器
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicReferenceFieldUpdater
原子累加器
LongAdder
Unsafe
unsafe没法直接获取,需要通过反射的方式
public class UnsafeTest {
//获取Unsafe实例
static Unsafe unsafe;
//记录偏移值
static long stateOffset;
private volatile long state = 0;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
//返回指定变量在所属类中的内存偏移地址
stateOffset = unsafe.objectFieldOffset(UnsafeTest.class.getDeclaredField("state"));
} catch (Exception e) {
System.out.println(e.getLocalizedMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
//创建实例
UnsafeTest test = new UnsafeTest();
//比较对象中偏移量为offset的值是否与expect相等
Boolean success = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(success);
}
}
共享模型之不可变
在多线程下使用SimpleDateFormat
会报错
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(sdf.parse("1561-05-11"));
} catch (ParseException e) {
e.printStackTrace();
}
}).start();
}
解决方法一:加锁
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf){
try {
System.out.println(sdf.parse("1561-05-11"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}).start();
}
解决方法二:使用JDK8提供的日期格式化方法DateTimeFormatter
DateTimeFormatter dateFormat =DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
System.out.println(dateFormat.parse("1561-05-11"));
}).start();
}