一、创建线程的几种方式
二、查看进程的方法
三、线程运行原理–栈桢Debug
栈与栈帧
Java Virtual Machine Stacks (Java 虚拟机栈)
我们都知道 JVM 中由堆、栈、方法区所组成,其中栈内存是给谁用的呢?其实就是线程,每个线程启动后,虚拟机就会为其分配一块栈内存。
- 每个栈由多个栈帧(Frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
- 栈桢以线程为单位,相互之间是独立的
public class Test {
public static void main(String[] args) {
method1(10);
}
public static void method1(int x){
int y = x + 1;
Object o = method2();
System.out.println(o);
}
public static Object method2(){
Object o = new Object();
return o;
}
}
下图,走到断点时,产生了一个main栈桢,栈桢里面有一个局部变量:
方法走到下图标记的位置的时候,有两个栈桢,method1栈桢是新加入的,也有局部变量:
走到下图标记的位置时,添加了method2栈桢,有三个栈桢:
走到下图标记的时候,只有两个栈在桢了,因为走完method2,method2栈桢释放了,同时,也说明一个问题,栈是后进先出的:
debug到这里已经将问题说明白了,就不再继续了.
四、线程运行原理图解
4.1 类加载
加载字节码文件,将字节码文件加载到方法区的内存中,这里为了好理解,就没有写二进制的代码了,写的是java代码.
4.2 启动main线程
类加载完成后,JVM会启动main线程,并且分配一块栈内存给它。接下来这个线程就交给了任务调度器去调度执行,如果抢到CPU了,main方法是方法的执行入口,会给main方法分配一个栈桢内存.
栈内存中有局部变量表、返回地址、锁记录、操作数栈。main栈桢的局部变量表是args,返回地址是程序的退出地址。
程序计数器:记录下一次该执行什么命令,例如,记录了下一个执行的方法method1(10)
继续执行:
现在methd2方法被执行完了,需要释放掉内存:
然后method1执行结束释放内存,main执行完成,释放内存。
五、线程上下文切换(Thread Context Switch)
因为以下一些原因导致 cpu 不再执行当前的线程,转而执行另一个线程的代码
- 线程的 cpu 时间片用完
- 垃圾回收
- 有更高优先级的线程需要运行
- 线程自己调用了 sleep、yield、wait、join、park、synchronized、lock 等方法
当 Context Switch 发生时,需要由操作系统保存当前线程的状态,并恢复另一个线程的状态,Java 中对应的概念就是程序计数器(Program Counter Register),它的作用是记住下一条 jvm 指令的执行地址,是线程私有的
- 状态包括程序计数器、虚拟机栈中每个栈帧的信息,如局部变量、操作数栈、返回地址等
- Context Switch 频繁发生会影响性能
六、常用方法
6.1 run和start
- 直接调用 run 是在主线程中执行了 run,没有启动新的线程
- 使用 start 是启动新的线程,通过新的线程间接执行 run 中的代码
6.2 sleep和yield
sleep
- 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞)
- 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException
- 睡眠结束后的线程未必会立刻得到执行
- 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
yield
- 调用 yield 会让当前线程从 Running 进入 Runnable 就绪状态,然后调度执行其它线程
- 具体的实现依赖于操作系统的任务调度器
对比:
1.就绪状态,还是有机会被任务调度器调用的,但是阻塞状态,任务调度器是不会分配时间片给这种状态的线程的
2.sleep是有具体的等待时间可设置的,而yield几乎是没有等待时间。
sleep打断:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
System.out.println("enter sleep...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("wake up...");
e.printStackTrace();
}
}
};
t1.start();
Thread.sleep(1000);
System.out.println("interrupt...");
t1.interrupt();
}
当然,推荐使用这样的方式进行睡眠,代码可读性更好:
TimeUnit.SECONDS.sleep(2);
执行结果:
6.3 线程优先级
- 线程优先级会提示(hint)调度器优先调度该线程,但它仅仅是一个提示,调度器可以忽略它
- 如果 cpu 比较忙,那么优先级高的线程会获得更多的时间片,但 cpu 闲时,优先级几乎没作用
6.4 sleep方法的一个应用
while(true) {
try {
//Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
上面代码在单核CPU下运行,CPU会占用到100%,如果将注释的代码放开,即加上sleep方法,CPU只有3%左右。找一台单核的linux虚拟机,使用top命令查看。
6.5 join方法
join方法:等待线程结束,谁来调用这个方法,就等待谁的线程结束。
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
System.out.println("开始");
Thread t1 = new Thread(() -> {
System.out.println("开始");
TimeUnit.SECONDS.sleep(1);
System.out.println("结束");
r = 10;
});
t1.start();
System.out.println("结果为:" + r);
System.out.println("结束");
}
执行结果:
如果我们希望结果是10呢?
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
System.out.println("开始");
Thread t1 = new Thread(() -> {
System.out.println("开始");
TimeUnit.SECONDS.sleep(1);
System.out.println("结束");
r = 10;
});
t1.start();
t1.join();
System.out.println("结果为:" + r);
System.out.println("结束");
}
上面代码在start之后,添加了join方法,表示等t1线程结果返回,才能继续往下执行。体现了同步应用。
6.6 join同步应用
加入两个依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
在测试类上添加注解:
@Slf4j(topic = "c.Test")
static int r = 0;
static int r1 = 0;
static int r2 = 0;
private static void test2() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
Thread t2 = new Thread(() -> {
try {
sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
r2 = 20;
});
t1.start();
t2.start();
long start = System.currentTimeMillis();
log.debug("join begin");
t2.join();
log.debug("t2 join end");
t1.join();
log.debug("t1 join end");
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
执行结果:
如果将上面的两个join方法调用位置,执行结果还是3ms。
6.7 join限时同步
public static void test3() throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
r1 = 10;
});
long start = System.currentTimeMillis();
t1.start();
// 线程执行结束会导致 join 结束
log.debug("join begin");
t1.join(1000);
long end = System.currentTimeMillis();
log.debug("r1: {} r2: {} cost: {}", r1, r2, end - start);
}
执行结果:
只在1003ms结束,所以r1的值还是0。
如果将t1.join(3000);
打印的结果为:
r1的值已经是10了,耗时2000ms,说明join中的参数时间,如果大于线程的执行时间,就以线程执行完毕为准,如果小于线程执行时间,就以设置的时间为准,所以是限时同步。
6.8 interrupt打断阻塞
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
log.debug("打断标记:{}", t1.isInterrupted());
}
执行结果:
如果在sleep时被打断,被标记为true,但是sleep方法会清除标记,导致标记为false。
视频中说打断标记为false,但是这里的结果是true,此处存疑!
解惑:
观察上面结果,打断标记的输出,在异常抛出之前就输出了
调试过程:
首先,我在catch块中加入了System.out.println(Thread.currentThread().isInterrupted());
发现打印的结果是false,说明打断标记确实为false,再结合上面输出结果,发现:打印语句其实在catch代码块执行之前执行了。所以,我们如果想要看到正确的结果,需要在打印语句之后休眠一段时间,完整代码如下:
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("sleep...");
try {
Thread.sleep(5000); // wait, join
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println(Thread.currentThread().isInterrupted());
}
},"t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
Thread.sleep(1000);
log.debug("打断标记:{}", t1.isInterrupted());
}
执行结果如下:
6.9 interrupt打断正常运行的线程
Thread t1 = new Thread(() -> {
while(true) {
boolean interrupted = Thread.currentThread().isInterrupted();
if(interrupted) {
log.debug("被打断了, 退出循环");
break;
}
}
}, "t1");
t1.start();
Thread.sleep(1000);
log.debug("interrupt");
t1.interrupt();
执行结果:
使用这种方式可以优雅的终止一个线程,并不是立刻将线程杀死,而是给了线程一个料理后事的机会。
6.10 线程设计模式之两段终止模式
在一个线程 T1 中如何“优雅”终止线程 T2?这里的【优雅】指的是给 T2 一个料理后事的机会。
错误思路
使用线程对象的 stop() 方法停止线程
- stop 方法会真正杀死线程,如果这时线程锁住了共享资源,那么当它被杀死后就再也没有机会释放锁,其它线程将永远无法获取锁
- 使用 System.exit(int) 方法停止线程目的仅是停止一个线程,但这种做法会让整个程序都停止
两阶段终止模式
@Slf4j(topic = "c.Test")
public class Test {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination tpt = new TwoPhaseTermination();
tpt.start();
Thread.sleep(3500);
tpt.stop();
}
}
@Slf4j(topic = "c.Test")
class TwoPhaseTermination {
// 监控线程
private Thread monitorThread;
// 停止标记
private volatile boolean stop = false;
// 判断是否执行过 start 方法
private boolean starting = false;
// 启动监控线程
public void start() {
synchronized (this) {
if (starting) { // false
return;
}
starting = true;
}
monitorThread = new Thread(() -> {
while (true) {
Thread current = Thread.currentThread();
// 是否被打断
if (current.isInterrupted()) {
log.debug("料理后事");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
current.interrupt();//再次调用,将false变为true,执行下一次循环时,发现标记是true,执行料理后事的代码
}
}
}, "monitor");
monitorThread.start();
}
// 停止监控线程
public void stop() {
stop = true;
monitorThread.interrupt();
System.out.println(Thread.currentThread().isInterrupted());
}
}
执行结果:
如果在sleep时被打断,被标记为true,但是sleep方法会清除标记,导致标记为false,会抛出异常,进入catch代码,执行catch代码后,标记会记为true。
如果在执行监控记录时被打断,不会抛出代码,打断标记被记为true。
6.11 静态的Thread.interrupted()
- Thread.interrupted();也是判断线程是否被打断,但是它会清除打断标记
- isInterrupted方法判断线程是否被打断,但是它不会清除打断标记
6.12 interrupt打断park
打断标记为true的情况下,park会失效。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
log.debug("park...");
LockSupport.park();
log.debug("unpark...");
log.debug("打断状态:{}", Thread.currentThread().isInterrupted());
}, "t1");
t1.start();
sleep(1);
t1.interrupt();
}
执行结果:
上面的结果可见:输出park后,由于调用了park方法,暂停了1s,后来执行了t1.interrupt();
打断标记变为true,导致了park失效,继续执行后面的代码。
当然,如果再是打断标记为false,park方法立即会生效。
例如将Thread.currentThread().isInterrupted()
变为Thread.currentThread().interrupt()
6.13 过时的方法
- stop() 停止线程运行
- suspend() 挂起(暂停)线程运行
- resume() 恢复线程运行
6.14 守护线程
只要有一个线程运行,整个JAVA进程都不会结束
有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束。
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while (true) {
if (Thread.currentThread().isInterrupted()) {
break;
}
}
log.debug("结束");
}, "t1");
t1.setDaemon(true);
t1.start();
Thread.sleep(1000);
log.debug("结束");
}
结果:
主线程结束了,即使t1线程没有执行完,也会被结束。
- 垃圾回收器线程就是一种守护线程,如果程序停止了,垃圾回收线程也会被强制停止
- Tomcat 中的 Acceptor 和 Poller 线程都是守护线程,所以 Tomcat 接收到 shutdown 命令后,不会等待它们处理完当前请求
6.15 线程的五种状态
这是从 操作系统 层面来描述的
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【可运行状态】(就绪状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】。与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑
调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
6.16 六种状态
这是从 Java API 层面来描述的
根据 Thread.State 枚举,分为六种状态
- NEW 线程刚被创建,但是还没有调用 start() 方法,五种状态的划分是重叠的。
- RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的
【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
- BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分,后面会在状态转换一节详述
- TERMINATED 当线程代码运行结束
6.17 六种状态的演示
public static void main(String[] args) throws IOException {
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable 既有可能分到时间片,又可能没有分到,都是runable状态
}
}
};
t2.start();
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (Test.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting 等待t2线程执行完成
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (Test.class) { // blocked 由于t4线程获得了锁,没有释放,导致t6一直获取不到锁
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
执行结果:
6.18 临界区与竞态条件
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
例如,下面代码中的临界区
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。
七、线程安全问题分析
使用全局变量list:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadUnsafe test = new ThreadUnsafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
method2();
method3();
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
执行结果:
使用局部变量list:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafe test = new ThreadSafe();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
这个是线程安全的,没有报错。
下面这个例子同样是使用局部变量,但是method方法是public的,被继承重写了:
public class Test {
static final int THREAD_NUMBER = 2;
static final int LOOP_NUMBER = 200;
public static void main(String[] args) {
ThreadSafeSubClass test = new ThreadSafeSubClass();
for (int i = 0; i < THREAD_NUMBER; i++) {
new Thread(() -> {
test.method1(LOOP_NUMBER);
}, "Thread" + (i+1)).start();
}
}
}
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
public void method2(ArrayList<String> list) {
list.add("1");
}
public void method3(ArrayList<String> list) {
System.out.println(1);
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
System.out.println(2);
new Thread(() -> {
list.remove(0);
}).start();
}
}
执行结果:
因为子类重新开启了一个线程,和之前的线程共享list,导致了线程安全问题,所以最好就是将method3方法变成私有的,不让子类重写。
常见的线程安全类:
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
案例分析:
public class MyServlet extends HttpServlet {
// 是否安全?
Map<String,Object> map = new HashMap<>(); //no
// 是否安全?
String S1 = "..."; //yes
// 是否安全?
final String S2 = "..."; //yes
// 是否安全?
Date D1 = new Date(); //no
// 是否安全?
final Date D2 = new Date(); //no
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
servlet是运行在tomcat上的一个实例,是单实例的,被tomcat多个线程共享使用。
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl(); //no
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0; //no
public void update() {
// ...
count++;
}
}
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L; //no,MyAspect单例,多个线程可能共享这个变量
@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(); //yes,不可变,没有提供修改
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();//yes,虽然是成员变量,但是没提供修改
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("","","")){ //yes
// ...
} 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();
}
}
其中 foo 的行为是不确定的,可能导致不安全的发生,被称之为外星方法
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();
}
}
上例泄露引用。