Java多线程入门二
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,接着试图占有对象2
- 线程2 首先占有对象2,接着试图占有对象1
- 线程1 等待线程2释放对象2
- 与此同时,线程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 代码看到的线程状态而已。
线程池
每一个线程的启动和结束都是比较消耗时间和占用资源的。
如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。
为了解决这个问题,引入线程池这种设计思想。
线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务
线程池的思路和生产者消费者模型是很接近的。
- 准备一个任务容器
- 一次性启动10个 消费者线程
- 刚开始任务容器是空的,所以线程都wait在上面。
- 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
- 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
- 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
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,接着试图占有对象2
- 线程2 首先占有对象2,接着试图占有对象1
- 线程1 等待线程2释放对象2
- 与此同时,线程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 代码看到的线程状态而已。
线程池
每一个线程的启动和结束都是比较消耗时间和占用资源的。
如果在系统中用到了很多的线程,大量的启动和结束动作会导致系统的性能变卡,响应变慢。
为了解决这个问题,引入线程池这种设计思想。
线程池的模式很像生产者消费者模式,消费的对象是一个一个的能够运行的任务
线程池的思路和生产者消费者模型是很接近的。
- 准备一个任务容器
- 一次性启动10个 消费者线程
- 刚开始任务容器是空的,所以线程都wait在上面。
- 直到一个外部线程往这个任务容器中扔了一个“任务”,就会有一个消费者线程被唤醒notify
- 这个消费者线程取出“任务”,并且执行这个任务,执行完毕后,继续等待下一次任务的到来。
- 如果短时间内,有较多的任务加入,那么就会有多个线程被唤醒,去执行这些任务。
在整个过程中,都不需要创建新的线程,而是循环使用这些已经存在的线程
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多线程入门二相关推荐
- java多线程入门1
java多线程入门1 并发和并行 并发: 主要是指多个任务交替执行.而且这个情况可能出现串行的 并行:一般是多个任务同时执行. 该图来自java高并发程序设计 死锁.饥饿.活锁的概念 死锁一般是指几个 ...
- Java总结篇系列:Java多线程(二)
本文承接上一篇文章<Java总结篇系列:Java多线程(一)>. 四.Java多线程的阻塞状态与线程控制 上文已经提到Java阻塞的几种具体类型.下面分别看下引起Java线程阻塞的主要方法 ...
- Java多线程系列(二):线程的五大状态,以及线程之间的通信与协作
在Java面试的时候,经常会问到Java并发编程相关的多线程.线程池.线程锁.线程通信等面试必考点,比如: Java并发编程系列:Java线程池的使用方式,核心运行原理.以及注意事项 Java并发编程 ...
- Java多线程学习(二)---线程创建方式
线程创建方式 摘要: 1. 通过继承Thread类来创建并启动多线程的方式 2. 通过实现Runnable接口来创建并启动线程的方式 3. 通过实现Callable接口来创建并启动线程的方式 4. 总 ...
- Java多线程(二)之Atomic:原子变量与原子类
一.何谓Atomic? Atomic一词跟原子有点关系,后者曾被人认为是最小物质的单位.计算机中的Atomic是指不能分割成若干部分的意思.如果一段代码被认为是Atomic,则表示这段代码在执行过程中 ...
- Java多线程(二)——多线程基本特性
目录 一.引言 二.优先级 三.睡眠sleep 四.加入线程join 五.礼让线程yield 六.守护线程daemon 七.中断线程 八.总结 一.引言 在jdk1.5之前多线程有很多基础的功能,下面 ...
- Java多线程入门(狂神说)
文章目录 前言 线程与进程 线程 进程 线程的创建 线程的三种创建方式 1.继承Thread类 2.实现Runnable接口 3.实现Callable接口 线程状态 线程的五大状态 获取线程状态 线程 ...
- java多线程学习二、安全与不安全示例:12306买票和银行取钱、java内存模型、内存可见性、线程同步块和方法
文章目录 前言 1. 什么是块,分为几种 2. 静态块与构造块的区别 一. 举例说明:并发情况下,线程不安全 1. 示例1:unsafe12306取票 2. 示例2:unsafe银行取钱 二.线程不安 ...
- Java多线程(二):Thread类
Thread类的实例方法 start() start方法内部会调用方法start方法启动一个线程,该线程返回start方法,同时Java虚拟机调用native start0启动另一个线程调用run方法 ...
最新文章
- 《C陷阱与缺陷》一导读
- 公司规定所有接口都用 POST请求?
- html5常用模板下载网站
- Windows10系统的使用小技巧四 —— 剪贴板历史记录
- android自动更新列表,Android数据库表结构自动升级
- 你会么?图形不正,角度是随机的
- C语言百度翻译API的使用,c语言怎么翻译? 程序怎么运行?
- php transfer-encoding: chunked,php – 使用chunked transfer encoding和gzip
- 紫为云 2020春招开启!算法职位20K-50K!
- Python基础----字典
- 【报告分享】中国人工智能产业发展指数.pdf
- 【电商系统】—项目梳理(一)
- 【持续更新...】相关资源汇总
- '命名空间xxx中不存在类型或命名空间名xx(是否缺少程序集引用)'-异常报错的原因
- [翻译]CryEngine3中ClothShader详解
- python中的方法是什么_Python方法
- 三维体素图绘制实验与教程
- 穹顶之下的出行利器:百度地图热力
- 动态时间规整DWT(Dynamic Time Warping)
- Android开发艺术探索——第十四章:JNI和NDK编程
热门文章
- Caffe 模型微调 的场景、问题、技巧以及解决方案
- [IT幽默]IT业售后服务笑话大全(2)
- 计算机死机的解决方法及操作步骤,电脑突然死机怎么办?试试这几种方法
- 16个值得使用的免费外链建设工具
- Pangolin-0.4
- 总线驱动---IIC驱动
- 在淘宝购台电7寸MP5的体会
- 腾讯健康 养生 身体 心理 私密 话题 名医堂 图片 生活保健 搜狗 女人过瘦10大坏处:易造成贫血脱发甚至不孕...
- java itext 里表格_Java使用itext5实现PDF表格文档导出
- Getting started with the Projucer