线程的生命周期的定义
线程的生命周期是线程从生到死的过程中,经历的各种状态及状态转换。
Java线程的状态
Java总共定义了6种状态,全部定义在Thread类的内部枚举类种。分别为NEW; RUNNABLE; BLOCKED; WAITING; TIMED_WAITING; TERMINATED
线程的6种状态互相转换
说明过程:
- NEW新建状态,代码中New一个Thread类对象出来,此时这个线程对象就是一个新建状态。如果用这个线程对象调用了start方法,它就进入了Runnable可运行状态。
- 调用start方法进入可运行状态是因为它要等CPU调度之后才能跑,调了之后其实是一个所谓的可运行状态。一旦CPU把它调度了,然后假如它执行完毕了,或者在中间执行的时候出现了异常,都会进入终止状态。
- 可运行状态如果没有得到锁对象,就会进入到锁阻塞。(小明和小红两条线程都在运行,同时抢锁,假如小明没有抢到锁他就会进入锁阻塞,他要等到小红执行完了才能进入)。如果获得锁对象了它会再次进入可运行状态。
- 如果可运行的线程获得了锁对象之后调用了wait方法把自己等待了,就会进入Waiting无限等待状态。如果别人把这个线程给唤醒了,唤醒了之后它又要去抢锁,如果它还是没有抢到锁又会进入锁阻塞状态。假如抢到了锁就进入可运行状态。
- 如果线程处于可运行状态,调用sleep就进入了休眠状态,它就进入了Timed Waiting计时等待状态,但注意此时它的锁是不释放的。或者可运行的线程调用wait方法,它也会进入TimedWaiting计时等待状态,这个wait方法可以传以毫秒为单位的参数,假如传了3000,那么就代表它会让自己等待3秒钟,如果3秒中没有人唤醒它,它会自动醒的。但是wait跟sleep的区别在于wait是会释放锁的,所以到时间需要重新抢锁。在wait等待期间如果被唤醒也需要自己重新抢锁,抢到进入可执行,抢不到进入锁阻塞。
悲观锁和乐观锁
提问:假如我既要线程安全又要同时执行,请问阁下该如何应对?
答:使用乐观锁解决
悲观锁
首先回顾悲观锁示例如下:
CountRunnable.java
public class CountRunnable implements Runnable{
// 没必要声明成静态变量,到时候任务对象就只New一个就好了
// 记录浏览人数
private int count;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("count====>" + (++count));
}
}
}
OptimisticLockTest.java
public class OptimisticLockTest {
public static void main(String[] args) {
// 目标:悲观锁及乐观锁原理
// 悲观锁:一上来就加锁,没有安全感。每次只能一个线程进入,访问完毕后再解锁。 线程安全,性能较差。
// 乐观锁:一开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候再开始控制。 线程安全,性能较好。
// 需求:1个变量,100个线程,每个线程对其加100次。(相当于模拟个阅读人数统计)
Runnable target = new CountRunnable();
// 任务对象只有1个但是线程对象有100个,每个线程对象执行各自的run方法
for (int i = 1; i <= 100 ; i++) {
new Thread(target).start();
}
}
}
上述程序是线程不安全的,当运行主程序后会发现出现的结果可能是9997,9998,9999,10000这样子,比较随机。但总体冲突的几率是比较小的
原因说明:假设中途有一次变量变为10了,此时线程1将10拽出来,加个1变成11,与此同时线程2也将10拽出来,加个1也变成11,它俩都放回去,结果还是11,但实际上加了2次,缺失1次。这是导致结果不对的原因,多个线程访问同一个资源出现的线程安全问题。
IDEA中快速包裹结构快捷键:Ctrl + Alt + T
给涉及共享资源的地方加锁:
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + this + "count====>" + (++count));
}
}
}
这里this代表的是target对象,只有一份。最终结果如下:
Thread-65com.thd.optimistic_lock.CountRunnable@34a1e733count====>10000
线程安全问题解决,上述使用的是悲观锁,1001个线程排10000次队。也可以看到线程的抢夺是随机的,上述结果说明最后一次是由65号线程作运算的。
乐观锁
比较和交换算法CAS(Compare and Set)
假设中途有一次变量变为10了,此时线程1将10拽出来,先记录原有的状态10,加个1变成11,这时候再跟刚才记录的状态比较,如果还是10,说明没有人修改,那就将11替换上去。假设在比较的时候已经被改成11了,比较发现不同,就将这次修改作废,继续拿11继续作计算。上述就是乐观锁的机制,因为它并不加锁,所以一般比加锁的性能好很多。
由于乐观锁在Java开发中比较重要,实际上它早已内置了很多乐观锁的技术。典型的乐观锁比如整数修改乐观锁,是用原子类进行实现的。任务类可修改如下:
public class CountRunnable2 implements Runnable{
// 整数修改的乐观锁:原子类实现的
private AtomicInteger count = new AtomicInteger();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + this + "count====>" + (count.incrementAndGet()));
}
}
}
incrementAndGet()方法翻译过来就是先加个1再拿出来展示,方法内部并没有加锁。看看源码:
●this代表当前AtomicInteger原子对象,因为任务对象只有一个,所以原子对象也只有一个。
●valueOffset往上看就会发现是原子对象里面的value的地址,根据地址取value的值。
方法具体实现-使用乐观锁思想
练习题
有100份礼品,小红和小明两人同时发送,当剩下的礼品小于10份的时候不再送出,利用多线程模拟该过程并将该线程的名称打印出来,并最后在控制台分别打印小红,小明各自送出多少份礼物。
示例代码如下:
SendThread.java
public class SendThread extends Thread{
private final List<String> gift;
private int count;
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public SendThread(List<String> gift, String name){
super(name);
this.gift = gift;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
// 小明 小红发礼物出去,实现线程安全问题
// 注意:锁必须唯一
Random r = new Random();
while (true) {
// 这里并不能用this作为锁对象因为线程对象不唯一了
synchronized (gift) {
if(gift.size()<10){
break;
}
String rs = gift.remove(r.nextInt(gift.size()));
System.out.println(name + "send the "+ rs);
// 这里统计个数对于小红和小明两个线程对象互不影响
count++;
}
}
}
}
SendGift.java
public class SendGift {
public static void main(String[] args) throws Exception {
ArrayList<String> gift = new ArrayList<>();
String[] names = {"aaaaa","bbbbb","ccccc","ddddd","eeeee"};
Random r = new Random();
for (int i = 0; i < 100; i++) {
gift.add(names[r.nextInt(names.length)] + (i+1));
}
System.out.println(gift);
SendThread xm = new SendThread(gift, "小明");
xm.start();
SendThread xh = new SendThread(gift, "小红");
xh.start();
//需要等上述的线程都执行完毕才能拿最终的统计结果
xm.join();
xh.join();
System.out.println(xm.getCount());
System.out.println(xh.getCount());
}
}