线程的状态 新建(New)状态:当一个线程对象被创建,但还未调用 start () 方法启动时,处于新建状态。此时线程仅仅是一个 Java 对象,系统尚未为其分配资源。 就绪(Runnable)状态:一旦调用了线程的 start () 方法,线程就进入就绪状态它等待着系统分配资源和调度,以便能够在 CPU 上运行,或者说正在CPU上运行的也可以叫做就绪状态 等待状态(Waiting):线程可以通过调用wait () 方法或者 Thread.join () 方法进入等待状态。与阻塞状态不同的是,处于等待状态的线程需要被其他线程通过 notify () 或 notifyAll () 方法唤醒,或者等待特定的时间后自动唤醒。 超时等待状态(Timed Waiting)线程可以通过调用 sleep (long millis) 方法,wait (long timeout) 方法或者 join方法的带参数版本进入超时等待状态。在这种状态下,线程会等待一段时间,如果在这段时间内没有被唤醒,它会自动唤醒并进入就绪状态。 阻塞状态(Blocked) 线程在运行过程中可能会因为某些原因进入阻塞状态。常见的阻塞情况有:
- 等待获取锁:当一个线程试图进入一个同步代码块,但该代码块被其他线程占用时,它会进入阻塞状态,等待获取锁。
- 等待 IO 操作完成:当线程进行输入 / 输出操作,如读取文件或从网络接收数据,而这些操作尚未完成时,线程会进入阻塞状态。
- 调用 Object.wait () 方法:当一个线程在对象上调用 wait () 方法时,它会进入阻塞状态,等待另一个线程调用该对象的 notify () 或 notifyAll () 方法来唤醒它。 终止状态(Terminated) 当线程的 run () 方法执行完毕,或者在执行过程中出现异常而退出时,线程进入终止状态,此时虽然Thread对象还在,但是内核的线程已经销毁,一旦线程进入终止状态,就不能再被启动。 public class ThreadDemo9 { public static void main(String[] args) throws InterruptedException { Thread mainThread = Thread.currentThread(); Thread thread = new Thread(()->{ while (true){ System.out.println("mainThread: " + mainThread.getState()); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); System.out.println("thread start前: " + thread.getState()); thread.start(); System.out.println("thread start后:" + thread.getState()); thread.join(); } }
用jconsole可以直接看到线程的状态:
线程安全问题 先来看一个示例: public class ThreadDemo10 { public static int cnt = 0;
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
cnt++;
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
cnt++;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(cnt);
}
} 我们的目的是通过两个线程同时对cnt进行自增的操作,正常的情况下最终的输出应该是20000的,但是每一次的运行都是一个比20000小的数字,这就是线程安全问题
先来分析一下,对于cnt++这样的操作,在CPU中其实是分为三个命令的:
- 把内存中的数据取出来,读取到CPU寄存器中
- 把CPU寄存器里的数据+1
- 把寄存器里的值写回内存中 之后,由于CPU在调度进程的时候是“抢占式执行,随机调度”,指令是CPU运行的最小单位,一个指令执行完毕之后才会调度,但是由于上述操作占了三个指令,就可能在中间过程中被其他线程抢走,而上面是两个进程同时对cnt进行操作的,所以就会导致数据异常,例如,线程a刚把数据读出来,线程b就抢走了,并执行提交了数据,此时线程a再执行操作之后,读取的数据还是原来的,并不是线程b修改之后的,cnt就比预期的少加了1,这只是其中一种情况 原因就是:
- 线程在操作系统中,随机调度,抢占执行(根本原因)
- 多个线程同时修改同一个变量
- 修改操作不是“原子”的(也就是cnt++占用三个指令,a = 1这样的赋值操作是原子的) 再来看一个例子: 使用多线程实现三个窗口卖票的业务
这时就出现了一些小问题,售卖的票中有相同的票,也有超出范围的票,出现这个问题的原因就是线程执行时是有随机性的,当一个线程休眠时,其他的线程就可以抢到CPU了,休眠之后就又可以争夺CPU,此时如果一个线程刚好执行到target++,还没来得及打印,其他线程抢回了CPU,并且执行了target++,这时就可能出现以上的情况 解决办法:把操作共享数据的代码锁起来,锁默认打开,如果有现成进去之后,锁自动关闭,里面的代码全部执行完毕,线程出来,锁自动打开,这样就可以解决上述问题 volatile 关键字 线程安全的第四个原因:内存可见性引起的线程安全问题,也就是一个线程对共享变量的修改不能及时被其他线程看到,从而产生内存可见性问题 来看下面的一个例子: public class ThreadDemo13 { private static int flag = 0;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
while (flag == 0) {
}
System.out.println("t1线程退出循环");
});
Thread t2 = new Thread(() -> {
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个整数");
flag = sc.nextInt();
});
t1.start();
t2.start();
}
} 但是最终结果并没有和我们预料的那样,当线程2输入一个不为0的数后,线程一结束,程序一直是就绪状态,并且在jconsole中看到线程仍处于就绪状态
上面出现的问题就是内存可见性问题,这是因为在 Java 中,为了提高性能,编译器/JVM和处理器可能会对指令进行重排序。
这段代码分为两步进行:
- 从内存中读取数据到寄存器中(读取内存,相比之下速度慢)
- 通过类似与cmp的命令,比较寄存器中的数据和0的值(速度快) 在JVM看来,每次循环结果都一样,并且开销非常大,就把1的操作优化掉了,每次循环就不读取内存中的值了,直接读取寄存器/cache中的数据,但是这样的话,当用户修改flag的值的时候,虽然内存中已经改变了,但是内存中flag的改变对线程一来说是不可见的,这就引起了内存可见性问题 此时只需要加一个sleep就没有刚刚的问题了,因为相比sleep来说,读取内存的速度又是非常快的,就没有上述优化了
如果说不要sleep,就可以通过volatile关键字修饰变量,相当于给编译器注明这个变量是“易变”的,此时就不会再进行上面的优化了
锁 同步代码块 同步代码块是通过关键字synchronized来实现的,括号中需要传入一个锁对象,可以是任意的,但必须是唯一的,通常会使用Thread.class作为锁对象,因为字节码文件对象是唯一的 synchronized (锁对象){
} public class MyThread3 extends Thread { static int ticket = 0; @Override public void run() { while (true) { synchronized (Thread.class) { if (ticket < 100) { try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } ticket++; System.out.println("正在卖第" + ticket + "张票"); } else { break; } }
}
}
}
要注意的是,synchronized在这里不能写在while循环外面,不然的话只有线程一就把循环的内容执行完了,然后剩余的线程由于target不满足循环条件,就不会再执行了
同理,第一个例子也可以加上synchronized
如果说上面两个线程中,synchroized传入的锁对象不是同一个的话,那么两个线程的锁就没有任何关系,还是和之前一样的随机调度并发执行 通过使用锁,就把两个线程锁中的内容变成串行,剩下的内容仍然是并发执行的 如果说是多个线程都加锁的话,例如线程1,2,3都要加上锁,加入当1拿到锁并释放了锁之后,之后的锁谁拿到也是不确定的 同步方法 把synchronized加在方法上就是同步方法 格式:修饰符 synchronized 返回类型方法名(方法参数){...}; 特点:同步方法
