Java多线程入门二

基类

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class Hero {private String name;private int hp;private int damage;public Hero() {}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getHp() {return hp;}public void setHp(int hp) {this.hp = hp;}public int getDamage() {return damage;}public void setDamage(int damage) {this.damage = damage;}public Hero(String name, int hp, int damage) {this.name = name;this.hp = hp;this.damage = damage;}public void attack(Hero hero){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}hero.hp-=damage;log.info(name+"正在攻击"+hero.name+","+"血量="+hero.hp);if (hero.isDead()){log.info(hero.name+"死了");}}public boolean isDead(){return 0>=hp?true:false;}public synchronized void  recover(){hp=hp+1;}public synchronized  void hurted(){hp=hp-1;}
}

死锁

  1. 线程1 首先占有对象1,接着试图占有对象2
  2. 线程2 首先占有对象2,接着试图占有对象1
  3. 线程1 等待线程2释放对象2
  4. 与此同时,线程2等待线程1释放对象1
    就会。。。一直等待下去,直到天荒地老,海枯石烂,山无棱 ,天地合。。。
package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class SiSuoTest {public static void main(String[] args){Hero nanqiang =new Hero("法外狂徒",2000,10);Hero langren=new Hero("狼人",2000,10);Thread thread1= new Thread(){public void run(){log.info("线程1我已经占用了男枪啦");synchronized (nanqiang){try {//留出时间来让另一个线程占用狼人Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}log.info("线程1我还想占用狼人!");synchronized (langren){log.info("线程1尝试占用狼人。。。。。。");}}}};thread1.start();Thread thread2=new Thread(){public void run(){log.info("狼人已经被thread2占领啦");synchronized (langren){try {//留出时间来让另一个线程占用男枪Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}log.info("男枪thread2也想占用");synchronized (nanqiang){log.info("thread2尝试占用男枪中。。。。");}}}};thread2.start();}
}

日志是下面这样的,但是一直没有结束,需要手动关停才可以。

18:35:08.365 [Thread-0] INFO com.cv.DuoXianCheng.SiSuoTest - 线程1我已经占用了男枪啦
18:35:08.365 [Thread-1] INFO com.cv.DuoXianCheng.SiSuoTest - 狼人已经被thread2占领啦
18:35:09.368 [Thread-0] INFO com.cv.DuoXianCheng.SiSuoTest - 线程1我还想占用狼人!
18:35:09.368 [Thread-1] INFO com.cv.DuoXianCheng.SiSuoTest - 男枪thread2也想占用

wait、notify

场景

猪八戒打嫦娥,不忍心打死,每次给嫦娥打到剩下一滴血,就让嫦娥回血

⚠️⚠️⚠️注意:wait和notify是Object的方法

因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。

wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。

notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

在基类上修改回血和受伤的方法

    public synchronized void  recover(){hp=hp+1;log.info(name+"增加一滴血,增加后的血量为"+hp);// 通知那些等待在this对象上的线程,可以醒过来了,等待着的减血线程,苏醒过来this.notify();}public synchronized  void hurted(){if (hp==1){try {// 让占有this的减血线程,暂时释放对this的占有,并等待this.wait();}catch (InterruptedException e){e.printStackTrace();}}hp=hp-1;log.info(name+"减少一滴血,减少后的血量为"+hp);}

测试类

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class WaitAndNotifyTest {public static void main(String[] args){final Hero change=new Hero("嫦娥",10,1);Thread thread=new Thread(){public void run(){while (true){change.hurted();try{Thread.sleep(500);}catch (InterruptedException e){e.printStackTrace();}}}};thread.setPriority(Thread.MAX_PRIORITY);thread.start();Thread thread1=new Thread(){public void run(){while (true){change.recover();try{Thread.sleep(5000);}catch (InterruptedException e){e.printStackTrace();}}}};thread1.setPriority(Thread.MIN_PRIORITY);thread1.start();}
}

由于会一直循环下去,所以要手动关闭

20:36:06.892 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为9
20:36:06.894 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为10
20:36:07.398 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为9
20:36:07.904 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为8
20:36:08.408 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为7
20:36:08.911 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为6
20:36:09.415 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为5
20:36:09.920 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为4
20:36:10.421 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为3
20:36:10.926 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为2
20:36:11.426 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:11.899 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:11.929 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:16.900 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:16.900 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:21.905 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:21.905 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:26.909 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:26.909 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:31.913 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:31.913 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:36.917 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:36.918 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1Process finished with exit code 130 (interrupted by signal 2: SIGINT)

interrupt(),isInterrupted(),interrupted()

多线程先明白一个术语“中断状态”,中断状态为true,线程中断。

interrupt():就是通知中止线程的,使“中断状态”为true。

isInterrupted():就是打印中断状态的,然后不对中断状态有任何操作。

interrupted():检测运行这个方法的线程的中断状态,注意,是运行这个方法的线程,且会清除中断状态

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class InterrupectTest {public static void main(String[] args){Thread thread=new Thread(){public void run(){for(int i=0;i<10;i++){log.info("第"+i+"次的状态为"+this.isInterrupted());if (i==4){this.interrupt();}if (i==6){this.interrupted();}}}};thread.start();}
}

日志

14:24:14.404 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第0次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第1次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第2次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第3次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第4次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第5次的状态为true
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第6次的状态为true
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第7次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第8次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第9次的状态为falseProcess finished with exit code 0

线程的状态

知道了前面的内容,我们再来认识一下线程的状态,才能更好的理解其中的转换关系。

new 新建

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态,但是:线程还是没有开始执行

有状态了,那肯定是已经创建好线程对象了(如果对象都没有,何来状态这说),
问题的焦点就在于还没有开始执行,当调用线程的start()方法时,线程不一定会马上执行,因为Java线程是映射到操作系统的线程执行,此时可能还需要等操作系统调度,但此时该线程的状态已经为RUNNABLE

runable 就绪

只是说你有资格运行,调度程序没有挑选到你,你就永远是可运行状态。

调用start(),进入可运行态

.当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态

.当前线程时间片用完,调用当前线程的yield()方法,当前线程进入可运行状态

.锁池里的线程拿到对象锁后,进入可运行状态

这个状态是最有争议的,注释中说了,它表示线程在JVM层面是执行的,但在操作系统层面不一定,它举例是CPU,毫无疑问CPU是一个操作系统资源,但这也就意味着在等操作系统其他资源的时候,线程也会是这个状态

blocked 阻塞

被挂起,线程因为某种原因放弃了cpu timeslice,暂时停止运行。
当前线程调用Thread.sleep(),进入阻塞态

运行在当前线程里的其它线程调用join(),当前线程进入阻塞态。

等待用户输入的时候,当前线程进入阻塞态。

等待阻塞

运行的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中

同步阻塞

运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中

其他阻塞

运行的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态

当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态

线程在阻塞等待monitor lock(监视器锁)

一个线程在进入synchronized修饰的临界区的时候,或者在synchronized临界区中调用Object.wait然后被唤醒重新进入synchronized临界区都对应该态。

结合上面RUNNABLE的分析,也就是I/O阻塞不会进入BLOCKED状态,只有synchronized会导致线程进入该状态

关于BLOCKED状态,注释里只提到一种情况就是进入synchronized声明的临界区时会导致,这个也很好理解,synchronized是JVM自己控制的,所以这个阻塞事件它自己能够知道(对比理解上面的操作系统层面)。

interrupt()是无法唤醒的!只是做个标记而已!

waiting

线程拥有对象锁后进入到相应的代码区后,调用相应的“锁对象”的wait()后产生的一种结果

它们也是在等待另一个对象事件的发生,也就是描述了等待的意思。

waiting和blocked的区别

BLOCKED是虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区
wait()的先决条件是要进入临界区,也就是线程已经拿到了“门票”,自己可能进去做了一些事情,但此时通过判定某些业务上的参数(由具体业务决定),发现还有一些其他配合的资源没有准备充分,那么自己就等等再做其他的事情
有一个非常典型的案例就是通过wait()和notify()完成生产者/消费者模型
当生产者生产过快,发现仓库满了,即消费者还没有把东西拿走(空位资源还没准备好) 时,生产者就等待有空位再做事情,消费者拿走东西时会发出“有空位了”的消息,那么生产者就又开始工作了
反过来也是一样,当消费者消费过快发现没有存货时,消费者也会等存货到来,生产者生产出内容后发出“有存货了”的消息,消费者就又来抢东西了。

在这种状态下,如果发生了对该线程的interrupt()是有用的,处于该状态的线程内部会抛出一个InerruptedException
这个异常应当在run()里面捕获,使得run()正常地执行完成。当然在run()内部捕获异常后,还可以让线程继续运行,这完全是根据具体的应用场景来决定的。

在这种状态下,如果某线程对该锁对象做了notify(),那么将从等待池中唤醒一个线程重新恢复到RUNNABLE
除notify()外,还有一个notifyAll() ,前者是
唤醒一个处于WAITING的线程,而后者是唤醒所有的线程。

Object.wait()是否需要死等呢?

不是,除中断外,它还有两个重构方法

Object.wait(int timeout),传入的timeout 参数是超时的毫秒值,超过这个值后会自动唤醒,继续做下面的操作(不会抛出InterruptedException ,但是并不意味着我们不去捕获,因为不排除其他线程会对它做interrup())。
Object.wait(int timeout,int nanos) 这是一个更精确的超时设置,理论上可以精确到纳秒,这个纳秒值可接受的范围是0~999999 (因为100000onS 等于1ms)。

这些方法都会有类似的重构方法来设置超时,达到类似的目的,不过此时的状态不再是WAITING,而是TIMED.WAITING

通常写代码的人肯定不想让程序死掉,但是又希望通过这些等待、通知的方式来实现某些平衡,这样就不得不去尝试采用“超时+重试+失败告知”等方式来达到目的。

timed waiting

当调用Thread.sleep()时,相当于使用某个时间资源作为锁对象,进而达到等待的目的,当时间达到时触发线程回到工作状态。

running 正在运行的

terminated 结束

这个线程对象也许是活的,但是,它已经不是一个单独执行的线程,在一个死去的线程上调用start()方法,会抛java.lang.IllegalThreadStateException.
线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
run()走完了,线程就处于这种状态。其实这只是Java 语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java 代码看到的线程状态而已。

线程池

每一个线程的启动和结束都是比较消耗时间和占用资源的。

如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

为了解决这个问题,引入线程池这种设计思想。

线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务

线程池的思路和生产者消费者模型是很接近的。

  1. 准备一个任务容器
  2. 一次性启动10个 消费者线程
  3. 刚开始任务容器是空的,所以线程都wait在上面。
  4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
  5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
  6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadPoolTest {public static void main(String[] args){ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10,15,60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());/*** 第一个参数10 表示这个线程池初始化了10个线程在里面工作* 第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。* 第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个* 第四个参数TimeUnit.SECONDS 如上* 第五个参数 new LinkedBlockingQueue() 用来放任务的集合,当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常* */for(int i=0;i<100;i++){Thread thread=new Thread(){public void run(){while (true){log.info("重生之我是刘姥姥");}}};threadPoolExecutor.execute(thread);}}
}

日志上来看,最多也就到10了,因为LinkedBlockingDeque没有设置容量,但是如果设置了容量,就能到15个了

...
10:53:36.322 [pool-1-thread-1] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-1] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-10] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-10] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
...

Java多线程入门二

基类

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class Hero {private String name;private int hp;private int damage;public Hero() {}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getHp() {return hp;}public void setHp(int hp) {this.hp = hp;}public int getDamage() {return damage;}public void setDamage(int damage) {this.damage = damage;}public Hero(String name, int hp, int damage) {this.name = name;this.hp = hp;this.damage = damage;}public void attack(Hero hero){try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}hero.hp-=damage;log.info(name+"正在攻击"+hero.name+","+"血量="+hero.hp);if (hero.isDead()){log.info(hero.name+"死了");}}public boolean isDead(){return 0>=hp?true:false;}public synchronized void  recover(){hp=hp+1;}public synchronized  void hurted(){hp=hp-1;}
}

死锁

  1. 线程1 首先占有对象1,接着试图占有对象2
  2. 线程2 首先占有对象2,接着试图占有对象1
  3. 线程1 等待线程2释放对象2
  4. 与此同时,线程2等待线程1释放对象1
    就会。。。一直等待下去,直到天荒地老,海枯石烂,山无棱 ,天地合。。。
package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class SiSuoTest {public static void main(String[] args){Hero nanqiang =new Hero("法外狂徒",2000,10);Hero langren=new Hero("狼人",2000,10);Thread thread1= new Thread(){public void run(){log.info("线程1我已经占用了男枪啦");synchronized (nanqiang){try {//留出时间来让另一个线程占用狼人Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}log.info("线程1我还想占用狼人!");synchronized (langren){log.info("线程1尝试占用狼人。。。。。。");}}}};thread1.start();Thread thread2=new Thread(){public void run(){log.info("狼人已经被thread2占领啦");synchronized (langren){try {//留出时间来让另一个线程占用男枪Thread.sleep(1000);}catch (InterruptedException e){e.printStackTrace();}log.info("男枪thread2也想占用");synchronized (nanqiang){log.info("thread2尝试占用男枪中。。。。");}}}};thread2.start();}
}

日志是下面这样的,但是一直没有结束,需要手动关停才可以。

18:35:08.365 [Thread-0] INFO com.cv.DuoXianCheng.SiSuoTest - 线程1我已经占用了男枪啦
18:35:08.365 [Thread-1] INFO com.cv.DuoXianCheng.SiSuoTest - 狼人已经被thread2占领啦
18:35:09.368 [Thread-0] INFO com.cv.DuoXianCheng.SiSuoTest - 线程1我还想占用狼人!
18:35:09.368 [Thread-1] INFO com.cv.DuoXianCheng.SiSuoTest - 男枪thread2也想占用

wait、notify

场景

猪八戒打嫦娥,不忍心打死,每次给嫦娥打到剩下一滴血,就让嫦娥回血

⚠️⚠️⚠️注意:wait和notify是Object的方法

因为所有的Object都可以被用来作为同步对象,所以准确的讲,wait和notify是同步对象上的方法。

wait()的意思是: 让占用了这个同步对象的线程,临时释放当前的占用,并且等待。 所以调用wait是有前提条件的,一定是在synchronized块里,否则就会出错。

notify() 的意思是,通知一个等待在这个同步对象上的线程,你可以苏醒过来了,有机会重新占用当前对象了。

notifyAll() 的意思是,通知所有的等待在这个同步对象上的线程,你们可以苏醒过来了,有机会重新占用当前对象了。

在基类上修改回血和受伤的方法

    public synchronized void  recover(){hp=hp+1;log.info(name+"增加一滴血,增加后的血量为"+hp);// 通知那些等待在this对象上的线程,可以醒过来了,等待着的减血线程,苏醒过来this.notify();}public synchronized  void hurted(){if (hp==1){try {// 让占有this的减血线程,暂时释放对this的占有,并等待this.wait();}catch (InterruptedException e){e.printStackTrace();}}hp=hp-1;log.info(name+"减少一滴血,减少后的血量为"+hp);}

测试类

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class WaitAndNotifyTest {public static void main(String[] args){final Hero change=new Hero("嫦娥",10,1);Thread thread=new Thread(){public void run(){while (true){change.hurted();try{Thread.sleep(500);}catch (InterruptedException e){e.printStackTrace();}}}};thread.setPriority(Thread.MAX_PRIORITY);thread.start();Thread thread1=new Thread(){public void run(){while (true){change.recover();try{Thread.sleep(5000);}catch (InterruptedException e){e.printStackTrace();}}}};thread1.setPriority(Thread.MIN_PRIORITY);thread1.start();}
}

由于会一直循环下去,所以要手动关闭

20:36:06.892 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为9
20:36:06.894 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为10
20:36:07.398 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为9
20:36:07.904 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为8
20:36:08.408 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为7
20:36:08.911 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为6
20:36:09.415 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为5
20:36:09.920 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为4
20:36:10.421 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为3
20:36:10.926 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为2
20:36:11.426 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:11.899 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:11.929 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:16.900 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:16.900 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:21.905 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:21.905 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:26.909 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:26.909 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:31.913 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:31.913 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1
20:36:36.917 [Thread-1] INFO com.cv.DuoXianCheng.Hero - 嫦娥增加一滴血,增加后的血量为2
20:36:36.918 [Thread-0] INFO com.cv.DuoXianCheng.Hero - 嫦娥减少一滴血,减少后的血量为1Process finished with exit code 130 (interrupted by signal 2: SIGINT)

interrupt(),isInterrupted(),interrupted()

多线程先明白一个术语“中断状态”,中断状态为true,线程中断。

interrupt():就是通知中止线程的,使“中断状态”为true。

isInterrupted():就是打印中断状态的,然后不对中断状态有任何操作。

interrupted():检测运行这个方法的线程的中断状态,注意,是运行这个方法的线程,且会清除中断状态

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;@Slf4j
public class InterrupectTest {public static void main(String[] args){Thread thread=new Thread(){public void run(){for(int i=0;i<10;i++){log.info("第"+i+"次的状态为"+this.isInterrupted());if (i==4){this.interrupt();}if (i==6){this.interrupted();}}}};thread.start();}
}

日志

14:24:14.404 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第0次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第1次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第2次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第3次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第4次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第5次的状态为true
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第6次的状态为true
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第7次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第8次的状态为false
14:24:14.406 [Thread-0] INFO com.cv.DuoXianCheng.InterrupectTest - 第9次的状态为falseProcess finished with exit code 0

线程的状态

知道了前面的内容,我们再来认识一下线程的状态,才能更好的理解其中的转换关系。

new 新建

实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态,但是:线程还是没有开始执行

有状态了,那肯定是已经创建好线程对象了(如果对象都没有,何来状态这说),
问题的焦点就在于还没有开始执行,当调用线程的start()方法时,线程不一定会马上执行,因为Java线程是映射到操作系统的线程执行,此时可能还需要等操作系统调度,但此时该线程的状态已经为RUNNABLE

runable 就绪

只是说你有资格运行,调度程序没有挑选到你,你就永远是可运行状态。

调用start(),进入可运行态

.当前线程sleep()结束,其他线程join()结束,等待用户输入完毕,某个线程拿到对象锁,这些线程也将进入可运行状态

.当前线程时间片用完,调用当前线程的yield()方法,当前线程进入可运行状态

.锁池里的线程拿到对象锁后,进入可运行状态

这个状态是最有争议的,注释中说了,它表示线程在JVM层面是执行的,但在操作系统层面不一定,它举例是CPU,毫无疑问CPU是一个操作系统资源,但这也就意味着在等操作系统其他资源的时候,线程也会是这个状态

blocked 阻塞

被挂起,线程因为某种原因放弃了cpu timeslice,暂时停止运行。
当前线程调用Thread.sleep(),进入阻塞态

运行在当前线程里的其它线程调用join(),当前线程进入阻塞态。

等待用户输入的时候,当前线程进入阻塞态。

等待阻塞

运行的线程执行o.wait()方法,JVM会把该线程放入等待队列(waitting queue)中

同步阻塞

运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池(lock pool)中

其他阻塞

运行的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态

当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态

线程在阻塞等待monitor lock(监视器锁)

一个线程在进入synchronized修饰的临界区的时候,或者在synchronized临界区中调用Object.wait然后被唤醒重新进入synchronized临界区都对应该态。

结合上面RUNNABLE的分析,也就是I/O阻塞不会进入BLOCKED状态,只有synchronized会导致线程进入该状态

关于BLOCKED状态,注释里只提到一种情况就是进入synchronized声明的临界区时会导致,这个也很好理解,synchronized是JVM自己控制的,所以这个阻塞事件它自己能够知道(对比理解上面的操作系统层面)。

interrupt()是无法唤醒的!只是做个标记而已!

waiting

线程拥有对象锁后进入到相应的代码区后,调用相应的“锁对象”的wait()后产生的一种结果

它们也是在等待另一个对象事件的发生,也就是描述了等待的意思。

waiting和blocked的区别

BLOCKED是虚拟机认为程序还不能进入某个区域,因为同时进去就会有问题,这是一块临界区
wait()的先决条件是要进入临界区,也就是线程已经拿到了“门票”,自己可能进去做了一些事情,但此时通过判定某些业务上的参数(由具体业务决定),发现还有一些其他配合的资源没有准备充分,那么自己就等等再做其他的事情
有一个非常典型的案例就是通过wait()和notify()完成生产者/消费者模型
当生产者生产过快,发现仓库满了,即消费者还没有把东西拿走(空位资源还没准备好) 时,生产者就等待有空位再做事情,消费者拿走东西时会发出“有空位了”的消息,那么生产者就又开始工作了
反过来也是一样,当消费者消费过快发现没有存货时,消费者也会等存货到来,生产者生产出内容后发出“有存货了”的消息,消费者就又来抢东西了。

在这种状态下,如果发生了对该线程的interrupt()是有用的,处于该状态的线程内部会抛出一个InerruptedException
这个异常应当在run()里面捕获,使得run()正常地执行完成。当然在run()内部捕获异常后,还可以让线程继续运行,这完全是根据具体的应用场景来决定的。

在这种状态下,如果某线程对该锁对象做了notify(),那么将从等待池中唤醒一个线程重新恢复到RUNNABLE
除notify()外,还有一个notifyAll() ,前者是
唤醒一个处于WAITING的线程,而后者是唤醒所有的线程。

Object.wait()是否需要死等呢?

不是,除中断外,它还有两个重构方法

Object.wait(int timeout),传入的timeout 参数是超时的毫秒值,超过这个值后会自动唤醒,继续做下面的操作(不会抛出InterruptedException ,但是并不意味着我们不去捕获,因为不排除其他线程会对它做interrup())。
Object.wait(int timeout,int nanos) 这是一个更精确的超时设置,理论上可以精确到纳秒,这个纳秒值可接受的范围是0~999999 (因为100000onS 等于1ms)。

这些方法都会有类似的重构方法来设置超时,达到类似的目的,不过此时的状态不再是WAITING,而是TIMED.WAITING

通常写代码的人肯定不想让程序死掉,但是又希望通过这些等待、通知的方式来实现某些平衡,这样就不得不去尝试采用“超时+重试+失败告知”等方式来达到目的。

timed waiting

当调用Thread.sleep()时,相当于使用某个时间资源作为锁对象,进而达到等待的目的,当时间达到时触发线程回到工作状态。

running 正在运行的

terminated 结束

这个线程对象也许是活的,但是,它已经不是一个单独执行的线程,在一个死去的线程上调用start()方法,会抛java.lang.IllegalThreadStateException.
线程run()、main() 方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
run()走完了,线程就处于这种状态。其实这只是Java 语言级别的一种状态,在操作系统内部可能已经注销了相应的线程,或者将它复用给其他需要使用线程的请求,而在Java语言级别只是通过Java 代码看到的线程状态而已。

线程池

每一个线程的启动和结束都是比较消耗时间和占用资源的。

如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。

为了解决这个问题,引入线程池这种设计思想。

线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务

线程池的思路和生产者消费者模型是很接近的。

  1. 准备一个任务容器
  2. 一次性启动10个 消费者线程
  3. 刚开始任务容器是空的,所以线程都wait在上面。
  4. 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
  5. 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
  6. 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。

在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程

package com.cv.DuoXianCheng;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadPoolTest {public static void main(String[] args){ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10,15,60, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());/*** 第一个参数10 表示这个线程池初始化了10个线程在里面工作* 第二个参数15 表示如果10个线程不够用了,就会自动增加到最多15个线程:线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。* 第三个参数60 结合第四个参数TimeUnit.SECONDS,表示经过60秒,多出来的线程还没有接到活儿,就会回收,最后保持池子里就10个* 第四个参数TimeUnit.SECONDS 如上* 第五个参数 new LinkedBlockingQueue() 用来放任务的集合,当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常* */for(int i=0;i<100;i++){Thread thread=new Thread(){public void run(){while (true){log.info("重生之我是刘姥姥");}}};threadPoolExecutor.execute(thread);}}
}

日志上来看,最多也就到10了,因为LinkedBlockingDeque没有设置容量,但是如果设置了容量,就能到15个了

...
10:53:36.322 [pool-1-thread-1] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-1] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-10] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
10:53:36.322 [pool-1-thread-10] INFO com.cv.DuoXianCheng.ThreadPoolTest - 重生之我是刘姥姥
...

悲观锁

当要对数据库中的一条数据进行修改的时候,为了避免同时被其他人修改,最好的办法就是直接对该数据进行加锁以防止并发。这种借助数据库锁机制,在修改数据之前先锁定,再修改的方式被称之为悲观并发控制【Pessimistic Concurrency Control,缩写“PCC”,又名“悲观锁”】。

悲观锁,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度。因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

之所以叫做悲观锁,是因为这是一种对数据的修改持有悲观态度的并发控制方式。总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:

1、传统的关系型数据库使用这种锁机制,比如行锁、表锁、读锁、写锁等,都是在操作之前先上锁。

2、Java 里面的同步 synchronized 关键字的实现。

悲观锁主要分为共享锁和排他锁:

1、共享锁【shared locks】又称为读锁,简称 S 锁。顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

2、排他锁【exclusive locks】又称为写锁,简称 X 锁。顾名思义,排他锁就是不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁。获取排他锁的事务可以对数据行读取和修改。

乐观锁

乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果冲突,则返回给用户异常信息,让用户决定如何去做。乐观锁适用于读多写少的场景,这样可以提高程序的吞吐量。

乐观锁的实现:

1、CAS 实现:Java 中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种 CAS 实现方式。

2、版本号控制:一般是在数据表中加上一个数据版本号 version 字段,表示数据被修改的次数。当数据被修改时,version 值会 +1。当线程 A 要更新数据时,在读取数据的同时也会读取 version 值,在提交更新时,若刚才读取到的 version 值与当前数据库中的 version 值相等时才更新,否则重试更新操作,直到更新成功。

其实关于多线程还有很多东西要学习,比如说悲观锁乐观锁,如何确保线程安全等等等等,这两篇内容就如标题所说一样,只是一个入门。有时间继续深入一下。

Java多线程入门二相关推荐

  1. java多线程入门1

    java多线程入门1 并发和并行 并发: 主要是指多个任务交替执行.而且这个情况可能出现串行的 并行:一般是多个任务同时执行. 该图来自java高并发程序设计 死锁.饥饿.活锁的概念 死锁一般是指几个 ...

  2. Java总结篇系列:Java多线程(二)

    本文承接上一篇文章<Java总结篇系列:Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法 ...

  3. Java多线程系列(二):线程的五大状态,以及线程之间的通信与协作

    在Java面试的时候,经常会问到Java并发编程相关的多线程.线程池.线程锁.线程通信等面试必考点,比如: Java并发编程系列:Java线程池的使用方式,核心运行原理.以及注意事项 Java并发编程 ...

  4. Java多线程学习(二)---线程创建方式

    线程创建方式 摘要: 1. 通过继承Thread类来创建并启动多线程的方式 2. 通过实现Runnable接口来创建并启动线程的方式 3. 通过实现Callable接口来创建并启动线程的方式 4. 总 ...

  5. Java多线程(二)之Atomic:原子变量与原子类

    一.何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位.计算机中的Atomic是指不能分割成若干部分的意思.如果一段代码被认为是Atomic,则表示这段代码在执行过程中 ...

  6. Java多线程(二)——多线程基本特性

    目录 一.引言 二.优先级 三.睡眠sleep 四.加入线程join 五.礼让线程yield 六.守护线程daemon 七.中断线程 八.总结 一.引言 在jdk1.5之前多线程有很多基础的功能,下面 ...

  7. Java多线程入门(狂神说)

    文章目录 前言 线程与进程 线程 进程 线程的创建 线程的三种创建方式 1.继承Thread类 2.实现Runnable接口 3.实现Callable接口 线程状态 线程的五大状态 获取线程状态 线程 ...

  8. java多线程学习二、安全与不安全示例:12306买票和银行取钱、java内存模型、内存可见性、线程同步块和方法

    文章目录 前言 1. 什么是块,分为几种 2. 静态块与构造块的区别 一. 举例说明:并发情况下,线程不安全 1. 示例1:unsafe12306取票 2. 示例2:unsafe银行取钱 二.线程不安 ...

  9. Java多线程(二):Thread类

    Thread类的实例方法 start() start方法内部会调用方法start方法启动一个线程,该线程返回start方法,同时Java虚拟机调用native start0启动另一个线程调用run方法 ...

最新文章

  1. 《C陷阱与缺陷》一导读
  2. 公司规定所有接口都用 POST请求?
  3. html5常用模板下载网站
  4. Windows10系统的使用小技巧四 —— 剪贴板历史记录
  5. android自动更新列表,Android数据库表结构自动升级
  6. 你会么?图形不正,角度是随机的
  7. C语言百度翻译API的使用,c语言怎么翻译? 程序怎么运行?
  8. php transfer-encoding: chunked,php – 使用chunked transfer encoding和gzip
  9. 紫为云 2020春招开启!算法职位20K-50K!
  10. Python基础----字典
  11. 【报告分享】中国人工智能产业发展指数.pdf
  12. 【电商系统】—项目梳理(一)
  13. 【持续更新...】相关资源汇总
  14. '命名空间xxx中不存在类型或命名空间名xx(是否缺少程序集引用)'-异常报错的原因
  15. [翻译]CryEngine3中ClothShader详解
  16. python中的方法是什么_Python方法
  17. 三维体素图绘制实验与教程
  18. 穹顶之下的出行利器:百度地图热力
  19. 动态时间规整DWT(Dynamic Time Warping)
  20. Android开发艺术探索——第十四章:JNI和NDK编程

热门文章

  1. Caffe 模型微调 的场景、问题、技巧以及解决方案
  2. [IT幽默]IT业售后服务笑话大全(2)
  3. 计算机死机的解决方法及操作步骤,电脑突然死机怎么办?试试这几种方法
  4. 16个值得使用的免费外链建设工具
  5. Pangolin-0.4
  6. 总线驱动---IIC驱动
  7. 在淘宝购台电7寸MP5的体会
  8. 腾讯健康 养生 身体 心理 私密 话题 名医堂 图片 生活保健 搜狗 女人过瘦10大坏处:易造成贫血脱发甚至不孕...
  9. java itext 里表格_Java使用itext5实现PDF表格文档导出
  10. Getting started with the Projucer