目录
volatile型变量的特性 可见性,禁止指令重排序,不保证原子性
进程与线程
进程与线程的概念
进程(资源分配的最小单位):是一个动态概念,是竞争计算机系统资源(CPU、内存、IO等)的基本单位,是并发执行的程序在执行过程中分配和管理资源的基本单位。
线程(程序执行的最小单位):是比进程更小的独立运行的基本单位,线程也被称为轻量级进程。是CPU调度和分派的基本单位。
一个程序至少一个进程,一个进程至少一个线程
进程与线程的区别
1、地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;
2、资源拥有:同一进程内的线程共享本进程的资源,如IO、内存、CPU等,但是进程之间的资源是独立的;
3、所以多进程程序要比多线程更加健壮,多线程程序只要有一个线程死掉,整个进程就死掉;但是一个进程崩溃后,在保护模式下不会对其他进程产生影响,因为进程有自己的独立的地址空间;
4、进程切换时,消耗的资源大,效率高。所以在涉及到频繁切换时,使用线程要比进程好。如果同时进行并且又要共享某些变量的并发操作,只能用线程。
5、执行过程:进程相当于一个应用程序执行的实例,所以每个独立的进程都有一个程序运行的入口、程序顺序执行序列和程序运行出口。每个线程相当于这个应用程序(进程)的一个执行流,所以不能独立执行必须依存在应用程序(进程)之中,由应用程序提供多个线程执行控制;
6、线程是处理器调度的基本单位,但是进程不是;
7、线程和进程都可以并发执行;
线程状态
![线程状态](https://img-blog.csdnimg.cn/1a61b8c003f14487b04b8cd9eff100b2.png)
创建线程
1.继承Thread类,重写run()方法
2.实现Runnable接口,并实现该接口的run()方法
3.实现Callable接口,重写call()方法
中断线程
void interrupt():中断线程,例如线程A运行时,线程B可以调用线程A的interrupt方法来设置线程A的中断标志位true。注意:这里仅仅是设置了标志,线程A并没有中断,它会继续往下执行。如果线程A调用了wait系列函数,join方法或sleep方法而被阻塞挂起,这时候如果线程B调用了线程A的interrupt()方法,线程A会在调用这些方法的地方抛出InterruptedException异常而返回。
boolean isInterrupted():检测当前线程是否被中断,如果是返回true,否则返回false.
boolean interrupted():检测当前线程是否被中断,如果是返回true,否则返回false;这个方法是Thread类的静态方法;另外与interrupt()方法的区别在于,如果发现当前线程被中断,则会清除中断 标志;另外需要注意的是:interrupted()方法是获取当前调用线程(正在运行的线程)的中断标志而不是调用interrupted()方法的实例对象的中断标志。(关键字:检测中断标志并清除中断标志)
public class Main {
public static void main(String[] args) {
}
public static void test1() {
Thread t = new Thread(){
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println("thread t running");
}
System.out.println("thread t end");
}
};
t.start();
System.out.println("interrupt thread t end");
t.interrupt();
}
public static void test2() {
Thread t = new Thread(){
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println("thread t running");
// 线程在休眠的状态下
// 如果catch里不加break,则线程不会中断,只会打印日志
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 线程在休眠状态下,interrupt会先设置标志位但是又会清除在休眠状态下,
// interrupt主要是会触发InterruptedException异常,进入到catch循环里,
// 是否终止线程以及如何终止线程的逻辑都可以在catch里实现,这就使得程序对于异常的处理更加灵活
e.printStackTrace();
}
}
System.out.println("thread t end");
}
};
t.start();
System.out.println("interrupt thread t end");
t.interrupt();
}
public static void test3(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()){
System.out.println("thread t running");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
//这里直接break,会立即中断线程
break;
}
}
System.out.println("thread t end");
}
};
t.start();
System.out.println("interrupt thread t end");
t.interrupt();
}
}
test2 输出
set thread t end
thread t running
thread t running
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at Main$2.run(Main.java:32)
thread t running
thread t running
thread t running
thread t running
同步
在多线程应用中,两个或者两个以上的线程需要共享对同一个数据的存取。如果两个线程存取相同的对象,并且每一个线程都调用了修改该对象的方法,这种情况通常被称为竞争条件。
ReentrantLock和Condition
重入锁ReentrantLock是Java SE 5.0引入的,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。
使用方法
private final ReentrantLock mLock = new ReentrantLock();
private void doWork()
{
final Thread thread = new Thread(() -> {
// 获得锁
mLock.lock();
try {
//...
}
finally {
// 解锁
mLock.unlock();
}
});
thread.start();
}
发生异常,必须要释放锁,不然后面线程会一直阻塞
条件对象又称作条件变量,当一个线程获得锁后,却发现不满足条件,这时候就需要它。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.test();
}
private Condition mCondition;
// 假设的条件
private boolean mPermission = false;
public void test()
{
// 得到条件对象
mCondition = mLock.newCondition();
doWork();
requestPermission();
}
private final ReentrantLock mLock = new ReentrantLock();
private void doWork()
{
final Thread applyThread = new Thread(() -> {
// 获得锁
mLock.lock();
try {
// 真正工作中,发起授权可能会有些额外代码,这里只做输出。
System.out.println("正在发起授权");
while (!mPermission)
{
System.out.println("无授权,进入条件阻塞状态");
mCondition.await();
}
System.out.println("已获得授权");
} catch (InterruptedException pException) {
pException.printStackTrace();
} finally {
// 解锁
mLock.unlock();
}
});
applyThread.start();
}
private void requestPermission()
{
final Thread permissionThread = new Thread(() -> {
mLock.lock();
mPermission = true;
System.out.println("已允许给予授权");
// 让所有因此条件进入阻塞的线程进入 可 运行状态。
mCondition.signalAll();
mLock.unlock();
});
permissionThread.start();
}
}
代码执行过程:
1. 首先执行doWork(),获得了锁,但条件并不满足
2. requestPermission()也有同样的锁,同样的锁只能有一个线程,这就说明,doWork()永远无法等待requestPermission()的授权,因为requestPermission()已经被锁阻塞了
3. 这时候,就需要条件阻塞doWork()
synchronized
一个方法用synchronized 修饰,也就使用了方法所在的对象的锁来保护这个方法
private synchronized void method() {}
等于
private final ReentrantLock mLock = new ReentrantLock();
private void method() {
mLock.lock();
try {
// todo
} finally {
mLock.unlock();
}
}
同步代码快
public class Demo {
// 两把不同的锁对象
private Object object = new Object();
public void method(){
synchronized (object){
//TODO
}
}
}
volatile
Java内存模型
Java 内存模型定义了线程和主存之间的抽象关系:
线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程共享变量的副本。
Java 内存模型的抽象示意图
![](https://img-blog.csdnimg.cn/f4f6f473e9be4222bd17f83d3ccbb3ec.png)
原子性、可见性和有序性
x = 10; //语句1
y = x; //语句2
x++; //语句3
语句1是原子性操作
语句2包含了两个操作,它先读取x的值,再将x的值写入工作内存。读取x的值以及将x的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。
语句3包括3个操作:读取x的值、对x的值进行加1、向工作内存写入新值。
一个语句含有多个操作时,就不是原子性操作,只有简单地读取和赋值(将数字赋值给某个变量)才是原子性操作。
原子操作
read(读取):从主内存读取数据
load(载入):将主内存读取到的数据写入工作内存use(使用):从工作内有读取数据来计算
assign(赋值):将计算好的值重新赋值到工作内存中store(存储):将工作内存数据写入主内存
write(写入):将store过去的变量值赋值给主内存中的变量
lock(锁定):将主内存变量加锁,标识为线程独占状态
unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
public class Main {
private static boolean initFlag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("initializing data...");
while (!initFlag) {
}
System.out.println("Success...");
}
});
Thread.sleep(2000);
// 线程2
new Thread(new Runnable() {
@Override
public void run() {
initFlag = true;
}
}).start();
}
}
主内存中有initFlag变量,初始值是false。
线程1 执行read操作,将false值从主内存中读取到线程1的工作内存,执行load操作将false值放入initFlag变量副本中,执行use 操作从工作内存读取initFlag = false到执行引擎进行计算。
线程2 执行read操作,将false值从主内存中读取到线程2的工作内存,执行load操作将false值放入initFlag变量副本,执行use操作从工作内存读取initFlag = false到执行引擎进行计算,在执行引擎中修改initFlag的值为true,执行assign操作将执行引擎中的true重新赋值给工作内存的initFlag变量副本,使得工作内存中initFlag = true,执行store操作将initFlag = true传递到主内存,执行write操作将true存入到主内存中的initFlag变量。
MESI协议 lock锁的是store到主内存的过程,锁的密度小
线程2执行store操作将initFlag变量副本被修改后的true值传回到主内存并执行write操作将true值写回给主内存的initFlag变量。线程2要将新值写回主内存,会向总线发消息,线程1会通过总线嗅探机制监听到自己工作内存中的initFlag变量被修改了,线程1会立即将自己工作内存的initFlag = false失效,当线程1的执行引擎需要用到initFlag变量时,线程1要重新执行read 和 load操作,那么线程1就能拿到最新值了。
volatile型变量的特性 可见性,禁止指令重排序,不保证原子性
可见性
一个线程修改了被volatile修饰的变量,新值对其他线程来说是可以立即得知的。
禁止指令重排序
在多线程并发的场景下,重排序可能会存在问题,最经典的例子就是双重检查锁定单例(Double Check Lock Singleton)
对共享对象加volatile,可以禁止指令重排序。底层实现是内存屏障
不保证原子性
public class Main {
volatile static int x = 1;
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(x);
}
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
x++;
}
}
}
}
运行结果
13854
count++在执行引擎中被分成了两步操作:
- count = 0,先将count值初始化为0
- count=count+1,再执行+1操作
这两步操作在左边的线程执行完第一步,但还没执行第二步时右边的线程抢过CPU控制权开始完成+1的操作后写入到主内存,于是左边的线程工作内存中的count副本失效了,相当于左边这一次+1的操作就被覆盖掉了。
因此,Volatile不能保证原子性。
加锁保证原子性
public class Main {
volatile static int count = 0;
static Object object = new Object();
public static void main(String[] args) {
Thread t1 = new MyThread();
Thread t2 = new MyThread();
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(count);
}
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 10000; ++i) {
synchronized (object) {
count++;
}
}
}
}
}
线程池
线程池对线程进行管理。在Java 1.5中提供了Executor框架用于把任务的提交和执行解耦,任务的提交交给Runnable或者Callable,而Executor框架用来处理任务。
ThreadPoolExecutor
通过ThreadPoolExecutor来创建一个线程池,最多参数的构造方法如下所示:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
corePoolSize 核心线程数。默认情况下线程池是空的,只有任务提交时才会创建线程。如果当前运行的线程数少于corePoolSize,则创建新线程来处理任务;如果等于或者多于corePoolSize,则不再创建。
maximumPoolSize 线程池允许创建的最大线程数。如果任务队列满了并且线程数小于
maximumPoolSize时,则线程池仍旧会创建新的线程来处理任务。
keepAliveTime 非核心线程闲置的超时时间。超过这个时间则回收。如果任务很多,并且每个任务的执行事件很短,则可以调大keepAliveTime来提高线程的利用率。另外,如果设置
allowCoreThreadTimeOut属性为true时,keepAliveTime也会应用到核心线程上
TimeUnit keepAliveTime参数的时间单位。可选的单位有天(DAYS)、小时(HOURS)、分钟
(MINUTES)、秒(SECONDS)、毫秒(MILLISECONDS)等。
workQueue 任务队列。如果当前线程数大于corePoolSize,则将任务添加到此任务队列中。该任务队列是BlockingQueue类型的,也就是阻塞队列。这在前面已经介绍过了,这里就不赘述了。
ThreadFactory 线程工厂。可以用线程工厂给每个创建出来的线程设置名字。一般情况下无须设置该参数。
RejectedExecutionHandler 饱和策略。这是当任务队列和线程池都满了时所采取的应对策略,默认是AbordPolicy,表示无法处理新任务,并抛出RejectedExecutionException异常。此外还有3种策略,它们分别如下。
(1)CallerRunsPolicy:用调用者所在的线程来处理任务。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
(2)DiscardPolicy:不能执行的任务,并将该任务删除。
(3)DiscardOldestPolicy:丢弃队列最近的任务,并执行当前的任务。
线程池的原理
(1)提交任务后,线程池先判断线程数是否达到了核心线程数(corePoolSize)。如果未达到核心线程数,则创建核心线程处理任务;否则,就执行下一步操作。
(2)接着线程池判断任务队列是否满了。如果没满,则将任务添加到任务队列中;否则,就执行下一步操作。
(3)接着因为任务队列满了,线程池就判断线程数是否达到了最大线程数。如果未达到,则创建非核心线程处理任务;否则,就执行饱和策略,默认会抛出 RejectedExecutionException异常。
我们执行ThreadPoolExecutor的execute方法,会遇到各种情况:
(1)如果线程池中的线程数未达到核心线程数,则创建核心线程处理任务。
(2)如果线程数大于或者等于核心线程数,则将任务加入任务队列,线程池中的空闲线程会不断地从任务队列中取出任务进行处理。
(3)如果任务队列满了,并且线程数没有达到最大线程数,则创建非核心线程去处理任务。
(4)如果线程数超过了最大线程数,则执行饱和策略。
线程池分类
FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
![](https://img-blog.csdnimg.cn/4d6fb0a4af6c4683a78688a2f551b0f2.png)
CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}