一、守护线程
- 守护线程是什么?
守护线程是一类比较特殊的线程,一般用于处理后台的工作,比如JDK的垃圾回收线程。
- 守护线程的作用?
JVM(Java Virtual Machine)java虚拟机在正常情况下退出如下:
那么JVM何时会退出呢?官方的说明:The java virtual machine exits when the only thread running are all daemon thread.
当运行的线程都是守护进程线程时,java虚拟机退出。如果还有其他的任意一个用户线程还在,JVM就不会退出。当主线程结束时,结束其余的子线程(守护线程)自动关闭,就免去了还要继续关闭子线程的麻烦。
由此知守护线程具备自动结束生命周期的特点,非守护线程不具备这样的特点。
例如:
创建一个匿名线程作为子线程,让它睡眠1ms,在主线程中睡眠1000ms,
public class test3 {
public static void main(String[] args) {
//匿名子线程创建,睡眠1ms
Thread thread = new Thread(){
@Override
public void run() {
while(true){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
//主线程睡眠1000ms
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread finished");
}
}
运行后发现打印了Main thread finished信息,但是JVM没有结束。因为thread是一个非守护线程,main线程正常运行结束之后,JVM进程并没有退出。
之后将子线程设置为守护线程,ps:守护线程 thread.setDaemon(true)必须在thread.start()之前设置,否则会报错IllegalThreadStateException异常
thread.setDaemon(true);
将thread设置为守护线程,main线程结束生命周期,JVM进程退出。
JVM正常结束。
二、并发与并行
- 并发:并发指的是多个线程操作同一个资源,不是同时执行,而是交替执行,单核cpu,只不过 cpu时间片很短,执行速度很快,看起来同时执行。
- 并行:并行才是真正的同时执行,多核cpu。每一个线程使用一个单独的cpu运行。
几个常用的概念:
-
QPS: 每秒能够响应的请求数
-
吞吐量: 单位时间内能够处理的请求数
-
平均响应时间: 系统对某一个请求的平均响应时间 QPS = 并发数/平均响应时间
-
并发用户数: 系统可以承载的最大用户数
三、线程生命周期
1.线程状态
-
new(新建状态): new关键字创建一个线程对象,它并不是处于一个执行状态,因为还没有start启动线程
-
Runnable(就绪状态) : 线程对象调用start方法,才是在JVM中真正创建了一个线程,线程一经启动并不会立即执行,该状态的所有线程位于就绪线程池中,等待操作系统的资源,如:处理器,获得cpu的使用权
-
Blocked(阻塞状态) : 等待一个监视器锁进入同步代码块或者同步方法,代码块/方法某一时刻只能够有一个线程执行,其他线程只能等待
-
Waiting(等待状态): Object.wait()/Thread.join()/LockSupport.park()都会使得线程阻塞,从Runnable转换到Waiting状态
-
Timed_Waiting(睡眠状态) : 调用加超时参数的Object.wait(long mills)/Thread.slepp(long mills)/LockSupport.parkNano()/LockSupport.parkUntil()
-
Terminated(终止状态) : 是一个线程的最终状态,线程进入到Terminated状态,意味着该线程的生命周期结束了,下面这些情况都会使得线程进入Terminated状态: 1)线程执行正常结束 2)线程运行出错意外结束 3)JVM crash
2.线程状态之间的转换
四、线程常用方法
1.start
启动一个线程,将线程添加一个线程组中,线程状态会从New状态转换到Runnable状态,然后获取Cpu之后进入Running状态执行run
2.sleep
sleep是一个静态方法,其中存在两个重载函数:
public static native void sleep(long millis)
public static void sleep(long millis, int nanos)
使得线程进入睡眠状态,暂停执行,sleep不会放弃monitor lock的所有权
jdk1.5之后,jdk引入一个枚举TimeUnit,其对sleep方法做了封装,直接使用从而时间单位换算的步骤,比如线程睡眠3h27min8sec88msec
如:
new Thread(()->{
try {
//Thread.sleep(5000);
TimeUnit.HOURS.sleep(3);
TimeUnit.MINUTES.sleep(27);
TimeUnit.SECONDS.sleep(8);
TimeUnit.MILLISECONDS.sleep(88);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
3.yeild
yield表示提醒cpu调度器我愿意放弃当前的cpu资源,(属于启发式方法),如果cpu资源不紧张的情况下,会忽略这种提示
yield和sleep的区别 :
- jdk1.5之前,yield调用了sleep
- sleep使得当前线程暂停执行,不会占用cpu资源 , yield只是对于cpu调度器的一个提示
- sleep会导致线程短暂的阻塞,yield会使得线程Running->Runnable
- sleep会捕获到中断异常,yield不会捕获到中断异常
4.join
在线程B中join某个线程A,会使得B线程进入等待,直到线程A结束生命周期,或者达到给定的时间, 在这期间线程B会处于等待状态
例如有A,B,C三个线程 使得A,B,C三个线程顺序打印0~9:
class threadTest extends Thread{
private Thread thread;
private String name;
public threadTest(String name, Thread thread) {
this.name = name;
this.thread = thread;
}
@Override
public void run() {
if(thread != null){
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for (int i = 0; i < 10; i++) {
System.out.println(name+":"+i);
}
}
}
public class test4 {
public static void main(String[] args) {
threadTest threadA = new threadTest("threadA:", null);
threadTest threadB = new threadTest("threadB:", threadA);
threadTest threadC = new threadTest("threadC:", threadB);
threadA.start();
threadB.start();
threadC.start();
}
}
5.wait,notify,notifyAll
wait :调用某个对象的wait()方法可以让当前线程阻塞
notify :调用当前对象notify才能够唤醒这个对象所在的线程 ,随机唤醒其中一个
notifyAll :唤醒所有线程,然后所有线程开始抢锁
注意:使用这三个方法需要让当前线程拥有当前对象的monitor lock
例如,在线程A中,先调用wait方法释放当前对象的monitor lock ,进入Waiting状态,即当前线程进入到阻塞状态,然后在线程B中利用notify进行唤醒。
public class test5 {
public static void main(String[] args) {
String str = new String("test str!");
new Thread("thread A"){
@Override
public void run() {
synchronized (str){
try {
str.wait();
System.out.println("the current thread is:" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("thread B"){
@Override
public void run() {
synchronized (str){
str.notify();
// str.notifyAll();
}
}
}.start();
}
}
6.线程中断方法
每个Java线程都会有一个中断状态位,程序可以检测这个中断状态位判读线程是否执行结束。
1)interrupt()
由线程对象调用,将中断位置置为true
如下方法能够使得当前线程进入阻塞状态,调用interrupt方法可以打断阻塞,因此这种方法被称之为可中断方法 :
- Object.wait()/wait(long)
- Thread.sleep(long)/TimUnit.XXX.sleep(long)
- Thread.join()/Thread.join(long)
2)isInterrupted()
判断当前线程的中断状态位是否为true
3)interrupted()
调用interrupted会擦除中断状态位的标识
使用方法:例如子线程睡眠10s,当发生异常时打印提示信息,主线程睡眠200ms,然后调用interrupt方法,此时子线程被中断,捕获到异常后打印提示信息,wait方法为可打断方法。
public static void main(String[] args) {
Thread thread = new Thread(){
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
System.out.println("I am interrupted!");
}
}
};
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
如果一个线程被interrupt,设置interrupt flag;如果当前线程正在执行可中断方法,调用 interrupt方法,反而导致interrupt flag被清除
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println("test interrupt");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
//e.printStackTrace();
System.out.println(Thread.currentThread().isInterrupted());
}
}
}
};
thread.start();
thread.interrupt(); //将thread的中断状态位置为true
System.out.println("Thread interrupt flag: "+thread.isInterrupted());
开始线程中断位置为true,在捕获到异常后,标志位被置为false
sleep方法会擦除中断状态位,如下,开始标志位为false,调用interrupt方法后应该为true,但是sleep会擦除状态位,会变成false
Thread thread = new Thread(){
@Override
public void run() {
while(true){
try {
TimeUnit.MILLISECONDS.sleep(1); //sleep会擦除中断状态位
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread is interrupted: "+thread.isInterrupted());
thread.interrupt();
System.out.println("Thread is interrupted: "+thread.isInterrupted());
interrupted调用时也会擦除中断标志位,持续监控标志位,将它设为守护线程,开始标志位为false,在调用interrupted后变成true,之后又恢复为false,其中发生了一次中断。
Thread thread = new Thread(){
@Override
public void run() {
while(true){
System.out.println(Thread.interrupted());
}
}
};
thread.setDaemon(true);
thread.start();
try {
TimeUnit.MILLISECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
五、并发编程
1.概念
并发编程是为了提高程序的执行速度,在宏观上使得多个任务同时执行,则需要启动多个线程 ,但事实启动多个线程之后,不管针对单核cpu还是多核cpu线程进行上下文切换(cpu 通过)给每一个线程分配时间片,只有说拿到时间片的线程才可以执行,通常时间片很短,所以才会感觉到多个线程在并行操作,存在线程不安全性。一个正确执行的并发程序,满足并发编程的三大特性,原子性、可见性、有序性。
1)原子性:
所谓原子性是指一次或者多次操作中,要么所有的操作全部执行要么所有的操作都不执行,原子操作是不可分割的操作,一个原子操作中间是不会被其他线程所打断的
例如:
- int a = 10; //1 10赋值给线程工作内存中的变量a 原子操作
- a++; //2 拿a 进行a+1 赋值a 非原子操作
- int b = a; //3 拿a b=a 非原子操作
- a = a+1; //4 拿a 进行a+1 赋值a 非原子操作
2)可见性:
如果在一个线程对共享变量做了修改,那么另外的线程立即可以看到修改后的最新值。
例如:两个线程A与B,A中持续判定localValue的值是否小于5,比较value与localValue是否相等,不等的话打印输出。线程B中获取到value的值,赋给localValue,若小于5,就打印输出并使localValue++,然后将localValue赋给value。之后睡眠线程切换为A进行打印输出。
private static int value = 0;
public static void main(String[] args) {
new Thread("A"){
@Override
public void run() {
int localValue = value;
while(localValue < 5){
if(localValue != value){
System.out.println("the value is updated to "+value);
localValue = value;
}
}
}
}.start();
new Thread("B"){
@Override
public void run() {
int localValue = value;
while(localValue < 5){
System.out.println("the value will be changed to "+(++localValue));
value = localValue;
//短暂睡眠,使得A线程进行输出
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
输出结果如下,A线程仅打印了一次,预想是发生一次更新,打印一次。造成这种结果的原因是线程A有一份地址空间,线程B有一份地址空间,他们的值发生更新后没有同步到主存中,也就是不可见。
3)有序性
有序性指的程序代码在执行过程中的先后顺序,由于编译器或计算机的优化,导致代码的执行未必是开发者编写代码时的顺序。
如:
int x = 1;
int y = 2;
x++;
计算机或编译器在执行过程中会进行优化,先执行x=1,然后进行x++操作,在不影响最终结果的情况下速度更快。
x++在int y=0之前得到执行,这种情况叫做指令重排序
在单线程环境中,无论怎么重排序,都不会影响最终的结果
在多线程环境中,如果有序性得不到保证,最终结果也可能与预期不符