1:同步与异步 16:park()与unpark()
2:start()和run() 17:park(),unpark()&wait(),notify()
3:sleep() 与 yield() 18:死锁
4:线程优先级 19:Lock接口
5:join()
6:join(毫秒数)
7:interrupt() 方法
8:提前结束线程
9:守护线程
10:临界区与竞态条件(如何解决)
11:synchronized
12:线程8锁
13:线程安全性分析
14:wait()和notify(),notifyAll()
15:sleep()与wait()方法对比
- 需要等待结果返回,才能继续运行就是同步
- 不需要等待结果返回,就能继续运行就是异步
区别:
- 调用start()开启一个新的线程,调用run()方法就是普通方法的调用,不会创建新线程。
- 调用start()方法,会执行run()方法中的代码。
- start()方法不能多次调用,多次调用会出现IllegalThreadsStateException异常。
sleep():让线程进入睡眠
-
调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)。
-
其它线程可以使用 interrupt() 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException。
-
睡眠结束后的线程未必会立刻得到执行,会先进入就绪状态等待cpu分配时间片。
-
建议用 TimeUnit(jdk1.5之后) 的 sleep 代替 Thread 的 sleep 来获得更好的可读性。
//都是睡眠1秒 TimeUnit.SECONDS.sleep(1); Thread.sleep(1000);
yield():让运行状态线程进入就绪
- 调用 Thread.yield() 会让当前线程放弃当前时间片,从 Running(运行状态) 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
二者区别:
- sleep():会释放当前线程占有的时间片,直到睡眠时间结束或被interrupt() 打断后,cpu才会重新为其分配时间片等待cpu重新调用。
- yield():同样会让出时间片,等待cpu重新为其分配时间片,cpu重新调度,只不过没有睡眠时间。
线程优先级后会提示任务调度器去优先调度该线程,但仅仅只是提示,最终的任务调度情况还得由调度器决定,比如cpu比较繁忙时,优先级高的线程会获得更多的时间片。而cpu空闲时,优先级甚至会被忽略进而导致不起作用。
设置优先级:
t1.setPriority(n);
//n[ 1 , 10 ]
//1:最小优先级
//5:默认优先级
//10:最大优先级
join():让当前线程等待另一个线程执行结束后再继续执行当前线程
案例:
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("结束");
r = 10;
});
t1.start();
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
当前案例执行结果为 **r=0** 而不是 **r=10**
分析:
因为主线程和线程 t1 是并行执行的,t1 线程需要 1 秒之后才能算出 r=10
而主线程一开始就要打印 r 的结果,所以只能打印出 r=0。
解决方案:
想要让最终结果为r=10,就需要让主线程等待t1线程执行完毕后,主线程再去打印最终结果(异步变为同步)。即调用join() 方法。
解释: 当前线程等待另一个线程执行,与join() 这个无参数的方法不同,join()方法会让当前线程等待另一个线程结束。而join(500) 表示当前线程最多等待另一个线程500毫秒,如果在5秒没另一个线程没有执行完毕,将不再等待,继续执行当前线程。
static int r1 = 0;
public static void main(String[] args) throws InterruptedException {
test3();
}
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
t1.join(500);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, end - start);
}
当前案例执行结果为 **r=0** 而不是 **r=10**
-
打断sleep,wait,join 的线程 打断标记为false,并抛出异常
Thread t1 = new Thread(() -> { try TimeUnit.SECONDS.sleep(1);//睡眠1000毫秒 } catch (InterruptedException e) { e.printStackTrace(); } }, "t1"); t1.start(); Thread.sleep(500);//让主线程睡眠500毫秒,防止后面代码先被执行 t1.interrupt(); log.debug(" 打断状态: {}", t1.isInterrupted());//打印打断标记
结果:打断标记为false
-
打断正常运行的线程 打断标记为true,不会抛出异常
private static void f() throws InterruptedException { Thread t2 = new Thread(() -> { while (true) { Thread current = Thread.currentThread();//返回线程执行状态信息 boolean interrupted = current.isInterrupted();//返回线程打断标记 if (interrupted) { log.debug(" 打断状态: {}", interrupted); break; } } }, "t2"); t2.start(); Thread.sleep(500); t2.interrupt(); }
结果:打断标记为true
- 方式1:stop()方法,由于种种原因已被废弃
- 方式2:调用System.exit(int)方法,但是这个方法会停止整个进程,小题大做。
- 方式3:调用interrupt()方法,通过两阶段终止模式进行终止。
两阶段终止模式:
代码实现:
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test6")
public class test6 {
public static void main(String[] args) throws InterruptedException {
Moniter m = new Moniter();
m.startOK();
Thread.sleep(5000);
m.stopOK();
}
}
//监控类
@Slf4j(topic = "c.Moniter")
class Moniter {
private Thread moniter;
//启动监控线程
public void startOK() {
moniter = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();//获取当前线程信息
if (current.isInterrupted()) { //判断是否被打断
System.out.println("料理后事");//如果被打断
break;
}
try {//如果没有被打断,进行监控操作
Thread.sleep(2000);// 如果这里被打断,打断标记会变为false,同时执行catch代码块中代码
// (至于为什么会走catch代码块,请到文章开头,点击->interrupt() 方法)
log.debug("执行监控记录");// 如果这里被打断,打断标记会变为true
} catch (InterruptedException e) {
e.printStackTrace();
current.interrupt();//重新设置打断标记
}
}
});
moniter.start();
}
//停止当前线程
public void stopOK() {
moniter.interrupt();
}
}
结果:
补充细节:
- isInterrupted():返回打断标记。
- interrupt():返回打断标记,如果打断标记为true,将打断标记改为false。
默认情况下,Java 进程需要等待所有线程都运行结束,才会结束。有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
log.debug("开始运行...");
Thread t1 = new Thread(() -> {
log.debug("开始运行...");
sleep(2);
log.debug("运行结束...");
}, "daemon");
// 设置该线程为守护线程
t1.setDaemon(true);
t1.start();
sleep(1);
log.debug("运行结束...");
注意:
- 垃圾回收器线程就是一种守护线程。
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求。
-
临界区: 一段代码块内如果存在多个线程对共享资源的读写操作,称这段代码块为临界区
让线程t1加a 5000次,线程t2减a 5000次(下面的代码结果不为0) public class test6 { private static int a = 0; public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(() -> { for (int i = 0; i < 5000; i++) a++; }, "t1"); Thread t2 = new Thread(() -> { for (int i = 0; i < 5000; i++) a--; }, "t1"); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(a); }
}
```
- 竞态条件: 多个线程在临界区内执行,此时出现线程的上下文切换,称之为发生了竞态条件
synchronized 解决方案
- 阻塞式的解决方案:synchronized,Lock
(俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换)
语法
synchronized(对象)
{
临界区
}
正确代码:
public class test6 {
private static int a = 0;
private static Object lockOK = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
synchronized (lockOK) {
a++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++)
synchronized (lockOK) {
a--;
}
}, "t1");
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(a);
}
}
}
- 非阻塞式的解决方案:原子变量
synchronized 跳转链接
- 加在成员方法上
public class Test6 {
//synchronized 锁的是调用当前方法的对象
public synchronized void test() {
}
}
等价于
public class Test6 {
public void test() {
synchronized (this) {
}
}
}
- 加在静态方法上:
public class Test6 {
//synchronized 锁的是当前类对象:Test6.class
public synchronized static void test() {
}
}
等价于
public class Test6 {
public static void test() {
synchronized (Test6.class) {
}
}
}
-
成员变量和静态变量
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
-
局部变量
- 局部变量是线程安全的
public static void test1() { int i = 10; i++; }
(每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享)
- 但局部变量引用的对象则未必 点击观看尚硅谷视频
- 如果该对象没有逃离方法的作用范围,它是线程安全的
- 如果该对象逃离方法的作用范围,需要考虑线程安全
- obj.wait() 让当前线程释放锁进入 object 监视器的线程到 waitSet(等待区) 等待
- obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒
- obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒
- obj.wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify
- 它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
}).start();
// 主线程两秒后执行
sleep(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
- sleep() 是Thread中的方法,wait() 是Object中的方法。
- sleep() 不需要强制和 synchronized 配合使用,但 wait() 需要和 synchronized 一起用。
- sleep() 在睡眠的同时,不会释放对象锁,但 wait() 在等待的时候会释放对象锁。
LockSupport.park(); // 暂停当前线程
LockSupport.unpark(暂停线程对象); // 恢复某个线程的运行
代码案例:
案例1:线park在unpark
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
sleep(2);
log.debug("unpark...");
LockSupport.unpark(t1);
结果:
案例2:先 unpark 再 park
Thread t1 = new Thread(() -> {
log.debug("start...");
sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
结果:
实现线程有序执行
park(),unpark()&wait(),notify()
- wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必.
- park & unpark 是以线程为单位,可以精确的【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么【精确】。
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify。
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁
例:
t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 。
t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁。
代码:(多次运行后总有一次发生死锁情况)
public class Test {
private static final X x = new X();
private static final Y y = new Y();
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
synchronized (x) {
System.out.println("获得X锁");
synchronized (y) {
System.out.println("获得Y锁");
}
}
},"线程1").start();
new Thread(() -> {
synchronized (y) {
System.out.println("获得X锁");
synchronized (x) {
System.out.println("获得Y锁");
}
}
},"线程2").start();
}
}
class X {
}
class Y {
}
定位死锁
- 第一步: 终端输入
jps
查看所有java进程
- 第二步: 终端输入
jstack 进程号
查看当前java进程的具体信息
Lock与synchronized的区别:
- Lock不是Java语言中内置的,synchronized是Java一样中的关键字。
- 采用synchronized不需要用户去手动释放锁,当synchronized方法或者synchronized代码块执行完之后,系统会自动让线程释放锁。
- 采用Lock则必须要用户去手动释放锁,如果没有手动释放锁,会有死锁的可能。
- 通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
- 在性能上来说,如果竞争资源不激烈,两者的性能差不多的,而当资源竞争非常激烈时(有大量线程同时竞争时),此时Lock的性能要远优于synchronized,所以Lock提高多个线程进行读操作的效率。