文章目录
概念
进程与线程
- 进程:资源分配的基本单位
- 线程:程序执行的最小单位(资源调度的最小单位)
- 进程:有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
- 线程:是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
- 线程: 之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据
- 进程: 之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。
- 多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
并行与并发
- 并发:是同一时间应对多件事情的能力
- 如果某个系统支持两个或者多个动作同时存在,那么这个系统就是一个并发系统。
- 并行:是同一时间动手做多件事情的能力
- 如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统
那么单核CPU的并发微观上其实串行执行的,,而且不会发生并行
创建线程
- 1.继承Thread
// 创建线程对象
Thread t = new Thread() {
public void run() {
// 要执行的任务
}
};
t.start();
- 2.使用 Runnable 配合 Thread
Runnable runnable = new Runnable() {
public void run(){
// 要执行的任务
}
};
Thread t = new Thread( runnable );
t.start();
方法1 是把线程和任务合并在了一起,方法2 是把线程和任务分开了
用 Runnable 更容易与线程池等高级 API 配合
用 Runnable 让任务类脱离了 Thread 继承体系,更灵活
- 3.FutureTask 配合 Thread
FutureTask 能够接收 Callable 类型的参数,用来处理有返回结果的情况
常见方法
- join() 等待线程运行结束
- join(long n) 等待线程运行结束,最多等待 n 毫秒
- getId() 获取线程长整型id
- getName()
- setName(String)
- .setDaemon(boolean) 设置守护线程
- getPriority() 获取线程优先级
- setPriority(int) 设置线程优先级
- getState() ----线程状态----NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING, TERMINATED
- isInterrupted() 判断是否被打断,不会清除打断标记
- isAlive() -----线程是否存活
- interrupt() -----打断线程
如果被打断线程正在 sleep,wait,join 会导致被打断 的线程抛出InterruptedException,并清除 打断标记 ;
如果打断的正在运行的线程,则会设置打断标记 ;
park 的线程被打断,也会设置打断标记 - interrupted() ----static-----判断线程是否被打断,会清除打断标记
- currentThread() ----static-----获取当前正在执 行的线程
- yield() —static----提示线程调度器 让出当前线程对CPU的使用, Running 进入 Runnable 就绪状态
join
比如在主线程中调用 thread.join();
那么主线程会等待 thread 线程执行完毕在继续执行
thread.join(n);
主线程最多等待thread线程执行 n 毫秒。
join的底层是调用wait(),所以join调用时候也会释放锁资源
interrupt
打断 sleep,wait,join 的线程,这几个方法都会让线程进入阻塞状态
打断正常运行的线程,isInterruptted为true
打断 sleep 的线程, 会清空打断状态为false
打断 park 线程, 不会清空打断状态,如果打断标记已经是 true, 则 park 会失效
线程状态
NEW
线程刚被创建,但是还没有调用 start() 方法RUNNABLE
当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的 【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为 是可运行)BLOCKED , WAITING , TIMED_WAITING
都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节 详述TERMINATED
当线程代码运行结束
synchronized
临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
int counter = 0;
void increment() // 临界区
{
counter++;
}
void decrement() // 临界区
{
counter--;
}
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
阻塞式的解决方案:synchronized,,即俗称的【对象锁】
它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】
这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
常见的线程安全类
这里说它们是线程安全的是指:==它们的每个方法是原子的 ==
String
Integer
StringBuffer
Random
Vector
Hashtable java.util.concurrent 包下的类
但是,方法的组合未必线程安全,如下情况:
wait 、notify
必须在synchronize中使用
wait(long n)
有时限的等待, 到 n 毫秒后结束等待,或是被 notify
- sleep 在睡眠的同时,不会释放对象锁的,
- wait 在等待的时候会释放对象锁
常用方式
Park 、 Unpark
它们是 LockSupport 类中的方法
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(thread)
与Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是
以线程为单位
来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】 - park & unpark 可以先 unpark,而 wait & notify 不能先 notify
unpark
可以调用多次,不过效果和调用一次相同。
死锁-哲学家就餐问题
有五位哲学家,围坐在圆桌旁。
- 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
- 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
- 如果筷子被身边的人拿着,自己就得等待
哲学家-解决1:synchronize
同时锁住左边和右边的筷子,才能吃饭
示例,新建5支筷子和5个哲学家,那么总有一个时候会发生每个人都只锁住他们所需要的的第一个筷子的情况,那么出现死锁。
这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况
活锁
活锁出现在两个线程互相改变对方的结束条件,后谁也无法结束
比如,有一个数n=100
线程1循环执行 n++
线程2循环执行 n–
饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束。
解决:顺序加锁
哲学家-解决2:顺序加锁
回到哲学家问题,出现死锁的原因是每个人每个人都是从左边开始拿第一支筷子,形成闭环。
那么可以改变第5个哲学家的取筷子顺序,先拿1号筷子,再拿5号筷子,就可以解决死锁的问题了!
一号:1,2
二号:2,3
三号:3,4
四号:4,5
五号:1:5
不过这样可能会出现饥饿现象,在演示过程中,四号频繁获得锁,而五号几乎不获得锁(些许概率问题)
ReentrantLock
相对于 synchronized 它具备如下特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (默认不公平)
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
可重入 :可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁,如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
可打断
使用 lock.lockInterruptibly();
线程可以被thread.interrupted()
打断
如果lock.lock();
则thread.interrupted()
不起作用
可超时
lock.tryLock()
、尝试获取锁,获取失败,立即返回
lock.tryLock(1, TimeUnit.SECONDS)
、一秒内尝试获取锁,获取失败再返回
哲学家-解决3:tryLock()
公平锁
ReentrantLock 默认是不公平的
条件变量 await,signal
synchronized 中也有条件变量,就是我们讲原理时那个waitSet 休息室,当条件不满足时进入 waitSet 等待 ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 是那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitbreakfastQueue = lock.newCondition();
static volatile boolean hasCigrette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
while (!hasCigrette) {
try {
System.out.println( "等烟");
waitCigaretteQueue . await( ) ;
} catch (InterruptedException e) {
e. printStackTrace();
}
}
System.out.println( "等到了它的烟");
} finally {
lock . unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
System.out.println("等早餐");
waitbreakfastQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("等来了早餐");
} finally {
lock . unlock();
}
}).start();
//没有先后问题
sleep(1000);
sendBreakfast();
sendCigarette();
}
private static void sendCigarette() {
lock.lock();
try {
System.out.println( "送烟来了");
hasCigrette = true;
waitCigaretteQueue . signal();
} finally {
lock . unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try{
System.out.println( "送早餐来了");
hasBreakfast = true;
waitbreakfastQueue. signal();
} finally {
lock . unlock();
}
}
执行结果
交替输出多组abc
Object:wait()、notify()
Condition:await()、signal()
LockSupport:park()、unpark()