目录
- 一、Java多线程
- 1、进程与线程
- 2、并行与并发
- 二、线程的礼让
- 三、线程的优先级
- 四、守护线程
- 五、线程的阻塞
- 六、线程的打断
- 七、线程的相关方法总结
- 同步锁
- 线程安全
- synchronized
- 线程通信
- wait+notify
一、Java多线程
1、进程与线程
进程
- 当一个程序被运行,就开启了一个进程, 比如启动了qq,word
- 程序由指令和数据组成,指令要运行,数据要加载,指令被cpu加载运行,数据被加载到内存,指令运行时可由cpu调度硬盘、网络等设备
线程
- 一个进程内可分为多个线程
- 一个线程就是一个指令流,cpu调度的最小单位,由cpu一条一条执行指令
2、并行与并发
并发:单核cpu运行多线程时,时间片进行很快的切换。线程轮流执行cpu
并行:多核cpu运行 多线程时,真正的在同一时刻运行
二、线程的礼让
yield()方法会让运行中的线程切换到就绪状态,重新争抢cpu的时间片,争抢时是否获取到时间片看cpu的分配。
示例代码:
// 方法的定义
public static native void yield();Runnable r1 = () -> {int count = 0;for (;;){log.info("---- 1>" + count++);}
};
Runnable r2 = () -> {int count = 0;for (;;){Thread.yield(); //礼让log.info(" ---- 2>" + count++);}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.start();
t2.start();
运行结果:
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129504
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129505
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129506
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129507
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129508
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129509
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129510
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129511
11:49:15.796 [t1] INFO thread.TestYield - ---- 1>129512
11:49:15.798 [t2] INFO thread.TestYield - ---- 2>293
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129513
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129514
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129515
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129516
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129517
11:49:15.798 [t1] INFO thread.TestYield - ---- 1>129518
如上结果所示,t2线程每次执行时进行了yield(),线程1执行的机会明显比线程2要多。
三、线程的优先级
线程内部用1~10的数来调整线程的优先级,默认的线程优先级为NORM_PRIORITY:5
cpu比较忙时,优先级高的线程获取更多的时间片
cpu比较闲时,优先级设置基本没用
public final static int MIN_PRIORITY = 1;public final static int NORM_PRIORITY = 5;public final static int MAX_PRIORITY = 10;// 方法的定义public final void setPriority(int newPriority) {}
cpu比较忙时
Runnable r1 = () -> {int count = 0;for (;;){log.info("---- 1>" + count++);}
};
Runnable r2 = () -> {int count = 0;for (;;){log.info(" ---- 2>" + count++);}
};
Thread t1 = new Thread(r1,"t1");
Thread t2 = new Thread(r2,"t2");
t1.setPriority(Thread.NORM_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();// 可能的运行结果
11:59:00.696 [t1] INFO thread.TestYieldPriority - ---- 1>44102
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135903
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135904
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135905
11:59:00.696 [t2] INFO thread.TestYieldPriority - ---- 2>135906
四、守护线程
默认情况下,java进程需要等待所有线程都运行结束,才会结束,有一种特殊线程叫守护线程,当所有的非守护线程都结束后,即使它没有执行完,也会强制结束。
默认的线程都是非守护线程。
垃圾回收线程就是典型的守护线程
// 方法的定义
public final void setDaemon(boolean on) {
}Thread thread = new Thread(() -> {while (true) {}
});
// 具体的api。设为true表示未守护线程,当主线程结束后,守护线程也结束。
// 默认是false,当主线程结束后,thread继续运行,程序不停止
thread.setDaemon(true);
thread.start();
log.info("结束");
五、线程的阻塞
线程的阻塞可以分为好多种,从操作系统层面和java层面阻塞的定义可能不同,但是广义上使得线程阻塞的方式有下面几种:
- BIO阻塞,即使用了阻塞式的io流
- sleep(long time) 让线程休眠进入阻塞状态
- a.join() 调用该方法的线程进入阻塞,等待a线程执行完恢复运行
- sychronized或ReentrantLock 造成线程未获得锁进入阻塞状态 (同步锁章节细说)
- 获得锁之后调用wait()方法 也会让线程进入阻塞状态 (同步锁章节细说)
- LockSupport.park() 让线程进入阻塞状态 (同步锁章节细说)
六、线程的打断
// 相关方法的定义
public void interrupt() {
}
public boolean isInterrupted() {
}
public static boolean interrupted() {
}
打断标记:线程是否被打断,true表示被打断了,false表示没有
isInterrupted() 获取线程的打断标记 ,调用后不会修改线程的打断标记
interrupt()方法用于中断线程:
- 可以打断sleep,wait,join等显式的抛出InterruptedException方法的线程,但是打断后,线程的打断标记还是false
- 打断正常线程 ,线程不会真正被中断,但是线程的打断标记为true
interrupted() 获取线程的打断标记,调用后清空打断标记 即如果获取为true 调用后打断标记为false (不常用)
interrupt实例: 有个后台监控线程不停的监控,当外界打断它时,就结束运行。代码如下
@Slf4j
class TwoPhaseTerminal{// 监控线程private Thread monitor;public void start(){monitor = new Thread(() ->{// 不停的监控while (true){Thread thread = Thread.currentThread();// 判断当前线程是否被打断if (thread.isInterrupted()){log.info("当前线程被打断,结束运行");break;}try {Thread.sleep(1000);// 监控逻辑中被打断后,打断标记为truelog.info("监控");} catch (InterruptedException e) {// 睡眠时被打断时抛出异常 在该处捕获到 此时打断标记还是false// 在调用一次中断 使得中断标记为truethread.interrupt();}}});monitor.start();}public void stop(){monitor.interrupt();}
}
七、线程的相关方法总结
主要总结Thread类中的核心方法
方法名称 | 是否static | 方法说明 |
---|---|---|
start() | 否 | 让线程启动,进入就绪状态,等待cpu分配时间片 |
run() | 否 | 重写Runnable接口的方法,线程获取到cpu时间片时执行的具体逻辑 |
yield() | 是 | 线程的礼让,使得获取到cpu时间片的线程进入就绪状态,重新争抢时间片 |
sleep(time) | 是 | 线程休眠固定时间,进入阻塞状态,休眠时间完成后重新争抢时间片,休眠可被打断 |
join()/join(time) | 否 | 调用线程对象的join方法,调用者线程进入阻塞,等待线程对象执行完或者到达指定时间才恢复,重新争抢时间片 |
isInterrupted() | 否 | 获取线程的打断标记,true:被打断,false:没有被打断。调用后不会修改打断标记 |
interrupt() | 否 | 打断线程,抛出InterruptedException异常的方法均可被打断,但是打断后不会修改打断标记,正常执行的线程被打断后会修改打断标记 |
interrupted() | 否 | 获取线程的打断标记。调用后会清空打断标记 |
stop() | 否 | 停止线程运行 不推荐 |
suspend() | 否 | 挂起线程 不推荐 |
resume() | 否 | 恢复线程运行 不推荐 |
currentThread() | 是 | 获取当前线程 |
Object中与线程相关方法
方法名称 | 方法说明 |
---|---|
wait()/wait(long timeout) | 获取到锁的线程进入阻塞状态 |
notify() | 随机唤醒被wait()的一个线程 |
notifyAll(); | 唤醒被wait()的所有线程,重新争抢时间片 |
同步锁
线程安全
- 一个程序运行多个线程本身是没有问题的
-
问题有可能出现在多个线程访问共享资源
- 多个线程都是读共享资源也是没有问题的
- 当多个线程读写共享资源时,如果发生指令交错,就会出现问题
临界区: 一段代码如果对共享资源的多线程读写操作,这段代码就被称为临界区。
注意的是 指令交错指的是 java代码在解析成字节码文件时,java代码的一行代码在字节码中可能有多行,在线程上下文切换时就有可能交错。
线程安全指的是多线程调用同一个对象的临界区的方法时,对象的属性值一定不会发生错误,这就是保证了线程安全。
如下面不安全的代码:
// 对象的成员变量
private static int count = 0;public static void main(String[] args) throws InterruptedException {// t1线程对变量+5000次Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count++;}});// t2线程对变量-5000次Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {count--;}});t1.start();t2.start();// 让t1 t2都执行完t1.join();t2.join();System.out.println(count);
}// 运行结果
-1399
上面的代码 两个线程,一个+5000次,一个-5000次,如果线程安全,count的值应该还是0。
但是运行很多次,每次的结果不同,且都不是0,所以是线程不安全的。
线程安全的类一定所有的操作都线程安全吗?
开发中经常会说到一些线程安全的类,如ConcurrentHashMap,线程安全指的是类里每一个独立的方法是线程安全的,但是方法的组合就不一定是线程安全的。
成员变量和静态变量是否线程安全?
- 如果没有多线程共享,则线程安全
-
如果存在多线程共享
- 多线程只有读操作,则线程安全
- 多线程存在写操作,写操作的代码又是临界区,则线程不安全
局部变量是否线程安全?
- 局部变量是线程安全的
-
局部变量引用的对象未必是线程安全的
- 如果该对象没有逃离该方法的作用范围,则线程安全
- 如果该对象逃离了该方法的作用范围,比如:方法的返回值,需要考虑线程安全
synchronized
同步锁也叫对象锁,是锁在对象上的,不同的对象就是不同的锁。
该关键字是用于保证线程安全的,是阻塞式的解决方案。
让同一个时刻最多只有一个线程能持有对象锁,其他线程在想获取这个对象锁就会被阻塞,不用担心上下文切换的问题。
注意: 不要理解为一个线程加了锁 ,进入 synchronized代码块中就会一直执行下去。如果时间片切换了,也会执行其他线程,再切换回来会紧接着执行,只是不会执行到有竞争锁的资源,因为当前线程还未释放锁。
当一个线程执行完synchronized的代码块后 会唤醒正在等待的线程
synchronized实际上使用对象锁保证临界区的原子性 临界区的代码是不可分割的 不会因为线程切换所打断
基本使用:
// 加在方法上 实际是对this对象加锁
private synchronized void a() {
}// 同步代码块,锁对象可以是任意的,加在this上 和a()方法作用相同
private void b(){synchronized (this){}
}// 加在静态方法上 实际是对类对象加锁
private synchronized static void c() {}// 同步代码块 实际是对类对象加锁 和c()方法作用相同
private void d(){synchronized (TestSynchronized.class){}
}// 上述b方法对应的字节码源码 其中monitorenter就是加锁的地方0 aload_01 dup2 astore_13 monitorenter4 aload_15 monitorexit6 goto 14 (+8)9 astore_2
10 aload_1
11 monitorexit
12 aload_2
13 athrow
14 return
线程安全的代码:
private static int count = 0;private static Object lock = new Object();private static Object lock2 = new Object();// t1线程和t2对象都是对同一对象加锁。保证了线程安全。此段代码无论执行多少次,结果都是0
public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (lock) {count++;}}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (lock) {count--;}}});t1.start();t2.start();// 让t1 t2都执行完t1.join();t2.join();System.out.println(count);
}
重点:加锁是加在对象上,一定要保证是同一对象,加锁才能生效
线程通信
wait+notify
线程间通信可以通过共享变量+wait()¬ify()来实现
wait()将线程进入阻塞状态,notify()将线程唤醒
当多线程竞争访问对象的同步方法时,锁对象会关联一个底层的Monitor对象(重量级锁的实现)
如下图所示 Thread0,1先竞争到锁执行了代码后,2,3,4,5线程同时来执行临界区的代码,开始竞争锁。
- Thread-0先获取到对象的锁,关联到monitor的owner,同步代码块内调用了锁对象的wait()方法,调用后会进入waitSet等待,Thread-1同样如此,此时Thread-0的状态为Waitting
- Thread2、3、4、5同时竞争,2获取到锁后,关联了monitor的owner,3、4、5只能进入EntryList中等待,此时2线程状态为 Runnable,3、4、5状态为Blocked
- 2执行后,唤醒entryList中的线程,3、4、5进行竞争锁,获取到的线程即会关联monitor的owner
- 3、4、5线程在执行过程中,调用了锁对象的notify()或notifyAll()时,会唤醒waitSet的线程,唤醒的线程进入entryList等待重新竞争锁
注意:
- Blocked状态和Waitting状态都是阻塞状态
- Blocked线程会在owner线程释放锁时唤醒
- wait和notify使用场景是必须要有同步,且必须获得对象的锁才能调用,使用锁对象去调用,否则会抛异常
- wait() 释放锁 进入 waitSet 可传入时间,如果指定时间内未被唤醒 则自动唤醒
- notify()随机唤醒一个waitSet里的线程
- notifyAll()唤醒waitSet中所有的线程
static final Object lock = new Object();
new Thread(() -> {synchronized (lock) {log.info("开始执行");try {// 同步代码内部才能调用lock.wait();} catch (InterruptedException e) {e.printStackTrace();}log.info("继续执行核心逻辑");}
}, "t1").start();new Thread(() -> {synchronized (lock) {log.info("开始执行");try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}log.info("继续执行核心逻辑");}
}, "t2").start();try {Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
}
log.info("开始唤醒");synchronized (lock) {// 同步代码内部才能调用lock.notifyAll();
}
// 执行结果
14:29:47.138 [t1] INFO TestWaitNotify - 开始执行
14:29:47.141 [t2] INFO TestWaitNotify - 开始执行
14:29:49.136 [main] INFO TestWaitNotify - 开始唤醒
14:29:49.136 [t2] INFO TestWaitNotify - 继续执行核心逻辑
14:29:49.136 [t1] INFO TestWaitNotify - 继续执行核心逻辑
本文转自:万字图解Java多线程 - 个人文章 - SegmentFault 思否